fireworks-engine 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +24 -0
- package/README.md +297 -0
- package/index.d.ts +691 -0
- package/package.json +49 -0
- package/src/burstConfigBuilder.js +262 -0
- package/src/burstSpawn.js +764 -0
- package/src/colorUtils.js +80 -0
- package/src/configBuilder.js +300 -0
- package/src/defaults.js +87 -0
- package/src/index.js +13 -0
- package/src/mathResolvers.js +77 -0
- package/src/presets.js +811 -0
- package/src/renderer.js +863 -0
- package/src/rocketLaunch.js +791 -0
- package/src/runtime.js +796 -0
- package/src/runtimeCapabilities.js +7 -0
- package/src/runtimeLifecycle.js +330 -0
- package/src/update.js +307 -0
- package/src/valueNormalizers.js +160 -0
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clamp,
|
|
3
|
+
randomRange,
|
|
4
|
+
resolveEnvelopeTimeScale,
|
|
5
|
+
resolveScaledInteger,
|
|
6
|
+
resolveScaledInverseOverride,
|
|
7
|
+
resolveScaledOverride,
|
|
8
|
+
resolveSizeScale,
|
|
9
|
+
} from './mathResolvers.js';
|
|
10
|
+
import {
|
|
11
|
+
WHITE_BRIGHTNESS_REFERENCE,
|
|
12
|
+
} from './defaults.js';
|
|
13
|
+
import {
|
|
14
|
+
isPlainRecord,
|
|
15
|
+
normalizeGroupedPublicOptions,
|
|
16
|
+
resolveBurstColorMode,
|
|
17
|
+
resolveBurstEntityToggle,
|
|
18
|
+
} from './valueNormalizers.js';
|
|
19
|
+
import {
|
|
20
|
+
normalizeHexColor,
|
|
21
|
+
} from './colorUtils.js';
|
|
22
|
+
import { buildBurstOptions } from './burstConfigBuilder.js';
|
|
23
|
+
import {
|
|
24
|
+
launchBurst,
|
|
25
|
+
resolveBurstBaseColor,
|
|
26
|
+
} from './burstSpawn.js';
|
|
27
|
+
import {
|
|
28
|
+
ensureCanvas,
|
|
29
|
+
resolveScaledDelay,
|
|
30
|
+
scheduleTimeout,
|
|
31
|
+
shouldReduceMotion,
|
|
32
|
+
startLoop,
|
|
33
|
+
} from './runtimeLifecycle.js';
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
const ROCKET_TARGET_TUNING = {
|
|
37
|
+
targetXMinRatio: 0.25,
|
|
38
|
+
targetXMaxRatio: 0.75,
|
|
39
|
+
targetXJitter: 80,
|
|
40
|
+
targetYMinRatio: 0.14,
|
|
41
|
+
targetYMaxRatio: 0.38,
|
|
42
|
+
explicitStartJitterX: 72,
|
|
43
|
+
explicitStartOffsetY: 48,
|
|
44
|
+
fallbackStartOffsetYMin: 20,
|
|
45
|
+
fallbackStartOffsetYMax: 80,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const ROCKET_FLIGHT_GUARDS = {
|
|
49
|
+
minDistanceEpsilon: 0.0001,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const ROCKET_DURATION_LIMITS = {
|
|
53
|
+
defaultMin: 0.7,
|
|
54
|
+
defaultMax: 1.0,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function resolveRocketDuration(distance, duration, speed) {
|
|
58
|
+
if (speed > 0 && distance > ROCKET_FLIGHT_GUARDS.minDistanceEpsilon) {
|
|
59
|
+
return distance / speed;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return duration;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const ROCKET_CASCADE_LIMITS = {
|
|
66
|
+
burstCountMax: 50,
|
|
67
|
+
defaultDelayMs: 48,
|
|
68
|
+
defaultSpread: 18,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const ROCKET_CASCADE_MODES = {
|
|
72
|
+
burst: 'burst',
|
|
73
|
+
rocket: 'rocket',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const ROCKET_CASCADE_DIRECTION_TUNING = {
|
|
77
|
+
minConeDeg: 0,
|
|
78
|
+
maxConeDeg: 360,
|
|
79
|
+
defaultDirectionDeg: 0,
|
|
80
|
+
defaultConeDeg: 360,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const ROCKET_CASCADE_DIRECTION_CONSTANTS = {
|
|
84
|
+
fullCircleRad: Math.PI * 2,
|
|
85
|
+
degToRad: Math.PI / 180,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const ROCKET_TAIL_EMBER_TUNING = {
|
|
89
|
+
speedMin: 0.1,
|
|
90
|
+
fadeMin: 0.1,
|
|
91
|
+
countMin: 1,
|
|
92
|
+
spreadMin: 0,
|
|
93
|
+
sizeMin: 0.05,
|
|
94
|
+
lifeBaseDivisor: 0.09,
|
|
95
|
+
trailBlendDistance: 18,
|
|
96
|
+
trailBlendMin: 0.18,
|
|
97
|
+
requestedCountMin: 1,
|
|
98
|
+
requestedCountFactorBase: 0.12,
|
|
99
|
+
requestedCountFactorPerEmber: 0.05,
|
|
100
|
+
cloudLengthMin: 6,
|
|
101
|
+
cloudLengthSpreadYFactor: 1.15,
|
|
102
|
+
cloudLengthSpreadXFactor: 0.25,
|
|
103
|
+
cloudLengthSpeedFactor: 2.6,
|
|
104
|
+
cloudLengthSizeFactor: 3.4,
|
|
105
|
+
cloudWidthMin: 1.25,
|
|
106
|
+
cloudWidthSpreadXFactor: 0.78,
|
|
107
|
+
cloudWidthSizeFactor: 1.35,
|
|
108
|
+
progressMin: 0.08,
|
|
109
|
+
progressMax: 0.98,
|
|
110
|
+
depthPowMin: 0.05,
|
|
111
|
+
depthPowMax: 1,
|
|
112
|
+
depthPowExponent: 1.45,
|
|
113
|
+
cloudJitterFactor: 0.16,
|
|
114
|
+
cloudOffsetFactor: 0.18,
|
|
115
|
+
cloudOffsetBase: 0.55,
|
|
116
|
+
cloudOffsetTrailBlendFactor: 0.25,
|
|
117
|
+
trailPushMin: 0.5,
|
|
118
|
+
positionJitterFactor: 0.18,
|
|
119
|
+
lifeRandomMin: 0.72,
|
|
120
|
+
lifeRandomMax: 1.15,
|
|
121
|
+
lifeMin: 0.02,
|
|
122
|
+
lifeMax: 0.42,
|
|
123
|
+
speedRandomMin: 0.12,
|
|
124
|
+
speedRandomMax: 0.52,
|
|
125
|
+
distanceScaleBase: 0.38,
|
|
126
|
+
distanceScaleTrailBlendFactor: 0.52,
|
|
127
|
+
lateralVelocityFactor: 0.24,
|
|
128
|
+
gravityBase: 24,
|
|
129
|
+
gravitySpeedFactor: 3.1,
|
|
130
|
+
radiusRandomMin: 0.85,
|
|
131
|
+
radiusRandomMax: 1.2,
|
|
132
|
+
drag: 0.82,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
function resolveRocketCascadeMode(value) {
|
|
136
|
+
return value === ROCKET_CASCADE_MODES.rocket
|
|
137
|
+
? ROCKET_CASCADE_MODES.rocket
|
|
138
|
+
: ROCKET_CASCADE_MODES.burst;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function resolveOptionalPositiveRange(minValue, maxValue) {
|
|
142
|
+
const hasMin = Number.isFinite(minValue) && minValue > 0;
|
|
143
|
+
const hasMax = Number.isFinite(maxValue) && maxValue > 0;
|
|
144
|
+
|
|
145
|
+
if (!hasMin && !hasMax) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const resolvedMin = hasMin ? minValue : maxValue;
|
|
150
|
+
const resolvedMax = hasMax ? maxValue : minValue;
|
|
151
|
+
|
|
152
|
+
if (resolvedMin <= resolvedMax) {
|
|
153
|
+
return {
|
|
154
|
+
min: resolvedMin,
|
|
155
|
+
max: resolvedMax,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
min: resolvedMax,
|
|
161
|
+
max: resolvedMin,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function resolveCascadeChildLaunchOptions(baseChildOptions, cascade, distance) {
|
|
166
|
+
const childRocketOptions = isPlainRecord(baseChildOptions?.rocket)
|
|
167
|
+
? baseChildOptions.rocket
|
|
168
|
+
: {};
|
|
169
|
+
const speedRange = resolveOptionalPositiveRange(
|
|
170
|
+
cascade?.childSpeedMin,
|
|
171
|
+
cascade?.childSpeedMax,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
let childDuration = null;
|
|
175
|
+
if (speedRange && distance > ROCKET_FLIGHT_GUARDS.minDistanceEpsilon) {
|
|
176
|
+
const speed = randomRange(speedRange.min, speedRange.max);
|
|
177
|
+
childDuration = speed > 0 ? distance / speed : null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!(Number.isFinite(childDuration) && childDuration > 0)) {
|
|
181
|
+
if (Number.isFinite(childRocketOptions.speed) && childRocketOptions.speed > 0) {
|
|
182
|
+
return baseChildOptions;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const durationRange = resolveOptionalPositiveRange(
|
|
186
|
+
childRocketOptions.durationMin,
|
|
187
|
+
childRocketOptions.durationMax,
|
|
188
|
+
);
|
|
189
|
+
if (!durationRange) {
|
|
190
|
+
return baseChildOptions;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
childDuration = randomRange(durationRange.min, durationRange.max);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
...baseChildOptions,
|
|
198
|
+
rocket: {
|
|
199
|
+
...childRocketOptions,
|
|
200
|
+
speed: 0,
|
|
201
|
+
durationMin: childDuration,
|
|
202
|
+
durationMax: childDuration,
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function resolveCascadeDirectedAngle(directionDeg, coneDeg) {
|
|
208
|
+
const safeDirectionDeg = Number.isFinite(directionDeg)
|
|
209
|
+
? directionDeg
|
|
210
|
+
: ROCKET_CASCADE_DIRECTION_TUNING.defaultDirectionDeg;
|
|
211
|
+
const safeConeDeg = Number.isFinite(coneDeg)
|
|
212
|
+
? coneDeg
|
|
213
|
+
: ROCKET_CASCADE_DIRECTION_TUNING.defaultConeDeg;
|
|
214
|
+
const boundedConeDeg = clamp(
|
|
215
|
+
safeConeDeg,
|
|
216
|
+
ROCKET_CASCADE_DIRECTION_TUNING.minConeDeg,
|
|
217
|
+
ROCKET_CASCADE_DIRECTION_TUNING.maxConeDeg,
|
|
218
|
+
);
|
|
219
|
+
const coneRad = boundedConeDeg * ROCKET_CASCADE_DIRECTION_CONSTANTS.degToRad;
|
|
220
|
+
|
|
221
|
+
if (coneRad >= ROCKET_CASCADE_DIRECTION_CONSTANTS.fullCircleRad) {
|
|
222
|
+
return randomRange(0, ROCKET_CASCADE_DIRECTION_CONSTANTS.fullCircleRad);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const directionRad = -safeDirectionDeg * ROCKET_CASCADE_DIRECTION_CONSTANTS.degToRad;
|
|
226
|
+
const halfCone = coneRad / 2;
|
|
227
|
+
const startAngle = directionRad - halfCone;
|
|
228
|
+
return randomRange(startAngle, startAngle + coneRad);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function resolveCascadeTargetPoint(engine, originX, originY, spreadX, spreadY, cascade) {
|
|
232
|
+
const angle = resolveCascadeDirectedAngle(
|
|
233
|
+
cascade?.childDirectionDeg,
|
|
234
|
+
cascade?.childConeDeg,
|
|
235
|
+
);
|
|
236
|
+
const distanceRatio = Math.sqrt(randomRange(0, 1));
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
x: clamp(originX + (Math.cos(angle) * spreadX * distanceRatio), 0, engine.width),
|
|
240
|
+
y: clamp(originY + (Math.sin(angle) * spreadY * distanceRatio), 0, engine.height),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function resolveCascadeRandomTargetPoint(engine, originX, originY, spreadX, spreadY) {
|
|
245
|
+
const angle = randomRange(0, ROCKET_CASCADE_DIRECTION_CONSTANTS.fullCircleRad);
|
|
246
|
+
const distanceRatio = Math.sqrt(randomRange(0, 1));
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
x: clamp(originX + (Math.cos(angle) * spreadX * distanceRatio), 0, engine.width),
|
|
250
|
+
y: clamp(originY + (Math.sin(angle) * spreadY * distanceRatio), 0, engine.height),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function resolveDefaultCascadeChildLaunchOptions(rocket, cascade) {
|
|
255
|
+
const burstOptions = rocket.burst ?? {};
|
|
256
|
+
const tailEmbers = rocket.tailEmbers ?? {};
|
|
257
|
+
const cascadeBursts = clamp(
|
|
258
|
+
Math.floor(cascade?.childBursts ?? cascade?.bursts ?? 0),
|
|
259
|
+
0,
|
|
260
|
+
ROCKET_CASCADE_LIMITS.burstCountMax,
|
|
261
|
+
);
|
|
262
|
+
const cascadeDelay = cascade?.delay ?? ROCKET_CASCADE_LIMITS.defaultDelayMs;
|
|
263
|
+
const cascadeSpreadX = cascade?.spreadX ?? ROCKET_CASCADE_LIMITS.defaultSpread;
|
|
264
|
+
const cascadeSpreadY = cascade?.spreadY ?? ROCKET_CASCADE_LIMITS.defaultSpread;
|
|
265
|
+
const childDuration = Math.max(
|
|
266
|
+
ROCKET_DURATION_LIMITS.defaultMin,
|
|
267
|
+
Number(rocket.maxLife) || ROCKET_DURATION_LIMITS.defaultMin,
|
|
268
|
+
);
|
|
269
|
+
const childDurationMin = cascade?.childDurationMin ?? childDuration;
|
|
270
|
+
const childDurationMax = cascade?.childDurationMax ?? childDurationMin;
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
simple: !!rocket.simple,
|
|
274
|
+
rocket: {
|
|
275
|
+
enabled: true,
|
|
276
|
+
colorMode: 'single',
|
|
277
|
+
singleColor: rocket.color,
|
|
278
|
+
streakLength: rocket.sparkLength,
|
|
279
|
+
finalStreakLength: rocket.sparkFinalLength,
|
|
280
|
+
thickness: rocket.thickness,
|
|
281
|
+
speed: 0,
|
|
282
|
+
durationMin: childDurationMin,
|
|
283
|
+
durationMax: childDurationMax,
|
|
284
|
+
cascade: {
|
|
285
|
+
mode: ROCKET_CASCADE_MODES.burst,
|
|
286
|
+
bursts: cascadeBursts,
|
|
287
|
+
delay: cascadeDelay,
|
|
288
|
+
spreadX: cascadeSpreadX,
|
|
289
|
+
spreadY: cascadeSpreadY,
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
rocketTailEmbers: {
|
|
293
|
+
enabled: !!tailEmbers.enabled,
|
|
294
|
+
count: tailEmbers.count,
|
|
295
|
+
brightness: tailEmbers.brightness,
|
|
296
|
+
fade: tailEmbers.fade,
|
|
297
|
+
spreadX: tailEmbers.spreadX,
|
|
298
|
+
spreadY: tailEmbers.spreadY,
|
|
299
|
+
speed: tailEmbers.speed,
|
|
300
|
+
size: tailEmbers.size,
|
|
301
|
+
},
|
|
302
|
+
burstForms: { ...burstOptions.burstForms },
|
|
303
|
+
burstSparks: { ...burstOptions.burstSparks },
|
|
304
|
+
burstCenterFlash: { ...burstOptions.burstCenterFlash },
|
|
305
|
+
burstTwinkleCloud: { ...burstOptions.burstTwinkleCloud },
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
function launchRocketWithExplicitTarget(engine, options, explicitTarget) {
|
|
311
|
+
if (shouldReduceMotion(engine)) {
|
|
312
|
+
engine.stop();
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!ensureCanvas(engine)) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const wasRunning = engine.running;
|
|
321
|
+
if (!startLoop(engine)) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
createRocket(engine, options, explicitTarget);
|
|
326
|
+
|
|
327
|
+
if (!wasRunning) {
|
|
328
|
+
engine.finishWhenDone();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
function createRocket(engine, options = {}, explicitTarget = null) {
|
|
336
|
+
const normalizedOptions = normalizeGroupedPublicOptions(options);
|
|
337
|
+
const {
|
|
338
|
+
rocket: rocketInput = {},
|
|
339
|
+
rocketTailEmbers: rocketTailEmbersInput = {},
|
|
340
|
+
} = normalizedOptions;
|
|
341
|
+
const rocketEnabled = resolveBurstEntityToggle(rocketInput.enabled) ?? engine.rocket.enabled;
|
|
342
|
+
const rocketCascadeInput = isPlainRecord(rocketInput.cascade) ? rocketInput.cascade : {};
|
|
343
|
+
const rocketCascadeMode = resolveRocketCascadeMode(rocketCascadeInput.mode);
|
|
344
|
+
const rocketCascadeChild = isPlainRecord(rocketCascadeInput.child)
|
|
345
|
+
? rocketCascadeInput.child
|
|
346
|
+
: null;
|
|
347
|
+
const sizeScale = resolveSizeScale(normalizedOptions.sizeScale, engine.sizeScale);
|
|
348
|
+
const envelopeTimeScale = resolveEnvelopeTimeScale(sizeScale);
|
|
349
|
+
const rocketCascadeChildSpeedRange = resolveOptionalPositiveRange(
|
|
350
|
+
rocketCascadeInput.childSpeedMin,
|
|
351
|
+
rocketCascadeInput.childSpeedMax,
|
|
352
|
+
);
|
|
353
|
+
const rocketCascadeChildDirectionDeg = Number.isFinite(rocketCascadeInput.childDirectionDeg)
|
|
354
|
+
? rocketCascadeInput.childDirectionDeg
|
|
355
|
+
: ROCKET_CASCADE_DIRECTION_TUNING.defaultDirectionDeg;
|
|
356
|
+
const rocketCascadeChildConeDeg = Number.isFinite(rocketCascadeInput.childConeDeg)
|
|
357
|
+
? clamp(
|
|
358
|
+
rocketCascadeInput.childConeDeg,
|
|
359
|
+
ROCKET_CASCADE_DIRECTION_TUNING.minConeDeg,
|
|
360
|
+
ROCKET_CASCADE_DIRECTION_TUNING.maxConeDeg,
|
|
361
|
+
)
|
|
362
|
+
: ROCKET_CASCADE_DIRECTION_TUNING.defaultConeDeg;
|
|
363
|
+
const cascadeBurstOverrides = isPlainRecord(rocketCascadeInput.burst) ? rocketCascadeInput.burst : null;
|
|
364
|
+
const cascadeBurstFormsInput = cascadeBurstOverrides?.burstForms;
|
|
365
|
+
const burstFormsInput = isPlainRecord(cascadeBurstFormsInput) ? cascadeBurstFormsInput : {};
|
|
366
|
+
const hasRocketColorAlias = typeof rocketInput.color === 'string';
|
|
367
|
+
const rocketColorMode = hasRocketColorAlias
|
|
368
|
+
? 'single'
|
|
369
|
+
: resolveBurstColorMode(rocketInput.colorMode ?? engine.rocket.colorMode);
|
|
370
|
+
const rocketSingleColor = normalizeHexColor(
|
|
371
|
+
hasRocketColorAlias ? rocketInput.color : rocketInput.singleColor,
|
|
372
|
+
engine.rocket.singleColor,
|
|
373
|
+
);
|
|
374
|
+
const rocketPalette = Array.isArray(rocketInput.palette) && rocketInput.palette.length > 0
|
|
375
|
+
? rocketInput.palette
|
|
376
|
+
: engine.rocket.palette;
|
|
377
|
+
const tailEmbersEnabled = resolveBurstEntityToggle(rocketTailEmbersInput.enabled)
|
|
378
|
+
?? engine.rocketTailEmbers.enabled;
|
|
379
|
+
const tailEmberCount = resolveScaledInteger(
|
|
380
|
+
rocketTailEmbersInput.count,
|
|
381
|
+
engine.rocketTailEmbers.count,
|
|
382
|
+
1,
|
|
383
|
+
sizeScale,
|
|
384
|
+
);
|
|
385
|
+
const tailEmberBrightness = rocketTailEmbersInput.brightness ?? engine.rocketTailEmbers.brightness;
|
|
386
|
+
const tailEmberFade = resolveScaledInverseOverride(
|
|
387
|
+
rocketTailEmbersInput.fade,
|
|
388
|
+
engine.rocketTailEmbers.fade,
|
|
389
|
+
envelopeTimeScale,
|
|
390
|
+
);
|
|
391
|
+
const tailEmberSpreadX = resolveScaledOverride(
|
|
392
|
+
rocketTailEmbersInput.spreadX,
|
|
393
|
+
engine.rocketTailEmbers.spreadX,
|
|
394
|
+
sizeScale,
|
|
395
|
+
);
|
|
396
|
+
const tailEmberSpreadY = resolveScaledOverride(
|
|
397
|
+
rocketTailEmbersInput.spreadY,
|
|
398
|
+
engine.rocketTailEmbers.spreadY,
|
|
399
|
+
sizeScale,
|
|
400
|
+
);
|
|
401
|
+
const tailEmberSpeed = resolveScaledOverride(
|
|
402
|
+
rocketTailEmbersInput.speed,
|
|
403
|
+
engine.rocketTailEmbers.speed,
|
|
404
|
+
envelopeTimeScale,
|
|
405
|
+
);
|
|
406
|
+
const tailEmberSize = resolveScaledOverride(
|
|
407
|
+
rocketTailEmbersInput.size,
|
|
408
|
+
engine.rocketTailEmbers.size,
|
|
409
|
+
sizeScale,
|
|
410
|
+
);
|
|
411
|
+
const hasExplicitTarget = Number.isFinite(explicitTarget?.x) && Number.isFinite(explicitTarget?.y);
|
|
412
|
+
const hasExplicitStart = Number.isFinite(explicitTarget?.startX) && Number.isFinite(explicitTarget?.startY);
|
|
413
|
+
const targetX = hasExplicitTarget
|
|
414
|
+
? (hasExplicitStart ? explicitTarget.x : clamp(explicitTarget.x, 0, engine.width))
|
|
415
|
+
: randomRange(engine.width * ROCKET_TARGET_TUNING.targetXMinRatio, engine.width * ROCKET_TARGET_TUNING.targetXMaxRatio) + randomRange(-ROCKET_TARGET_TUNING.targetXJitter, ROCKET_TARGET_TUNING.targetXJitter);
|
|
416
|
+
const targetY = hasExplicitTarget
|
|
417
|
+
? (hasExplicitStart ? explicitTarget.y : clamp(explicitTarget.y, 0, engine.height))
|
|
418
|
+
: randomRange(engine.height * ROCKET_TARGET_TUNING.targetYMinRatio, engine.height * ROCKET_TARGET_TUNING.targetYMaxRatio);
|
|
419
|
+
const startX = hasExplicitStart
|
|
420
|
+
? explicitTarget.startX
|
|
421
|
+
: hasExplicitTarget
|
|
422
|
+
? clamp(targetX + randomRange(-ROCKET_TARGET_TUNING.explicitStartJitterX, ROCKET_TARGET_TUNING.explicitStartJitterX), 0, engine.width)
|
|
423
|
+
: randomRange(engine.width * 0.25, engine.width * 0.75);
|
|
424
|
+
const startY = hasExplicitStart
|
|
425
|
+
? explicitTarget.startY
|
|
426
|
+
: hasExplicitTarget
|
|
427
|
+
? engine.height + ROCKET_TARGET_TUNING.explicitStartOffsetY
|
|
428
|
+
: engine.height + randomRange(ROCKET_TARGET_TUNING.fallbackStartOffsetYMin, ROCKET_TARGET_TUNING.fallbackStartOffsetYMax);
|
|
429
|
+
const dx = targetX - startX;
|
|
430
|
+
const dy = targetY - startY;
|
|
431
|
+
const durationMin = resolveScaledOverride(
|
|
432
|
+
rocketInput.durationMin,
|
|
433
|
+
ROCKET_DURATION_LIMITS.defaultMin,
|
|
434
|
+
envelopeTimeScale,
|
|
435
|
+
);
|
|
436
|
+
const durationMax = resolveScaledOverride(
|
|
437
|
+
rocketInput.durationMax,
|
|
438
|
+
ROCKET_DURATION_LIMITS.defaultMax,
|
|
439
|
+
envelopeTimeScale,
|
|
440
|
+
);
|
|
441
|
+
const fallbackDuration = hasExplicitTarget
|
|
442
|
+
? ((durationMin + durationMax) / 2)
|
|
443
|
+
: randomRange(durationMin, durationMax);
|
|
444
|
+
const distance = Math.hypot(dx, dy);
|
|
445
|
+
const rocketSpeed = rocketInput.speed ?? engine.rocket.speed;
|
|
446
|
+
const duration = resolveRocketDuration(distance, fallbackDuration, rocketSpeed);
|
|
447
|
+
const totalDx = dx;
|
|
448
|
+
const totalDy = dy;
|
|
449
|
+
const totalDistanceSq = (totalDx * totalDx) + (totalDy * totalDy);
|
|
450
|
+
const normalizedDistance = distance > ROCKET_FLIGHT_GUARDS.minDistanceEpsilon ? distance : 1;
|
|
451
|
+
const directionX = distance > ROCKET_FLIGHT_GUARDS.minDistanceEpsilon ? dx / normalizedDistance : 0;
|
|
452
|
+
const directionY = distance > ROCKET_FLIGHT_GUARDS.minDistanceEpsilon ? dy / normalizedDistance : -1;
|
|
453
|
+
const launchColor = resolveBurstBaseColor(
|
|
454
|
+
engine,
|
|
455
|
+
rocketPalette ?? burstFormsInput.palette ?? engine.burstForms.palette,
|
|
456
|
+
rocketColorMode,
|
|
457
|
+
rocketSingleColor,
|
|
458
|
+
);
|
|
459
|
+
const travelSpeed = distance > ROCKET_FLIGHT_GUARDS.minDistanceEpsilon
|
|
460
|
+
? distance / duration
|
|
461
|
+
: 0;
|
|
462
|
+
const rotationAngle = Math.atan2(directionY, directionX);
|
|
463
|
+
const sparkLength = resolveScaledOverride(
|
|
464
|
+
rocketInput.streakLength,
|
|
465
|
+
engine.rocket.streakLength,
|
|
466
|
+
sizeScale,
|
|
467
|
+
);
|
|
468
|
+
const sparkFinalLength = resolveScaledOverride(
|
|
469
|
+
rocketInput.finalStreakLength,
|
|
470
|
+
engine.rocket.finalStreakLength,
|
|
471
|
+
sizeScale,
|
|
472
|
+
);
|
|
473
|
+
const rocketThickness = resolveScaledOverride(
|
|
474
|
+
rocketInput.thickness,
|
|
475
|
+
engine.rocket.thickness,
|
|
476
|
+
sizeScale,
|
|
477
|
+
);
|
|
478
|
+
const burstCurrentConfig = {
|
|
479
|
+
sizeScale: engine.sizeScale,
|
|
480
|
+
burstForms: engine.burstForms,
|
|
481
|
+
burstSparks: engine.burstSparks,
|
|
482
|
+
burstCenterFlash: engine.burstCenterFlash,
|
|
483
|
+
burstTwinkleCloud: engine.burstTwinkleCloud,
|
|
484
|
+
};
|
|
485
|
+
const baseBurstBuildInput = {
|
|
486
|
+
burstForms: normalizedOptions.burstForms,
|
|
487
|
+
burstSparks: normalizedOptions.burstSparks,
|
|
488
|
+
burstCenterFlash: normalizedOptions.burstCenterFlash,
|
|
489
|
+
burstTwinkleCloud: normalizedOptions.burstTwinkleCloud,
|
|
490
|
+
sizeScale,
|
|
491
|
+
};
|
|
492
|
+
const burstOptions = buildBurstOptions(baseBurstBuildInput, burstCurrentConfig);
|
|
493
|
+
if (!rocketEnabled) {
|
|
494
|
+
launchBurst(engine, targetX, targetY, burstOptions);
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const cascadeBurst = cascadeBurstOverrides
|
|
499
|
+
? buildBurstOptions(
|
|
500
|
+
{
|
|
501
|
+
...baseBurstBuildInput,
|
|
502
|
+
burstForms: {
|
|
503
|
+
...baseBurstBuildInput.burstForms,
|
|
504
|
+
...cascadeBurstOverrides.burstForms,
|
|
505
|
+
},
|
|
506
|
+
burstSparks: {
|
|
507
|
+
...baseBurstBuildInput.burstSparks,
|
|
508
|
+
...cascadeBurstOverrides.burstSparks,
|
|
509
|
+
},
|
|
510
|
+
burstCenterFlash: {
|
|
511
|
+
...baseBurstBuildInput.burstCenterFlash,
|
|
512
|
+
...cascadeBurstOverrides.burstCenterFlash,
|
|
513
|
+
},
|
|
514
|
+
burstTwinkleCloud: {
|
|
515
|
+
...baseBurstBuildInput.burstTwinkleCloud,
|
|
516
|
+
...cascadeBurstOverrides.burstTwinkleCloud,
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
burstCurrentConfig,
|
|
520
|
+
)
|
|
521
|
+
: null;
|
|
522
|
+
const cascadeBurstCount = clamp(
|
|
523
|
+
Math.floor((rocketCascadeInput.bursts ?? 0) * sizeScale),
|
|
524
|
+
0,
|
|
525
|
+
ROCKET_CASCADE_LIMITS.burstCountMax,
|
|
526
|
+
);
|
|
527
|
+
const cascadeChildBurstCount = clamp(
|
|
528
|
+
Math.floor((rocketCascadeInput.childBursts ?? rocketCascadeInput.bursts ?? 0) * sizeScale),
|
|
529
|
+
0,
|
|
530
|
+
ROCKET_CASCADE_LIMITS.burstCountMax,
|
|
531
|
+
);
|
|
532
|
+
const burstCascadeDelayMs = resolveScaledOverride(
|
|
533
|
+
rocketCascadeInput.delay,
|
|
534
|
+
ROCKET_CASCADE_LIMITS.defaultDelayMs,
|
|
535
|
+
envelopeTimeScale,
|
|
536
|
+
);
|
|
537
|
+
const cascadeChildDurationMin = resolveScaledOverride(
|
|
538
|
+
rocketCascadeInput.childDurationMin,
|
|
539
|
+
0,
|
|
540
|
+
envelopeTimeScale,
|
|
541
|
+
);
|
|
542
|
+
const cascadeChildDurationMax = resolveScaledOverride(
|
|
543
|
+
rocketCascadeInput.childDurationMax,
|
|
544
|
+
cascadeChildDurationMin,
|
|
545
|
+
envelopeTimeScale,
|
|
546
|
+
);
|
|
547
|
+
const boundedCascadeChildDurationMax = Math.max(
|
|
548
|
+
cascadeChildDurationMin,
|
|
549
|
+
cascadeChildDurationMax,
|
|
550
|
+
);
|
|
551
|
+
const burstCascadeSpreadX = resolveScaledOverride(
|
|
552
|
+
rocketCascadeInput.spreadX,
|
|
553
|
+
ROCKET_CASCADE_LIMITS.defaultSpread,
|
|
554
|
+
sizeScale,
|
|
555
|
+
);
|
|
556
|
+
const burstCascadeSpreadY = resolveScaledOverride(
|
|
557
|
+
rocketCascadeInput.spreadY,
|
|
558
|
+
ROCKET_CASCADE_LIMITS.defaultSpread,
|
|
559
|
+
sizeScale,
|
|
560
|
+
);
|
|
561
|
+
const cascade = {
|
|
562
|
+
mode: rocketCascadeMode,
|
|
563
|
+
bursts: cascadeBurstCount,
|
|
564
|
+
childBursts: cascadeChildBurstCount,
|
|
565
|
+
childSpeedMin: rocketCascadeChildSpeedRange?.min ?? null,
|
|
566
|
+
childSpeedMax: rocketCascadeChildSpeedRange?.max ?? null,
|
|
567
|
+
childDurationMin: cascadeChildDurationMin > 0 ? cascadeChildDurationMin : null,
|
|
568
|
+
childDurationMax: boundedCascadeChildDurationMax > 0 ? boundedCascadeChildDurationMax : null,
|
|
569
|
+
childDirectionDeg: rocketCascadeChildDirectionDeg,
|
|
570
|
+
childConeDeg: rocketCascadeChildConeDeg,
|
|
571
|
+
delay: burstCascadeDelayMs,
|
|
572
|
+
spreadX: burstCascadeSpreadX,
|
|
573
|
+
spreadY: burstCascadeSpreadY,
|
|
574
|
+
burst: cascadeBurst ?? burstOptions,
|
|
575
|
+
child: rocketCascadeChild,
|
|
576
|
+
};
|
|
577
|
+
const rocket = {
|
|
578
|
+
x: startX,
|
|
579
|
+
y: startY,
|
|
580
|
+
prevX: startX,
|
|
581
|
+
prevY: startY,
|
|
582
|
+
startX,
|
|
583
|
+
startY,
|
|
584
|
+
targetX,
|
|
585
|
+
targetY,
|
|
586
|
+
totalDx,
|
|
587
|
+
totalDy,
|
|
588
|
+
totalDistanceSq,
|
|
589
|
+
hasExplicitTarget,
|
|
590
|
+
hasExplicitStart,
|
|
591
|
+
vx: dx / duration,
|
|
592
|
+
vy: dy / duration,
|
|
593
|
+
travelSpeed,
|
|
594
|
+
life: duration,
|
|
595
|
+
maxLife: duration,
|
|
596
|
+
color: launchColor,
|
|
597
|
+
directionX,
|
|
598
|
+
directionY,
|
|
599
|
+
rotationAngle,
|
|
600
|
+
sparkLength,
|
|
601
|
+
sparkFinalLength,
|
|
602
|
+
thickness: rocketThickness,
|
|
603
|
+
trailTimer: 0,
|
|
604
|
+
tailEmberAccumulator: 0,
|
|
605
|
+
tailEmbers: {
|
|
606
|
+
enabled: !!tailEmbersEnabled,
|
|
607
|
+
count: tailEmberCount,
|
|
608
|
+
brightness: tailEmberBrightness,
|
|
609
|
+
fade: tailEmberFade,
|
|
610
|
+
spreadX: tailEmberSpreadX,
|
|
611
|
+
spreadY: tailEmberSpreadY,
|
|
612
|
+
speed: tailEmberSpeed,
|
|
613
|
+
size: tailEmberSize,
|
|
614
|
+
},
|
|
615
|
+
burst: burstOptions,
|
|
616
|
+
cascade,
|
|
617
|
+
simple: !!normalizedOptions.simple,
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
engine.rockets.push(rocket);
|
|
621
|
+
return rocket;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
export function launchRocket(engine, options = {}) {
|
|
625
|
+
const count = options.count ?? 1;
|
|
626
|
+
|
|
627
|
+
for (let index = 0; index < count; index += 1) {
|
|
628
|
+
createRocket(engine, options);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export function launchRocketAt(engine, targetX, targetY, options = {}) {
|
|
633
|
+
if (!Number.isFinite(targetX) || !Number.isFinite(targetY)) {
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return launchRocketWithExplicitTarget(engine, options, {
|
|
638
|
+
x: targetX,
|
|
639
|
+
y: targetY,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export function launchRocketFromTo(engine, startX, startY, targetX, targetY, options = {}) {
|
|
644
|
+
if (
|
|
645
|
+
!Number.isFinite(startX)
|
|
646
|
+
|| !Number.isFinite(startY)
|
|
647
|
+
|| !Number.isFinite(targetX)
|
|
648
|
+
|| !Number.isFinite(targetY)
|
|
649
|
+
) {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return launchRocketWithExplicitTarget(engine, options, {
|
|
654
|
+
startX,
|
|
655
|
+
startY,
|
|
656
|
+
x: targetX,
|
|
657
|
+
y: targetY,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export function scheduleCascadeBursts(engine, rocket) {
|
|
662
|
+
const cascade = rocket.cascade ?? {};
|
|
663
|
+
const cascadeMode = resolveRocketCascadeMode(cascade.mode);
|
|
664
|
+
const cascadeBurstCount = cascade.bursts ?? 0;
|
|
665
|
+
if (cascadeBurstCount <= 0) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const burstOptions = cascade.burst ?? rocket.burst;
|
|
670
|
+
const spreadX = cascade.spreadX ?? ROCKET_CASCADE_LIMITS.defaultSpread;
|
|
671
|
+
const spreadY = cascade.spreadY ?? ROCKET_CASCADE_LIMITS.defaultSpread;
|
|
672
|
+
const delay = cascade.delay ?? ROCKET_CASCADE_LIMITS.defaultDelayMs;
|
|
673
|
+
const originX = rocket.x;
|
|
674
|
+
const originY = rocket.y;
|
|
675
|
+
const cascadeChildOptions = isPlainRecord(cascade.child)
|
|
676
|
+
? cascade.child
|
|
677
|
+
: resolveDefaultCascadeChildLaunchOptions(rocket, cascade);
|
|
678
|
+
|
|
679
|
+
if (cascadeMode === ROCKET_CASCADE_MODES.rocket) {
|
|
680
|
+
for (let index = 0; index < cascadeBurstCount; index += 1) {
|
|
681
|
+
const { x, y } = resolveCascadeTargetPoint(
|
|
682
|
+
engine,
|
|
683
|
+
originX,
|
|
684
|
+
originY,
|
|
685
|
+
spreadX,
|
|
686
|
+
spreadY,
|
|
687
|
+
cascade,
|
|
688
|
+
);
|
|
689
|
+
const distance = Math.hypot(x - originX, y - originY);
|
|
690
|
+
const childLaunchOptions = resolveCascadeChildLaunchOptions(
|
|
691
|
+
cascadeChildOptions,
|
|
692
|
+
cascade,
|
|
693
|
+
distance,
|
|
694
|
+
);
|
|
695
|
+
launchRocketFromTo(engine, originX, originY, x, y, childLaunchOptions);
|
|
696
|
+
}
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
for (let index = 0; index < cascadeBurstCount; index += 1) {
|
|
701
|
+
scheduleTimeout(engine, () => {
|
|
702
|
+
if (!engine.running) {
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const { x, y } = resolveCascadeRandomTargetPoint(
|
|
707
|
+
engine,
|
|
708
|
+
originX,
|
|
709
|
+
originY,
|
|
710
|
+
spreadX,
|
|
711
|
+
spreadY,
|
|
712
|
+
);
|
|
713
|
+
|
|
714
|
+
launchBurst(engine, x, y, burstOptions);
|
|
715
|
+
}, resolveScaledDelay(engine, delay * (index + 1)));
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
export function spawnRocketTailEmbers(engine, rocket, spawnCount = null) {
|
|
720
|
+
const tailEmbers = rocket.tailEmbers;
|
|
721
|
+
if (!tailEmbers?.enabled) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const directionX = rocket.directionX;
|
|
726
|
+
const directionY = rocket.directionY;
|
|
727
|
+
const lateralX = -directionY;
|
|
728
|
+
const lateralY = directionX;
|
|
729
|
+
const baseSpeed = Math.max(ROCKET_TAIL_EMBER_TUNING.speedMin, tailEmbers.speed ?? engine.rocketTailEmbers.speed);
|
|
730
|
+
const tailEmberBrightness = tailEmbers.brightness ?? engine.rocketTailEmbers.brightness;
|
|
731
|
+
const tailEmberBrightnessScale = clamp(tailEmberBrightness / WHITE_BRIGHTNESS_REFERENCE, 0, 1);
|
|
732
|
+
const emberFade = Math.max(ROCKET_TAIL_EMBER_TUNING.fadeMin, tailEmbers.fade ?? engine.rocketTailEmbers.fade);
|
|
733
|
+
const emberCount = Math.max(ROCKET_TAIL_EMBER_TUNING.countMin, Math.floor(tailEmbers.count ?? engine.rocketTailEmbers.count));
|
|
734
|
+
const emberSpreadX = Math.max(ROCKET_TAIL_EMBER_TUNING.spreadMin, tailEmbers.spreadX ?? engine.rocketTailEmbers.spreadX);
|
|
735
|
+
const emberSpreadY = Math.max(ROCKET_TAIL_EMBER_TUNING.spreadMin, tailEmbers.spreadY ?? engine.rocketTailEmbers.spreadY);
|
|
736
|
+
const emberSize = Math.max(ROCKET_TAIL_EMBER_TUNING.sizeMin, tailEmbers.size ?? engine.rocketTailEmbers.size);
|
|
737
|
+
const emberLifeBase = ROCKET_TAIL_EMBER_TUNING.lifeBaseDivisor / emberFade;
|
|
738
|
+
const previousX = rocket.prevX;
|
|
739
|
+
const previousY = rocket.prevY;
|
|
740
|
+
const travelX = rocket.x - previousX;
|
|
741
|
+
const travelY = rocket.y - previousY;
|
|
742
|
+
const travelDistance = Math.hypot(travelX, travelY);
|
|
743
|
+
const trailBlend = clamp(travelDistance / ROCKET_TAIL_EMBER_TUNING.trailBlendDistance, ROCKET_TAIL_EMBER_TUNING.trailBlendMin, 1);
|
|
744
|
+
const requestedCount = spawnCount == null
|
|
745
|
+
? Math.max(ROCKET_TAIL_EMBER_TUNING.requestedCountMin, Math.min(emberCount, Math.ceil(travelDistance * (ROCKET_TAIL_EMBER_TUNING.requestedCountFactorBase + (emberCount * ROCKET_TAIL_EMBER_TUNING.requestedCountFactorPerEmber)))))
|
|
746
|
+
: Math.floor(spawnCount);
|
|
747
|
+
const emberSpawnCount = Math.max(ROCKET_TAIL_EMBER_TUNING.requestedCountMin, requestedCount);
|
|
748
|
+
const emberCloudLength = Math.max(
|
|
749
|
+
ROCKET_TAIL_EMBER_TUNING.cloudLengthMin,
|
|
750
|
+
(emberSpreadY * ROCKET_TAIL_EMBER_TUNING.cloudLengthSpreadYFactor) + (emberSpreadX * ROCKET_TAIL_EMBER_TUNING.cloudLengthSpreadXFactor) + (baseSpeed * ROCKET_TAIL_EMBER_TUNING.cloudLengthSpeedFactor) + (emberSize * ROCKET_TAIL_EMBER_TUNING.cloudLengthSizeFactor),
|
|
751
|
+
);
|
|
752
|
+
const emberCloudWidth = Math.max(
|
|
753
|
+
ROCKET_TAIL_EMBER_TUNING.cloudWidthMin,
|
|
754
|
+
(emberSpreadX * ROCKET_TAIL_EMBER_TUNING.cloudWidthSpreadXFactor) + (emberSize * ROCKET_TAIL_EMBER_TUNING.cloudWidthSizeFactor),
|
|
755
|
+
);
|
|
756
|
+
|
|
757
|
+
for (let index = 0; index < emberSpawnCount; index += 1) {
|
|
758
|
+
const progress = randomRange(ROCKET_TAIL_EMBER_TUNING.progressMin, ROCKET_TAIL_EMBER_TUNING.progressMax);
|
|
759
|
+
const pathX = previousX + (travelX * progress);
|
|
760
|
+
const pathY = previousY + (travelY * progress);
|
|
761
|
+
const cloudDepth = Math.pow(randomRange(ROCKET_TAIL_EMBER_TUNING.depthPowMin, ROCKET_TAIL_EMBER_TUNING.depthPowMax), ROCKET_TAIL_EMBER_TUNING.depthPowExponent) * emberCloudLength;
|
|
762
|
+
const cloudLateral = randomRange(-emberCloudWidth, emberCloudWidth);
|
|
763
|
+
const cloudJitter = randomRange(-emberCloudWidth * ROCKET_TAIL_EMBER_TUNING.cloudJitterFactor, emberCloudWidth * ROCKET_TAIL_EMBER_TUNING.cloudJitterFactor);
|
|
764
|
+
const cloudOffset = randomRange(-emberCloudWidth * ROCKET_TAIL_EMBER_TUNING.cloudOffsetFactor, emberCloudWidth * ROCKET_TAIL_EMBER_TUNING.cloudOffsetFactor) * (ROCKET_TAIL_EMBER_TUNING.cloudOffsetBase + (ROCKET_TAIL_EMBER_TUNING.cloudOffsetTrailBlendFactor * trailBlend));
|
|
765
|
+
const trailPush = Math.max(ROCKET_TAIL_EMBER_TUNING.trailPushMin, cloudDepth + cloudJitter);
|
|
766
|
+
const lateralOffset = cloudLateral + cloudOffset;
|
|
767
|
+
const jitterX = randomRange(-ROCKET_TAIL_EMBER_TUNING.positionJitterFactor, ROCKET_TAIL_EMBER_TUNING.positionJitterFactor) * emberSize;
|
|
768
|
+
const jitterY = randomRange(-ROCKET_TAIL_EMBER_TUNING.positionJitterFactor, ROCKET_TAIL_EMBER_TUNING.positionJitterFactor) * emberSize;
|
|
769
|
+
const life = clamp(emberLifeBase * randomRange(ROCKET_TAIL_EMBER_TUNING.lifeRandomMin, ROCKET_TAIL_EMBER_TUNING.lifeRandomMax), ROCKET_TAIL_EMBER_TUNING.lifeMin, ROCKET_TAIL_EMBER_TUNING.lifeMax);
|
|
770
|
+
const spawnSpeed = randomRange(ROCKET_TAIL_EMBER_TUNING.speedRandomMin, ROCKET_TAIL_EMBER_TUNING.speedRandomMax) * baseSpeed;
|
|
771
|
+
const distanceScale = ROCKET_TAIL_EMBER_TUNING.distanceScaleBase + (trailBlend * ROCKET_TAIL_EMBER_TUNING.distanceScaleTrailBlendFactor);
|
|
772
|
+
|
|
773
|
+
const particle = engine.emberPool.length > 0 ? engine.emberPool.pop() : {};
|
|
774
|
+
|
|
775
|
+
particle.type = 'rocketEmber';
|
|
776
|
+
particle.x = pathX - (directionX * trailPush) + (lateralX * lateralOffset) + jitterX;
|
|
777
|
+
particle.y = pathY - (directionY * trailPush) + (lateralY * lateralOffset) + jitterY;
|
|
778
|
+
particle.vx = (-directionX * spawnSpeed * distanceScale) + (lateralX * lateralOffset * ROCKET_TAIL_EMBER_TUNING.lateralVelocityFactor);
|
|
779
|
+
particle.vy = (-directionY * spawnSpeed * distanceScale) + (lateralY * lateralOffset * ROCKET_TAIL_EMBER_TUNING.lateralVelocityFactor);
|
|
780
|
+
particle.gravity = ROCKET_TAIL_EMBER_TUNING.gravityBase + (baseSpeed * ROCKET_TAIL_EMBER_TUNING.gravitySpeedFactor);
|
|
781
|
+
particle.life = life;
|
|
782
|
+
particle.maxLife = life;
|
|
783
|
+
particle.radius = emberSize * randomRange(ROCKET_TAIL_EMBER_TUNING.radiusRandomMin, ROCKET_TAIL_EMBER_TUNING.radiusRandomMax);
|
|
784
|
+
particle.brightness = tailEmberBrightness;
|
|
785
|
+
particle.brightnessScale = tailEmberBrightnessScale;
|
|
786
|
+
particle.drag = ROCKET_TAIL_EMBER_TUNING.drag;
|
|
787
|
+
particle.simple = rocket.simple;
|
|
788
|
+
|
|
789
|
+
engine.particles.push(particle);
|
|
790
|
+
}
|
|
791
|
+
}
|