Skip to content

feat(smash): projectiles you can see - #1044

Merged
andrewgazelka merged 1 commit into
mainfrom
feat/visible-projectiles
Jul 28, 2026
Merged

feat(smash): projectiles you can see#1044
andrewgazelka merged 1 commit into
mainfrom
feat/visible-projectiles

Conversation

@andrewgazelka

Copy link
Copy Markdown
Member

Twenty of the fifty-one abilities fire a projectile, and until now a client saw none of them. projectile::fire created a game-half entity with Flight, Payload and FiredBy, integrated it, detected the hit and dealt the damage — all correct, all tested against the mock, and all invisible, because nothing ever told a client the thing existed. A Skeleton's Barrage sent five arrows' worth of damage and not one arrow.

The flight stays where it is

It is authoritative for the hit, it is deterministic, and tests/abilities.rs and smash-e2e both rest on it; moving it would fork test from production. What was missing was only the picture, so only the picture is added.

Each projectile now carries a Visual(EntityKind) — a generated enum named directly, the choice #1035 made for particles. Exact where a vanilla entity is the thing (an arrow is Arrow, a wither skull a WitherSkull, a thrown cub a Wolf) and the closest always-rendered projectile where it is not (a thrown coal is a SmallFireball, an ink pellet a Snowball), each [APPROXIMATED] marked at the call site.

crate::draw, on the host, turns that into an entity a client draws: it decorates the existing projectile with the components hyperion's egress reads — kind, position, per-tick velocity, facing — and enqueues the spawn. The client dead-reckons the arc from the velocity the add_entity packet carries, which is how vanilla draws a thrown projectile between updates. It is the same entity, not a shadow, so when fly destructs it on the hit the drawn thing dies too and hyperion's despawn observer tells every client to drop it — no second lifetime to keep in step.

Two things it deliberately does not do

Each has a reason that is a property of the engine, not a preference.

No Owner. That is what update_projectile_positions requires to integrate and collide an entity; omitting it is what stops hyperion re-flying and re-hitting a projectile Flight already owns.

Not spawn::spawn, which makes a new entity. Decorating the existing one is the whole reason despawn is free.

The honest limitation

The drawn arc and the hit are computed by two integrators that do not share constants — hyperion's client dead-reckoning uses the vanilla gravity for the kind, Flight uses what the ability set. They agree exactly for a zero-gravity projectile (a hook, a line of cows) and drift for a heavy one over its flight. The projectile vanishes at the hit either way, because the hit is what destructs it. Sharing one integrator is the larger change docs/smash-design.md flags; this is the visible-now half.

Proof, both halves of the seam

every_projectile_that_flies_can_be_seen fires every ability and asserts that whatever projectiles appeared carry a Visual — the input to drawing, for all twenty, checked in the game half against the mock. It sweeps the roster rather than a hand-list of projectile abilities, so a kit added tomorrow that forgets the visual fails here.

tools/smash-match.py decodes add_entity and proves "a projectile was drawn" on the wire: over the sweep, firing the abilities produces add_entity packets, which before draw existed never left the server. During the sweep the only new entities are projectiles, because every player spawned before it began.

Guard broken and watched to fail, then restored

.set(visual) removed from fire:

Skeleton / Barrage put 2 projectile(s) in the world with no Visual
Spider / Needler put 6 projectile(s) in the world with no Visual
Sky Squid / Ink Shotgun put 7 projectile(s) in the world with no Visual
... and every other projectile ability

What this does not settle

The wire half cannot be run to green from here: my smash-e2e attempts die on the load box's connection sweep, as they have all session. The in-process Visual proof is the local evidence; the add_entity census is structural and runs on the gate.

