From 221f629ec296976f0bbb3e99c56ecd88221c9c67 Mon Sep 17 00:00:00 2001 From: pllx Date: Thu, 30 Apr 2026 10:29:11 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=BA=A7=E9=A9=BE=E7=89=B9?= =?UTF-8?q?=E6=95=88=E5=85=A5=E5=9C=BA=E6=A0=87=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Events/EffectBroadcast.php | 6 ++++ app/Http/Controllers/ChatController.php | 16 ++++++++++- app/Services/RideService.php | 1 + resources/js/chat-room/chat-events.js | 7 ++++- resources/js/chat-room/initial-state.js | 2 +- resources/js/effects/99a.js | 15 ++++++---- resources/js/effects/df5c.js | 13 +++++---- resources/js/effects/effect-manager.js | 37 ++++++++++++++----------- resources/js/effects/fujian.js | 13 +++++---- resources/js/effects/j35.js | 15 ++++++---- resources/views/chat/frame.blade.php | 1 + tests/Feature/ChatControllerTest.php | 6 ++++ 12 files changed, 91 insertions(+), 41 deletions(-) diff --git a/app/Events/EffectBroadcast.php b/app/Events/EffectBroadcast.php index 92f32f3..e9bf8a9 100644 --- a/app/Events/EffectBroadcast.php +++ b/app/Events/EffectBroadcast.php @@ -39,6 +39,8 @@ class EffectBroadcast implements ShouldBroadcastNow * @param string $operator 触发特效的用户名(购买者) * @param string|null $targetUsername 接收者用户名(null = 全员) * @param string|null $giftMessage 附带赠言 + * @param string|null $effectTitle 特效画面标题 + * @param string|null $rideName 座驾名称 */ public function __construct( public readonly int $roomId, @@ -46,6 +48,8 @@ class EffectBroadcast implements ShouldBroadcastNow public readonly string $operator, public readonly ?string $targetUsername = null, public readonly ?string $giftMessage = null, + public readonly ?string $effectTitle = null, + public readonly ?string $rideName = null, ) {} /** @@ -73,6 +77,8 @@ class EffectBroadcast implements ShouldBroadcastNow 'operator' => $this->operator, 'target_username' => $this->targetUsername, // null = 全员 'gift_message' => $this->giftMessage, + 'effect_title' => $this->effectTitle, + 'ride_name' => $this->rideName, ]; } } diff --git a/app/Http/Controllers/ChatController.php b/app/Http/Controllers/ChatController.php index 2a74b10..2e49a6e 100644 --- a/app/Http/Controllers/ChatController.php +++ b/app/Http/Controllers/ChatController.php @@ -119,6 +119,7 @@ class ChatController extends Controller // 3. 广播和初始化欢迎(仅限初次进入) $newbieEffect = null; $initialRideEffect = null; + $initialRideEffectOptions = null; $initialPresenceTheme = null; $initialWelcomeMessage = null; $initialWelcomeMessages = []; @@ -246,15 +247,27 @@ class ChatController extends Controller 'welcome_kind' => 'ride_presence', 'ride_key' => $ridePresencePayload['ride_key'], 'ride_name' => $ridePresencePayload['ride_name'], + 'effect_title' => $ridePresencePayload['effect_title'], 'sent_at' => now()->toDateTimeString(), ]; // 座驾进场独立追加一条播报,并广播全屏特效给其他在线用户。 $this->chatState->pushMessage($id, $rideWelcomeMsg); broadcast(new MessageSent($id, $rideWelcomeMsg)); - broadcast(new \App\Events\EffectBroadcast($id, $ridePresencePayload['ride_key'], $user->username))->toOthers(); + broadcast(new \App\Events\EffectBroadcast( + $id, + $ridePresencePayload['ride_key'], + $user->username, + effectTitle: $ridePresencePayload['effect_title'], + rideName: $ridePresencePayload['ride_name'], + ))->toOthers(); $initialRideEffect = $ridePresencePayload['ride_key']; + $initialRideEffectOptions = [ + 'effect_title' => $ridePresencePayload['effect_title'], + 'ride_name' => $ridePresencePayload['ride_name'], + 'operator' => $user->username, + ]; $initialWelcomeMessages[] = $rideWelcomeMsg; } } @@ -345,6 +358,7 @@ class ChatController extends Controller 'weekEffect' => $this->shopService->getActiveWeekEffect($user), 'newbieEffect' => $newbieEffect, 'initialRideEffect' => $initialRideEffect, + 'initialRideEffectOptions' => $initialRideEffectOptions, 'initialPresenceTheme' => $initialPresenceTheme, 'initialWelcomeMessage' => $initialWelcomeMessage, 'initialWelcomeMessages' => $initialWelcomeMessages, diff --git a/app/Services/RideService.php b/app/Services/RideService.php index f4b9d2a..fc459af 100644 --- a/app/Services/RideService.php +++ b/app/Services/RideService.php @@ -239,6 +239,7 @@ class RideService 'ride_key' => $rideKey, 'ride_name' => $item->name, 'ride_icon' => (string) ($item->icon ?? '🚘'), + 'effect_title' => "{$user->username} 乘坐【{$item->name}】闪亮登场", 'welcome_text' => ChatContentSanitizer::htmlText($rendered), ]; } diff --git a/resources/js/chat-room/chat-events.js b/resources/js/chat-room/chat-events.js index 9d92f79..1578209 100644 --- a/resources/js/chat-room/chat-events.js +++ b/resources/js/chat-room/chat-events.js @@ -482,10 +482,15 @@ export function bindChatEvents() { const target = e.detail?.target_username; const operator = e.detail?.operator; const myName = window.chatContext?.username; + const effectOptions = { + effect_title: e.detail?.effect_title, + ride_name: e.detail?.ride_name, + operator, + }; if (type && typeof EffectManager !== "undefined") { if (!target || target === myName || operator === myName) { - EffectManager.play(type); + EffectManager.play(type, effectOptions); } } }); diff --git a/resources/js/chat-room/initial-state.js b/resources/js/chat-room/initial-state.js index 73c65a5..7b0ebfd 100644 --- a/resources/js/chat-room/initial-state.js +++ b/resources/js/chat-room/initial-state.js @@ -81,7 +81,7 @@ function playEntryEffect(initialState) { } window.setTimeout(() => { - window.EffectManager?.play?.(initialState.entryEffect); + window.EffectManager?.play?.(initialState.entryEffect, initialState.entryEffectOptions || {}); }, 1000); } diff --git a/resources/js/effects/99a.js b/resources/js/effects/99a.js index e54d3db..653370e 100644 --- a/resources/js/effects/99a.js +++ b/resources/js/effects/99a.js @@ -446,8 +446,9 @@ const Type99AEffect = (() => { * @param {number} w 画布宽度 * @param {number} h 画布高度 * @param {number} progress 播放进度 + * @param {string} title 入场标题 */ - function drawHud(ctx, w, h, progress) { + function drawHud(ctx, w, h, progress, title) { const enter = Math.min(1, Math.max(0, (progress - 0.14) / 0.2)); const leave = Math.min(1, Math.max(0, (1 - progress) / 0.16)); const alpha = easeInOutSine(enter) * leave; @@ -461,7 +462,7 @@ const Type99AEffect = (() => { ctx.fillStyle = "rgba(28,25,23,0.66)"; ctx.strokeStyle = "rgba(253,230,138,0.72)"; ctx.lineWidth = 2; - roundRect(ctx, w * 0.5 - 226, y - 42, 452, 88, 18); + roundRect(ctx, w * 0.5 - 320, y - 46, 640, 96, 18); ctx.fill(); ctx.stroke(); @@ -469,8 +470,8 @@ const Type99AEffect = (() => { ctx.font = "700 16px serif"; ctx.fillText("ZTZ-99A ARMORED FORCE", w * 0.5, y - 12); ctx.fillStyle = "#ffffff"; - ctx.font = "900 40px serif"; - ctx.fillText("99A主战坦克 重装入场", w * 0.5, y + 28); + ctx.font = "900 38px serif"; + ctx.fillText(title, w * 0.5, y + 28, 590); ctx.restore(); } @@ -503,13 +504,15 @@ const Type99AEffect = (() => { * * @param {HTMLCanvasElement} canvas 全屏特效画布 * @param {Function} onEnd 结束回调 + * @param {object} options 特效附加参数 * @returns {{cancel: Function}} */ - function start(canvas, onEnd) { + function start(canvas, onEnd, options = {}) { const ctx = canvas.getContext("2d"); const w = canvas.width; const h = canvas.height; const dust = createDust(w, h); + const title = String(options.effect_title || "99A主战坦克 重装入场").trim() || "99A主战坦克 重装入场"; const startTime = performance.now(); let animId = null; let finished = false; @@ -553,7 +556,7 @@ const Type99AEffect = (() => { drawDust(ctx, dust, w, progress); drawShockwave(ctx, w, h, progress); drawTank(ctx, tankX, tankY, scale, progress); - drawHud(ctx, w, h, progress); + drawHud(ctx, w, h, progress, title); if (progress < 1) { animId = requestAnimationFrame(animate); diff --git a/resources/js/effects/df5c.js b/resources/js/effects/df5c.js index 9178251..35bf29a 100644 --- a/resources/js/effects/df5c.js +++ b/resources/js/effects/df5c.js @@ -269,8 +269,9 @@ const Df5cEffect = (() => { * @param {number} w 画布宽度 * @param {number} h 画布高度 * @param {number} progress 播放进度 + * @param {string} title 入场标题 */ - function drawHud(ctx, w, h, progress) { + function drawHud(ctx, w, h, progress, title) { const enter = Math.min(1, Math.max(0, (progress - 0.1) / 0.18)); const leave = Math.min(1, Math.max(0, (1 - progress) / 0.14)); const alpha = easeInOutCubic(enter) * leave; @@ -282,7 +283,7 @@ const Df5cEffect = (() => { ctx.fillStyle = "rgba(15,23,42,0.68)"; ctx.strokeStyle = "rgba(248,113,113,0.72)"; ctx.lineWidth = 2; - roundRect(ctx, w * 0.5 - 246, y - 42, 492, 88, 18); + roundRect(ctx, w * 0.5 - 330, y - 46, 660, 96, 18); ctx.fill(); ctx.stroke(); ctx.shadowColor = "rgba(248,113,113,0.95)"; @@ -292,7 +293,7 @@ const Df5cEffect = (() => { ctx.fillText("DF-5C STRATEGIC LAUNCH PREVIEW", w * 0.5, y - 12); ctx.fillStyle = "#ffffff"; ctx.font = "900 38px serif"; - ctx.fillText("东风-5C 洲际导弹 升空", w * 0.5, y + 28); + ctx.fillText(title, w * 0.5, y + 28, 610); ctx.restore(); } @@ -325,13 +326,15 @@ const Df5cEffect = (() => { * * @param {HTMLCanvasElement} canvas 全屏特效画布 * @param {Function} onEnd 结束回调 + * @param {object} options 特效附加参数 * @returns {{cancel: Function}} */ - function start(canvas, onEnd) { + function start(canvas, onEnd, options = {}) { const ctx = canvas.getContext("2d"); const w = canvas.width; const h = canvas.height; const particles = createParticles(120); + const title = String(options.effect_title || "东风-5C 洲际导弹 升空").trim() || "东风-5C 洲际导弹 升空"; const startTime = performance.now(); let animId = null; let finished = false; @@ -375,7 +378,7 @@ const Df5cEffect = (() => { drawLaunchPad(ctx, w, h, progress); drawExhaust(ctx, particles, tailX, tailY, progress); drawMissile(ctx, launchX, launchY, scale, progress); - drawHud(ctx, w, h, progress); + drawHud(ctx, w, h, progress, title); if (progress < 1) { animId = requestAnimationFrame(animate); diff --git a/resources/js/effects/effect-manager.js b/resources/js/effects/effect-manager.js index 5f51446..194e862 100644 --- a/resources/js/effects/effect-manager.js +++ b/resources/js/effects/effect-manager.js @@ -221,9 +221,9 @@ const EffectManager = (() => { } if (playNext) { - const nextType = _dequeueNextType(); - if (nextType) { - play(nextType); + const nextEffect = _dequeueNextType(); + if (nextEffect) { + play(nextEffect.type, nextEffect.options || {}); } } } @@ -232,8 +232,9 @@ const EffectManager = (() => { * 将特效加入有限队列,同类型短时间重复触发时只保留一份。 * * @param {string} type 待播放特效类型 + * @param {object} options 特效附加参数 */ - function _enqueue(type) { + function _enqueue(type, options = {}) { const existingIndex = _queue.findIndex((item) => item.type === type); if (existingIndex !== -1) { _queue.splice(existingIndex, 1); @@ -241,6 +242,7 @@ const EffectManager = (() => { _queue.push({ type, + options, queuedAt: Date.now(), keepUntilPlayed: type === "wedding-fireworks", }); @@ -252,7 +254,7 @@ const EffectManager = (() => { /** * 取出下一个仍然有效的排队特效。 * - * @returns {string|null} + * @returns {{type: string, options: object}|null} */ function _dequeueNextType() { const now = Date.now(); @@ -260,7 +262,7 @@ const EffectManager = (() => { while (_queue.length > 0) { const next = _queue.shift(); if (next.keepUntilPlayed || now - next.queuedAt <= QUEUED_EFFECT_TTL) { - return next.type; + return next; } } @@ -330,14 +332,15 @@ const EffectManager = (() => { * @param {HTMLCanvasElement} canvas 全屏特效画布 * @param {Function} finishCurrent 当前特效结束回调 * @param {string} startMethod 启动方法名称 + * @param {object} options 特效附加参数 * @returns {boolean} 是否成功找到并启动特效 */ - function _startEffect(effectObject, canvas, finishCurrent, startMethod = "start") { + function _startEffect(effectObject, canvas, finishCurrent, startMethod = "start", options = {}) { if (!effectObject || typeof effectObject[startMethod] !== "function") { return false; } - _bindEffectController(effectObject[startMethod](canvas, finishCurrent)); + _bindEffectController(effectObject[startMethod](canvas, finishCurrent, options)); return true; } @@ -345,8 +348,9 @@ const EffectManager = (() => { * 播放指定特效 * * @param {string} type 特效类型:fireworks / rain / lightning / snow / sakura / meteors / gold-rain / hearts / confetti / fireflies / j35 / 99a / df5c / fujian + * @param {object} options 特效附加参数 */ - function play(type) { + function play(type, options = {}) { if (document.hidden) { return; } @@ -358,19 +362,20 @@ const EffectManager = (() => { // 防重入:同时只允许一个特效 if (_current) { - _enqueue(type); + _enqueue(type, options); return; } - _play(type); + _play(type, options); } /** * 加载模块后播放指定特效。 * * @param {string} type 特效类型 + * @param {object} options 特效附加参数 */ - async function _play(type) { + async function _play(type, options = {}) { _current = type; const token = _playToken; @@ -439,16 +444,16 @@ const EffectManager = (() => { started = _startEffect(window.FirefliesEffect, canvas, finishCurrent); break; case "j35": - started = _startEffect(window.J35Effect, canvas, finishCurrent); + started = _startEffect(window.J35Effect, canvas, finishCurrent, "start", options); break; case "99a": - started = _startEffect(window.Type99AEffect, canvas, finishCurrent); + started = _startEffect(window.Type99AEffect, canvas, finishCurrent, "start", options); break; case "df5c": - started = _startEffect(window.Df5cEffect, canvas, finishCurrent); + started = _startEffect(window.Df5cEffect, canvas, finishCurrent, "start", options); break; case "fujian": - started = _startEffect(window.FujianEffect, canvas, finishCurrent); + started = _startEffect(window.FujianEffect, canvas, finishCurrent, "start", options); break; default: console.warn(`[EffectManager] 未知特效类型:${type}`); diff --git a/resources/js/effects/fujian.js b/resources/js/effects/fujian.js index 9ab7de8..508b394 100644 --- a/resources/js/effects/fujian.js +++ b/resources/js/effects/fujian.js @@ -393,8 +393,9 @@ const FujianEffect = (() => { * @param {number} w 画布宽度 * @param {number} h 画布高度 * @param {number} progress 播放进度 + * @param {string} title 入场标题 */ - function drawHud(ctx, w, h, progress) { + function drawHud(ctx, w, h, progress, title) { const enter = Math.min(1, Math.max(0, (progress - 0.12) / 0.2)); const leave = Math.min(1, Math.max(0, (1 - progress) / 0.14)); const alpha = easeInOutCubic(enter) * leave; @@ -406,7 +407,7 @@ const FujianEffect = (() => { ctx.fillStyle = "rgba(15,23,42,0.68)"; ctx.strokeStyle = "rgba(103,232,249,0.72)"; ctx.lineWidth = 2; - roundRect(ctx, w * 0.5 - 236, y - 42, 472, 88, 18); + roundRect(ctx, w * 0.5 - 320, y - 46, 640, 96, 18); ctx.fill(); ctx.stroke(); ctx.shadowColor = "rgba(103,232,249,0.95)"; @@ -416,7 +417,7 @@ const FujianEffect = (() => { ctx.fillText("FUJIAN AIRCRAFT CARRIER PREVIEW", w * 0.5, y - 12); ctx.fillStyle = "#ffffff"; ctx.font = "900 38px serif"; - ctx.fillText("福建舰 航母入场", w * 0.5, y + 28); + ctx.fillText(title, w * 0.5, y + 28, 590); ctx.restore(); } @@ -449,13 +450,15 @@ const FujianEffect = (() => { * * @param {HTMLCanvasElement} canvas 全屏特效画布 * @param {Function} onEnd 结束回调 + * @param {object} options 特效附加参数 * @returns {{cancel: Function}} */ - function start(canvas, onEnd) { + function start(canvas, onEnd, options = {}) { const ctx = canvas.getContext("2d"); const w = canvas.width; const h = canvas.height; const waves = createWaves(w, h); + const title = String(options.effect_title || "福建舰 航母入场").trim() || "福建舰 航母入场"; const startTime = performance.now(); let animId = null; let finished = false; @@ -497,7 +500,7 @@ const FujianEffect = (() => { drawBackdrop(ctx, w, h, progress); drawWaves(ctx, waves, w, progress, carrierX, carrierY, scale); drawCarrier(ctx, carrierX, carrierY, scale, progress); - drawHud(ctx, w, h, progress); + drawHud(ctx, w, h, progress, title); if (progress < 1) { animId = requestAnimationFrame(animate); diff --git a/resources/js/effects/j35.js b/resources/js/effects/j35.js index f7f5c8f..528edc1 100644 --- a/resources/js/effects/j35.js +++ b/resources/js/effects/j35.js @@ -336,8 +336,9 @@ const J35Effect = (() => { * @param {number} w 画布宽度 * @param {number} h 画布高度 * @param {number} progress 播放进度 + * @param {string} title 入场标题 */ - function drawHud(ctx, w, h, progress) { + function drawHud(ctx, w, h, progress, title) { const enter = Math.min(1, Math.max(0, (progress - 0.13) / 0.18)); const leave = Math.min(1, Math.max(0, (1 - progress) / 0.16)); const alpha = easeInOutSine(enter) * leave; @@ -351,7 +352,7 @@ const J35Effect = (() => { ctx.fillStyle = "rgba(2,6,23,0.62)"; ctx.strokeStyle = "rgba(56,189,248,0.72)"; ctx.lineWidth = 2; - roundRect(ctx, w * 0.5 - 230, y - 44, 460, 92, 18); + roundRect(ctx, w * 0.5 - 320, y - 46, 640, 96, 18); ctx.fill(); ctx.stroke(); @@ -359,8 +360,8 @@ const J35Effect = (() => { ctx.font = "700 16px serif"; ctx.fillText("STEALTH FIGHTER ARRIVAL", w * 0.5, y - 12); ctx.fillStyle = "#ffffff"; - ctx.font = "900 42px serif"; - ctx.fillText("中国歼-35 破空入场", w * 0.5, y + 28); + ctx.font = "900 38px serif"; + ctx.fillText(title, w * 0.5, y + 28, 590); ctx.restore(); } @@ -393,13 +394,15 @@ const J35Effect = (() => { * * @param {HTMLCanvasElement} canvas 全屏特效画布 * @param {Function} onEnd 结束回调 + * @param {object} options 特效附加参数 * @returns {{cancel: Function}} */ - function start(canvas, onEnd) { + function start(canvas, onEnd, options = {}) { const ctx = canvas.getContext("2d"); const w = canvas.width; const h = canvas.height; const speedLines = createSpeedLines(w, h); + const title = String(options.effect_title || "中国歼-35 破空入场").trim() || "中国歼-35 破空入场"; const startTime = performance.now(); let animId = null; let finished = false; @@ -443,7 +446,7 @@ const J35Effect = (() => { drawSpeedLines(ctx, speedLines, w, progress); drawSonicRing(ctx, w, h, progress); drawJet(ctx, jetX, jetY, scale, progress); - drawHud(ctx, w, h, progress); + drawHud(ctx, w, h, progress, title); if (progress < 1) { animId = requestAnimationFrame(animate); diff --git a/resources/views/chat/frame.blade.php b/resources/views/chat/frame.blade.php index 1888324..9eb6e7e 100644 --- a/resources/views/chat/frame.blade.php +++ b/resources/views/chat/frame.blade.php @@ -127,6 +127,7 @@ 'welcomeMessage' => $initialWelcomeMessage ?? null, 'welcomeMessages' => $initialWelcomeMessages ?? [], 'entryEffect' => $newbieEffect ?: ($initialRideEffect ?? ($initialPresenceTheme['presence_effect'] ?? ($weekEffect ?? null))), + 'entryEffectOptions' => $newbieEffect ? null : ($initialRideEffectOptions ?? null), 'presenceTheme' => $initialPresenceTheme ?? null, 'pendingProposal' => $pendingProposal ?? null, 'pendingDivorce' => $pendingDivorce ?? null, diff --git a/tests/Feature/ChatControllerTest.php b/tests/Feature/ChatControllerTest.php index 5ee4ea8..9022719 100644 --- a/tests/Feature/ChatControllerTest.php +++ b/tests/Feature/ChatControllerTest.php @@ -1132,8 +1132,14 @@ class ChatControllerTest extends TestCase $this->assertNotNull($rideMessage); $this->assertSame('座驾播报', $rideMessage['from_user']); $this->assertSame('j35', $rideMessage['ride_key']); + $this->assertSame("{$user->username} 乘坐【歼-35测试座驾】闪亮登场", $rideMessage['effect_title']); $this->assertStringContainsString($user->username, $rideMessage['content']); $this->assertSame('j35', $response->viewData('initialRideEffect')); + $this->assertSame([ + 'effect_title' => "{$user->username} 乘坐【歼-35测试座驾】闪亮登场", + 'ride_name' => '歼-35测试座驾', + 'operator' => $user->username, + ], $response->viewData('initialRideEffectOptions')); } /**