109 lines
3.7 KiB
TypeScript
109 lines
3.7 KiB
TypeScript
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;
|
||
}
|