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>
47 lines
2.0 KiB
TypeScript
47 lines
2.0 KiB
TypeScript
/**
|
||
* A frame's collision outlines (#141) as a collider wears them (#193): centred on the collider's
|
||
* origin, one image pixel to one unit – the size a sprite of that frame is placed with.
|
||
*/
|
||
import type { TextureFrame } from './Asset';
|
||
import { convexPieces } from './polygon';
|
||
import type { Project } from './Project';
|
||
import type { Vec2 } from './Properties';
|
||
import { findFrame } from './texture';
|
||
|
||
export interface FrameOutline {
|
||
/** The frame's size in pixels, which is the collider's footprint. */
|
||
w: number;
|
||
h: number;
|
||
/** Every outline the frame has, in the collider's own units. */
|
||
rings: Vec2[][];
|
||
/** What collides: the convex pieces of every ring that is a polygon. */
|
||
pieces: Vec2[][];
|
||
}
|
||
|
||
/** One per drawn outline list, so a frame is cut into pieces once and not on every test. */
|
||
const cache = new WeakMap<TextureFrame, { from: TextureFrame['collision']; outline: FrameOutline }>();
|
||
|
||
function isCurrent(
|
||
known: { from: TextureFrame['collision']; outline: FrameOutline },
|
||
frame: TextureFrame,
|
||
): boolean {
|
||
return known.from === frame.collision && known.outline.w === frame.w && known.outline.h === frame.h;
|
||
}
|
||
|
||
/**
|
||
* What the frame with that id blocks with, or null when the id names no frame. A frame without a
|
||
* drawn outline has no rings: the collider then collides with nothing, like a broken polygon.
|
||
*/
|
||
export function frameOutline(project: Project | null, frameId: string): FrameOutline | null {
|
||
const found = project ? findFrame(project, frameId) : null;
|
||
if (!found) return null;
|
||
const { frame } = found;
|
||
const known = cache.get(frame);
|
||
if (known && isCurrent(known, frame)) return known.outline;
|
||
const centre = (p: Vec2): Vec2 => ({ x: p.x - frame.w / 2, y: p.y - frame.h / 2 });
|
||
const rings = (frame.collision ?? []).map((ring) => ring.map(centre));
|
||
const outline = { w: frame.w, h: frame.h, rings, pieces: rings.flatMap((ring) => convexPieces(ring)) };
|
||
cache.set(frame, { from: frame.collision, outline });
|
||
return outline;
|
||
}
|