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( project: Project, assetId: string, type: T, ): Extract | undefined { const asset = findAsset(project, assetId); return asset?.type === type ? (asset as Extract) : 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; }