From a4c5a790fee3c65a3b30eb7cec35eb9cf5004aab Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Wed, 5 Nov 2025 19:22:06 +0900 Subject: [PATCH] =?UTF-8?q?feat(live-room-like-heart):=20=ED=8F=AD?= =?UTF-8?q?=EB=B0=9C=20=ED=9B=84=20=ED=95=98=ED=8A=B8=20=EB=B9=84/?= =?UTF-8?q?=EC=9A=B0=EB=B0=95=20=EC=95=A0=EB=8B=88=EB=A9=94=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Live/Room/LiveRoomViewModel.swift | 99 +++++++++++++++++-- 1 file changed, 91 insertions(+), 8 deletions(-) diff --git a/SodaLive/Sources/Live/Room/LiveRoomViewModel.swift b/SodaLive/Sources/Live/Room/LiveRoomViewModel.swift index 746afec..af20c37 100644 --- a/SodaLive/Sources/Live/Room/LiveRoomViewModel.swift +++ b/SodaLive/Sources/Live/Room/LiveRoomViewModel.swift @@ -8,6 +8,7 @@ import Foundation import Moya import Combine +import UIKit import AgoraRtcKit import AgoraRtmKit @@ -23,6 +24,8 @@ struct BigHeartParticle: Identifiable { var rotation: Double var life: Double // 남은 수명 (초) var size: CGFloat // 파편 기본 크기 (pt) + var isRain: Bool // 낙하 파편 여부 + var gravityScale: CGFloat // 중력 계수(파티클별 낙하 속도 분산) } final class LiveRoomViewModel: NSObject, ObservableObject { @@ -232,6 +235,12 @@ final class LiveRoomViewModel: NSObject, ObservableObject { @Published var remoteWavePhase: CGFloat = 0 @Published var bigHeartParticles: [BigHeartParticle] = [] + // 최근 폭발 파편의 개수/크기 기록(낙하 효과에 사용) + private var lastExplosionCount: Int = 0 + private var lastExplosionSizes: [CGFloat] = [] + // 폭발 파편이 모두 사라진 직후 비(낙하)를 스폰해야 하는지 여부 + private var shouldSpawnRainAfterExplosionEnds: Bool = false + var signatureImageUrls = [String]() var signatureList = [LiveRoomDonationResponse]() var isShowSignatureImage = false @@ -2149,9 +2158,9 @@ final class LiveRoomViewModel: NSObject, ObservableObject { var vy = CGFloat(sin(angle)) * speed // 조금 더 위로 포물선을 그리도록 초기 위쪽 임펄스 추가 vy -= CGFloat.random(in: 120...220) - // 크기: 30~80pt + // 크기: 20~65pt let size = CGFloat.random(in: 20...65) - let scale: CGFloat = 1.0 // 요구사항: 최종 크기 20~65 유지 (시간 경과에 따라 약간 축소) + let scale: CGFloat = 1.0 // 최종 크기 유지(시간 경과에 따라 약간 축소) let life: Double = Double.random(in: 1.0...1.5) let particle = BigHeartParticle( id: UUID(), @@ -2163,45 +2172,119 @@ final class LiveRoomViewModel: NSObject, ObservableObject { scale: scale, rotation: Double.random(in: 0...360), life: life, - size: size + size: size, + isRain: false, + gravityScale: 1.0 ) particles.append(particle) } + // 기록: 낙하 효과에 동일 개수/크기를 사용하기 위해 저장 + self.lastExplosionCount = count + self.lastExplosionSizes = particles.map { $0.size } DispatchQueue.main.async { self.bigHeartParticles = particles } + // 폭발 파편이 모두 사라진 직후 낙하 하트를 생성하도록 플래그 설정 + self.shouldSpawnRainAfterExplosionEnds = true + } + + // MARK: - BIG HEART - Rain Particles (after explosion) + private func spawnHeartRainFromLastExplosion() -> [BigHeartParticle] { + let count = max(0, self.lastExplosionCount) + guard count > 0 else { return [] } + let bounds = UIScreen.main.bounds + let startYBase = -bounds.height * 0.5 - 40 // 화면 위쪽 바깥에서 시작(기본) + var rains: [BigHeartParticle] = [] + rains.reserveCapacity(count) + for i in 0..= floorY { + continue + } if p.life > 0 && p.opacity > 0.01 { next.append(p) } } + // 폭발 파편 존재 여부(현재 프레임) + let hasExplosionAfter = next.contains(where: { !$0.isRain }) + // 요구사항: 폭발 파편이 모두 사라진 직후 비를 시작 + if hadExplosionBefore && !hasExplosionAfter && self.shouldSpawnRainAfterExplosionEnds { + let rains = self.spawnHeartRainFromLastExplosion() + if !rains.isEmpty { + next.append(contentsOf: rains) + } + self.shouldSpawnRainAfterExplosionEnds = false + } self.bigHeartParticles = next if next.isEmpty { self.bigHeartParticleTimer?.cancel()