@ganziliang/desktop-pet 0.1.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.
@@ -0,0 +1,775 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 觉醒技能编排
5
+ *
6
+ * 每套服装对应一个觉醒,本质就是「往特效引擎的时间轴上排事件」。
7
+ * 统一节奏(毫秒):
8
+ * 0 ~ 1400 蓄力:黑屏切入 + 魔法阵 + 向心光点(此时主进程正在放大窗口)
9
+ * 1700 亮相:暗幕退 + 主爆发(角色出现在被放大的画面正中)
10
+ * 1700 ~ 4200 持续演出 + 技能名落字
11
+ * 4200 ~ 5400 收招:暗幕再进 + 窗口收回
12
+ *
13
+ * 窗口放大/收回由主进程做,暗幕负责把「窗口尺寸跳变」这一帧遮住 ——
14
+ * 这就是 DNF 黑屏切入的原始手法,看不到窗口在变形。
15
+ *
16
+ * 坐标一律用 a.point() 现取(角色被放大、窗口被放大都会改变它),
17
+ * 事件在触发的那一刻才计算坐标,所以扩窗过程跑完后再取就是最终位置。
18
+ */
19
+
20
+ /**
21
+ * 演出画面里文字的安全位置。
22
+ * 角色在觉醒时被顶到画面下方(.stage.awakening .anchor 的 bottom),
23
+ * 所以顶部这一条留白就是台词和技能名的位置 —— 写死成相对窗口高度的比例,
24
+ * 不改角色大小、也不改窗口比例时都不会压到脸。
25
+ */
26
+ const TEXT_Y = {
27
+ cast: (p) => p.h * 0.075,
28
+ name: (p) => p.h * 0.155,
29
+ };
30
+
31
+ const AWAKENS = {
32
+ /* ==================================================== 圣光 · 炽天使 */
33
+ seraph: {
34
+ id: 'seraph',
35
+ outfit: '圣光 · 炽天使',
36
+ skill: '圣裁之光',
37
+ cast: '圣光啊,请回应我的呼唤——',
38
+ dimAlpha: 0.94,
39
+ revealAt: 2000,
40
+ closeAt: 4400,
41
+ duration: 5700,
42
+ // 序列帧节拍:帧号由渲染层按这个排点写死,和特效时间轴对死。
43
+ // 蓄力 4 帧 × 360ms(560→1640),爆发 4 帧 × 300ms(2000→2900),之后停在最后一帧收招。
44
+ sheet: { charge: { at: 560, step: 360 }, burst: { at: 2000, step: 300 } },
45
+ palette: {
46
+ core: '#fffdf2',
47
+ main: '#ffd75e',
48
+ deep: '#ff9c3a',
49
+ glow: '#ffeaa8',
50
+ text: '#fff3c4',
51
+ stroke: '#3a2408',
52
+ },
53
+
54
+ build(a) {
55
+ const fx = a.fx;
56
+ const P = a.point;
57
+ const c = this.palette;
58
+ const ev = [];
59
+ const at = (t, fn) => ev.push([t, fn]);
60
+ const reveal = this.revealAt;
61
+
62
+ // --- 蓄力 ---
63
+ at(120, () => {
64
+ const p = P();
65
+ fx.sigil(p.footX, p.footY, { size: 470, color: c.main, life: 3400, spin: 0.45 });
66
+ });
67
+ at(420, () => {
68
+ const p = P();
69
+ fx.text(p.cx, TEXT_Y.cast(p), this.cast, { size: 30, color: c.glow, stroke: c.stroke, life: 1.5 });
70
+ });
71
+ for (let i = 0; i < 5; i++) {
72
+ at(320 + i * 230, () => {
73
+ const p = P();
74
+ fx.vortex(p.cx, p.cy, {
75
+ count: 14,
76
+ radius: 320 - i * 34,
77
+ speed: 215,
78
+ color: c.glow,
79
+ size: 20,
80
+ life: 1.0,
81
+ spin: 0.85,
82
+ });
83
+ });
84
+ }
85
+ at(620, () => {
86
+ const p = P();
87
+ fx.helix(p.footX, p.footY, {
88
+ count: 22,
89
+ radius: 115,
90
+ color: c.glow,
91
+ speed: 190,
92
+ size: 24,
93
+ life: 1.6,
94
+ });
95
+ });
96
+ at(1050, () => {
97
+ const p = P();
98
+ fx.pillar(p.footX, p.footY, {
99
+ w: 155,
100
+ h: 780,
101
+ color: c.main,
102
+ core: c.core,
103
+ life: 2600,
104
+ });
105
+ fx.ring(p.footX, p.footY, { r0: 20, r1: 340, width: 10, color: c.core, life: 0.9 });
106
+ });
107
+
108
+ // --- 亮相 + 主爆发 ---
109
+ at(reveal, () => {
110
+ const p = P();
111
+ fx.dim(0.34, 380, c.stroke, true);
112
+ fx.burst(p.cx, p.cy, {
113
+ count: 34,
114
+ size: 38,
115
+ color: c.glow,
116
+ speed0: 180,
117
+ speed1: 780,
118
+ life: 1.25,
119
+ grow: 0.5,
120
+ alpha: 0.66,
121
+ });
122
+ fx.ring(p.footX, p.footY, {
123
+ r0: 30,
124
+ r1: 580,
125
+ width: 16,
126
+ color: c.core,
127
+ life: 0.85,
128
+ inner: true,
129
+ });
130
+ // 光翼:四条斜向光带拼出翅膀轮廓
131
+ fx.beam(p.cx, p.cy + 30, p.cx - 240, p.cy - 260, { width: 130, color: c.main, core: c.core, life: 1.5 });
132
+ fx.beam(p.cx, p.cy + 30, p.cx + 240, p.cy - 260, { width: 130, color: c.main, core: c.core, life: 1.5 });
133
+ fx.beam(p.cx - 60, p.cy + 50, p.cx - 310, p.cy - 110, { width: 95, color: c.glow, life: 1.4 });
134
+ fx.beam(p.cx + 60, p.cy + 50, p.cx + 310, p.cy - 110, { width: 95, color: c.glow, life: 1.4 });
135
+ // 冲天光柱:蓄力那根已经弱下去了,亮相这一根才是「圣光落下」的主视觉
136
+ fx.pillar(p.footX, p.footY, { w: 210, h: 1000, color: c.main, core: c.core, life: 1700 });
137
+ fx.fallStream({
138
+ x: p.cx,
139
+ y: -60,
140
+ spreadX: p.w * 0.95,
141
+ spreadY: 220,
142
+ color: c.glow,
143
+ tex: fx.tex.feather(c.glow, 72),
144
+ dur: 2700,
145
+ rate: 20,
146
+ size: 38,
147
+ life: 3.4,
148
+ vy: 260,
149
+ });
150
+ });
151
+ at(reveal + 480, () => {
152
+ const p = P();
153
+ // 从地面向上喷的祈光,不要擞在脸上 —— 擞在胸前会直接把脸洗白
154
+ fx.burst(p.footX, p.footY, {
155
+ count: 22,
156
+ size: 30,
157
+ color: c.core,
158
+ a0: -Math.PI * 0.78,
159
+ a1: -Math.PI * 0.22,
160
+ speed0: 170,
161
+ speed1: 430,
162
+ grav: 520,
163
+ life: 1.5,
164
+ grow: 0.4,
165
+ alpha: 0.62,
166
+ });
167
+ });
168
+
169
+ // --- 技能名 + 二段冲击 ---
170
+ at(2650, () => {
171
+ const p = P();
172
+ fx.text(p.cx, TEXT_Y.name(p), this.skill, {
173
+ size: 54,
174
+ color: c.text,
175
+ stroke: c.stroke,
176
+ life: 2.6,
177
+ });
178
+ fx.ring(p.footX, p.footY, { r0: 40, r1: 660, width: 12, color: c.main, life: 0.9 });
179
+ });
180
+ at(3300, () => {
181
+ const p = P();
182
+ fx.sigil(p.footX, p.footY, { size: 640, color: c.core, life: 1800, spin: -0.7 });
183
+ });
184
+ at(this.closeAt - 300, () => {
185
+ const p = P();
186
+ fx.burst(p.cx, p.cy, {
187
+ count: 44,
188
+ size: 52,
189
+ color: c.glow,
190
+ speed0: 130,
191
+ speed1: 440,
192
+ life: 1.0,
193
+ });
194
+ });
195
+
196
+ return { events: ev, duration: this.duration };
197
+ },
198
+ },
199
+
200
+ /* ================================================= 暗影 · 血月狂想 */
201
+ nocturne: {
202
+ id: 'nocturne',
203
+ outfit: '暗影 · 血月狂想',
204
+ skill: '夜狱降临',
205
+ cast: '以血为契,睁开那只眼——',
206
+ dimAlpha: 0.97,
207
+ revealAt: 2000,
208
+ closeAt: 4400,
209
+ duration: 5700,
210
+ // 序列帧节拍:帧号由渲染层按这个排点写死,和特效时间轴对死。
211
+ // 蓄力 4 帧 × 360ms(560→1640),爆发 4 帧 × 300ms(2000→2900),之后停在最后一帧收招。
212
+ sheet: { charge: { at: 560, step: 360 }, burst: { at: 2000, step: 300 } },
213
+ palette: {
214
+ core: '#ffe2e6',
215
+ main: '#c81e3c',
216
+ deep: '#5c0f26',
217
+ glow: '#ff7d96',
218
+ text: '#ffd9e0',
219
+ stroke: '#1a0208',
220
+ },
221
+
222
+ build(a) {
223
+ const fx = a.fx;
224
+ const P = a.point;
225
+ const c = this.palette;
226
+ const ev = [];
227
+ const at = (t, fn) => ev.push([t, fn]);
228
+ const reveal = this.revealAt;
229
+
230
+ // --- 蓄力:血月升起 + 黑雾压下来 ---
231
+ at(80, () => {
232
+ const p = P();
233
+ fx.orb(p.cx + 30, p.cy - p.h * 0.16, {
234
+ size: p.w * 0.9,
235
+ color: c.deep,
236
+ alpha: 0.9,
237
+ fade: 'in',
238
+ norm: true,
239
+ life: 4200,
240
+ vy: -10,
241
+ });
242
+ });
243
+ at(220, () => {
244
+ const p = P();
245
+ fx.orb(p.cx + 30, p.cy - p.h * 0.1, {
246
+ size: 460,
247
+ color: c.main,
248
+ alpha: 0.95,
249
+ vy: -14,
250
+ life: 4000,
251
+ });
252
+ });
253
+ at(420, () => {
254
+ const p = P();
255
+ fx.text(p.cx, TEXT_Y.cast(p), this.cast, { size: 30, color: c.glow, stroke: c.stroke, life: 1.5 });
256
+ fx.sigil(p.footX, p.footY, { size: 480, color: c.main, life: 3600, spin: -0.4 });
257
+ });
258
+ for (let i = 0; i < 5; i++) {
259
+ at(520 + i * 220, () => {
260
+ const p = P();
261
+ fx.vortex(p.cx, p.cy, {
262
+ count: 10,
263
+ radius: 300 - i * 32,
264
+ speed: 230,
265
+ color: c.main,
266
+ size: 19,
267
+ life: 0.95,
268
+ spin: 1.05,
269
+ });
270
+ });
271
+ }
272
+
273
+ // --- 亮相 ---
274
+ at(reveal, () => {
275
+ const p = P();
276
+ fx.dim(0.34, 380, c.stroke, true);
277
+ fx.ring(p.footX, p.footY, { r0: 30, r1: 620, width: 18, color: c.main, life: 0.9, inner: true });
278
+ fx.burst(p.cx, p.cy, {
279
+ count: 30,
280
+ size: 36,
281
+ color: c.glow,
282
+ speed0: 200,
283
+ speed1: 720,
284
+ life: 1.1,
285
+ alpha: 0.7,
286
+ });
287
+ fx.burst(p.cx, p.cy, {
288
+ count: 22,
289
+ size: 44,
290
+ color: c.main,
291
+ tex: fx.tex.shard(c.main, 72),
292
+ speed0: 260,
293
+ speed1: 880,
294
+ life: 1.5,
295
+ grow: -0.3,
296
+ });
297
+ fx.flock(p.cx, p.cy, {
298
+ count: 6,
299
+ tex: fx.tex.bat('#100410', 110),
300
+ spread: 260,
301
+ spreadY: 160,
302
+ speed0: 170,
303
+ size: 52,
304
+ alpha: 0.92,
305
+ });
306
+ fx.flock(p.cx, p.cy - 40, {
307
+ count: 5,
308
+ tex: fx.tex.bat('#100410', 110),
309
+ spread: 320,
310
+ spreadY: 120,
311
+ speed0: 140,
312
+ size: 40,
313
+ alpha: 0.85,
314
+ });
315
+ });
316
+ at(reveal + 700, () => {
317
+ const p = P();
318
+ fx.bolt(p.cx - 120, 0, p.cx - 60, p.cy, { color: c.glow, width: 6, jit: 52, life: 0.34 });
319
+ fx.bolt(p.cx + 140, 0, p.cx + 70, p.cy, { color: c.main, width: 5, jit: 40, life: 0.30 });
320
+ });
321
+ // 血色残雷:中段不能空着
322
+ for (let i = 0; i < 6; i++) {
323
+ at(reveal + 1050 + i * 330, () => {
324
+ const p = P();
325
+ const x0 = p.cx + (Math.random() - 0.5) * p.w * 0.65;
326
+ fx.bolt(x0, 0, x0 + (Math.random() - 0.5) * 110, p.footY * (0.5 + Math.random() * 0.4), {
327
+ color: c.main,
328
+ width: 3 + Math.random() * 3,
329
+ jit: 58,
330
+ life: 0.28,
331
+ });
332
+ fx.orb(p.cx + 30, p.cy - p.h * 0.12, { size: 520, color: c.main, alpha: 0.5, life: 0.9 });
333
+ });
334
+ }
335
+
336
+ // --- 技能名 + 血色爆发 ---
337
+ at(2750, () => {
338
+ const p = P();
339
+ fx.text(p.cx, TEXT_Y.name(p), this.skill, {
340
+ size: 54,
341
+ color: c.text,
342
+ stroke: c.stroke,
343
+ life: 2.6,
344
+ });
345
+ fx.ring(p.footX, p.footY, { r0: 50, r1: 700, width: 14, color: c.main, life: 0.95 });
346
+ fx.flock(p.cx, p.cy - 30, {
347
+ count: 7,
348
+ tex: fx.tex.bat('#100410', 110),
349
+ spread: 300,
350
+ speed0: 210,
351
+ size: 46,
352
+ alpha: 0.9,
353
+ });
354
+ });
355
+ at(3400, () => {
356
+ const p = P();
357
+ fx.shatter(p.footX, p.footY, {
358
+ count: 26,
359
+ color: c.main,
360
+ tex: fx.tex.shard(c.main, 72),
361
+ speed0: 220,
362
+ speed1: 620,
363
+ size: 46,
364
+ });
365
+ fx.orb(p.cx + 30, p.cy - p.h * 0.1, { size: 520, color: c.main, alpha: 0.8, life: 1600 });
366
+ });
367
+ at(this.closeAt - 300, () => {
368
+ const p = P();
369
+ fx.burst(p.cx, p.cy, {
370
+ count: 40,
371
+ size: 48,
372
+ color: c.glow,
373
+ speed0: 140,
374
+ speed1: 460,
375
+ life: 1.0,
376
+ });
377
+ });
378
+
379
+ return { events: ev, duration: this.duration };
380
+ },
381
+ },
382
+
383
+ /* ==================================================== 雷霆 · 天罚 */
384
+ thunder: {
385
+ id: 'thunder',
386
+ outfit: '雷霆 · 天罚',
387
+ skill: '雷神裁决',
388
+ cast: '雷霆万钧,判你无罪也判你有罪——',
389
+ dimAlpha: 0.95,
390
+ revealAt: 2000,
391
+ closeAt: 4400,
392
+ duration: 5700,
393
+ // 序列帧节拍:帧号由渲染层按这个排点写死,和特效时间轴对死。
394
+ // 蓄力 4 帧 × 360ms(560→1640),爆发 4 帧 × 300ms(2000→2900),之后停在最后一帧收招。
395
+ sheet: { charge: { at: 560, step: 360 }, burst: { at: 2000, step: 300 } },
396
+ palette: {
397
+ core: '#eaf3ff',
398
+ main: '#7ea8ff',
399
+ deep: '#3d2ea8',
400
+ glow: '#cfa8ff',
401
+ text: '#dceaff',
402
+ stroke: '#0a1230',
403
+ },
404
+
405
+ build(a) {
406
+ const fx = a.fx;
407
+ const P = a.point;
408
+ const c = this.palette;
409
+ const ev = [];
410
+ const at = (t, fn) => ev.push([t, fn]);
411
+ const reveal = this.revealAt;
412
+
413
+ // 雷云(普通混合的暗团,加色混合画不出乌云)
414
+ const cloud = (alpha) => {
415
+ const p = P();
416
+ fx.orb(p.cx, p.h * 0.06, {
417
+ size: p.w * 1.15,
418
+ color: '#0c1030',
419
+ alpha,
420
+ norm: true,
421
+ fade: 'in',
422
+ life: 5000,
423
+ });
424
+ };
425
+
426
+ at(80, () => cloud(0.86));
427
+ at(300, () => {
428
+ const p = P();
429
+ fx.sigil(p.footX, p.footY, { size: 470, color: c.main, life: 3600, spin: 0.6 });
430
+ });
431
+ at(430, () => {
432
+ const p = P();
433
+ fx.text(p.cx, TEXT_Y.cast(p), this.cast, { size: 29, color: c.glow, stroke: c.stroke, life: 1.5 });
434
+ });
435
+ // 蓄力期的零散落雷
436
+ [700, 950, 1200, 1450].forEach((t, i) => {
437
+ at(t, () => {
438
+ const p = P();
439
+ const x0 = p.cx + (Math.random() - 0.5) * p.w * 0.6;
440
+ fx.bolt(x0, 0, x0 + (Math.random() - 0.5) * 120, p.footY, {
441
+ color: i % 2 ? c.glow : c.main,
442
+ width: 5 + Math.random() * 3,
443
+ jit: 60,
444
+ life: 0.28,
445
+ });
446
+ fx.burst(x0, p.footY * 0.75, {
447
+ count: 10,
448
+ size: 30,
449
+ color: c.glow,
450
+ speed0: 60,
451
+ speed1: 260,
452
+ life: 0.7,
453
+ });
454
+ });
455
+ });
456
+ for (let i = 0; i < 4; i++) {
457
+ at(560 + i * 250, () => {
458
+ const p = P();
459
+ fx.vortex(p.cx, p.cy, {
460
+ count: 10,
461
+ radius: 290 - i * 34,
462
+ speed: 240,
463
+ color: c.main,
464
+ size: 19,
465
+ life: 0.9,
466
+ spin: 1.2,
467
+ });
468
+ });
469
+ }
470
+
471
+ // --- 亮相:天罚落下 ---
472
+ at(reveal, () => {
473
+ const p = P();
474
+ fx.dim(0.34, 380, c.stroke, true);
475
+ fx.ring(p.footX, p.footY, { r0: 30, r1: 640, width: 16, color: c.core, life: 0.85, inner: true });
476
+ fx.ring(p.footX, p.footY, { r0: 20, r1: 420, width: 9, color: c.glow, life: 0.7 });
477
+ fx.burst(p.cx, p.cy, {
478
+ count: 32,
479
+ size: 34,
480
+ color: c.glow,
481
+ speed0: 220,
482
+ speed1: 820,
483
+ life: 1.15,
484
+ alpha: 0.7,
485
+ });
486
+ fx.pillar(p.footX, p.footY, { w: 160, h: 820, color: c.main, core: c.core, life: 2400 });
487
+ for (let i = 0; i < 4; i++) {
488
+ const x0 = p.cx + (i - 1.5) * p.w * 0.22;
489
+ fx.bolt(x0, 0, x0 + (Math.random() - 0.5) * 90, p.footY, {
490
+ color: c.core,
491
+ width: 8,
492
+ jit: 70,
493
+ life: 0.4,
494
+ });
495
+ }
496
+ });
497
+ // 连锁闪电
498
+ at(reveal + 600, () => {
499
+ const p = P();
500
+ fx.pillar(p.footX, p.footY, { w: 155, h: 950, color: c.main, core: c.core, life: 2700 });
501
+ for (let i = 0; i < 5; i++) {
502
+ const x0 = p.cx - p.w * 0.36 + i * p.w * 0.18;
503
+ fx.bolt(x0, p.cy - p.h * 0.24, x0 + p.w * 0.18, p.cy - p.h * 0.24 + (Math.random() - 0.5) * 60, {
504
+ color: c.main,
505
+ width: 4,
506
+ jit: 30,
507
+ life: 0.26,
508
+ });
509
+ }
510
+ });
511
+ // 持续落雷:光柱亮起来之后还得一直有雷下来,不然中间一段时间画面是空的
512
+ for (let i = 0; i < 6; i++) {
513
+ at(reveal + 980 + i * 330, () => {
514
+ const p = P();
515
+ const x0 = p.cx + (Math.random() - 0.5) * p.w * 0.7;
516
+ fx.bolt(x0, 0, x0 + (Math.random() - 0.5) * 130, p.footY * (0.6 + Math.random() * 0.35), {
517
+ color: Math.random() < 0.5 ? c.core : c.glow,
518
+ width: 4 + Math.random() * 4,
519
+ jit: 66,
520
+ life: 0.3,
521
+ });
522
+ fx.burst(x0, p.footY * 0.8, {
523
+ count: 9,
524
+ size: 28,
525
+ color: c.core,
526
+ speed0: 80,
527
+ speed1: 320,
528
+ life: 0.65,
529
+ alpha: 0.8,
530
+ });
531
+ if (i % 2 === 0) {
532
+ fx.ring(p.footX, p.footY, { r0: 40, r1: 430, width: 7, color: c.glow, life: 0.6, flat: 0.3 });
533
+ }
534
+ });
535
+ }
536
+
537
+ // --- 技能名 ---
538
+ at(2700, () => {
539
+ const p = P();
540
+ fx.text(p.cx, TEXT_Y.name(p), this.skill, {
541
+ size: 54,
542
+ color: c.text,
543
+ stroke: c.stroke,
544
+ life: 2.6,
545
+ });
546
+ fx.ring(p.footX, p.footY, { r0: 40, r1: 700, width: 13, color: c.core, life: 0.9 });
547
+ cloud(0.7);
548
+ });
549
+ at(3350, () => {
550
+ const p = P();
551
+ fx.shatter(p.footX, p.footY, {
552
+ count: 22,
553
+ color: c.glow,
554
+ tex: fx.tex.shard(c.glow, 64),
555
+ speed0: 200,
556
+ speed1: 700,
557
+ size: 40,
558
+ });
559
+ fx.burst(p.cx, p.cy, {
560
+ count: 30,
561
+ size: 36,
562
+ color: c.core,
563
+ speed0: 100,
564
+ speed1: 380,
565
+ life: 0.9,
566
+ });
567
+ });
568
+ at(this.closeAt - 350, () => {
569
+ const p = P();
570
+ fx.bolt(p.cx, 0, p.cx, p.footY, { color: c.core, width: 7, jit: 64, life: 0.32 });
571
+ });
572
+
573
+ return { events: ev, duration: this.duration };
574
+ },
575
+ },
576
+
577
+ /* ================================================== 幻梦 · 星海 */
578
+ astral: {
579
+ id: 'astral',
580
+ outfit: '幻梦 · 星海',
581
+ skill: '星海咏叹',
582
+ cast: '把愿望交给星星吧——',
583
+ dimAlpha: 0.93,
584
+ revealAt: 2000,
585
+ closeAt: 4400,
586
+ duration: 5700,
587
+ // 序列帧节拍:帧号由渲染层按这个排点写死,和特效时间轴对死。
588
+ // 蓄力 4 帧 × 360ms(560→1640),爆发 4 帧 × 300ms(2000→2900),之后停在最后一帧收招。
589
+ sheet: { charge: { at: 560, step: 360 }, burst: { at: 2000, step: 300 } },
590
+ palette: {
591
+ core: '#ffffff',
592
+ main: '#ffb3e6',
593
+ deep: '#6b4cff',
594
+ glow: '#c9b8ff',
595
+ text: '#ffe8f8',
596
+ stroke: '#241048',
597
+ },
598
+
599
+ build(a) {
600
+ const fx = a.fx;
601
+ const P = a.point;
602
+ const c = this.palette;
603
+ const ev = [];
604
+ const at = (t, fn) => ev.push([t, fn]);
605
+ const reveal = this.revealAt;
606
+
607
+ at(100, () => {
608
+ const p = P();
609
+ fx.sigil(p.footX, p.footY, { size: 460, color: c.main, life: 3400, spin: 0.5 });
610
+ });
611
+ at(380, () => {
612
+ const p = P();
613
+ fx.text(p.cx, TEXT_Y.cast(p), this.cast, { size: 30, color: c.glow, stroke: c.stroke, life: 1.5 });
614
+ });
615
+ // 星尘漩涡
616
+ for (let i = 0; i < 6; i++) {
617
+ at(260 + i * 200, () => {
618
+ const p = P();
619
+ fx.vortex(p.cx, p.cy, {
620
+ count: 11,
621
+ radius: 330 - i * 36,
622
+ speed: 200,
623
+ color: i % 2 ? c.glow : c.main,
624
+ tex: fx.tex.star(c.main, 96),
625
+ size: 20,
626
+ life: 1.05,
627
+ spin: 0.95,
628
+ });
629
+ });
630
+ }
631
+ at(900, () => {
632
+ const p = P();
633
+ fx.burst(p.cx, p.cy, {
634
+ count: 34,
635
+ size: 20,
636
+ color: c.core,
637
+ speed0: 30,
638
+ speed1: 160,
639
+ life: 2.4,
640
+ grow: 0.3,
641
+ });
642
+ });
643
+
644
+ // --- 亮相:星河 + 星爆 ---
645
+ at(reveal, () => {
646
+ const p = P();
647
+ fx.dim(0.34, 380, c.stroke, true);
648
+ fx.burst(p.cx, p.cy, {
649
+ count: 34,
650
+ size: 36,
651
+ color: c.glow,
652
+ speed0: 170,
653
+ speed1: 740,
654
+ life: 1.3,
655
+ grow: 0.5,
656
+ alpha: 0.66,
657
+ });
658
+ fx.burst(p.cx, p.cy, {
659
+ count: 18,
660
+ size: 38,
661
+ color: c.main,
662
+ tex: fx.tex.shard(c.main, 72),
663
+ speed0: 200,
664
+ speed1: 620,
665
+ life: 1.6,
666
+ grow: -0.2,
667
+ alpha: 0.8,
668
+ });
669
+ fx.ring(p.cx, p.cy, { r0: 30, r1: 560, width: 14, color: c.core, life: 0.9, flat: 0.75 });
670
+ // 星河光带:两条横贯画面的宽光带
671
+ fx.beam(0, p.cy - p.h * 0.12, p.w, p.cy - p.h * 0.24, { width: 150, color: c.deep, core: c.glow, life: 2.0, alpha: 0.75 });
672
+ fx.beam(0, p.cy + p.h * 0.02, p.w, p.cy - p.h * 0.06, { width: 110, color: c.main, core: c.core, life: 1.8, alpha: 0.7 });
673
+ fx.fallStream({
674
+ x: p.cx,
675
+ y: -60,
676
+ spreadX: p.w * 0.95,
677
+ spreadY: 220,
678
+ color: c.glow,
679
+ tex: fx.tex.star(c.core, 96),
680
+ dur: 2600,
681
+ rate: 17,
682
+ size: 26,
683
+ life: 3.4,
684
+ vy: 210,
685
+ });
686
+ });
687
+ // 心形星爆(向上扇形,粉白交替)
688
+ // 星屑喷泉(从脚下往上炸,不要撒在脸上)
689
+ at(reveal + 400, () => {
690
+ const p = P();
691
+ fx.burst(p.footX, p.footY, {
692
+ count: 20,
693
+ size: 30,
694
+ color: c.main,
695
+ tex: fx.tex.star(c.main, 96),
696
+ a0: -Math.PI * 0.92,
697
+ a1: -Math.PI * 0.08,
698
+ speed0: 200,
699
+ speed1: 560,
700
+ life: 1.6,
701
+ grav: 420,
702
+ alpha: 0.55,
703
+ });
704
+ });
705
+ // 星屑持续往上升,不然中段只剩背景
706
+ for (let i = 0; i < 6; i++) {
707
+ at(reveal + 900 + i * 320, () => {
708
+ const p = P();
709
+ fx.burst(p.footX + (Math.random() - 0.5) * p.w * 0.5, p.footY, {
710
+ count: 12,
711
+ size: 26,
712
+ color: i % 2 ? c.main : c.glow,
713
+ tex: fx.tex.star(c.core, 96),
714
+ a0: -Math.PI * 0.85,
715
+ a1: -Math.PI * 0.15,
716
+ speed0: 150,
717
+ speed1: 420,
718
+ grav: 320,
719
+ life: 1.4,
720
+ alpha: 0.7,
721
+ });
722
+ });
723
+ }
724
+
725
+ // --- 技能名 ---
726
+ at(2600, () => {
727
+ const p = P();
728
+ fx.text(p.cx, TEXT_Y.name(p), this.skill, {
729
+ size: 54,
730
+ color: c.text,
731
+ stroke: c.stroke,
732
+ life: 2.6,
733
+ });
734
+ fx.ring(p.footX, p.footY, { r0: 40, r1: 680, width: 12, color: c.main, life: 0.9 });
735
+ });
736
+ at(3250, () => {
737
+ const p = P();
738
+ fx.shatter(p.footX, p.footY, {
739
+ count: 24,
740
+ color: c.glow,
741
+ tex: fx.tex.shard(c.glow, 72),
742
+ speed0: 180,
743
+ speed1: 560,
744
+ size: 42,
745
+ });
746
+ fx.burst(p.cx, p.cy, {
747
+ count: 26,
748
+ size: 30,
749
+ color: c.core,
750
+ speed0: 80,
751
+ speed1: 300,
752
+ life: 1.2,
753
+ grow: 0.5,
754
+ });
755
+ });
756
+ at(this.closeAt - 300, () => {
757
+ const p = P();
758
+ fx.burst(p.cx, p.cy, {
759
+ count: 44,
760
+ size: 46,
761
+ color: c.glow,
762
+ speed0: 120,
763
+ speed1: 420,
764
+ life: 1.0,
765
+ });
766
+ });
767
+
768
+ return { events: ev, duration: this.duration };
769
+ },
770
+ },
771
+ };
772
+
773
+ const AWAKEN_ORDER = ['seraph', 'nocturne', 'thunder', 'astral'];
774
+
775
+ window.AWAKENS = { list: AWAKENS, order: AWAKEN_ORDER };