Files
feuerwerk/src/core/model/Project.ts
T
adminandClaude Opus 5.5 b2326085ed
game-maker / gate (push) Successful in 3m2s
game-maker / suite (push) Successful in 14m26s
Show each camera only its layers and lay overlay cameras on top (#302)
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>
2026-10-03 15:32:52 +02:00

144 lines
5.2 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';
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;
}