Colliders take a frame's outline and tiles have their own surface (#193)
Issue #193 asked where friction, bounciness and density of a tile live. Tiles had a fixed surface (friction 0.5, bounciness 0) and a collider could not use the outline drawn on a texture frame (#141). Both gaps are closed here. - A collider has a fifth shape "Frame collision" with a Frame row. The frame is picked like a sprite's texture and stored as the value 'frame'. - The shape is linked, not copied. Redrawing the outline in the texture tab changes every collider using that frame, in the editor and on the next play. - The frame centre sits on the collider origin, one image pixel is one unit. The shape turns, scales and mirrors like any other. - Every outline ring blocks. Each ring is split into convex parts the same way a polygon is. - Without a frame, with a whole texture or with a frame that has no outline, the collider is drawn red as a box and blocks nothing. - Move, Rapier, raycast, detection, hit test, drawing and footprint all resolve the frame. At runtime an entity looks the frame up live in the current project. - A tile has Friction and Bounciness next to Solid and One-way. The rows only show when Solid is on. Defaults and mixing rules match the collider. Density is left out because a tilemap never moves. - Tilemap cells are grouped by surface as well as by one-way angle. Ice and stone never merge into one rectangle, and every cell brings its surface to Move and Rapier. Findings: - A tile with a collision scale below 1 is no longer flush with a neighbour of another surface. It is inset there like at an open side, the same as next to one-way tiles today. - A bounce test first measured 0.84. The ball had landed on the hero at the origin, not on the tile. On the tile it measures 0.993. Tests: - ./check.sh: 130 files, 1931 tests, exit 0 (was 129 / 1917). - 14 new unit tests in tests/core/surface.test.ts. The Move tests run against the editor run and the physics system. - 3 new e2e checks: 40l (surface rows follow Solid), 42e (collider has frame size, a walker climbs the slope in the frame). - GM_STEPS=1-8,39-43 freshly seeded: ALL PASSED, 376 checks. - Three counter-probes bite: Move ignores the tile surface (2 fail), Rapier ignores it (1 fails), frame shape falls back to a box (5 fail). - Check sum 1198 is computed (1195 + 3), the full run belongs to CI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3ff7e91dbf
commit
f4ec23d3d3
20 files changed
+729
-83
No files matched your search
@@ -715,6 +715,9 @@ export async function tilemap(t) {
|
||||
await openTab(t, 'scene');
|
||||
await select({ kind: 'asset', assetId: 'at-grass' });
|
||||
const solidRow = await json("__gm.inspectorField('Solid')");
|
||||
const surfaceRows = () =>
|
||||
json("[__gm.inspectorField('Friction'), __gm.inspectorField('Bounciness')].map(Boolean)");
|
||||
const surfaceBefore = await surfaceRows();
|
||||
if (solidRow) await click(solidRow.x, solidRow.y);
|
||||
await settle();
|
||||
check(
|
||||
@@ -722,6 +725,13 @@ export async function tilemap(t) {
|
||||
Boolean(solidRow) && (await t.asset('at-grass'))?.props.solid === true,
|
||||
JSON.stringify({ solidRow, props: (await t.asset('at-grass'))?.props }),
|
||||
);
|
||||
// Its surface (#193) is a solid tile's business: the rows come with the switch.
|
||||
const surfaceAfter = await surfaceRows();
|
||||
check(
|
||||
'Friction and Bounciness show on a tile only once it is solid',
|
||||
JSON.stringify(surfaceBefore) === '[false,false]' && JSON.stringify(surfaceAfter) === '[true,true]',
|
||||
JSON.stringify({ surfaceBefore, surfaceAfter }),
|
||||
);
|
||||
await mutate(`__gm.lookAt(0, ${LANE})`);
|
||||
await settle();
|
||||
const outlined = await json(`__gm.tilemap.solids(${JSON.stringify(MAP_OBJECT)})`);
|
||||
|
||||
+68
-1
@@ -5,7 +5,8 @@ import { LANES, MAKE_OBJECT } from './lib/scaffold.mjs';
|
||||
* Capsules and polygons (#151), and the collision outline of a frame that a tile blocks with
|
||||
* (#141). What only a running editor can show: the corners of a polygon collider are edited on
|
||||
* its outline in the scene, the engine lets a capsule slide down a polygon ramp, the Texture tab
|
||||
* draws a frame's outline with clicks, and a painted map blocks with that outline in the game.
|
||||
* draws a frame's outline with clicks, and a painted map blocks with that outline in the game – as
|
||||
* does a collider that wears the frame's outline (#193).
|
||||
*/
|
||||
|
||||
/** A 32 × 32 picture, pasted like a file: the texture the outline is drawn on. */
|
||||
@@ -354,4 +355,70 @@ export async function shapes(t) {
|
||||
JSON.stringify({ frames: climb.length, highest, top: MAP_Y - 10, last: climb.at(-1) }),
|
||||
);
|
||||
await clearShapes();
|
||||
|
||||
// 42e. A collider wears the same frame's outline (#193), linked: shape "Frame collision", the frame
|
||||
// picked like a texture. It is the frame's size in the scene, and a walker climbs its slope –
|
||||
// a box of that size would have stopped it at its side.
|
||||
const F = Y; // the floor's top face; the wedge stands on it, its slope rising to x = 16
|
||||
await mutate(`
|
||||
const scene = __gm.state.activeScene;
|
||||
const assets = __gm.state.project.assets;
|
||||
const texture = assets.find((a) => a.id === ${JSON.stringify(texture?.id)});
|
||||
texture.frames.find((f) => f.id === 'sh-frame').collision = [[{ x: 0, y: 32 }, { x: 32, y: 0 }, { x: 32, y: 32 }]];
|
||||
${MAKE_OBJECT}
|
||||
${put}
|
||||
const behavior = structuredClone(assets.find((a) => a.id === 'mover'));
|
||||
Object.assign(behavior, { id: 'sh-climb', name: 'Climb', props: { enabled: true, runInEditor: false }, variables: [], graph: ${JSON.stringify(climbGraph)} });
|
||||
assets.push(behavior);
|
||||
assets.push({ id: 'sh-col-wedge', type: 'collider', name: 'sh-col-wedge',
|
||||
props: { shape: 'frame', frame: 'sh-frame', isTrigger: false } });
|
||||
assets.push({ id: 'sh-col-floor', type: 'collider', name: 'sh-col-floor',
|
||||
props: { shape: 'box', width: 400, height: 32, isTrigger: false } });
|
||||
assets.push({ id: 'sh-col-walker', type: 'collider', name: 'sh-col-walker',
|
||||
props: { shape: 'box', width: 20, height: 20, friction: 0, isTrigger: false } });
|
||||
put('sh-floor', 'crate', '', 0, ${F + 16}, 'Floor', { width: 20, height: 20 });
|
||||
put('sh-floor-col', 'sh-col-floor', 'sh-floor', 0, 0);
|
||||
put('sh-wedge', 'crate', '', 0, ${F - 16}, 'Wedge', { width: 20, height: 20 });
|
||||
put('sh-wedge-col', 'sh-col-wedge', 'sh-wedge', 0, 0);
|
||||
put('sh-walker', 'crate', '', -100, ${F - 10}, 'Walker', { width: 20, height: 20 });
|
||||
put('sh-walker-col', 'sh-col-walker', 'sh-walker', 0, 0);
|
||||
put('sh-walker-climb', 'sh-climb', 'sh-walker', 0, 0, 'Climb');
|
||||
__gm.state.select(null)`);
|
||||
await evaluate(`__gm.lookAt(0, ${F})`);
|
||||
await settle();
|
||||
const wedgeRect = await json(`__gm.objectRect('sh-wedge-col')`);
|
||||
const floorRect = await json(`__gm.objectRect('sh-floor-col')`);
|
||||
check(
|
||||
'a collider wearing a frame outline is the size of the frame in the scene, not its own width',
|
||||
wedgeRect &&
|
||||
floorRect &&
|
||||
Math.abs(wedgeRect.w - wedgeRect.h) < 1 &&
|
||||
Math.abs(floorRect.w / wedgeRect.w - 400 / 32) < 0.2,
|
||||
JSON.stringify({ wedgeRect, floorRect }),
|
||||
);
|
||||
await shot('42-frame-collider');
|
||||
await play();
|
||||
await evaluate(`(() => {
|
||||
const out = (window.__frameClimb = []);
|
||||
const begin = performance.now();
|
||||
const sample = () => {
|
||||
const w = __gm.world();
|
||||
if (!w) return;
|
||||
const e = w.entities.find((x) => x.objectId === 'sh-walker');
|
||||
out.push({ x: e?.worldX, y: e?.worldY });
|
||||
if (performance.now() - begin < 3000) requestAnimationFrame(sample);
|
||||
};
|
||||
requestAnimationFrame(sample);
|
||||
})()`);
|
||||
await sleep(3300);
|
||||
const frameClimb = await json('window.__frameClimb');
|
||||
await stop();
|
||||
// Over the wedge's top corner (16, F − 32) the walker's centre stands 10 higher.
|
||||
const peak = Math.min(...frameClimb.map((s) => s.y));
|
||||
check(
|
||||
"a walker whose Move collides climbs the slope drawn in the collider's frame and goes on past it",
|
||||
frameClimb.length > 100 && peak < F - 42 + 1 && peak > F - 42 - 3 && frameClimb.at(-1).x > 45,
|
||||
JSON.stringify({ frames: frameClimb.length, peak, top: F - 42, last: frameClimb.at(-1) }),
|
||||
);
|
||||
await clearShapes();
|
||||
}
|
||||
Reference in new issue
Block a user