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
@@ -20,7 +20,8 @@
|
||||
*
|
||||
* One-way tiles (#155) block only from above. They are worked out on their own, by the same rules –
|
||||
* each function takes `oneWay` – so a platform never merges with a wall into one rectangle, nor with
|
||||
* a platform of another angle (#159).
|
||||
* a platform of another angle (#159). The same holds for a tile's surface (#193): with a `material`,
|
||||
* a function looks only at the tiles of that friction and bounciness.
|
||||
*/
|
||||
import type { TileAsset, TilemapAsset } from './Asset';
|
||||
import { tileQuadrants } from './autotile';
|
||||
@@ -40,6 +41,7 @@ import {
|
||||
} from './tile';
|
||||
import { cellOrigin, inGrid, neighboursOf, parseCellKey, tilemapSize, tilemapTileSize } from './tilemap';
|
||||
import { type ColliderShape, oneWayAngle, type ShapeBounds } from '../properties/collider';
|
||||
import { colliderMaterial } from '../properties/rigidbody';
|
||||
|
||||
/** A rectangle in cells: top-left corner and size. Whole cells at collision scale 1, parts of one otherwise. */
|
||||
export interface CellRect {
|
||||
@@ -192,11 +194,51 @@ function tileOneWayAngle(tile: TileAsset): number {
|
||||
return oneWayAngle(tile.props[TILE_ONE_WAY_ANGLE_KEY]);
|
||||
}
|
||||
|
||||
function passesFilter(tile: TileAsset, oneWay: OneWayFilter): boolean {
|
||||
/** The surface of a solid tile (#193), read like a collider's. A map never moves, so no density. */
|
||||
export interface TileMaterial {
|
||||
friction: number;
|
||||
bounciness: number;
|
||||
}
|
||||
|
||||
export function tileMaterial(tile: TileAsset): TileMaterial {
|
||||
const { friction, bounciness } = colliderMaterial(tile.props);
|
||||
return { friction, bounciness };
|
||||
}
|
||||
|
||||
function sameMaterial(a: TileMaterial, b: TileMaterial): boolean {
|
||||
return a.friction === b.friction && a.bounciness === b.bounciness;
|
||||
}
|
||||
|
||||
/**
|
||||
* Which solid tiles a function looks at: those passing `oneWay`, and with a `material` only the
|
||||
* tiles of that surface – so ice and rubber never merge into one rectangle (#193).
|
||||
*/
|
||||
function passesFilter(tile: TileAsset, oneWay: OneWayFilter, material?: TileMaterial): boolean {
|
||||
if (material && !sameMaterial(tileMaterial(tile), material)) return false;
|
||||
if (!isTileOneWay(tile)) return oneWay === false;
|
||||
return oneWay === true || tileOneWayAngle(tile) === oneWay;
|
||||
}
|
||||
|
||||
/** The surfaces of the solid tiles painted on the map that pass `oneWay`, each once. */
|
||||
export function tileMaterials(
|
||||
project: Project,
|
||||
asset: TilemapAsset,
|
||||
oneWay: OneWayFilter = false,
|
||||
): TileMaterial[] {
|
||||
const materials: TileMaterial[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const layer of asset.layers)
|
||||
for (const tileId of Object.values(layer.cells)) {
|
||||
if (!tileId || seen.has(tileId)) continue;
|
||||
seen.add(tileId);
|
||||
const tile = findAssetOf(project, tileId, 'tile');
|
||||
if (!tile || !isTileSolid(tile) || !passesFilter(tile, oneWay)) continue;
|
||||
const material = tileMaterial(tile);
|
||||
if (!materials.some((known) => sameMaterial(known, material))) materials.push(material);
|
||||
}
|
||||
return materials.sort((a, b) => a.friction - b.friction || a.bounciness - b.bounciness);
|
||||
}
|
||||
|
||||
/** The angles of the solid one-way tiles painted on the map, each once, smallest first. */
|
||||
export function oneWayAngles(project: Project, asset: TilemapAsset): number[] {
|
||||
const angles = new Set<number>();
|
||||
@@ -216,13 +258,14 @@ function blockingCells(
|
||||
props: PropertyBag,
|
||||
asset: TilemapAsset,
|
||||
oneWay: OneWayFilter,
|
||||
material?: TileMaterial,
|
||||
): CellBlock[] {
|
||||
const tiles = new Map<string, { scale: number; shape: TileShape | null } | null>();
|
||||
const tileOf = (id: string) => {
|
||||
if (tiles.has(id)) return tiles.get(id)!;
|
||||
const tile = findAssetOf(project, id, 'tile');
|
||||
const value =
|
||||
tile && isTileSolid(tile) && passesFilter(tile, oneWay)
|
||||
tile && isTileSolid(tile) && passesFilter(tile, oneWay, material)
|
||||
? { scale: tileCollisionScale(tile), shape: tileShape(project, tile) }
|
||||
: null;
|
||||
tiles.set(id, value);
|
||||
@@ -279,8 +322,9 @@ export function solidCells(
|
||||
props: PropertyBag,
|
||||
asset: TilemapAsset,
|
||||
oneWay: OneWayFilter = false,
|
||||
material?: TileMaterial,
|
||||
): Required<SolidCell>[] {
|
||||
return blockingCells(project, props, asset, oneWay)
|
||||
return blockingCells(project, props, asset, oneWay, material)
|
||||
.filter((cell) => cell.box !== null)
|
||||
.map(({ x, y, box }) => ({ x, y, scale: box! }));
|
||||
}
|
||||
@@ -296,9 +340,10 @@ export function solidPolygons(
|
||||
props: PropertyBag,
|
||||
asset: TilemapAsset,
|
||||
oneWay: OneWayFilter = false,
|
||||
material?: TileMaterial,
|
||||
): CellPolygon[] {
|
||||
const polygons: CellPolygon[] = [];
|
||||
for (const cell of blockingCells(project, props, asset, oneWay)) {
|
||||
for (const cell of blockingCells(project, props, asset, oneWay, material)) {
|
||||
if (cell.box !== null) continue;
|
||||
for (const { outline, pieces } of cell.polygons) polygons.push({ x: cell.x, y: cell.y, outline, pieces });
|
||||
}
|
||||
@@ -410,8 +455,9 @@ export function solidRects(
|
||||
props: PropertyBag,
|
||||
asset: TilemapAsset,
|
||||
oneWay: OneWayFilter = false,
|
||||
material?: TileMaterial,
|
||||
): CellRect[] {
|
||||
return mergeCells(solidCells(project, props, asset, oneWay));
|
||||
return mergeCells(solidCells(project, props, asset, oneWay, material));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user