diff --git a/README.md b/README.md
index 0ba7befd..a869f2b2 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@ Live: · Widget playground: isModified(asset, def.key));
+}
/**
* Whether a placed object deviates from whatever it follows: the container template it was
diff --git a/src/editor/debugHook.ts b/src/editor/debugHook.ts
index ee098a40..6602a60b 100644
--- a/src/editor/debugHook.ts
+++ b/src/editor/debugHook.ts
@@ -143,6 +143,7 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
dropPreview: () => viewport.dropInfo(),
/** An asset row by id – two assets can share a name with the container picker's label. */
assetRow: (id: string) => assets.rowGlobal(id),
+ assetRowColor: (id: string) => assets.rowColor(id),
gameInfo: () => gameView.debugInfo(),
layerOf: (id: string) => viewport.layerOf(id),
drawOrder: () => viewport.drawOrder(),
diff --git a/src/editor/inspector/assetSection.ts b/src/editor/inspector/assetSection.ts
index b4e4f738..3709c307 100644
--- a/src/editor/inspector/assetSection.ts
+++ b/src/editor/inspector/assetSection.ts
@@ -12,12 +12,12 @@ import {
import type { Asset, BehaviorAsset } from '../../core/model/Asset';
import { findAsset, type Project } from '../../core/model/Project';
import {
- COMMON_PROPS,
type PropertyDef,
schemaForAsset,
VARIABLE_KINDS,
type VariableKind,
} from '../../core/properties/PropertySchema';
+import { markedAssetSchema } from '../../core/properties/deviation';
import { isModified, resolveAssetProps } from '../../core/properties/resolve';
import { Button, Dropdown, HStack, Label, Menu, Spacer, TextField, Theme } from '../../ui';
import { aspectActions, sizeCommand } from './aspectAction';
@@ -63,12 +63,8 @@ export function buildAssetSection(host: InspectorHost, assetId: string): void {
row.add(host.nameField, 1);
host.body.add(row);
- if (asset.type === 'behavior') {
- buildProps(host, asset, COMMON_PROPS);
- buildVariables(host, asset);
- } else {
- buildProps(host, asset, schemaForAsset(asset));
- }
+ buildProps(host, asset, markedAssetSchema(asset));
+ if (asset.type === 'behavior') buildVariables(host, asset);
}
/** Pushes the current asset values into the rows. False when the asset is gone (rebuild). */
diff --git a/src/editor/panels/AssetPanel.ts b/src/editor/panels/AssetPanel.ts
index 1f36eb2d..339b339e 100644
--- a/src/editor/panels/AssetPanel.ts
+++ b/src/editor/panels/AssetPanel.ts
@@ -8,13 +8,19 @@ import {
isPlaceableType,
} from '../../core/model/AssetTypes';
import { findAsset } from '../../core/model/Project';
+import { assetDeviates } from '../../core/properties/deviation';
import type { TextureStore } from '../../core/store/TextureStore';
import { Button, ListView, Menu, type MenuItem, Panel, ScrollContainer, startDrag } from '../../ui';
import { confirmDeleteAsset } from '../actions';
import type { EditorState } from '../state';
import { pickAndImport } from '../textureImport';
-/** Lists all assets of the project; creates and deletes them. Placing is a drag into a view. */
+/**
+ * Lists all assets of the project; creates and deletes them. Placing is a drag into a view.
+ *
+ * A row's name turns gold when the asset carries a property the inspector marks – the same rule,
+ * so the list cannot promise a deviation the inspector then fails to show.
+ */
export class AssetPanel extends Panel {
private readonly list = new ListView();
private readonly scroll = new ScrollContainer();
@@ -59,6 +65,7 @@ export class AssetPanel extends Panel {
label: asset.name,
hint: asset.type === 'scene' && asset.id === active ? 'scene ●' : asset.type,
swatch: hasColorSwatch(asset.type) ? Number(asset.props.color ?? 0) : undefined,
+ overridden: assetDeviates(asset),
})),
);
this.syncSelection();
@@ -98,6 +105,11 @@ export class AssetPanel extends Panel {
return this.list.rowGlobal(assetId);
}
+ /** Label colour of a row as `#rrggbb` – gold means the asset deviates from the defaults (test hook). */
+ rowColor(assetId: string): string | null {
+ return this.list.rowColor(assetId);
+ }
+
/** Hands the asset to the viewport under the pointer; a scene has nowhere to be placed into. */
private startPlacementDrag(assetId: string): void {
const asset = findAsset(this.state.project, assetId);
diff --git a/tests/core/deviation.test.ts b/tests/core/deviation.test.ts
index 2fa87b67..0147dad6 100644
--- a/tests/core/deviation.test.ts
+++ b/tests/core/deviation.test.ts
@@ -1,5 +1,11 @@
import { describe, expect, it } from 'vitest';
+import {
+ CreateAssetCommand,
+ ResetAssetPropertyCommand,
+ SetAssetPropertyCommand,
+} from '../../src/core/commands/assetCommands';
import { CreateContainerCommand } from '../../src/core/commands/containerCommands';
+import { AddVariableCommand } from '../../src/core/commands/variableCommands';
import {
ClearOverrideCommand,
SetOverrideCommand,
@@ -8,8 +14,8 @@ import {
import { instanceChildId } from '../../src/core/model/instance';
import { findAsset } from '../../src/core/model/Project';
import type { ContainerAsset } from '../../src/core/model/Asset';
-import { deviationMarks, objectDeviates } from '../../src/core/properties/deviation';
-import { makeSpriteScene, place } from '../helpers';
+import { assetDeviates, deviationMarks, objectDeviates } from '../../src/core/properties/deviation';
+import { makeProject, makeSpriteScene, place } from '../helpers';
describe('deviation marks', () => {
it('marks an object only once an override really deviates', () => {
@@ -48,3 +54,37 @@ describe('deviation marks', () => {
expect([...deviationMarks(f.project, f.sceneOf())].sort()).toEqual([content, create.objectId].sort());
});
});
+
+describe('asset deviation', () => {
+ const makeAsset = (type: 'sprite' | 'behavior', name: string) => {
+ const f = makeProject();
+ const create = new CreateAssetCommand(type, name);
+ f.stack.execute(create);
+ const asset = () => findAsset(f.project, create.asset.id)!;
+ expect(asset().type).toBe(type);
+ return { ...f, asset, assetId: create.asset.id };
+ };
+
+ it('marks an asset only once a property really differs from its default', () => {
+ const f = makeAsset('sprite', 'Box');
+ expect(assetDeviates(f.asset())).toBe(false);
+ // Written, but the default value – the inspector shows no gold, so neither does the list.
+ f.stack.execute(new SetAssetPropertyCommand(f.assetId, 'width', 100));
+ expect(f.asset().props.width).toBe(100);
+ expect(assetDeviates(f.asset())).toBe(false);
+ f.stack.execute(new SetAssetPropertyCommand(f.assetId, 'width', 250));
+ expect(assetDeviates(f.asset())).toBe(true);
+ f.stack.execute(new ResetAssetPropertyCommand(f.assetId, 'width'));
+ expect(assetDeviates(f.asset())).toBe(false);
+ });
+
+ it('leaves a behavior unmarked for its variables, which the inspector does not mark either', () => {
+ const f = makeAsset('behavior', 'Mover');
+ f.stack.execute(new AddVariableCommand(f.assetId, 'speed', 'number'));
+ f.stack.execute(new SetAssetPropertyCommand(f.assetId, 'speed', 42));
+ expect(f.asset().props.speed).toBe(42);
+ expect(assetDeviates(f.asset())).toBe(false);
+ f.stack.execute(new SetAssetPropertyCommand(f.assetId, 'enabled', false));
+ expect(assetDeviates(f.asset())).toBe(true);
+ });
+});
diff --git a/todo-notes/done/overwrite-textfarbe-auch-bei-assets.md b/todo-notes/done/overwrite-textfarbe-auch-bei-assets.md
new file mode 100644
index 00000000..dbff8f01
--- /dev/null
+++ b/todo-notes/done/overwrite-textfarbe-auch-bei-assets.md
@@ -0,0 +1,37 @@
+die textfarbe beim asset soll auch in diesem gold dargestellt werden, falls ein property des assets einen override hat.
+ist somit relativ simpel: im prinzip genau das checken was im inspector erscheint wenn man das asset auswählt => gibt es wo einen override (gold) dann auch den assetname links im panel gold darstellen.
+
+---
+
+**Umgesetzt (2026-09-09).** Der Name einer Asset-Zeile im Assets-Panel ist gold, solange das
+Asset irgendeine Property traegt, die der Inspector gold malen wuerde — dieselbe Farbe und
+dasselbe Kriterium wie in der Hierarchie.
+
+Die Regel steht als reine Funktion in `src/core/properties/deviation.ts`, neben dem schon
+vorhandenen `objectDeviates`:
+
+- `markedAssetSchema(asset)` — die Zeilen, die der Asset-Inspector als "modified" markiert.
+- `assetDeviates(asset)` — ob davon eine von ihrem Schema-Default abweicht (`isModified`).
+
+**Panel und Inspector benutzen dieselbe Liste**, statt die Bedingung zweimal zu formulieren:
+`buildAssetSection` baut seine Property-Zeilen jetzt aus `markedAssetSchema(asset)` (das ersetzt
+die Fallunterscheidung Behavior/Rest, die dort vorher stand). Damit kann die Liste keine
+Abweichung behaupten, die der Inspector danach nicht zeigt.
+
+**Ein Sonderfall, der eine Entscheidung brauchte: die Variablen eines Behaviors.** Sie sind Teil
+von `schemaForAsset`, aber der Inspector markiert sie bewusst nie (`setModified` ist dort ein
+No-op) — ihr Default ist auf der Variablen selbst deklariert, der Asset-Wert *ist* dieser
+Default. Ein Behavior mit `speed = 200` ist deshalb nicht gold. Sonst waere im E2E-Fixture der
+"Mover" gold gewesen, ohne dass der Inspector irgendwo etwas markiert haette — genau der
+Widerspruch, den die Notiz ausschliesst.
+
+**Belegt** (E2E-Modul 04, sechs neue Checks ueber den Test-Hook `__gm.assetRowColor`, der die
+tatsaechliche Label-Farbe der Zeile liest):
+
+- Crate (Hoehe 60 statt 100, eigene Farbe) → `#f2c84b`, Main camera (alles auf Default) → `#e6e8ee`
+- Mover (Variable `speed` = 200 gegen deklarierten Default 0) → `#e6e8ee`, wie der Inspector
+- ein ↺ zurueckgesetzt → bleibt gold, weil die zweite Abweichung noch steht
+- die zweite auch zurueckgesetzt → wird schlicht, **ohne Reload**; Undo bringt die Marke zurueck
+
+Screenshot `15-asset-reset`: im Panel sind "Level 1" (Hintergrundfarbe), "Crate" und
+"Crate collider" gold, "Main camera", "Mover", "Spawner" und das frisch angelegte "Sprite" weiss.
diff --git a/todo-notes/overwrite-textfarbe-auch-bei-assets.md b/todo-notes/overwrite-textfarbe-auch-bei-assets.md
deleted file mode 100644
index 5ec01b89..00000000
--- a/todo-notes/overwrite-textfarbe-auch-bei-assets.md
+++ /dev/null
@@ -1 +0,0 @@
-die textfarbe beim asset soll auch in diesem gold dargestellt werden, falls ein property des assets einen override hat
diff --git a/todo-notes/unreferenced-image-files.md b/todo-notes/unreferenced-image-files.md
new file mode 100644
index 00000000..42fdfe22
--- /dev/null
+++ b/todo-notes/unreferenced-image-files.md
@@ -0,0 +1,14 @@
+Zitat aus der readme: "Unreferenced image files are never collected."
+
+Ich würde sagen dass soll vorerst auch so bleiben.
+
+Was passt:
+ drag and drop file in den editor => textur wird hochgeladen und passende asset wird erstellt => das soll so bleiben, das passt
+
+Was wir auch haben aber ändern:
+ klickt man auf das `+` => `Texture`, dann öffnet sich der File System Browser.
+
+Anstelle des file system browser => ein dialog mit allen images die am server existieren.
+ - auswählen => ok, dann wird es wiederverwendet => dialog schließt sich => texture asset mit ausgewählten image wird erstellt.
+ - oder hochladen button => file system browser => OK => dialog schließt sich => texture asset mit hochgeladenen image wird erstellt (im prinzip das was wir jetzt auch haben)
+
\ No newline at end of file
diff --git a/tools/e2e/04-assets.mjs b/tools/e2e/04-assets.mjs
index dbab9e63..b69bc050 100644
--- a/tools/e2e/04-assets.mjs
+++ b/tools/e2e/04-assets.mjs
@@ -143,4 +143,42 @@ export async function assets(t) {
(await evaluate(`__gm.locate('↺')`))?.y === heightReset.y,
JSON.stringify(await evaluate(`__gm.locate('↺')`)),
);
+
+ // 12b. The same gold in the asset list: a row is marked while the asset carries anything the
+ // inspector marks. The Crate deviates twice, so it takes two resets to turn it plain.
+ const GOLD = '#f2c84b';
+ const PLAIN = '#e6e8ee';
+ const assetColor = (id) => json(`__gm.assetRowColor('${id}')`);
+ check('a deviating asset is marked gold in the list', (await assetColor('crate')) === GOLD);
+ check('an asset at its defaults is not', (await assetColor('cam')) === PLAIN, await assetColor('cam'));
+ // The Mover's `speed` is 200 against a declared variable default of 0. The inspector marks no
+ // variable, so neither may the list – the two must say the same thing.
+ check(
+ 'a behavior is not marked for a variable value, which the inspector does not mark either',
+ (await assetColor('mover')) === PLAIN,
+ await assetColor('mover'),
+ );
+ await click(heightReset.x, heightReset.y);
+ await sleep(150);
+ check(
+ 'the mark stays while the second deviation is left',
+ (await assetColor('crate')) === GOLD,
+ await assetColor('crate'),
+ );
+ const colorOnly = await evaluate(`__gm.locate('↺')`);
+ await click(colorOnly.x, colorOnly.y);
+ await sleep(150);
+ check(
+ 'resetting the last deviation clears the mark, without a reload',
+ (await assetColor('crate')) === PLAIN,
+ `${await assetColor('crate')} props=${JSON.stringify((await asset('crate')).props)}`,
+ );
+ await shot('15b-asset-marks');
+ await undo();
+ await undo();
+ check(
+ 'undo brings the mark back',
+ (await assetColor('crate')) === GOLD,
+ `${await assetColor('crate')} props=${JSON.stringify((await asset('crate')).props)}`,
+ );
}