@homie-rocks/studio 0.50.1 → 0.50.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/guides/game/RULES.md +14 -1
- package/lib/template-history.json +84 -0
- package/package.json +1 -1
- package/rules/math.ts +2 -2
- package/rules/rules.ts +2 -2
- package/rules/view.ts +27 -5
- package/worker/build-id.mjs +1 -1
- package/worker/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,35 @@ To bring a studio up to date, tell Claude: "Upgrade my studio to the newest Homi
|
|
|
17
17
|
`npx -y @homie-rocks/studio@latest upgrade`, which shows what's new since the version your studio pins (from this
|
|
18
18
|
file) and what the upgrade would change, and changes nothing until you agree.
|
|
19
19
|
|
|
20
|
+
## [0.50.3] - 2026-10-13
|
|
21
|
+
|
|
22
|
+
**Plugin 0.50.3** · A counter-dash continues from where the player sees it.
|
|
23
|
+
|
|
24
|
+
A player's own dash or jump pressed while prediction catches up after a knock or correction starts at once from where they see their character, never from a stale position that jumps them across the screen.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Fresh movement actions resume live prediction immediately and carry the visible position into the correction blend. In Copperwind's Cinder Circuit, before the fix, 161 of 168 counter-dashes jumped 51–173 px in one frame; after it, all 160 of 160 moved at most 28 px, across 0–300 ms connections with loss.
|
|
29
|
+
|
|
30
|
+
### Upgrade notes
|
|
31
|
+
|
|
32
|
+
- Follow [RULES.md](plugins/homie/skills/game/RULES.md): declare a discrete movement button as `f.press()` (or `f.pulse()` for a byte-valued action), send it through `room.input`, and implement predicted movement in `move`. Existing games with these declarations receive the fix automatically. To retain pulses through a knock or stun until control returns, opt into `player: {control: 'canMove', pulses: 'hold'}`; the default is `'drop'`. The game's action handler still decides cooldowns. This fix changes drawing only; server authority, input timestamps and replay rules are unchanged.
|
|
33
|
+
|
|
34
|
+
## [0.50.2] - 2026-10-13
|
|
35
|
+
|
|
36
|
+
**Plugin 0.50.2** · Keep your own runner out of solid ground.
|
|
37
|
+
|
|
38
|
+
Your player's own runner stays outside declared solid terrain in a 2D game on server rules, even on a lossy connection.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- Own-runner drawing now checks interpolation and correction against the solid map, stopping at contact instead of drawing the runner inside terrain. In Homie Arcade's Tiny Platformer, measurements went from 250 penetrating frames in 12 lossy runs before the fix to 0 frames penetrating more than 0.1 tile in 30 runs and 77,368 frames after it.
|
|
43
|
+
- Square 2D runners can declare a box body, and custom movement using substeps previews the remaining duration of held controls along the same movement path.
|
|
44
|
+
|
|
45
|
+
### Upgrade notes
|
|
46
|
+
|
|
47
|
+
- Follow [RULES.md](plugins/homie/skills/game/RULES.md): declare `map: './map'` in rules, put solid geometry in `map/main.json`, and keep it consistent with custom movement. Declare a square 2D runner with `body.shape: 'box'`, `radius` (its half-width) and `maxSpeed`. Use `ctx.substeps` for custom integration so the toolkit draws that path without a second view-side displacement. Blocked visual corrections do not change authoritative movement.
|
|
48
|
+
|
|
20
49
|
## [0.50.1] - 2026-10-13
|
|
21
50
|
|
|
22
51
|
**Plugin 0.50.1** · Studio events reach their functions and the owner's AI.
|
package/guides/game/RULES.md
CHANGED
|
@@ -54,7 +54,7 @@ its guarded `move`. Bots, AI and away people are always moved on the server.
|
|
|
54
54
|
`body.move: 'owner'` is an existing compatibility option that accepts bounded position
|
|
55
55
|
claims and cannot enforce walls against a modified browser; do not use it for new games.
|
|
56
56
|
A non-player entity may use `world.sweep(self, delta)` instead of a move function.
|
|
57
|
-
Bodies require radius and maxSpeed. In 2D use circle; in 3D use sphere, capsule or
|
|
57
|
+
Bodies require radius and maxSpeed. In 2D use circle or box (radius is its half-width); in 3D use sphere, capsule or
|
|
58
58
|
axis-aligned box, with full height for capsule/box. `sweep: true` adds automatic map
|
|
59
59
|
collision; explicit `ctx.map.sweep` moves and returns a contact for sliding.
|
|
60
60
|
|
|
@@ -1418,6 +1418,13 @@ Omit it to use the call time. A player can declare
|
|
|
1418
1418
|
stun and deliver them when control returns. The default `'drop'` preserves the
|
|
1419
1419
|
previous behaviour. The game's action handler still decides its cooldown.
|
|
1420
1420
|
|
|
1421
|
+
In 2D, own-body interpolation and correction are swept against the declared solid map.
|
|
1422
|
+
Keep that map consistent with custom movement and declare a square 2D runner as
|
|
1423
|
+
`body.shape: 'box'`. A blocked visual correction stops at contact; it never
|
|
1424
|
+
changes authoritative movement. Use `ctx.substeps` for custom integration so
|
|
1425
|
+
the toolkit draws the same path, without a second view-side displacement.
|
|
1426
|
+
Substep previews include the remaining duration of controls currently held.
|
|
1427
|
+
|
|
1421
1428
|
Residual drawing corrections use `room.predict.blendMs` (100 ms by default),
|
|
1422
1429
|
slowed on each moving axis to avoid reversing movement (jumping cannot spend
|
|
1423
1430
|
horizontal correction faster), with a one-second maximum
|
|
@@ -1430,3 +1437,9 @@ controls it used. Queued held state from an earlier placement cannot later
|
|
|
1430
1437
|
change the new body. Newly received discrete presses still belong to the player
|
|
1431
1438
|
when the browser has not yet learned of that placement. `idleHold` no longer keeps a stopped body displaced
|
|
1432
1439
|
indefinitely. These drawing settings never alter server truth.
|
|
1440
|
+
|
|
1441
|
+
A fresh discrete movement action (press or pulse) exits the own body's historical
|
|
1442
|
+
catch-up path. It resumes live prediction at once and carries the drawn pose into
|
|
1443
|
+
the normal correction blend, so a counter-dash or jump does not wait for the
|
|
1444
|
+
unseen impulse's history to finish. This changes drawing only; authority, input
|
|
1445
|
+
timestamps, cooldowns and replay rules are unchanged.
|
|
@@ -3237,6 +3237,90 @@
|
|
|
3237
3237
|
}
|
|
3238
3238
|
}
|
|
3239
3239
|
}
|
|
3240
|
+
},
|
|
3241
|
+
"0.50.2": {
|
|
3242
|
+
"files": {
|
|
3243
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
3244
|
+
"README.md": "684b3cca8001673c",
|
|
3245
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
3246
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
3247
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
3248
|
+
"videos/README.md": "16babc3437d5545b",
|
|
3249
|
+
"posts/README.md": "37841aae476bd540",
|
|
3250
|
+
"site/README.md": "1dbdf22c03a1e505",
|
|
3251
|
+
"site/src/worker.mjs": "6cc5c6131eaf6fd0"
|
|
3252
|
+
},
|
|
3253
|
+
"sections": {
|
|
3254
|
+
"AGENTS.md": {
|
|
3255
|
+
"(intro)": "394cbbe934e80722",
|
|
3256
|
+
"## Version-matched authoring guides": "f6261a0e5801b96b",
|
|
3257
|
+
"## Layout": "f3a280b8c96cd068",
|
|
3258
|
+
"## Apps: one screen, roles and parts": "c10aa0441ea89702",
|
|
3259
|
+
"## Commands (all through the pinned CLI in node_modules)": "5222e08ca55e326e",
|
|
3260
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
3261
|
+
"## Making games": "bb715272fd1a615b",
|
|
3262
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
3263
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
3264
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
3265
|
+
"## Servers and AI seats": "1ccfd549d2369a60",
|
|
3266
|
+
"## Selling things (the shop)": "9e020d69dd6a954e",
|
|
3267
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
3268
|
+
"## Rules": "b2234d80f6cbe71d",
|
|
3269
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
3270
|
+
"## Beta": "9d02638207a26e73"
|
|
3271
|
+
}
|
|
3272
|
+
},
|
|
3273
|
+
"named": {
|
|
3274
|
+
"Homie Arcade": {
|
|
3275
|
+
"sections": {
|
|
3276
|
+
"AGENTS.md": {
|
|
3277
|
+
"## Commands (all through the pinned CLI in node_modules)": "611b437f5b3303f0"
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
},
|
|
3283
|
+
"0.50.3": {
|
|
3284
|
+
"files": {
|
|
3285
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
3286
|
+
"README.md": "684b3cca8001673c",
|
|
3287
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
3288
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
3289
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
3290
|
+
"videos/README.md": "16babc3437d5545b",
|
|
3291
|
+
"posts/README.md": "37841aae476bd540",
|
|
3292
|
+
"site/README.md": "1dbdf22c03a1e505",
|
|
3293
|
+
"site/src/worker.mjs": "6cc5c6131eaf6fd0"
|
|
3294
|
+
},
|
|
3295
|
+
"sections": {
|
|
3296
|
+
"AGENTS.md": {
|
|
3297
|
+
"(intro)": "394cbbe934e80722",
|
|
3298
|
+
"## Version-matched authoring guides": "f6261a0e5801b96b",
|
|
3299
|
+
"## Layout": "f3a280b8c96cd068",
|
|
3300
|
+
"## Apps: one screen, roles and parts": "c10aa0441ea89702",
|
|
3301
|
+
"## Commands (all through the pinned CLI in node_modules)": "5222e08ca55e326e",
|
|
3302
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
3303
|
+
"## Making games": "bb715272fd1a615b",
|
|
3304
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
3305
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
3306
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
3307
|
+
"## Servers and AI seats": "1ccfd549d2369a60",
|
|
3308
|
+
"## Selling things (the shop)": "9e020d69dd6a954e",
|
|
3309
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
3310
|
+
"## Rules": "b2234d80f6cbe71d",
|
|
3311
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
3312
|
+
"## Beta": "9d02638207a26e73"
|
|
3313
|
+
}
|
|
3314
|
+
},
|
|
3315
|
+
"named": {
|
|
3316
|
+
"Homie Arcade": {
|
|
3317
|
+
"sections": {
|
|
3318
|
+
"AGENTS.md": {
|
|
3319
|
+
"## Commands (all through the pinned CLI in node_modules)": "611b437f5b3303f0"
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3240
3324
|
}
|
|
3241
3325
|
}
|
|
3242
3326
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homie-rocks/studio",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.3",
|
|
4
4
|
"description": "A studio for games, music and video on its own Cloudflare: scaffold, multiplayer starters on the netplay contract, a port toolkit that makes existing single-player web games multiplayer, the site Worker with public rooms (Table/Lobby Durable Objects and D1, all on the free Workers plan, no payment method), a site with the hub's sections (Home, Games, Music, Videos, Rooms, Posts with feeds) in the studio's own look, an epic landing page for every game, big songs and videos in the studio's own R2 once it has storage (uploaded, checked by SHA-256, served at the same addresses), deploy, and the homie.rocks directory.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
package/rules/math.ts
CHANGED
|
@@ -218,7 +218,7 @@ export function rayBox(px: number, py: number, dx: number, dy: number, min: Vec3
|
|
|
218
218
|
if (Math.abs(d[i]) < 1e-12) { if (p[i] <= lo[i] || p[i] >= hi[i]) return null; continue; }
|
|
219
219
|
let ta = (lo[i] - p[i]) / d[i]; let tb = (hi[i] - p[i]) / d[i];
|
|
220
220
|
if (ta > tb) { const s = ta; ta = tb; tb = s; }
|
|
221
|
-
if (ta
|
|
221
|
+
if (ta >= t0) { t0 = ta; nx = i === 0 ? (d[i] > 0 ? -1 : 1) : 0; ny = i === 1 ? (d[i] > 0 ? -1 : 1) : 0; }
|
|
222
222
|
if (tb < t1) t1 = tb;
|
|
223
223
|
if (t0 > t1) return null;
|
|
224
224
|
}
|
|
@@ -286,7 +286,7 @@ export function sweepMap(map: MapShapes, body: unknown, delta: unknown, radius:
|
|
|
286
286
|
put(body, 'pos', at); if (own(options, 'ground') !== false) put(body, 'grounded', restsOnMap(map, at, shape));
|
|
287
287
|
return h ? Object.freeze({ at, normal: v(h.nx, h.ny, h.nz), ...(h.id ? { entity: h.id } : {}) }) : undefined;
|
|
288
288
|
}
|
|
289
|
-
const { hit: h } = castMap(map, px, py, dx, dy, radius);
|
|
289
|
+
const { hit: h } = castMap(map, px, py, dx, dy, radius, undefined, false, shape.shape === 'box');
|
|
290
290
|
put(body, 'grounded', true);
|
|
291
291
|
if (!h) { put(body, 'pos', wrapPoint(v(fr(px + dx), fr(py + dy), 0), map.wrap)); return undefined; }
|
|
292
292
|
const l = Math.sqrt(dx * dx + dy * dy);
|
package/rules/rules.ts
CHANGED
|
@@ -599,7 +599,7 @@ export function compileRules(def: RulesDef, env: CompileEnv = {}): Compiled {
|
|
|
599
599
|
let body: KindTable['body'] = null;
|
|
600
600
|
if (e.body !== undefined) {
|
|
601
601
|
const b = e.body;
|
|
602
|
-
const shapes = dims === 2 ? ['circle'] : ['sphere', 'capsule', 'box'];
|
|
602
|
+
const shapes = dims === 2 ? ['circle', 'box'] : ['sphere', 'capsule', 'box'];
|
|
603
603
|
if (!b || !shapes.includes(b.shape)) throw new Error(`${at}.body.shape is ${shapes.map((s) => `'${s}'`).join(' or ')} when space.dims is ${dims}`);
|
|
604
604
|
if (typeof b.radius !== 'number' || typeof b.maxSpeed !== 'number' || !Number.isFinite(b.radius) || !Number.isFinite(b.maxSpeed) || !(b.radius > 0) || !(b.maxSpeed >= 0)) throw new Error(`${at}.body needs radius (metres) and maxSpeed (metres a second)`);
|
|
605
605
|
if (b.terrain !== undefined && b.terrain !== 'exact') throw new Error(`${at}.body.terrain is 'exact', or omitted for legacy casts`);
|
|
@@ -733,7 +733,7 @@ function publicField(field: Field, hidden=false): Field {
|
|
|
733
733
|
export function schemaOf(c: Compiled): Schema {
|
|
734
734
|
return {
|
|
735
735
|
contract: RULES_CONTRACT, dims: c.dims, seats: c.seats, ...(c.records?{records:true as const}:{}), ...(c.historySeconds ? { rewind: { historySeconds: c.historySeconds, ...(c.rewindSeconds !== undefined ? { capSeconds: c.rewindSeconds } : {}) } } : {}),
|
|
736
|
-
kinds: c.kinds.map((k) => ({ ...(k.query?{query:k.query}:{}), ...(k.collider?{collider:k.collider}:{}), ...(k.body?.terrain ? {terrain:k.body.terrain} : {}), name: k.name, player: Boolean(k.player), ...(k.player?.control ? {control:k.player.control}:{}), owner: Boolean(k.body?.owner), radius: k.body?.radius ?? 0, ...(c.dims === 3 && k.body ? { shape: k.body.shape, height: k.body.height || 2 * k.body.radius } : {}), maxSpeed: k.body?.maxSpeed ?? 0, score: k.score, fields: k.fields.map(([name,field])=>[name,publicField(field)]), motion: k.motion, input: k.input })),
|
|
736
|
+
kinds: c.kinds.map((k) => ({ ...(k.query?{query:k.query}:{}), ...(k.collider?{collider:k.collider}:{}), ...(k.body?.terrain ? {terrain:k.body.terrain} : {}), name: k.name, player: Boolean(k.player), ...(k.player?.control ? {control:k.player.control}:{}), owner: Boolean(k.body?.owner), ...(c.dims === 2 && k.body?.shape === 'box' ? {shape: 'box'} : {}), radius: k.body?.radius ?? 0, ...(c.dims === 3 && k.body ? { shape: k.body.shape, height: k.body.height || 2 * k.body.radius } : {}), maxSpeed: k.body?.maxSpeed ?? 0, score: k.score, fields: k.fields.map(([name,field])=>[name,publicField(field)]), motion: k.motion, input: k.input })),
|
|
737
737
|
effects: c.effects, effectNames: c.effectNames, commands: c.commands, shared: c.shared, rounds: c.rounds, settings: c.settings,
|
|
738
738
|
};
|
|
739
739
|
}
|
package/rules/view.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { disposeMoveContext } from './math.ts';
|
|
1
|
+
import { disposeMoveContext, sweepMap } from './math.ts';
|
|
2
2
|
import { boundPosition, periodicDelta, blendPosition, relativePoint, wrapPoint } from './topology.mjs';
|
|
3
3
|
import { rayQuery, raySnapshot, type QueryTarget } from './query.ts';
|
|
4
4
|
import { solidAt } from './collision.ts';
|
|
@@ -422,6 +422,7 @@ export function openRoom<R = unknown>(opts: OpenRoomOptions = {}): Room<R> {
|
|
|
422
422
|
mine = { id: u.id, kind: u.kind, r: u.r, pos: u.pos, vel: u.vel, heading: u.heading, grounded: u.grounded, motion: motionOf(u.kind, u.motion), fields: predictors[u.kind] ? thawFields(kindOf.get(u.kind)?.fields ?? [], u.fields) : u.fields };
|
|
423
423
|
}
|
|
424
424
|
/** The game's guarded `move` for one tick, counted as the server counts it, with the result rounded as the server rounds it. */
|
|
425
|
+
const substepKinds = new Set<string>();
|
|
425
426
|
function runMove(kindName: string, body: { pos: Vec3; vel: Vec3; heading: Vec3; grounded: boolean; motion: Record<string, unknown> }, input: Readonly<Record<string, unknown>>, t: number): typeof body & {steps?: readonly import('./substeps.ts').StepPose[]} {
|
|
426
427
|
const fn = moves[kindName];
|
|
427
428
|
if (!fn || (kindOf.get(kindName)?.control && !body.motion[kindOf.get(kindName)!.control!])) return body;
|
|
@@ -430,6 +431,7 @@ export function openRoom<R = unknown>(opts: OpenRoomOptions = {}): Room<R> {
|
|
|
430
431
|
// quota. Authority debits projection once per tick, not once per mover.
|
|
431
432
|
geometryAt();
|
|
432
433
|
const out = stepMove(fn, body, input, moveCtx, Math.max(1, Math.floor(schema.settings.budget.tick / 4)), kindOf.get(kindName)?.motion ?? [], dims, (err) => { if (!(err instanceof BudgetError)) console.warn('[room] move', err); });
|
|
434
|
+
if (out.steps?.length) substepKinds.add(kindName);
|
|
433
435
|
const kind = kindOf.get(kindName), r = kind?.radius ?? 0;
|
|
434
436
|
const height = kind?.height || 2 * r;
|
|
435
437
|
out.pos = vec3(boundPosition(out.pos,map,r,height,dims), dims);
|
|
@@ -930,6 +932,7 @@ export function openRoom<R = unknown>(opts: OpenRoomOptions = {}): Room<R> {
|
|
|
930
932
|
const kind = kindOf.get(mine.kind);
|
|
931
933
|
if (kind?.control && !server.motion[kind.control]) return entityOf(server, true);
|
|
932
934
|
let pos = mine.pos;
|
|
935
|
+
let safeOrigin = mine.pos;
|
|
933
936
|
let heading = mine.heading;
|
|
934
937
|
let velocity = mine.vel, grounded = mine.grounded, motion = mine.motion;
|
|
935
938
|
let settled = true;
|
|
@@ -940,7 +943,9 @@ export function openRoom<R = unknown>(opts: OpenRoomOptions = {}): Room<R> {
|
|
|
940
943
|
// A held field previews as its mean over the part of the tick that has passed. The body is drawn that fraction of
|
|
941
944
|
// the way to the preview, so it stands exactly where the control has carried it: it starts from rest at a press
|
|
942
945
|
// and stops dead at a release, with nothing to blend away afterwards.
|
|
943
|
-
|
|
946
|
+
// A recorded substep path needs the whole tick's forecast instead: include
|
|
947
|
+
// the remaining duration of controls held now, then sample its actual steps.
|
|
948
|
+
for (const [name, fd] of kind.input) now[name] = (fd.t === 'press' || fd.t === 'pulse') ? (pressed.get(name) ?? (fd.t === 'pulse' && pulseQueue[0]?.name===name ? pulseQueue[0].value : fd.t === 'press' ? false : 0)) : fd.t === 'held' ? coerce(fd, heldMean(name, fd.init, Math.min(clock(), edgeOf(stepped + 1)), false, substepKinds.has(kind.name) ? edgeOf(stepped + 1) : Math.min(clock(), edgeOf(stepped + 1))), dims) : coerce(fd, sample[name] ?? fd.init, dims);
|
|
944
949
|
const next = runMove(kind.name, { pos: mine.pos, vel: mine.vel, heading: mine.heading, grounded: mine.grounded, motion: motionOf(mine.kind, mine.motion) }, Object.freeze(now), stepped + 1);
|
|
945
950
|
// A landing does not take the whole tick (0.46.5): the body met the ground part way through it. How far through is
|
|
946
951
|
// read from the fall it would have made at the speed it entered the tick with; it is drawn down to the ground by
|
|
@@ -958,6 +963,7 @@ export function openRoom<R = unknown>(opts: OpenRoomOptions = {}): Room<R> {
|
|
|
958
963
|
if (next.steps?.length) {
|
|
959
964
|
const n = next.steps.length - 1, at = a * n, index = Math.min(n - 1, Math.floor(at)), f = at - index;
|
|
960
965
|
const before = next.steps[index], after = next.steps[index + 1];
|
|
966
|
+
safeOrigin = before.pos;
|
|
961
967
|
pos = vec3(blendPosition(before.pos, after.pos, f, map.wrap), dims);
|
|
962
968
|
heading = blendHeading(before.heading, after.heading, f, dims);
|
|
963
969
|
velocity = vec3({x:lerp(before.vel.x,after.vel.x,f),y:lerp(before.vel.y,after.vel.y,f),z:lerp(before.vel.z,after.vel.z,f)},dims);
|
|
@@ -979,14 +985,25 @@ export function openRoom<R = unknown>(opts: OpenRoomOptions = {}): Room<R> {
|
|
|
979
985
|
// Catch-up can reach the fractional current tick before its next whole step exists.
|
|
980
986
|
// Use the normal drawing preview, including its 3D pose, instead of holding then jumping.
|
|
981
987
|
const t = Math.floor(catchTick), a = history.get(t), b = history.get(t + 1) ?? (t === stepped ? preview : null);
|
|
982
|
-
if (a && b) { const f = catchTick - t; pos = blendPosition(a.pos, b.pos, f, map.wrap); heading = blendHeading(a.heading, b.heading, f, dims);
|
|
988
|
+
if (a && b) { safeOrigin = a.pos; const f = catchTick - t; pos = blendPosition(a.pos, b.pos, f, map.wrap); heading = blendHeading(a.heading, b.heading, f, dims);
|
|
983
989
|
velocity = vec3({ x: lerp(a.vel.x, b.vel.x, f), y: lerp(a.vel.y, b.vel.y, f), z: lerp(a.vel.z, b.vel.z, f) }, dims);
|
|
984
990
|
grounded = a.grounded && (f === 0 || b.grounded); motion = a.motion;
|
|
985
|
-
} else if (a) { pos = a.pos; heading = a.heading; velocity = a.vel; grounded = a.grounded; motion = a.motion; }
|
|
991
|
+
} else if (a) { safeOrigin = a.pos; pos = a.pos; heading = a.heading; velocity = a.vel; grounded = a.grounded; motion = a.motion; }
|
|
986
992
|
}
|
|
987
993
|
}
|
|
994
|
+
// Two-dimensional presentation follows collision too: interpolation and reconciliation are not
|
|
995
|
+
// permission to cross a floor. Sweep from a valid movement pose, never feed
|
|
996
|
+
// the clipped drawing back into prediction or authority.
|
|
997
|
+
const constrain = (from: Vec3, to: Vec3): Vec3 => {
|
|
998
|
+
if (dims !== 2 || !kind || kind.owner) return to;
|
|
999
|
+
const body = {pos: from};
|
|
1000
|
+
sweepMap(geometryAt(), body, {x:periodicDelta(from.x,to.x,map.wrap?.x),y:periodicDelta(from.y,to.y,map.wrap?.y),z:periodicDelta(from.z,to.z,map.wrap?.z)}, kind.radius, dims,
|
|
1001
|
+
{shape:kind.shape ?? 'circle',radius:kind.radius,height:kind.height || 2*kind.radius,terrain:kind.terrain});
|
|
1002
|
+
return body.pos;
|
|
1003
|
+
};
|
|
1004
|
+
pos = constrain(safeOrigin, pos);
|
|
988
1005
|
const offset = visualOffset(clock(), pos, settled);
|
|
989
|
-
pos = wrapPoint({ x: pos.x + offset.x, y: pos.y + offset.y, z: pos.z + offset.z },map.wrap);
|
|
1006
|
+
pos = constrain(pos, wrapPoint({ x: pos.x + offset.x, y: pos.y + offset.y, z: pos.z + offset.z },map.wrap));
|
|
990
1007
|
if (headingBlend) {
|
|
991
1008
|
const t = predict.blendMs > 0 ? Math.min(1, (clock() - headingBlend.at) / predict.blendMs) : 1;
|
|
992
1009
|
heading = blendHeading(headingBlend.from, heading, t, dims);
|
|
@@ -1164,6 +1181,11 @@ export function openRoom<R = unknown>(opts: OpenRoomOptions = {}): Room<R> {
|
|
|
1164
1181
|
const shown = changed ? meNow()?.pos : null;
|
|
1165
1182
|
for (const [name, fd] of kind?.input ?? []) if ((fd.t === 'press' || fd.t === 'pulse') && coerce(fd,s[name],dims)) { const value=coerce(fd,s[name],dims) as number | boolean; if(fd.t==='pulse'){pulseCount+=1;const target=Math.max(stepped+1,(pulseQueue.at(-1)?.t??stepped)+1);pulseQueue.push({name,value:Number(value),n:pulseCount,t:target});net.send('pulse',[epoch,name,value,target,pulseCount]);} else pressed.set(name,value); }
|
|
1166
1183
|
sample = next;
|
|
1184
|
+
// A discrete action belongs to the present, even while an unseen impulse is
|
|
1185
|
+
// being drawn from history. Resume the live prediction and let the existing
|
|
1186
|
+
// input correction below carry the drawn origin across that change of path.
|
|
1187
|
+
if (shown && catchTick !== null && kind?.input.some(([name, fd]) =>
|
|
1188
|
+
(fd.t === 'press' || fd.t === 'pulse') && Boolean(coerce(fd, s[name], dims)))) catchTick = null;
|
|
1167
1189
|
if (shown) {
|
|
1168
1190
|
const now = clock(), nextShown = meNow()?.pos;
|
|
1169
1191
|
if (nextShown && distance(shown, nextShown) > 0.00001) {
|
package/worker/build-id.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by lib/build-identity.mjs; identifies the bundled runtime sources.
|
|
2
|
-
export const STUDIO_BUILD = Object.freeze({"version":"0.50.
|
|
2
|
+
export const STUDIO_BUILD = Object.freeze({"version":"0.50.3","sha256":"2099723ed724a66028f1cddaea2a5f9dc0c8616282096b6bc98fb7857a84a800"});
|
package/worker/version.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The @homie-rocks/studio version this Worker code is (package.json's; a test keeps them equal). Cache-busts /_homie/site.js. */
|
|
2
|
-
export const STUDIO_VERSION_TAG = '0.50.
|
|
2
|
+
export const STUDIO_VERSION_TAG = '0.50.3';
|