Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .claude/skills/e2e-verify/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,11 @@ sibling modules checkout, every one SKIPS-never-fails when a source is missing):
`game-towers` (20), `game-stars-room` (36), `game-football` (102, two peers + late joiner;
FOOTBALL_TPSCENE / FOOTBALL_ZIP), `game-dungeon-realms` (64, two peers + late joiner, TWO zips:
DUNGEON_REALMS_TPSCENE / DUNGEON_KIT_ZIP / DUNGEON_REALMS_ZIP / MODULES_REPO), `game-untangle`
(46; UNTANGLE_TPSCENE / UNTANGLE_ZIP). Before a scenes row is released, point the TPSCENE env
at the staged file. The SESSIONS line (roadmap 22 R4-R6): `session-scenes` (36, three peers),
(46; UNTANGLE_TPSCENE / UNTANGLE_ZIP), `game-waves` (90, two peers + late joiner, TWO zips:
WAVES_TPSCENE / HEALTH_ZIP / WAVES_ZIP / MODULES_REPO — it picks the card in the REAL Games tab
through a routed feed, kills enemies with core's feedProbe, and reads core's own SCENES_BASE
fallback out of sceneTemplates.js so the #230 ref move follows by construction). Before a scenes
row is released, point the TPSCENE env at the staged file. The SESSIONS line (roadmap 22 R4-R6): `session-scenes` (36, three peers),
`scene-rename` (46), `scene-duplicate` (29). `embed-boot` (20) = the `?embed=1` flag.

## Assertion discipline (a check that cannot fail is not a check)
Expand Down
2 changes: 1 addition & 1 deletion scripts/author-templates.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -1162,7 +1162,7 @@ const BEAT_DEF = {
// `thumb.sceneGroups: ['dungeon-module']` to get it onto the card
// untangle — 21-C C7: the thin template (pose + level + room + HUD + graph); its
// board is scene-root content too (`thumb.sceneGroups: ['untangle-module']`)
const MODULE_DEFS = ['football', 'dungeon-realms', 'untangle'];
const MODULE_DEFS = ['football', 'dungeon-realms', 'untangle', 'waves'];

const DEFS = [
{
Expand Down
23 changes: 17 additions & 6 deletions src/lib/knock.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,13 +201,13 @@ function bodyVelocity(uuid) {
if (exact) {
_bodyVel.fromArray(exact.linvel);
_bodyAng.fromArray(exact.angvel);
return { linvel: _bodyVel, angvel: _bodyAng, held: exact.held };
return { linvel: _bodyVel, angvel: _bodyAng, held: exact.held, hold: exact.hold };
}
const ring = bodyTracks.get(uuid);
if (ring && ring.length >= 2) _bodyVel.copy(velocityFromSamples(ring).linvel);
else _bodyVel.set(0, 0, 0);
_bodyAng.set(0, 0, 0);
return { linvel: _bodyVel, angvel: _bodyAng, held: false };
return { linvel: _bodyVel, angvel: _bodyAng, held: false, hold: null };
}

/** @param {string} id @param {number} radius */
Expand Down Expand Up @@ -262,7 +262,9 @@ function evaluateProbe(probe, now, group, dyn) {
if (!dyn.has(uuid) || held.has(uuid)) continue;
const bounds = boundsOf(object);
const body = bodyVelocity(uuid);
if (body.held) continue; // somebody is carrying it: knocking it would fight their hold
// somebody is carrying it: knocking it would fight their hold. A body under an
// EXTERNAL hold (driven by a module or a peer's stream) is NOT skipped — see fireKnock
if (body.held) continue;
const contact = contactOf(pPos, probe.radius, pVel, bounds.centre, bounds.radius, body.linvel);
const may = cooldownStep(probe, uuid, contact.overlap, now);
if (!contact.overlap) continue;
Expand Down Expand Up @@ -291,8 +293,10 @@ function evaluateProbe(probe, now, group, dyn) {
}

/**
* The hit leaves here. Initiator: into the body first, and a refusal (held, gone)
* sends nothing and spends nothing. Otherwise: onto the wire, predicted locally when
* The hit leaves here. Initiator: into the body first, and a refusal (carried, gone)
* sends nothing and spends nothing — except a DRIVEN body (an `external` hold: a module
* walking it, a peer's move stream), which refuses the impulse and is still logged and
* sent, because the hand did hit it. Otherwise: onto the wire, predicted locally when
* the block says so. Either way it is logged HERE too — the sender never receives
* its own broadcast.
* @param {import('./knockMath').Probe} probe @param {any} object @param {number} speed
Expand Down Expand Up @@ -321,7 +325,14 @@ function fireKnock(probe, object, speed, point, response) {
probe: probe.id
};
if (isInitiator()) {
if (!applyHit(hit)) return false;
// A body under an EXTERNAL hold is DRIVEN: the impulse is refused (the next write
// would erase it) but the hit is real, so it is logged and sent like every other —
// the rule the receive side already keeps for a held crate (noteRemoteHit logs,
// applyHit refuses, independently). Only a body nobody drives that still refuses
// (gone, not dynamic) sends nothing and spends nothing. 29-F: the waves template's
// walkers were unhittable on the one peer that steps the world and hittable from
// every other, which read as "knocks land at random".
if (!applyHit(hit) && bodyVelocityOf(object.uuid)?.hold !== 'external') return false;
} else if (get(sceneKnock).predict) {
startPrediction(object, response.linvel);
}
Expand Down
10 changes: 7 additions & 3 deletions src/lib/physics.js
Original file line number Diff line number Diff line change
Expand Up @@ -1280,17 +1280,21 @@ export function applyHit(data) {
* A1: a dynamic body's EXACT velocity, for the knock's approach test on the peer that
* steps the world. Null off the initiator (there is no body) — knock.js then falls
* back to its own estimate off the poses it renders. `held` lets the probe skip a body
* somebody is carrying without a second lookup.
* somebody is CARRYING (a `user` hold) without a second lookup; `hold` is the raw kind,
* because an `external` hold is a different thing — a body some other writer drives (a
* module walking it every frame, a peer's move stream) is not carried, and a hand that
* meets it at speed has hit it. 29-F: `held: !!entry.hold` folded the two together, so
* the waves template's walkers could not be knocked on the one peer that steps the world.
* @param {string} uuid
* @returns {{linvel: number[], angvel: number[], held: boolean} | null}
* @returns {{linvel: number[], angvel: number[], held: boolean, hold: 'user'|'external'|null} | null}
*/
export function bodyVelocityOf(uuid) {
if (!world) return null;
const entry = bodies.find((e) => e.object.uuid === uuid && e.mode === 'dynamic');
if (!entry) return null;
const l = entry.body.linvel();
const a = entry.body.angvel();
return { linvel: [l.x, l.y, l.z], angvel: [a.x, a.y, a.z], held: !!entry.hold };
return { linvel: [l.x, l.y, l.z], angvel: [a.x, a.y, a.z], held: entry.hold === 'user', hold: entry.hold };
}

const FIXED_DT = 1 / 60;
Expand Down
Loading
Loading