A camera could only show the whole scene, so a HUD drawn in the world would run through a camera shader (#299) with everything else. Render layers say which cameras see an object, and overlay cameras draw their own picture over the main one. - The project has a list of up to 16 render layers, edited in Project settings next to the collision layers. It is a list of its own, read by the same rules (`layerNamesOf`). - Sprites, texts, tilemaps, particle sources and containers carry a row `Render layer` under their switches. Its default `Inherit` takes the parent's layer, at the top the first one. - A camera has `Sees` (all layers by default) and `Overlay`. The main camera is the first one switched on without Overlay, so two plain cameras still draw through the first one only. - Every view through a camera keeps only what that camera sees and lays one see-through view per overlay camera over itself. That view is a `WorldViewport` of its own, handed its view and frame from below. - An overlay is fitted into the main camera's whole picture without distortion. One of the same size and zoom covers it exactly. - `z` sorts within one camera only. The pictures lie on top of each other as a whole, which is what a shader per camera (#299) needs. - The Game tab's preview now collects the scene once per redraw. Before it collected it twice, with the cameras it would have been five times. - `SetCollisionLayersCommand` became `SetLayerNamesCommand`, which takes the list it sets. Follow-ups: #304 (the mouse through a chosen camera) and #305 (change the render layer at run time). Tests: - Gate: 226 files, 3487 tests (3480 + 7) - e2e 63 (new): 4 checks. A green overlay sprite at z -10 covers a red main sprite at z 1000, in the preview and in the running game. A `UI` sprite in front of the main camera and a `Default` one in front of the overlay stay away. Without Overlay the second camera draws nothing. The settings dialog adds a render layer at the end. - Counter-checks: inheriting from parents switched off fails 1 unit test. Overlays never drawn fails the two overlay checks in e2e 63. - e2e total 1720 (1716 + 4), computed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
144 lines
5.2 KiB
TypeScript
144 lines
5.2 KiB
TypeScript
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<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 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;
|
||
}
|