One call site in tests/relationship_traits.rs (from #1041/#1042) also calls fire; its signature fix is included — three lines, no reformatting of that file.

Checks

cargo test -p smash (21 binaries) and nix run --accept-flake-config .#lint both green on d11ca7a, which includes the flecs trait pass.

Twenty of the fifty-one abilities fire a projectile, and until now a
client saw none of them. `projectile::fire` created a game-half entity
with `Flight`, `Payload` and `FiredBy`, integrated it, detected the hit
and dealt the damage -- all correct, all tested against the mock, and all
invisible, because nothing ever told a client the thing existed. A
Skeleton's Barrage sent five arrows' worth of damage and not one arrow.

The flight stays where it is. It is authoritative for the hit, it is
deterministic, and `tests/abilities.rs` and `smash-e2e` both rest on it;
moving it would fork test from production. What was missing was only the
picture, so only the picture is added.

Each projectile now carries a `Visual(EntityKind)` -- a generated enum
named directly, the choice #1035 made for particles, exact where a
vanilla entity is the thing (an arrow is `Arrow`, a wither skull a
`WitherSkull`, a thrown cub a `Wolf`) and the closest always-rendered
projectile where it is not (a thrown coal is a `SmallFireball`, an ink
pellet a `Snowball`), each `[APPROXIMATED]` marked at the call site.

`crate::draw`, on the host, turns that into an entity a client draws: it
decorates the existing projectile with the components hyperion's egress
reads -- kind, position, per-tick velocity, facing -- and enqueues the
spawn. The client dead-reckons the arc from the velocity the add_entity
packet carries, which is how vanilla draws a thrown projectile between
updates. It is the same entity, not a shadow, so when `fly` destructs it
on the hit the drawn thing dies too and hyperion's despawn observer tells
every client to drop it -- no second lifetime to keep in step.

Two things it deliberately does not do, each with a reason that is a
property of the engine rather than a preference. No `Owner`: that is what
`update_projectile_positions` requires to integrate and collide an
entity, and omitting it is what stops hyperion re-flying and re-hitting a
projectile `Flight` already owns. Not `spawn::spawn`, which makes a new
entity: decorating the existing one is the whole reason despawn is free.

The drawn arc and the hit are computed by two integrators that do not
share constants -- hyperion's client dead reckoning uses the vanilla
gravity for the kind, `Flight` uses what the ability set. They agree
exactly for a zero-gravity projectile and drift for a heavy one over its
flight; the projectile vanishes at the hit either way, because the hit is
what destructs it. Sharing one integrator is the larger change
`docs/smash-design.md` flags; this is the visible-now half.

Proof, both halves of the seam:

`every_projectile_that_flies_can_be_seen` fires every ability and asserts
that whatever projectiles appeared carry a `Visual` -- the input to
drawing, for all twenty, checked in the game half against the mock. It
sweeps the roster rather than a hand-list of projectile abilities, so a
kit added tomorrow that forgets the visual fails here.

`tools/smash-match.py` decodes `add_entity` and proves "a projectile was
drawn" on the wire: over the sweep, firing the abilities produces
add_entity packets, which before `draw` existed never left the server.
During the sweep the only new entities are projectiles, because every
player spawned before it began.

Guard broken and watched to fail, then restored:

  `.set(visual)` removed from `fire`
    Skeleton / Barrage put 2 projectile(s) in the world with no Visual
    Spider / Needler put 6 projectile(s) in the world with no Visual
    Sky Squid / Ink Shotgun put 7 projectile(s) in the world with no Visual
    ... and every other projectile ability

The wire half cannot be run to green from here: my `smash-e2e` attempts
die on the load box's connection sweep, as before. The in-process Visual
proof is the local evidence; the add_entity census is structural and runs
on the gate.
@andrewgazelka
andrewgazelka merged commit 3d6094b into main Jul 28, 2026
7 of 11 checks passed
@andrewgazelka
andrewgazelka deleted the feat/visible-projectiles branch July 28, 2026 12:10
@github-actions github-actions Bot added the feat label Jul 28, 2026
@github-actions

Copy link
Copy Markdown

Benchmark Results for general

ray_intersection/aabb_size_0.1                     [  14.6 ns ...  14.6 ns ]      -0.47%
ray_intersection/aabb_size_1                       [  14.5 ns ...  14.5 ns ]      -0.09%
ray_intersection/aabb_size_10                      [  14.6 ns ...  14.6 ns ]      +0.03%
ray_intersection/ray_distance_1                    [   1.1 ns ...   1.1 ns ]      -0.02%
ray_intersection/ray_distance_5                    [   1.1 ns ...   1.1 ns ]      -0.31%
ray_intersection/ray_distance_20                   [   1.1 ns ...   1.1 ns ]      +0.25%
overlap/no_overlap                                 [  12.1 ns ...  12.1 ns ]      +0.03%
overlap/partial_overlap                            [  12.1 ns ...  12.1 ns ]      -0.00%
overlap/full_containment                           [  11.5 ns ...  11.5 ns ]      -0.04%
point_containment/inside                           [   4.7 ns ...   4.7 ns ]      -0.31%
point_containment/outside                          [   4.7 ns ...   4.7 ns ]      -0.09%
point_containment/boundary                         [   4.8 ns ...   4.8 ns ]      -0.36%

Comparing to d11ca7a

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant