A particle is a function of its age, not a thing that is stepped (#73)
game-maker / suite (push) Failing after 4m22s

The asset type `particles` is placeable and carries a graph at the same time —
`behavior` was the blueprint. Because a continuous emitter gives birth to particle n
at time n/rate, the whole field is a function of the clock rather than of the frames:
no ring-buffer bookkeeping, exact time jumps, reproducibility as a measurable property.

compile.ts follows compileShader's frame but emits JS closures into a Float64Array
instead of GLSL, and without eval; a node with two consumers is evaluated once.
Drawing is one Pixi ParticleContainer per source, so one draw call.

Collision, wind and braking in flight are out for good — that is the price of the
formula model, not an omission.

The tab count goes from nine to ten, which is exactly the case #84 was built for one
day earlier: at 1280 px the row no longer holds every tab, so guard 14b now asks for
hidden.length > 0 instead of === 0.
This commit is contained in:
root committed 2026-09-14 05:44:25 +02:00
1 parent f83c568999
commit 0f9e1611d1
39 files changed
+2386 -32

No files matched your search

+10 -4
View File
@@ -28,6 +28,7 @@ import {
} from '../../core/graph';
import type { GraphAsset } from '../../core/model/Asset';
import { graphLookup } from '../../core/model/graphLookup';
import { particleLibrary } from '../../viewport/ParticleLibrary';
import { shaderLibrary } from '../../viewport/ShaderLibrary';
import { findAsset, findGraphAsset } from '../../core/model/Project';
import { type Trace, TRACE_FADE } from '../../runtime/systems/BehaviorSystem';
@@ -219,8 +220,13 @@ export class GraphCanvas extends Widget {
}
const seen = new Set<string>();
const lookup = graphLookup(this.state.project);
// A shader node's problem is the compiler's, not a call's.
const shaderProblems = asset.type === 'shader' ? shaderLibrary.buildFor(asset).problems : null;
// In a compiled graph a node's problem is the compiler's, not a call's.
const compilerProblems =
asset.type === 'shader'
? shaderLibrary.buildFor(asset).problems
: asset.type === 'particles'
? particleLibrary.buildFor(asset).problems
: null;
for (const node of asset.graph.nodes) {
const def = resolveDef(node, asset.graph, lookup);
if (!def) continue;
@@ -233,8 +239,8 @@ export class GraphCanvas extends Widget {
this.views.set(node.id, view);
this.nodeLayer.addChild(view);
}
const problem = shaderProblems
? (shaderProblems[node.id] ?? null)
const problem = compilerProblems
? (compilerProblems[node.id] ?? null)
: callProblem(node, lookup, asset.id);
view.sync(node, asset.graph, this.state.project, def, problem);
}
+2
View File
@@ -53,6 +53,8 @@ export function categoryColor(category: NodeCategory): number {
return c.categoryState;
case 'shader':
return c.categoryShader;
case 'particles':
return c.categoryParticles;
}
}
+18 -1
View File
@@ -15,7 +15,14 @@ import type { GraphAsset } from '../../core/model/Asset';
import { graphLookup } from '../../core/model/graphLookup';
import type { PropertyBag } from '../../core/model/Properties';
import type { Project } from '../../core/model/Project';
import {
isParticlePropertyNode,
PARTICLE_PROPERTY,
PROPERTY_NAME_PARAM as PARTICLE_NAME_PARAM,
propertyNameOf,
} from '../../core/properties/particles';
import { nextPropertyName, PROPERTY_NAME_PARAM, SHADER_PROPERTY } from '../../core/properties/shaders';
import { nextParticlePropertyName, PARTICLE_NODES, PARTICLE_OUTPUT } from '../../core/particles/nodes';
import { SHADER_NODES, SHADER_OUTPUT } from '../../core/shader/nodes';
/**
@@ -48,9 +55,19 @@ export function paletteEntries(project: Project, asset: GraphAsset): PaletteEntr
return { id: def.type, hint: def.category, def, params };
});
}
if (asset.type === 'particles') {
const hasOutput = asset.graph.nodes.some((n) => n.type === PARTICLE_OUTPUT);
const taken = new Set(asset.graph.nodes.filter(isParticlePropertyNode).map(propertyNameOf));
return PARTICLE_NODES.filter((def) => def.type !== PARTICLE_OUTPUT || !hasOutput).map((def) => {
const params: PropertyBag =
def.type === PARTICLE_PROPERTY ? { [PARTICLE_NAME_PARAM]: nextParticlePropertyName(taken) } : {};
return { id: def.type, hint: def.category, def, params };
});
}
const entries: PaletteEntry[] = [];
for (const def of allNodeDefs()) {
if (def.type === RUN_BEHAVIOR || def.category === 'state' || def.category === 'shader') continue;
if (def.type === RUN_BEHAVIOR || def.category === 'state') continue;
if (def.category === 'shader' || def.category === 'particles') continue;
if (isTerminalType(def.type) && terminalNode(asset.graph, def.type)) continue;
entries.push({ id: def.type, hint: def.category, def, params: {} });
}