One-way platforms, raycast and collision stay with contact data: a collider can be one-way with a side it blocks from (turning with its object, drawn thick in the scene view) and a tile can be one-way from above, one-way cells never merge with solid tiles, a contact blocks only when its normal is within 45 degrees of the blocking side, the new node Ignore one-way lets an object drop through for a while, and it all holds for Move and for dynamic bodies in Rapier, where the decision is made before each step because the hook may not touch the engine (#155); the new Raycast node answers hit, object, collider, point, normal and distance in our own f64 geometry, the same in editor run and play, and the editor run draws each ray for 0.25 s (#153); On collision stay gains an interval with its own clock per pair, and stay and enter gain the contact point next to normal and impulse (#149); fixes that every Rapier collider of a tilemap had the shape of its last cell
game-maker / gate (push) Successful in 1m55s
game-maker / suite (push) Successful in 6m50s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-19 11:36:11 +02:00
1 parent 947a6b1977
commit 6a31f5e0ad
33 files changed
+2064 -125

No files matched your search

+6
View File
@@ -20,6 +20,8 @@ export const TILE_FPS_KEY = 'fps';
export const TILE_PLAYBACK_KEY = 'playback';
/** Whether a cell of this tile blocks movement in the running game (#132). */
export const TILE_SOLID_KEY = 'solid';
/** Whether a solid tile only blocks from above, as a platform one jumps through from below (#155). */
export const TILE_ONE_WAY_KEY = 'oneWay';
/** How big the blocking box of a cell is against the cell, 1 = the whole cell (#140). */
export const TILE_COLLISION_SCALE_KEY = 'collisionScale';
export const TILE_COLLISION_SCALE_MIN = 0.1;
@@ -49,6 +51,10 @@ export function isTileSolid(asset: TileAsset): boolean {
return asset.props[TILE_SOLID_KEY] === true;
}
export function isTileOneWay(asset: TileAsset): boolean {
return asset.props[TILE_ONE_WAY_KEY] === true;
}
/** The collision scale, held to the range the field offers; anything unreadable is the whole cell. */
export function tileCollisionScale(asset: TileAsset): number {
const scale = Number(asset.props[TILE_COLLISION_SCALE_KEY] ?? 1);
+34 -9
View File
@@ -17,6 +17,9 @@
* autotile's cell is pieced together from four quarters of its block, picked by the neighbours – so
* its outlines are cut along the same quarters, and each cell takes the pieces of exactly the
* quarters it is drawn from (see `tileShape`).
*
* 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.
*/
import type { TileAsset, TilemapAsset } from './Asset';
import { tileQuadrants } from './autotile';
@@ -25,7 +28,7 @@ import { clipToRect, convexPieces, signedArea } from './polygon';
import type { PropertyBag, Vec2 } from './Properties';
import { findAssetOf, type Project } from './Project';
import { findFrame } from './texture';
import { isTileSolid, readTile, tileCollisionScale, tileKind, type TileKind } from './tile';
import { isTileOneWay, isTileSolid, readTile, tileCollisionScale, tileKind, type TileKind } from './tile';
import { cellOrigin, inGrid, neighboursOf, parseCellKey, tilemapTileSize } from './tilemap';
import type { ColliderShape } from '../properties/collider';
@@ -169,13 +172,20 @@ function placed(outline: TileOutline, x: number, y: number, size: number): TileO
return { outline: outline.outline.map(place), pieces: outline.pieces.map((piece) => piece.map(place)) };
}
function blockingCells(project: Project, props: PropertyBag, asset: TilemapAsset): CellBlock[] {
function blockingCells(
project: Project,
props: PropertyBag,
asset: TilemapAsset,
oneWay: boolean,
): 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) ? { scale: tileCollisionScale(tile), shape: tileShape(project, tile) } : null;
tile && isTileSolid(tile) && isTileOneWay(tile) === oneWay
? { scale: tileCollisionScale(tile), shape: tileShape(project, tile) }
: null;
tiles.set(id, value);
return value;
};
@@ -225,8 +235,13 @@ function blockingCells(project: Project, props: PropertyBag, asset: TilemapAsset
* as any does. An autotile cell built only from quarters that block whole is a box too, at scale 1.
* A cell whose only solid tiles block with outlines is left to `solidPolygons`.
*/
export function solidCells(project: Project, props: PropertyBag, asset: TilemapAsset): Required<SolidCell>[] {
return blockingCells(project, props, asset)
export function solidCells(
project: Project,
props: PropertyBag,
asset: TilemapAsset,
oneWay = false,
): Required<SolidCell>[] {
return blockingCells(project, props, asset, oneWay)
.filter((cell) => cell.box !== null)
.map(({ x, y, box }) => ({ x, y, scale: box! }));
}
@@ -237,9 +252,14 @@ export function solidCells(project: Project, props: PropertyBag, asset: TilemapA
* blocks whole. A tile blocking as a box on another layer of the same cell covers the whole cell
* already, so there the outlines are left out.
*/
export function solidPolygons(project: Project, props: PropertyBag, asset: TilemapAsset): CellPolygon[] {
export function solidPolygons(
project: Project,
props: PropertyBag,
asset: TilemapAsset,
oneWay = false,
): CellPolygon[] {
const polygons: CellPolygon[] = [];
for (const cell of blockingCells(project, props, asset)) {
for (const cell of blockingCells(project, props, asset, oneWay)) {
if (cell.box !== null) continue;
for (const { outline, pieces } of cell.polygons) polygons.push({ x: cell.x, y: cell.y, outline, pieces });
}
@@ -346,8 +366,13 @@ export function mergeCells(cells: readonly SolidCell[]): CellRect[] {
}
/** The blocking rectangles of a map, in cells. */
export function solidRects(project: Project, props: PropertyBag, asset: TilemapAsset): CellRect[] {
return mergeCells(solidCells(project, props, asset));
export function solidRects(
project: Project,
props: PropertyBag,
asset: TilemapAsset,
oneWay = false,
): CellRect[] {
return mergeCells(solidCells(project, props, asset, oneWay));
}
/**