The one-way tolerance angle is a field: a one-way collider and a one-way tile gain One-way angle (0 to 90 degrees, default 45), a contact blocks when its normal is within that angle of the blocking side, without a value the limit is bit-identical to before, and the rule holds for Move, Raycast and dynamic bodies in Rapier; tiles with different angles never merge into one rectangle, the scene view draws the angle as a cone in the middle of the thick side, and the inspector shows Blocks from and One-way angle only while One-way is on, through the new schema option shownWith (#159)
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 13:13:29 +02:00
1 parent d4ea04d4d6
commit 5385d9e152
14 files changed
+454 -62

No files matched your search

+12 -5
View File
@@ -19,7 +19,12 @@ import { findAsset, type Project } from '../../core/model/Project';
import type { PropertyBag, PropertyValue } from '../../core/model/Properties';
import { isGlobal, readScope, scopeOf } from '../../core/model/Scope';
import { type StructFieldKind, VARIABLE_KINDS } from '../../core/properties/kinds';
import { type PropertyDef, schemaForAsset, type StructFieldDef } from '../../core/properties/PropertySchema';
import {
isRowShown,
type PropertyDef,
schemaForAsset,
type StructFieldDef,
} from '../../core/properties/PropertySchema';
import { markedAssetSchema } from '../../core/properties/deviation';
import { isModified, resolveAssetProps } from '../../core/properties/resolve';
import { Dropdown, HStack, Label, Menu, Theme } from '../../ui';
@@ -35,15 +40,17 @@ import { assetValue } from './valueSpecs';
const newField = (key: string, kind: StructFieldKind): StructFieldDef => ({ key, kind });
/**
* Structure-defining facts of an asset selection: the schema keys and kinds, plus the fields of a
* struct – those are rows of their own, so adding one has to rebuild the section.
* Structure-defining facts of an asset selection: the schema keys and kinds of the rows shown, plus
* the fields of a struct – those are rows of their own, so adding one has to rebuild the section.
*/
export function assetSignature(project: Project, assetId: string): string {
const asset = findAsset(project, assetId);
if (!asset) return 'missing';
// A machine's own rows are none, so its scope has to be named here or switching it rebuilds nothing.
const machine = asset.type === 'statemachine' ? `|${scopeOf(asset)}` : '';
return schemaForAsset(project, asset).map(defSignature).join(',') + machine;
const values = resolveAssetProps(project, asset);
const rows = schemaForAsset(project, asset).filter((def) => isRowShown(def, values));
return rows.map(defSignature).join(',') + machine;
}
export function defSignature(def: PropertyDef): string {
@@ -101,7 +108,7 @@ export function refreshAssetSection(host: InspectorHost, assetId: string): boole
function buildProps(host: InspectorHost, asset: Asset, schema: readonly PropertyDef[]): void {
const project = () => host.state.project;
const values = resolveAssetProps(project(), asset);
for (const def of schema) {
for (const def of schema.filter((d) => isRowShown(d, values))) {
const row = createFieldRow(
host,
def,
+5 -3
View File
@@ -31,6 +31,7 @@ import type { SceneObject } from '../../core/model/SceneObject';
import {
COMMON_PROPS,
isCommonProp,
isRowShown,
isTransformProp,
type PropertyDef,
schemaFor,
@@ -65,7 +66,7 @@ export function objectRows(project: Project, asset: Asset, object: SceneObject):
: schemaForObject(project, asset, object).filter((def) => !isCommonProp(def.key));
}
/** Structure-defining facts of an object selection: parent, template link, schema and painting. */
/** Structure-defining facts of an object selection: parent, template link, the rows shown and painting. */
export function objectSignature(
state: EditorState,
holderId: string,
@@ -84,7 +85,8 @@ export function objectSignature(
: isLiveInstance(project, holder, object)
? 'instance'
: '';
const rows = schemaForObject(project, asset, object);
const values = resolveSceneObjectProps(project, asset, object);
const rows = schemaForObject(project, asset, object).filter((def) => isRowShown(def, values));
// A tilemap warns when it is placed more than once, so the number of placements shapes it too.
const placed = asset.type === 'tilemap' ? `|placed:${findUsages(project, asset.id).length}` : '';
return `${object.parentId}|${kind}${placed}|` + rows.map(defSignature).join(',');
@@ -202,7 +204,7 @@ export function buildObjectSection(host: InspectorHost, holderId: string, object
// Asset properties (or behavior variables) with overrides; the switches are shown above.
const values = resolveSceneObjectProps(host.state.project, asset, object);
const own = objectRows(host.state.project, asset, object);
const own = objectRows(host.state.project, asset, object).filter((def) => isRowShown(def, values));
if (own.length > 0 || asset.type === 'behavior')
addTitle(host, asset.type === 'behavior' ? 'VARIABLES' : 'PROPERTIES');
if (own.length === 0 && asset.type === 'behavior') addNote(host, 'The behavior declares no variables.');
+4 -2
View File
@@ -61,7 +61,7 @@ import {
import { gripAt, withCornerAfter, withCornerMoved, withoutCorner } from '../polygonEdit';
import { collectHolderItems, type DrawItem } from '../../viewport/items';
import type { LayerFocus } from '../../viewport/tilemap';
import { dashedLine, drawSourceIcon, markBlockingSide, placeNode } from '../../viewport/symbols';
import { CONE_LENGTH, dashedLine, drawSourceIcon, markBlockingSide, placeNode } from '../../viewport/symbols';
import { screenToWorld, type View, worldToScreen } from '../../viewport/view';
import { type CastRay, RAY_SHOWN, type World } from '../../runtime/World';
import { FreeViewport } from './FreeViewport';
@@ -879,7 +879,8 @@ export class SceneViewport extends FreeViewport {
stroke(boundary.solid, width);
fill(oneWay.solids, oneWay.outlines, 0.06);
stroke(boundary.oneWay, width / 2);
// A platform's top is what blocks, so that is what stands out.
// A platform's top is what blocks, so that is what stands out, with the cone it lands within.
const length = CONE_LENGTH / (scale * scaleOf(item.m));
for (const rect of oneWay.solids) {
const a = cellOrigin(item.props, rect.x, rect.y);
const b = cellOrigin(item.props, rect.x + rect.w, rect.y + rect.h);
@@ -889,6 +890,7 @@ export class SceneViewport extends FreeViewport {
{ x: 0, y: -1 },
width * 2,
color,
{ angle: rect.angle, length },
);
}
}