diff --git a/lib/pages/player/widgets/player_double_tap_hearts.dart b/lib/pages/player/widgets/player_double_tap_hearts.dart index c0efa04..76d0d57 100644 --- a/lib/pages/player/widgets/player_double_tap_hearts.dart +++ b/lib/pages/player/widgets/player_double_tap_hearts.dart @@ -18,6 +18,7 @@ class PlayerDoubleTapHeartBurst extends StatefulWidget { static const int minParticleCount = 4; static const int maxParticleCount = 5; + static const int minimumBurstDurationMs = 2000; final int burstId; final VoidCallback onCompleted; @@ -60,11 +61,11 @@ class _PlayerDoubleTapHeartBurstState extends State 1, ), (index) { - if (index > 0) delay += 40 + random.nextInt(21); + if (index > 0) delay += 80 + random.nextInt(41); return _HeartParticle( delay: delay, - duration: 900 + random.nextInt(201), - travel: 120 + random.nextInt(41), + duration: 1500 + random.nextInt(301), + travel: 100 + random.nextInt(31), drift: -30 + random.nextInt(61), size: (22 + random.nextInt(13)).toDouble(), color: random.nextBool() @@ -73,9 +74,22 @@ class _PlayerDoubleTapHeartBurstState extends State ); }, ); - final total = _particles + var total = _particles .map((particle) => particle.delay + particle.duration) .reduce(max); + if (total < PlayerDoubleTapHeartBurst.minimumBurstDurationMs) { + final last = _particles.last; + final requiredDuration = + PlayerDoubleTapHeartBurst.minimumBurstDurationMs - last.delay; + final adjustedDuration = max( + last.duration, + requiredDuration, + ).clamp(1500, 1800).toInt(); + _particles[_particles.length - 1] = last.withDuration(adjustedDuration); + total = _particles + .map((particle) => particle.delay + particle.duration) + .reduce(max); + } if (kDebugMode) { debugPrint('[player] phase=heart-burst-init id=${widget.burstId}'); debugPrint( @@ -97,7 +111,7 @@ class _PlayerDoubleTapHeartBurstState extends State final elapsedMs = _controller.duration?.inMilliseconds ?? 0; debugPrint( '[player] phase=heart-burst-complete ' - 'id=${widget.burstId} elapsedMs=$elapsedMs', + 'id=${widget.burstId} configuredMs=$elapsedMs', ); } widget.onCompleted(); @@ -106,9 +120,12 @@ class _PlayerDoubleTapHeartBurstState extends State @override void dispose() { if (kDebugMode) { + final elapsedMs = _completed + ? _controller.duration?.inMilliseconds ?? 0 + : _lifetime.elapsedMilliseconds; debugPrint( '[player] phase=heart-burst-dispose ' - 'id=${widget.burstId} elapsedMs=${_lifetime.elapsedMilliseconds} ' + 'id=${widget.burstId} elapsedMs=$elapsedMs ' 'completed=${_completed ? 1 : 0}', ); } @@ -184,4 +201,15 @@ class _HeartParticle { final int drift; final double size; final Color color; + + _HeartParticle withDuration(int nextDuration) { + return _HeartParticle( + delay: delay, + duration: nextDuration, + travel: travel, + drift: drift, + size: size, + color: color, + ); + } } diff --git a/test/player/player_double_tap_hearts_test.dart b/test/player/player_double_tap_hearts_test.dart index 3841031..98d7cc4 100644 --- a/test/player/player_double_tap_hearts_test.dart +++ b/test/player/player_double_tap_hearts_test.dart @@ -21,7 +21,7 @@ void main() { ), ), ); - await tester.pump(const Duration(milliseconds: 400)); + await tester.pump(const Duration(milliseconds: 600)); final hearts = find.byIcon(Icons.favorite_rounded); final count = hearts.evaluate().length; @@ -53,6 +53,35 @@ void main() { expect(PlayerDoubleTapHeartBurst.minParticleCount, 4); expect(PlayerDoubleTapHeartBurst.maxParticleCount, 5); }); + + testWidgets('heart burst completes after at least two simulated seconds', ( + tester, + ) async { + var completed = false; + var elapsedMs = 0; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: PlayerDoubleTapHeartBurst( + burstId: 2, + random: Random(7), + onCompleted: () => completed = true, + ), + ), + ), + ); + + while (!completed && elapsedMs < 4000) { + await tester.pump(const Duration(milliseconds: 16)); + elapsedMs += 16; + } + + expect(completed, isTrue); + expect(elapsedMs, greaterThanOrEqualTo(2000)); + expect(elapsedMs, lessThanOrEqualTo(2600)); + final count = find.byIcon(Icons.favorite_rounded).evaluate().length; + expect(count, inInclusiveRange(4, 5)); + }); } double _size(WidgetTester tester, int index) {