diff --git a/SodaLive/Sources/Live/Room/LiveRoomViewModel.swift b/SodaLive/Sources/Live/Room/LiveRoomViewModel.swift index 8f59d52..9237c45 100644 --- a/SodaLive/Sources/Live/Room/LiveRoomViewModel.swift +++ b/SodaLive/Sources/Live/Room/LiveRoomViewModel.swift @@ -256,6 +256,15 @@ final class LiveRoomViewModel: NSObject, ObservableObject { private let sequenceMinInterval: TimeInterval = 1.0 // 마지막 폭발 후 비 시작 허용 시각(지연 0.5s 반영) private var rainEligibleAt: Date? = nil + private enum BigHeartSequenceState { + case idle + case exploding + case waitingForRain + case raining + } + private var bigHeartSequenceState: BigHeartSequenceState = .idle + private var bigHeartAnimationQueueCount: Int = 0 + private var isBigHeartAnimationPlaying: Bool = false var signatureImageUrls = [String]() var signatureList = [LiveRoomDonationResponse]() @@ -2113,6 +2122,15 @@ final class LiveRoomViewModel: NSObject, ObservableObject { } private func addBigHeartAnimation() { + if isBigHeartAnimationPlaying { + bigHeartAnimationQueueCount += 1 + return + } + isBigHeartAnimationPlaying = true + startBigHeartAnimation() + } + + private func startBigHeartAnimation() { // 로컬 발신 직후 1회는 원격 물 채움 연출을 생략하고 바로 연쇄 폭발만 실행 if suppressNextRemoteWaterFill { suppressNextRemoteWaterFill = false @@ -2120,10 +2138,14 @@ final class LiveRoomViewModel: NSObject, ObservableObject { startExplosionSequence() return } - // 스로틀: 과도한 중복 실행 방지 + // 스로틀: 과도한 중복 실행 방지(드랍 대신 지연) let now = Date() - if now.timeIntervalSince(lastSequenceStartedAt) < sequenceMinInterval { - DEBUG_LOG("BIG_HEART: throttled to protect performance") + let elapsed = now.timeIntervalSince(lastSequenceStartedAt) + if elapsed < sequenceMinInterval { + let delay = sequenceMinInterval - elapsed + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in + self?.startBigHeartAnimation() + } return } lastSequenceStartedAt = now @@ -2240,6 +2262,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject { DispatchQueue.main.async { [weak self] in guard let self = self else { return } + self.bigHeartSequenceState = .exploding self.pendingExplosionBursts = 7 let bounds = UIScreen.main.bounds @@ -2257,8 +2280,10 @@ final class LiveRoomViewModel: NSObject, ObservableObject { self.explosionSequenceTimer = nil // 마지막 폭발 후 0.5초 지연 뒤 비 시작 + self.bigHeartSequenceState = .waitingForRain DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in guard let self = self else { return } + self.bigHeartSequenceState = .raining let rains = self.spawnHeartRainFromLastExplosion() if !rains.isEmpty { self.bigHeartParticles.append(contentsOf: rains) @@ -2266,6 +2291,9 @@ final class LiveRoomViewModel: NSObject, ObservableObject { self.startParticlesTimer() } } + if rains.isEmpty && self.pendingExplosionBursts == 0 && self.bigHeartParticles.isEmpty { + self.completeBigHeartAnimation() + } } return } @@ -2404,8 +2432,17 @@ final class LiveRoomViewModel: NSObject, ObservableObject { } self.bigHeartParticles = merged if merged.isEmpty && self.pendingExplosionBursts == 0 { - self.bigHeartParticleTimer?.cancel() - self.bigHeartParticleTimer = nil + switch self.bigHeartSequenceState { + case .waitingForRain, .exploding: + break + case .raining: + self.completeBigHeartAnimation() + self.bigHeartParticleTimer?.cancel() + self.bigHeartParticleTimer = nil + case .idle: + self.bigHeartParticleTimer?.cancel() + self.bigHeartParticleTimer = nil + } } } } @@ -2414,6 +2451,16 @@ final class LiveRoomViewModel: NSObject, ObservableObject { } } + private func completeBigHeartAnimation() { + self.bigHeartSequenceState = .idle + self.isBigHeartAnimationPlaying = false + if self.bigHeartAnimationQueueCount > 0 { + self.bigHeartAnimationQueueCount -= 1 + self.isBigHeartAnimationPlaying = true + self.startBigHeartAnimation() + } + } + private func invalidateChat() { messageChangeFlag.toggle() if messages.count > 100 {