/** * 糖分突袭 · 视觉特效状态(飘字、粒子、连消横幅、震屏、闪屏) * 特效风格参考「开心消消乐」:糖果色星屑、弹性扩散光环、大消除闪屏。 */ import { ref } from 'vue' let fxSeq = 0 function nextFxId(prefix) { fxSeq += 1 return `${prefix}-${fxSeq}-${Date.now()}` } // 开心消消乐式糖果色板(彩虹爆裂用) const CANDY_COLORS = ['#FF5E9C', '#FFC93C', '#5BD16A', '#4D9DFF', '#FF8A3D', '#B57BFF', '#FF6F91', '#2FE6C8'] // 星屑字形 const STAR_GLYPHS = ['✦', '✧', '★', '✨'] export function useGameFx() { const floatingTexts = ref([]) const burstParticles = ref([]) const ripples = ref([]) const comboBanner = ref({ show: false, text: '', tier: 1 }) const screenShaking = ref(false) const flash = ref({ active: false, color: '#ffffff', opacity: 0 }) let comboBannerTimer = null let shakeTimer = null let flashTimer = null let flashFadeTimer = null function spawnFloatingText(payload) { const id = nextFxId('ft') floatingTexts.value.push({ id, ...payload }) const duration = payload.danger ? 1500 : payload.isCombo ? 1400 : 1200 setTimeout(() => { floatingTexts.value = floatingTexts.value.filter((f) => f.id !== id) }, duration) return id } /** * 糖果爆裂粒子(星屑 + 圆点混合,上抛后受重力下落,带旋转与缓动)。 * @param {number} cx,cy 爆裂中心(屏幕坐标) * @param {string} color 基础颜色 * @param {number} count 粒子数量 * @param {object} opts { star, rainbow, gravity, sizeMin, sizeMax, distMin, distMax, life } */ function spawnBurst(cx, cy, color, count = 8, opts = {}) { const { star = true, rainbow = false, gravity = 64, sizeMin = 8, sizeMax = 16, distMin = 36, distMax = 110, life = 850 } = opts const dur = (life / 1000).toFixed(2) for (let i = 0; i < count; i++) { const id = nextFxId('p') const isStar = star && Math.random() < 0.65 const pSize = Math.random() * (sizeMax - sizeMin) + sizeMin const angle = Math.random() * Math.PI * 2 const distance = Math.random() * (distMax - distMin) + distMin const tx = Math.cos(angle) * distance // 先上抛、再叠加重力下落,形成喷泉抛物线 const ty = Math.sin(angle) * distance - distance * 0.35 + gravity const half = pSize / 2 const pColor = rainbow ? CANDY_COLORS[Math.floor(Math.random() * CANDY_COLORS.length)] : color const rot = Math.random() * 720 - 360 const glyph = isStar ? STAR_GLYPHS[Math.floor(Math.random() * STAR_GLYPHS.length)] : '' const particle = { id, left: cx - half, top: cy - half, size: pSize, color: pColor, char: glyph, opacity: 1, transform: 'scale(0.4) rotate(0deg)', transition: 'none' } burstParticles.value.push(particle) setTimeout(() => { particle.transition = `left ${dur}s cubic-bezier(0.16,0.7,0.3,1), top ${dur}s cubic-bezier(0.3,0.1,0.4,1), opacity ${dur}s ease-in, transform ${dur}s ease-out` particle.left = cx - half + tx particle.top = cy - half + ty particle.opacity = 0 particle.transform = `scale(${isStar ? 1.15 : 0.5}) rotate(${rot}deg)` }, 16) setTimeout(() => { burstParticles.value = burstParticles.value.filter((p) => p.id !== id) }, life + 80) } } /** * 弹性扩散发光光环(开心消消乐式的扩散光圈)。 * @param {object} opts { thickness, life } */ function spawnRipple(cx, cy, color, tier = 1, opts = {}) { const id = nextFxId('rp') const size = 36 + tier * 18 const { thickness = Math.max(3, tier + 2), life = 600 } = opts ripples.value.push({ id, left: cx, top: cy, size, color, thickness, opacity: 0.95 }) setTimeout(() => { ripples.value = ripples.value.filter((r) => r.id !== id) }, life) } /** * 全屏闪光(大消除瞬间的爆闪),先点亮再淡出。 */ function triggerFlash(color = '#ffffff', intensity = 0.5, life = 280) { if (flashFadeTimer) clearTimeout(flashFadeTimer) if (flashTimer) clearTimeout(flashTimer) flash.value = { active: true, color, opacity: intensity } flashFadeTimer = setTimeout(() => { flash.value = { ...flash.value, opacity: 0 } }, 24) flashTimer = setTimeout(() => { flash.value = { active: false, color, opacity: 0 } }, life) } function showComboBanner(chain, points) { if (chain <= 1) return const tier = chain >= 4 ? 3 : chain >= 3 ? 2 : 1 comboBanner.value = { show: true, text: `连消 ×${chain} +${points}`, tier } if (comboBannerTimer) clearTimeout(comboBannerTimer) comboBannerTimer = setTimeout(() => { comboBanner.value = { ...comboBanner.value, show: false } }, 1100) } function triggerShake(intensity = 1) { screenShaking.value = true if (shakeTimer) clearTimeout(shakeTimer) shakeTimer = setTimeout(() => { screenShaking.value = false }, intensity >= 2 ? 420 : 280) } function clearAll() { floatingTexts.value = [] burstParticles.value = [] ripples.value = [] comboBanner.value = { show: false, text: '', tier: 1 } screenShaking.value = false flash.value = { active: false, color: '#ffffff', opacity: 0 } } return { floatingTexts, burstParticles, ripples, comboBanner, screenShaking, flash, spawnFloatingText, spawnBurst, spawnRipple, showComboBanner, triggerShake, triggerFlash, clearAll } }