simplify growth animation
remove stream, calculate manually with the analytics feedback for XP, new vocab and new morphs
This commit is contained in:
parent
b4f46938cb
commit
4dd64de133
7 changed files with 198 additions and 186 deletions
|
|
@ -1,9 +1,23 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/setting_keys.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
|
|
@ -74,19 +88,6 @@ import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'
|
|||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/share_scaffold_dialog.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
|
||||
import '../../utils/localized_exception_extension.dart';
|
||||
import 'send_file_dialog.dart';
|
||||
import 'send_location_dialog.dart';
|
||||
|
|
@ -191,7 +192,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
StreamSubscription? _levelSubscription;
|
||||
StreamSubscription? _constructsSubscription;
|
||||
StreamSubscription? _tokensSubscription;
|
||||
StreamSubscription? _constructLevelSubscription;
|
||||
|
||||
StreamSubscription? _botAudioSubscription;
|
||||
final timelineUpdateNotifier = _TimelineUpdateNotifier();
|
||||
|
|
@ -531,16 +531,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
choreographer.timesDismissedIT.addListener(_onCloseIT);
|
||||
final updater = Matrix.of(context).analyticsDataService.updateDispatcher;
|
||||
|
||||
_constructLevelSubscription =
|
||||
updater.constructLevelUpdateStream.stream.listen((update) {
|
||||
if (update.targetID != null) {
|
||||
OverlayUtil.showGrowthOverlay(
|
||||
context,
|
||||
update.targetID!,
|
||||
level: update.level,
|
||||
);
|
||||
}
|
||||
});
|
||||
_levelSubscription = updater.levelUpdateStream.stream.listen(_onLevelUp);
|
||||
|
||||
_constructsSubscription =
|
||||
|
|
@ -812,7 +802,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
_levelSubscription?.cancel();
|
||||
_botAudioSubscription?.cancel();
|
||||
_constructsSubscription?.cancel();
|
||||
_constructLevelSubscription?.cancel();
|
||||
_tokensSubscription?.cancel();
|
||||
_router.routeInformationProvider.removeListener(_onRouteChanged);
|
||||
choreographer.timesDismissedIT.removeListener(_onCloseIT);
|
||||
|
|
@ -2141,12 +2130,12 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
}
|
||||
}
|
||||
|
||||
void _sendMessageAnalytics(
|
||||
Future<void> _sendMessageAnalytics(
|
||||
String? eventId, {
|
||||
PangeaRepresentation? originalSent,
|
||||
PangeaMessageTokens? tokensSent,
|
||||
ChoreoRecordModel? choreo,
|
||||
}) {
|
||||
}) async {
|
||||
// There's a listen in my_analytics_controller that decides when to auto-update
|
||||
// analytics based on when / how many messages the logged in user send. This
|
||||
// stream sends the data for newly sent messages.
|
||||
|
|
@ -2169,8 +2158,8 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
),
|
||||
];
|
||||
|
||||
_showAnalyticsFeedback(constructs, eventId);
|
||||
addAnalytics(constructs, eventId);
|
||||
await addAnalytics(constructs, eventId);
|
||||
await _showAnalyticsFeedback(constructs, eventId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2215,11 +2204,11 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
final constructs = stt.constructs(roomId, eventId);
|
||||
if (constructs.isEmpty) return;
|
||||
|
||||
_showAnalyticsFeedback(constructs, eventId);
|
||||
Matrix.of(context).analyticsDataService.updateService.addAnalytics(
|
||||
await Matrix.of(context).analyticsDataService.updateService.addAnalytics(
|
||||
eventId,
|
||||
constructs,
|
||||
);
|
||||
await _showAnalyticsFeedback(constructs, eventId);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
|
|
@ -2355,15 +2344,34 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
ConstructTypeEnum.vocab,
|
||||
);
|
||||
|
||||
// Show growth animation for new vocab (seeds level)
|
||||
final newLeafConstructs = await analyticsService.getLeveledUpConstructCount(
|
||||
constructs,
|
||||
ConstructLevelEnum.greens,
|
||||
);
|
||||
final newFlowerConstructs =
|
||||
await analyticsService.getLeveledUpConstructCount(
|
||||
constructs,
|
||||
ConstructLevelEnum.flowers,
|
||||
);
|
||||
|
||||
// Show all growth animations at once
|
||||
final Map<ConstructLevelEnum, int> levelCounts = {};
|
||||
if (newVocabConstructs > 0) {
|
||||
for (int i = 0; i < newVocabConstructs; i++) {
|
||||
OverlayUtil.showGrowthOverlay(
|
||||
context,
|
||||
eventId,
|
||||
level: ConstructLevelEnum.seeds,
|
||||
);
|
||||
}
|
||||
levelCounts[ConstructLevelEnum.seeds] = newVocabConstructs;
|
||||
}
|
||||
if (newLeafConstructs > 0) {
|
||||
levelCounts[ConstructLevelEnum.greens] = newLeafConstructs;
|
||||
}
|
||||
if (newFlowerConstructs > 0) {
|
||||
levelCounts[ConstructLevelEnum.flowers] = newFlowerConstructs;
|
||||
}
|
||||
|
||||
if (levelCounts.isNotEmpty) {
|
||||
OverlayUtil.showGrowthOverlay(
|
||||
context,
|
||||
eventId,
|
||||
levelCounts: levelCounts,
|
||||
);
|
||||
}
|
||||
|
||||
OverlayUtil.showOverlay(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_data/analytics_database.dart';
|
||||
import 'package:fluffychat/pangea/analytics_data/analytics_database_builder.dart';
|
||||
import 'package:fluffychat/pangea/analytics_data/analytics_sync_controller.dart';
|
||||
|
|
@ -9,6 +11,7 @@ import 'package:fluffychat/pangea/analytics_data/analytics_update_service.dart';
|
|||
import 'package:fluffychat/pangea/analytics_data/construct_merge_table.dart';
|
||||
import 'package:fluffychat/pangea/analytics_data/derived_analytics_data_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_data/level_up_analytics_service.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
|
||||
|
|
@ -21,7 +24,6 @@ import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
|
|||
import 'package:fluffychat/pangea/languages/language_model.dart';
|
||||
import 'package:fluffychat/pangea/user/analytics_profile_model.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class _AnalyticsClient {
|
||||
final Client client;
|
||||
|
|
@ -368,6 +370,52 @@ class AnalyticsDataService {
|
|||
return newConstructCount;
|
||||
}
|
||||
|
||||
Future<int> getLeveledUpConstructCount(
|
||||
List<OneConstructUse> newConstructs,
|
||||
ConstructLevelEnum targetLevel,
|
||||
) async {
|
||||
await _ensureInitialized();
|
||||
final blocked = blockedConstructs;
|
||||
final uses =
|
||||
newConstructs.where((c) => !blocked.contains(c.identifier)).toList();
|
||||
|
||||
final Map<ConstructIdentifier, int> constructPoints = {};
|
||||
for (final use in uses) {
|
||||
constructPoints[use.identifier] ??= 0;
|
||||
constructPoints[use.identifier] =
|
||||
constructPoints[use.identifier]! + use.xp;
|
||||
}
|
||||
|
||||
final constructs = await getConstructUses(constructPoints.keys.toList());
|
||||
|
||||
int leveledUpCount = 0;
|
||||
for (final entry in constructPoints.entries) {
|
||||
final construct = constructs[entry.key]!;
|
||||
// Use uncapped points to correctly calculate previous level
|
||||
// even when construct is already at max (flowers)
|
||||
final uncapped = construct.uncappedPoints;
|
||||
final prevUncapped = uncapped - entry.value;
|
||||
final prevLevel = _getLevelFromPoints(prevUncapped);
|
||||
final newLevel = _getLevelFromPoints(uncapped);
|
||||
|
||||
if (prevLevel.xpNeeded < targetLevel.xpNeeded &&
|
||||
newLevel == targetLevel) {
|
||||
leveledUpCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return leveledUpCount;
|
||||
}
|
||||
|
||||
ConstructLevelEnum _getLevelFromPoints(int points) {
|
||||
if (points < AnalyticsConstants.xpForGreens) {
|
||||
return ConstructLevelEnum.seeds;
|
||||
} else if (points >= AnalyticsConstants.xpForFlower) {
|
||||
return ConstructLevelEnum.flowers;
|
||||
}
|
||||
return ConstructLevelEnum.greens;
|
||||
}
|
||||
|
||||
Future<void> updateXPOffset(int offset) async {
|
||||
_invalidateCaches();
|
||||
await _analyticsClientGetter.database.updateXPOffset(offset);
|
||||
|
|
@ -445,18 +493,6 @@ class AnalyticsDataService {
|
|||
for (final entry in newConstructs.entries) {
|
||||
final prevConstruct = prevConstructs[entry.key];
|
||||
if (prevConstruct == null) continue;
|
||||
|
||||
final prevLevel = prevConstruct.lemmaCategory;
|
||||
final newLevel = entry.value.lemmaCategory;
|
||||
if (newLevel.xpNeeded > prevLevel.xpNeeded) {
|
||||
events.add(
|
||||
ConstructLevelUpEvent(
|
||||
entry.key,
|
||||
newLevel,
|
||||
update.targetID,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (update.blockedConstruct != null) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import 'package:fluffychat/pangea/analytics_data/analytics_data_service.dart';
|
|||
import 'package:fluffychat/pangea/analytics_data/analytics_update_events.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/user_set_lemma_info.dart';
|
||||
|
||||
class LevelUpdate {
|
||||
|
|
@ -17,18 +16,6 @@ class LevelUpdate {
|
|||
});
|
||||
}
|
||||
|
||||
class ConstructLevelUpdate {
|
||||
final ConstructIdentifier constructId;
|
||||
final ConstructLevelEnum level;
|
||||
final String? targetID;
|
||||
|
||||
ConstructLevelUpdate({
|
||||
required this.constructId,
|
||||
required this.level,
|
||||
this.targetID,
|
||||
});
|
||||
}
|
||||
|
||||
class AnalyticsUpdate {
|
||||
final List<OneConstructUse> addedConstructs;
|
||||
final ConstructIdentifier? blockedConstruct;
|
||||
|
|
@ -59,9 +46,6 @@ class AnalyticsUpdateDispatcher {
|
|||
final StreamController<Set<ConstructIdentifier>> newConstructsStream =
|
||||
StreamController<Set<ConstructIdentifier>>.broadcast();
|
||||
|
||||
final StreamController<ConstructLevelUpdate> constructLevelUpdateStream =
|
||||
StreamController<ConstructLevelUpdate>.broadcast();
|
||||
|
||||
final StreamController<MapEntry<ConstructIdentifier, UserSetLemmaInfo>>
|
||||
_lemmaInfoUpdateStream = StreamController<
|
||||
MapEntry<ConstructIdentifier, UserSetLemmaInfo>>.broadcast();
|
||||
|
|
@ -74,7 +58,6 @@ class AnalyticsUpdateDispatcher {
|
|||
unlockedConstructsStream.close();
|
||||
levelUpdateStream.close();
|
||||
_lemmaInfoUpdateStream.close();
|
||||
constructLevelUpdateStream.close();
|
||||
}
|
||||
|
||||
Stream<UserSetLemmaInfo> lemmaUpdateStream(
|
||||
|
|
@ -118,9 +101,6 @@ class AnalyticsUpdateDispatcher {
|
|||
case final ConstructBlockedEvent e:
|
||||
_onBlockedConstruct(e.blockedConstruct);
|
||||
break;
|
||||
case final ConstructLevelUpEvent e:
|
||||
_onConstructLevelUp(e.constructId, e.level, e.targetID);
|
||||
break;
|
||||
case final NewConstructsEvent e:
|
||||
_onNewConstruct(e.newConstructs);
|
||||
break;
|
||||
|
|
@ -164,20 +144,6 @@ class AnalyticsUpdateDispatcher {
|
|||
constructUpdateStream.add(update);
|
||||
}
|
||||
|
||||
void _onConstructLevelUp(
|
||||
ConstructIdentifier constructId,
|
||||
ConstructLevelEnum level,
|
||||
String? targetID,
|
||||
) {
|
||||
constructLevelUpdateStream.add(
|
||||
ConstructLevelUpdate(
|
||||
constructId: constructId,
|
||||
level: level,
|
||||
targetID: targetID,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onNewConstruct(Set<ConstructIdentifier> constructIds) {
|
||||
if (constructIds.isEmpty) return;
|
||||
newConstructsStream.add(constructIds);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
|
||||
|
||||
sealed class AnalyticsUpdateEvent {}
|
||||
|
||||
|
|
@ -14,17 +13,6 @@ class MorphUnlockedEvent extends AnalyticsUpdateEvent {
|
|||
MorphUnlockedEvent(this.unlocked);
|
||||
}
|
||||
|
||||
class ConstructLevelUpEvent extends AnalyticsUpdateEvent {
|
||||
final ConstructIdentifier constructId;
|
||||
final ConstructLevelEnum level;
|
||||
final String? targetID;
|
||||
ConstructLevelUpEvent(
|
||||
this.constructId,
|
||||
this.level,
|
||||
this.targetID,
|
||||
);
|
||||
}
|
||||
|
||||
class XPGainedEvent extends AnalyticsUpdateEvent {
|
||||
final int points;
|
||||
final String? targetID;
|
||||
|
|
|
|||
|
|
@ -37,6 +37,13 @@ class ConstructUses {
|
|||
);
|
||||
}
|
||||
|
||||
int get uncappedPoints {
|
||||
return _uses.fold<int>(
|
||||
0,
|
||||
(total, use) => total + use.xp,
|
||||
);
|
||||
}
|
||||
|
||||
DateTime? get lastUsed => _uses.lastOrNull?.timeStamp;
|
||||
DateTime? get cappedLastUse => cappedUses.lastOrNull?.timeStamp;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,37 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class _GrowthItem {
|
||||
final ConstructLevelEnum level;
|
||||
final double horizontalOffset;
|
||||
final double wiggleAmplitude;
|
||||
final double wiggleFrequency;
|
||||
final int delayMs;
|
||||
|
||||
_GrowthItem({
|
||||
required this.level,
|
||||
required this.horizontalOffset,
|
||||
required this.wiggleAmplitude,
|
||||
required this.wiggleFrequency,
|
||||
required this.delayMs,
|
||||
});
|
||||
}
|
||||
|
||||
class GrowthAnimation extends StatefulWidget {
|
||||
final String targetID;
|
||||
final ConstructLevelEnum level;
|
||||
final double horizontalOffset;
|
||||
final VoidCallback? onComplete;
|
||||
|
||||
final int durationMs;
|
||||
final Map<ConstructLevelEnum, int> levelCounts;
|
||||
final int itemDurationMs;
|
||||
final double riseDistance;
|
||||
|
||||
const GrowthAnimation({
|
||||
super.key,
|
||||
required this.targetID,
|
||||
required this.level,
|
||||
this.horizontalOffset = 0,
|
||||
this.onComplete,
|
||||
this.durationMs = 1600,
|
||||
required this.levelCounts,
|
||||
this.itemDurationMs = 1600,
|
||||
this.riseDistance = 72,
|
||||
});
|
||||
|
||||
|
|
@ -30,40 +42,59 @@ class GrowthAnimation extends StatefulWidget {
|
|||
class _GrowthAnimationState extends State<GrowthAnimation>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
late final Animation<double> _progress;
|
||||
late final Random _random;
|
||||
late final double _wiggleAmplitude;
|
||||
late final double _wiggleFrequency;
|
||||
late final int _actualDuration;
|
||||
late final List<_GrowthItem> _items;
|
||||
late final int _totalDurationMs;
|
||||
final Random _random = Random();
|
||||
|
||||
static const int _staggerDelayMs = 50;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Create random variations for animation
|
||||
_random = Random();
|
||||
_wiggleAmplitude = 4.0 + _random.nextDouble() * 4.0;
|
||||
_wiggleFrequency = 1.5 + _random.nextDouble() * 1.0;
|
||||
_actualDuration = widget.durationMs + (_random.nextInt(400) - 200);
|
||||
_items = _buildItems();
|
||||
final maxDelay = _items.isEmpty ? 0 : _items.last.delayMs;
|
||||
_totalDurationMs = maxDelay + widget.itemDurationMs;
|
||||
|
||||
_controller = AnimationController(
|
||||
duration: Duration(milliseconds: _actualDuration),
|
||||
duration: Duration(milliseconds: _totalDurationMs),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_progress = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
|
||||
_controller.forward().then((_) {
|
||||
if (!mounted) return;
|
||||
MatrixState.pAnyState
|
||||
.closeOverlay("${widget.targetID}_growth_${widget.hashCode}");
|
||||
widget.onComplete?.call();
|
||||
MatrixState.pAnyState.closeOverlay("${widget.targetID}_growth");
|
||||
});
|
||||
}
|
||||
|
||||
List<_GrowthItem> _buildItems() {
|
||||
final items = <_GrowthItem>[];
|
||||
int index = 0;
|
||||
|
||||
for (final level in [
|
||||
ConstructLevelEnum.seeds,
|
||||
ConstructLevelEnum.greens,
|
||||
ConstructLevelEnum.flowers,
|
||||
]) {
|
||||
final count = widget.levelCounts[level] ?? 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
final side = index % 2 == 0 ? 1 : -1;
|
||||
final distance = ((index + 1) ~/ 2) * 30.0;
|
||||
|
||||
items.add(
|
||||
_GrowthItem(
|
||||
level: level,
|
||||
horizontalOffset: side * distance,
|
||||
wiggleAmplitude: 4.0 + _random.nextDouble() * 4.0,
|
||||
wiggleFrequency: 1.5 + _random.nextDouble() * 1.0,
|
||||
delayMs: index * _staggerDelayMs,
|
||||
),
|
||||
);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
|
|
@ -72,27 +103,41 @@ class _GrowthAnimationState extends State<GrowthAnimation>
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_items.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: AnimatedBuilder(
|
||||
animation: _progress,
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
final clampedT = _progress.value.clamp(0.0, 1.0);
|
||||
final dy = -widget.riseDistance * clampedT;
|
||||
final opacity = clampedT < 0.5 ? clampedT * 2 : (1.0 - clampedT) * 2;
|
||||
|
||||
final wiggle =
|
||||
sin(clampedT * pi * _wiggleFrequency) * _wiggleAmplitude;
|
||||
|
||||
return Opacity(
|
||||
opacity: opacity,
|
||||
child: Transform.translate(
|
||||
offset: Offset(widget.horizontalOffset + wiggle, dy),
|
||||
child: widget.level.icon(24),
|
||||
),
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: _items.map(_buildItem).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItem(_GrowthItem item) {
|
||||
final elapsedMs = _controller.value * _totalDurationMs;
|
||||
final itemElapsedMs =
|
||||
(elapsedMs - item.delayMs).clamp(0.0, widget.itemDurationMs.toDouble());
|
||||
final t = (itemElapsedMs / widget.itemDurationMs).clamp(0.0, 1.0);
|
||||
|
||||
if (t <= 0) return const SizedBox.shrink();
|
||||
|
||||
final curvedT = Curves.easeOut.transform(t);
|
||||
final dy = -widget.riseDistance * curvedT;
|
||||
final opacity = t < 0.5 ? t * 2 : (1.0 - t) * 2;
|
||||
final wiggle = sin(t * pi * item.wiggleFrequency) * item.wiggleAmplitude;
|
||||
|
||||
return Transform.translate(
|
||||
offset: Offset(item.horizontalOffset + wiggle, dy),
|
||||
child: Opacity(
|
||||
opacity: opacity.clamp(0.0, 1.0),
|
||||
child: item.level.icon(24),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/growth_animation.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/level_up/star_rain_widget.dart';
|
||||
|
|
@ -13,9 +16,6 @@ import 'package:fluffychat/pangea/common/widgets/overlay_container.dart';
|
|||
import 'package:fluffychat/pangea/common/widgets/transparent_backdrop.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/language_mismatch_popup.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../config/themes.dart';
|
||||
import '../../../widgets/matrix.dart';
|
||||
import 'error_handler.dart';
|
||||
|
|
@ -27,9 +27,6 @@ enum OverlayPositionEnum {
|
|||
}
|
||||
|
||||
class OverlayUtil {
|
||||
static int _growthAnimationCount = 0;
|
||||
static final Set<String> _activeGrowthAnimations = {};
|
||||
|
||||
static bool showOverlay({
|
||||
required BuildContext context,
|
||||
required Widget child,
|
||||
|
|
@ -320,59 +317,24 @@ class OverlayUtil {
|
|||
static void showGrowthOverlay(
|
||||
BuildContext context,
|
||||
String targetId, {
|
||||
required ConstructLevelEnum level,
|
||||
required Map<ConstructLevelEnum, int> levelCounts,
|
||||
}) {
|
||||
// Clean up any stale entries from dismissed overlays
|
||||
_cleanupStaleGrowthAnimations();
|
||||
|
||||
final animationKey = "${targetId}_growth_${_growthAnimationCount + 1}";
|
||||
|
||||
// Calculate offset based on how many are currently active
|
||||
final activeCount = _activeGrowthAnimations.length;
|
||||
final double horizontalOffset;
|
||||
|
||||
if (activeCount == 0) {
|
||||
// Start in the middle if no animations are running
|
||||
horizontalOffset = 0.0;
|
||||
} else {
|
||||
final side = activeCount % 2 == 0 ? 1 : -1;
|
||||
final distance = ((activeCount + 1) ~/ 2) * 30.0;
|
||||
horizontalOffset = side * distance;
|
||||
}
|
||||
|
||||
_growthAnimationCount++;
|
||||
_activeGrowthAnimations.add(animationKey);
|
||||
|
||||
showOverlay(
|
||||
overlayKey: animationKey,
|
||||
overlayKey: "${targetId}_growth",
|
||||
followerAnchor: Alignment.topCenter,
|
||||
targetAnchor: Alignment.topCenter,
|
||||
context: context,
|
||||
child: GrowthAnimation(
|
||||
targetID: targetId,
|
||||
level: level,
|
||||
horizontalOffset: horizontalOffset,
|
||||
onComplete: () {
|
||||
_activeGrowthAnimations.remove(animationKey);
|
||||
},
|
||||
levelCounts: levelCounts,
|
||||
),
|
||||
transformTargetId: targetId,
|
||||
closePrevOverlay: false,
|
||||
backDropToDismiss: false,
|
||||
ignorePointer: true,
|
||||
offset: Offset(horizontalOffset, 0),
|
||||
);
|
||||
}
|
||||
|
||||
static void _cleanupStaleGrowthAnimations() {
|
||||
// Remove any tracked animations that are no longer in the overlay system
|
||||
_activeGrowthAnimations.removeWhere((key) {
|
||||
final isActive =
|
||||
MatrixState.pAnyState.entries.any((entry) => entry.key == key);
|
||||
return !isActive;
|
||||
});
|
||||
}
|
||||
|
||||
static void showLanguageMismatchPopup({
|
||||
required BuildContext context,
|
||||
required String targetId,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue