diff --git a/SodaLive/Sources/Live/Room/LiveRoomViewModel.swift b/SodaLive/Sources/Live/Room/LiveRoomViewModel.swift index fa59594..4819a63 100644 --- a/SodaLive/Sources/Live/Room/LiveRoomViewModel.swift +++ b/SodaLive/Sources/Live/Room/LiveRoomViewModel.swift @@ -9,6 +9,7 @@ import Foundation import Moya import Combine import UIKit +import SwiftUI import AgoraRtcKit import AgoraRtmKit @@ -238,6 +239,8 @@ final class LiveRoomViewModel: NSObject, ObservableObject { @Published var isShowRemoteBigHeart: Bool = false @Published var remoteWaterProgress: CGFloat = 0 @Published var remoteWavePhase: CGFloat = 0 + // 수신자 하트 스케일(0~1): 1.0~1.5초 랜덤으로 0→1 스케일 업 + @Published var remoteHeartScale: CGFloat = 1.0 @Published var bigHeartParticles: [BigHeartParticle] = [] // 최근 폭발 파편의 개수/크기 기록(낙하 효과에 사용) @@ -245,6 +248,8 @@ final class LiveRoomViewModel: NSObject, ObservableObject { private var lastExplosionSizes: [CGFloat] = [] // 폭발 파편이 모두 사라진 직후 비(낙하)를 스폰해야 하는지 여부 private var shouldSpawnRainAfterExplosionEnds: Bool = false + // 진행 중인 연쇄 폭발(버스트) 남은 횟수(중앙 1회 + 랜덤 6회 = 7) + private var pendingExplosionBursts: Int = 0 var signatureImageUrls = [String]() var signatureList = [LiveRoomDonationResponse]() @@ -2102,29 +2107,34 @@ final class LiveRoomViewModel: NSObject, ObservableObject { } private func addBigHeartAnimation() { - // 로컬 발신 직후 1회는 원격 물 채움 연출을 생략하고 바로 폭발만 실행 + // 로컬 발신 직후 1회는 원격 물 채움 연출을 생략하고 바로 연쇄 폭발만 실행 if suppressNextRemoteWaterFill { suppressNextRemoteWaterFill = false - spawnHeartExplosion() + scheduleExplosionBursts() startParticlesTimer() return } - // 요구사항 변경: 물 채우기(1초) 연출 제거. - // 가득 찬 하트를 잠깐(0.15초) 보여준 뒤 폭발 이펙트를 실행. + // 요구사항: 수신자 하트는 0→현재 크기까지 0.5초 동안 스케일 업 후 연쇄 폭발 DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.remoteWaterTimer?.cancel() self.remoteWaterTimer = nil self.remoteWavePhase = 0 self.remoteWaterProgress = 1.0 + self.remoteHeartScale = 0.0 self.isShowRemoteBigHeart = true - DEBUG_LOG("BIG_HEART: show filled heart, then explode after 0.30s") - DispatchQueue.main.asyncAfter(deadline: .now() + 0.30) { [weak self] in + let growDuration = Double.random(in: 1.0...1.5) + withAnimation(.easeOut(duration: growDuration)) { + self.remoteHeartScale = 1.0 + } + DEBUG_LOG("BIG_HEART: scale-up \(growDuration)s then sequential 7 bursts") + DispatchQueue.main.asyncAfter(deadline: .now() + growDuration) { [weak self] in guard let self = self else { return } self.isShowRemoteBigHeart = false self.remoteWaterProgress = 0 self.remoteWavePhase = 0 - self.spawnHeartExplosion() + self.remoteHeartScale = 1.0 + self.scheduleExplosionBursts() self.startParticlesTimer() } } @@ -2165,11 +2175,11 @@ final class LiveRoomViewModel: NSObject, ObservableObject { } // MARK: - BIG HEART - Explosion Particles - private func spawnHeartExplosion() { - // 중심(0,0)에서 폭발하는 작은 하트 파편 생성 - // 색상은 View에서 #ff959a로 렌더링 + private func spawnHeartExplosion(origin: CGPoint = .zero) { + // 주어진 origin에서 폭발하는 작은 하트 파편 생성 + let explosionDuration: Double = 0.7 // 1회 폭발 수명 var particles: [BigHeartParticle] = [] - let count = Int.random(in: 20...35) // 요구사항: 파편 개수 20~35개 + let count = Int.random(in: 20...35) // 파편 개수 20~35개 for i in 0.. [BigHeartParticle] { - let count = max(0, self.lastExplosionCount) - guard count > 0 else { return [] } + // 요구사항: 폭발 후 비 내리는 하트는 80~100개 랜덤 + let count = Int.random(in: 80...100) let bounds = UIScreen.main.bounds let startYBase = -bounds.height * 0.5 - 40 // 화면 위쪽 바깥에서 시작(기본) var rains: [BigHeartParticle] = [] rains.reserveCapacity(count) - for i in 0..