Files
feuerwerk/src/core/model/Project.ts
T
adminandClaude Opus 5 4f55f85ad4
game-maker / gate (push) Successful in 2m41s
game-maker / suite (push) Failing after 8m21s
Collision layers and masks: a collider carries a layer and a mask of the layers it meets, and the pair only meets when both masks name the other's layer - the rule is one pure module (src/core/model/collisionLayers.ts) with layerBit, layersMeet and the names a project starts with, so the three places that have to agree read the same function: the editor run through CollisionSystem, the Solids sweep per pair and Rapier, where the mask becomes the collider's interaction groups; the project file gained collisionLayers: ["Default"] (default one layer, no formatVersion bump), the collider schema two new kinds - layer as a dropdown of the project's layers and layerMask as a menu of tickboxes that stays open while it is ticked (Menu.ts learned rows that do not close on a pick) - and the names are edited through "Collision layers..." in the toolbar's Project menu, not a Project tab, which is the decision I made myself because the question ran into the MCP timeout: the dialog (src/editor/CollisionLayersDialog.ts) holds up to 16 names, Add layer appends "Layer <n>" and Apply writes all of them as one undo step (SetCollisionLayersCommand); a mask read from a file goes through a check that refuses anything that is not a finite number, because Number(null) is 0 and a collidesWith: null would silently have meant "meets nothing"; 9 new e2e checks in module 8 cover the menu entry, the dialog, Add layer, Apply and its undo, both inspector rows, the tickbox menu, and the mover walking straight through the crate that 8c blocked on once their layers no longer meet, and 21 new unit tests cover the rule, the schema and the command (#152)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 15:31:22 +02:00

109 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
type Asset,
type ContainerAsset,
type GraphAsset,
isGraphAsset,
isSceneAsset,
type SceneAsset,
} from './Asset';
import { DEFAULT_LAYER_NAME } from './collisionLayers';
/** 2: a placed container carries no copy of its template. */
export const PROJECT_FORMAT_VERSION = 2;
export interface Project {
formatVersion: number;
id: string;
name: string;
assets: Asset[];
/** Empty string = none. */
activeSceneId: string;
/**
* Where the game begins – what a player would see first, as opposed to whatever scene happens to
* be open in the editor. Empty (or dangling) means none has been named, and a run falls back to
* the active scene; no format bump needed for that, since an older project reads as exactly that.
*/
startSceneId: string;
/**
* The plugin files (`data/plugins/*.js`) this project switches on (#119). The editor loads them
* before the project itself, so what they register is there before anything is read. A project
* written before there was such a field names none, which is also what a new one does.
*/
plugins: string[];
/**
* The named collision layers (#152), the first one being layer 0. A collider names one of them
* and the ones it collides with; a project written before there were any has just "Default",
* which is also what every collider starts on, so nothing collides differently than it did.
*/
collisionLayers: string[];
}
export function createEmptyProject(id: string, name: string): Project {
return {
formatVersion: PROJECT_FORMAT_VERSION,
id,
name,
assets: [],
activeSceneId: '',
startSceneId: '',
plugins: [],
collisionLayers: [DEFAULT_LAYER_NAME],
};
}
/** The scene a real run starts from: the named one, or the active one while none is named. */
export function startScene(project: Project): SceneAsset | undefined {
return findScene(project, project.startSceneId) ?? findScene(project, project.activeSceneId);
}
export function findAsset(project: Project, assetId: string): Asset | undefined {
return project.assets.find((a) => a.id === assetId);
}
export function findAssetOf<T extends Asset['type']>(
project: Project,
assetId: string,
type: T,
): Extract<Asset, { type: T }> | undefined {
const asset = findAsset(project, assetId);
return asset?.type === type ? (asset as Extract<Asset, { type: T }>) : undefined;
}
export function findScene(project: Project, sceneId: string): SceneAsset | undefined {
const asset = findAsset(project, sceneId);
return asset && isSceneAsset(asset) ? asset : undefined;
}
export function listScenes(project: Project): SceneAsset[] {
return project.assets.filter(isSceneAsset);
}
export function findGraphAsset(project: Project, assetId: string): GraphAsset | undefined {
const asset = findAsset(project, assetId);
return asset && isGraphAsset(asset) ? asset : undefined;
}
/**
* Everything that holds placed objects. A container's `objects` are a template, so anything
* walking "all placed objects" must cover both or it silently skips the templates.
*/
export type ObjectHolder = SceneAsset | ContainerAsset;
export function isObjectHolder(asset: Asset): asset is ObjectHolder {
return asset.type === 'scene' || asset.type === 'container';
}
export function objectHolders(project: Project): ObjectHolder[] {
return project.assets.filter(isObjectHolder);
}
/**
* The scene or container asset an object belongs to. Editing a container's template is the same
* work as editing a scene, so the object commands resolve their target through this rather than
* through `findScene`.
*/
export function findObjectHolder(project: Project, holderId: string): ObjectHolder | undefined {
const asset = findAsset(project, holderId);
return asset && isObjectHolder(asset) ? asset : undefined;
}