Files
feuerwerk/tests/core/texture-commands.test.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

98 lines
4.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 { describe, expect, it } from 'vitest';
import { CreateAssetCommand, CreateTextureCommand } from '../../src/core/commands/assetCommands';
import { CommandError } from '../../src/core/commands/Command';
import { findAsset } from '../../src/core/model/Project';
import { spriteFromTexture } from '../../src/core/properties/aspect';
import { makeProject } from '../helpers';
const FILE = `${'b'.repeat(64)}.webp`;
describe('CreateTextureCommand', () => {
it('adds a texture asset carrying the uploaded image, and removes it again', () => {
const { project, stack } = makeProject();
const command = new CreateTextureCommand('Hero', { file: FILE, width: 64, height: 32 });
stack.execute(command);
const asset = findAsset(project, command.asset.id);
expect(asset).toMatchObject({ type: 'texture', name: 'Hero', file: FILE, width: 64, height: 32 });
// The schema default comes along, so the new texture is a complete asset – nothing to fill in later.
expect(asset?.props).toEqual({ filter: 'linear', sliceScale: 1 });
stack.undo();
expect(project.assets).toHaveLength(0);
stack.redo();
expect(findAsset(project, command.asset.id)?.name).toBe('Hero');
});
it('keeps the caller’s name even though the upload result carries one of its own', () => {
const { project, stack } = makeProject();
// What `TextureStore.upload` actually answers: the file's original name and its byte count
// ride along. Copied wholesale they would land in the asset and rename it behind the caller.
const upload = { file: FILE, name: 'hero-idle.png', width: 64, height: 32, bytes: 900 };
const command = new CreateTextureCommand('Hero', upload);
stack.execute(command);
const asset = findAsset(project, command.asset.id);
expect(asset?.name).toBe('Hero');
expect(asset).not.toHaveProperty('bytes');
});
it('rejects a file name the server cannot have produced – in execute, not in the constructor', () => {
const { project, stack } = makeProject();
const command = new CreateTextureCommand('Hero', { file: '../secrets.png', width: 1, height: 1 });
expect(() => stack.execute(command)).toThrow(CommandError);
expect(project.assets).toHaveLength(0);
});
it('keeps two assets for the same file apart', () => {
const { project, stack } = makeProject();
stack.execute(new CreateTextureCommand('Hero', { file: FILE, width: 8, height: 8 }));
stack.execute(new CreateTextureCommand('Hero copy', { file: FILE, width: 8, height: 8 }));
expect(project.assets.map((a) => a.id)).toHaveLength(2);
expect(new Set(project.assets.map((a) => a.id)).size).toBe(2);
});
});
describe('CreateAssetCommand with starting props', () => {
it('creates a sprite wearing the texture at its pixel size, and removes it again', () => {
const { project, stack } = makeProject();
const texture = new CreateTextureCommand('Hero', { file: FILE, width: 64, height: 32 });
stack.execute(texture);
const sprite = new CreateAssetCommand('sprite', 'Hero 2', spriteFromTexture(project, texture.asset.id));
stack.execute(sprite);
expect(findAsset(project, sprite.asset.id)?.props).toEqual({
enabled: true,
keepOnSceneLoad: false,
scale: 1,
rotation: 0,
mirrorX: false,
mirrorY: false,
sortWithParent: true,
renderLayer: -1,
width: 64,
height: 32,
color: 0xffffff,
texture: texture.asset.id,
shader: '',
});
stack.undo();
expect(findAsset(project, sprite.asset.id)).toBeUndefined();
stack.redo();
expect(findAsset(project, sprite.asset.id)?.props.texture).toBe(texture.asset.id);
});
it('passes each value through the schema', () => {
const { project, stack } = makeProject();
const sprite = new CreateAssetCommand('sprite', 'Tiny', { width: -5 });
stack.execute(sprite);
expect(findAsset(project, sprite.asset.id)?.props.width).toBe(1);
});
it('rejects a property the type does not have, leaving the project untouched', () => {
const { project, stack } = makeProject();
expect(() => stack.execute(new CreateAssetCommand('sprite', 'Odd', { zoom: 2 }))).toThrow(CommandError);
expect(project.assets).toHaveLength(0);
});
});