Time is an argument, so speed scales it and a prewarm offsets it (#87)
game-maker / suite (push) Successful in 9m48s
game-maker / suite (push) Successful in 9m48s
Speed and Prewarm are not state and not a second loop: both are arithmetic on the clock argument the field already passes to every particle formula. fieldTime = elapsed * timeScale + prewarmOffset, a pure rule next to liveRange, called at the one place ParticleField.sync forms its time, so preview and world inherit it together. Prewarm offsets by one lifetime because that is the smallest offset which fills a continuous emitter; a burst is dead at time >= lifetime, so the same offset would skip it entirely and it is suppressed there. The tab's slider counts wall-clock seconds, so its span divides by the scale. A missing row falls back to 1 / off, so old project files need no migration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fbb2d157f0
commit
8ae20d044f
8 files changed
+191
-10
No files matched your search
@@ -259,10 +259,27 @@ export async function particles(t) {
|
||||
edited.p.count === 8 && edited.c.span === 6,
|
||||
JSON.stringify({ ...edited.p, span: edited.c.span, defaultsWouldGive: { count: 21, span: 3 } }),
|
||||
);
|
||||
|
||||
// 31d-vi. (#87) The slider is in the seconds a person waits, not in the source's own: at twice the
|
||||
// speed the same three lifetimes are over in half the time, so the slider reaches half as
|
||||
// far – and the second it is held at is twice as far into the field.
|
||||
await mutate(`
|
||||
const a = __gm.state.project.assets.find((x) => x.id === ${JSON.stringify(assetId)});
|
||||
a.props.timeScale = 2;
|
||||
`);
|
||||
await sleep(300);
|
||||
const scaled = await json('{ c: __gm.particles.controls(), p: __gm.particles.preview() }');
|
||||
check(
|
||||
'a time scale of 2 halves what the slider spans and doubles what a held second shows',
|
||||
scaled.c.span === 3 && scaled.p.time === 1.5 && scaled.p.count === 10,
|
||||
JSON.stringify({ ...scaled.p, span: scaled.c.span, atScaleOne: { count: 8, span: 6 } }),
|
||||
);
|
||||
|
||||
await mutate(`
|
||||
const a = __gm.state.project.assets.find((x) => x.id === ${JSON.stringify(assetId)});
|
||||
delete a.props.lifetime;
|
||||
delete a.props.rate;
|
||||
delete a.props.timeScale;
|
||||
`);
|
||||
await sleep(200);
|
||||
await evaluate('__gm.particles.hold(null)');
|
||||
@@ -403,6 +420,85 @@ export async function particles(t) {
|
||||
JSON.stringify({ count: burst?.count, alphas: [...alphas], after: afterBurst?.count }),
|
||||
);
|
||||
|
||||
// 31k-ii. (#87) Prewarm means nothing to a burst: one lifetime in, the burst it would start past is
|
||||
// already over, so the offset would hide the effect instead of warming it up.
|
||||
await mutate(`
|
||||
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
||||
o.overrides.prewarm = true;`);
|
||||
await sleep(200);
|
||||
await rewind();
|
||||
await at(0.5);
|
||||
const warmBurst = await drawn(7);
|
||||
check(
|
||||
'a prewarmed burst is still a burst: the offset that fills a stream would empty this one',
|
||||
warmBurst?.count === 7,
|
||||
JSON.stringify({ count: warmBurst?.count, ifItApplied: 0 }),
|
||||
);
|
||||
|
||||
// 31k-iii. (#87) A continuous source starts one lifetime in instead, which is exactly the second
|
||||
// its field is full at: its first frame already carries a whole lifetime of particles.
|
||||
await mutate(`
|
||||
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
||||
Object.assign(o.overrides, { mode: 'continuous', drift: 0 });
|
||||
delete o.overrides.count;`);
|
||||
await sleep(200);
|
||||
await rewind();
|
||||
await at(0);
|
||||
const warmStart = await drawn(1);
|
||||
await mutate(`
|
||||
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
||||
delete o.overrides.prewarm;`);
|
||||
await sleep(200);
|
||||
await rewind();
|
||||
await at(0);
|
||||
const coldStart = await drawn(1);
|
||||
check(
|
||||
'a prewarmed stream is full at its first frame, where a cold one has just the first particle',
|
||||
warmStart?.count === RATE && coldStart?.count === 1,
|
||||
JSON.stringify({ warm: warmStart?.count, cold: coldStart?.count }),
|
||||
);
|
||||
|
||||
// 31k-iv. (#87) Time scale is a factor on that same clock argument and nothing else: at twice the
|
||||
// speed, half a second in is the very picture the same source shows a whole second in.
|
||||
await mutate(`
|
||||
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
||||
o.overrides.timeScale = 2;`);
|
||||
await sleep(200);
|
||||
await rewind();
|
||||
await at(0.5);
|
||||
const fast = await drawn(3);
|
||||
await mutate(`
|
||||
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
||||
delete o.overrides.timeScale;`);
|
||||
await sleep(200);
|
||||
await rewind();
|
||||
await at(1);
|
||||
const plain = await drawn(3);
|
||||
check(
|
||||
'twice the time scale is the same field half the time in, particle for particle',
|
||||
fast?.count === RATE && JSON.stringify(fast?.drawn) === JSON.stringify(plain?.drawn),
|
||||
JSON.stringify({ fast: fast?.count, plain: plain?.count, oldest: fast?.drawn[0] }),
|
||||
);
|
||||
|
||||
// 31k-v. (#87) Both rows stand with the ones about time, under the lifetime they are measured in.
|
||||
const timeRows = await json(`{
|
||||
lifetime: __gm.inspectorField('Lifetime'),
|
||||
scale: __gm.inspectorField('Time scale'),
|
||||
prewarm: __gm.inspectorField('Prewarm'),
|
||||
}`);
|
||||
check(
|
||||
'the source shows a Time scale and a Prewarm row, both below Lifetime',
|
||||
Boolean(timeRows.scale && timeRows.prewarm && timeRows.lifetime) &&
|
||||
timeRows.scale.y > timeRows.lifetime.y &&
|
||||
timeRows.prewarm.y > timeRows.scale.y,
|
||||
JSON.stringify(timeRows),
|
||||
);
|
||||
|
||||
await mutate(`
|
||||
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
||||
Object.assign(o.overrides, { mode: 'burst', count: 7 });`);
|
||||
await sleep(200);
|
||||
|
||||
// 31l. It really draws: the pixel where a burst particle sits is not the scene background.
|
||||
await mutate(`
|
||||
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
||||
|
||||
Reference in new issue
Block a user