import { type Asset, type ContainerAsset, type GraphAsset, isGraphAsset, isSceneAsset, type SceneAsset, } from './Asset'; import { DEFAULT_LAYER_NAME } from './collisionLayers'; import { DEFAULT_GAME_LINK_ZOOM, DEFAULT_GAME_SCALE, DEFAULT_PLAY_FROM, DEFAULT_WHILE_PLAYING, type GameLinkZoom, type GameScale, type PlayFrom, type WhilePlaying, } from './playSettings'; import { defaultSceneVisibility, type SceneVisibility } from './sceneVisibility'; import { DEFAULT_SYMBOL_SCALE } from './symbolScale'; /** 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[]; /** * The named render layers (#302), read the same way: which cameras see an object. A project * written before there were any has just "Default", which every camera sees. */ renderLayers: string[]; /** * Size of the scene view's symbol icons as a factor, 0.1 to 4 (see `symbolScale.ts`). A project * written before there was one reads as 1, the size the icons always had. */ symbolScale: number; /** Which scene Play starts (see `playSettings.ts`). */ playFrom: PlayFrom; /** How closely the panels follow a running game (see `playSettings.ts`). */ whilePlaying: WhilePlaying; /** The scale the game view draws the camera at. Saved, but not an undo step: it is how you look. */ gameScale: GameScale; /** How the page of the released game sizes the camera frame (see `playSettings.ts`). */ gameLinkZoom: GameLinkZoom; /** Which symbols, names and collider outlines the scene view shows, per type. Saved like `gameScale`. */ sceneVisibility: SceneVisibility; } export function createEmptyProject(id: string, name: string): Project { return { formatVersion: PROJECT_FORMAT_VERSION, id, name, assets: [], activeSceneId: '', startSceneId: '', plugins: [], collisionLayers: [DEFAULT_LAYER_NAME], renderLayers: [DEFAULT_LAYER_NAME], symbolScale: DEFAULT_SYMBOL_SCALE, playFrom: DEFAULT_PLAY_FROM, whilePlaying: DEFAULT_WHILE_PLAYING, gameScale: DEFAULT_GAME_SCALE, gameLinkZoom: DEFAULT_GAME_LINK_ZOOM, sceneVisibility: defaultSceneVisibility(), }; } /** 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 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; 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; }