diff --git a/packages/trashy_road/analysis_options.yaml b/packages/trashy_road/analysis_options.yaml index 9ee96862..5443356c 100644 --- a/packages/trashy_road/analysis_options.yaml +++ b/packages/trashy_road/analysis_options.yaml @@ -2,6 +2,7 @@ include: package:very_good_analysis/analysis_options.5.1.0.yaml analyzer: exclude: - lib/**/*.gen.dart + - lib/l10n/arb/**.dart linter: rules: public_member_api_docs: false diff --git a/packages/trashy_road/assets/audio/bounce_back.mp3 b/packages/trashy_road/assets/audio/bounce_back.mp3 new file mode 100644 index 00000000..51d663c0 Binary files /dev/null and b/packages/trashy_road/assets/audio/bounce_back.mp3 differ diff --git a/packages/trashy_road/assets/audio/deposit_trash_1.wav b/packages/trashy_road/assets/audio/deposit_trash_1.wav new file mode 100644 index 00000000..e77f537a Binary files /dev/null and b/packages/trashy_road/assets/audio/deposit_trash_1.wav differ diff --git a/packages/trashy_road/assets/audio/deposit_trash_2.wav b/packages/trashy_road/assets/audio/deposit_trash_2.wav new file mode 100644 index 00000000..81e88116 Binary files /dev/null and b/packages/trashy_road/assets/audio/deposit_trash_2.wav differ diff --git a/packages/trashy_road/assets/audio/deposit_trash_3.wav b/packages/trashy_road/assets/audio/deposit_trash_3.wav new file mode 100644 index 00000000..23157c25 Binary files /dev/null and b/packages/trashy_road/assets/audio/deposit_trash_3.wav differ diff --git a/packages/trashy_road/assets/audio/deposit_trash_4.wav b/packages/trashy_road/assets/audio/deposit_trash_4.wav new file mode 100644 index 00000000..eeb973d7 Binary files /dev/null and b/packages/trashy_road/assets/audio/deposit_trash_4.wav differ diff --git a/packages/trashy_road/assets/audio/deposit_trash_5.wav b/packages/trashy_road/assets/audio/deposit_trash_5.wav new file mode 100644 index 00000000..8f4642f4 Binary files /dev/null and b/packages/trashy_road/assets/audio/deposit_trash_5.wav differ diff --git a/packages/trashy_road/assets/audio/hinting_arrow.mp3 b/packages/trashy_road/assets/audio/hinting_arrow.mp3 new file mode 100644 index 00000000..a5973d8c Binary files /dev/null and b/packages/trashy_road/assets/audio/hinting_arrow.mp3 differ diff --git a/packages/trashy_road/assets/audio/rating_stars0.mp3 b/packages/trashy_road/assets/audio/rating_stars0.mp3 new file mode 100644 index 00000000..2af571dd Binary files /dev/null and b/packages/trashy_road/assets/audio/rating_stars0.mp3 differ diff --git a/packages/trashy_road/assets/audio/rating_stars1.mp3 b/packages/trashy_road/assets/audio/rating_stars1.mp3 new file mode 100644 index 00000000..61431a0b Binary files /dev/null and b/packages/trashy_road/assets/audio/rating_stars1.mp3 differ diff --git a/packages/trashy_road/assets/audio/rating_stars2.mp3 b/packages/trashy_road/assets/audio/rating_stars2.mp3 new file mode 100644 index 00000000..515f928e Binary files /dev/null and b/packages/trashy_road/assets/audio/rating_stars2.mp3 differ diff --git a/packages/trashy_road/assets/audio/rating_stars3.mp3 b/packages/trashy_road/assets/audio/rating_stars3.mp3 new file mode 100644 index 00000000..de38f4fc Binary files /dev/null and b/packages/trashy_road/assets/audio/rating_stars3.mp3 differ diff --git a/packages/trashy_road/assets/audio/running_time.mp3 b/packages/trashy_road/assets/audio/running_time.mp3 new file mode 100644 index 00000000..38fd311f Binary files /dev/null and b/packages/trashy_road/assets/audio/running_time.mp3 differ diff --git a/packages/trashy_road/assets/audio/step1.mp3 b/packages/trashy_road/assets/audio/step1.mp3 new file mode 100644 index 00000000..c4501ddf Binary files /dev/null and b/packages/trashy_road/assets/audio/step1.mp3 differ diff --git a/packages/trashy_road/assets/audio/steps.mp3 b/packages/trashy_road/assets/audio/steps.mp3 new file mode 100644 index 00000000..e296620d Binary files /dev/null and b/packages/trashy_road/assets/audio/steps.mp3 differ diff --git a/packages/trashy_road/assets/audio/trash_collected.mp3 b/packages/trashy_road/assets/audio/trash_collected.mp3 new file mode 100644 index 00000000..e500ef10 Binary files /dev/null and b/packages/trashy_road/assets/audio/trash_collected.mp3 differ diff --git a/packages/trashy_road/assets/audio/wrong_bin.mp3 b/packages/trashy_road/assets/audio/wrong_bin.mp3 new file mode 100644 index 00000000..7853c416 Binary files /dev/null and b/packages/trashy_road/assets/audio/wrong_bin.mp3 differ diff --git a/packages/trashy_road/assets/images/display/slot_plastic_bottle.png b/packages/trashy_road/assets/images/display/slot_plastic_bottle.png index 3e9ed5f6..85affc5b 100644 Binary files a/packages/trashy_road/assets/images/display/slot_plastic_bottle.png and b/packages/trashy_road/assets/images/display/slot_plastic_bottle.png differ diff --git a/packages/trashy_road/l10n.yaml b/packages/trashy_road/l10n.yaml new file mode 100644 index 00000000..0c1cf4a9 --- /dev/null +++ b/packages/trashy_road/l10n.yaml @@ -0,0 +1,4 @@ +arb-dir: lib/l10n/arb +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart +synthetic-package: false diff --git a/packages/trashy_road/lib/gen/assets.gen.dart b/packages/trashy_road/lib/gen/assets.gen.dart index 7cc3c1d5..de0828ce 100644 --- a/packages/trashy_road/lib/gen/assets.gen.dart +++ b/packages/trashy_road/lib/gen/assets.gen.dart @@ -18,11 +18,74 @@ class $AssetsAudioGen { /// File path: assets/audio/background_music.mp3 String get backgroundMusic => 'assets/audio/background_music.mp3'; + /// File path: assets/audio/bounce_back.mp3 + String get bounceBack => 'assets/audio/bounce_back.mp3'; + + /// File path: assets/audio/deposit_trash_1.wav + String get depositTrash1 => 'assets/audio/deposit_trash_1.wav'; + + /// File path: assets/audio/deposit_trash_2.wav + String get depositTrash2 => 'assets/audio/deposit_trash_2.wav'; + + /// File path: assets/audio/deposit_trash_3.wav + String get depositTrash3 => 'assets/audio/deposit_trash_3.wav'; + + /// File path: assets/audio/deposit_trash_4.wav + String get depositTrash4 => 'assets/audio/deposit_trash_4.wav'; + + /// File path: assets/audio/deposit_trash_5.wav + String get depositTrash5 => 'assets/audio/deposit_trash_5.wav'; + + /// File path: assets/audio/hinting_arrow.mp3 + String get hintingArrow => 'assets/audio/hinting_arrow.mp3'; + /// File path: assets/audio/plastic_bottle.mp3 String get plasticBottle => 'assets/audio/plastic_bottle.mp3'; + /// File path: assets/audio/rating_stars0.mp3 + String get ratingStars0 => 'assets/audio/rating_stars0.mp3'; + + /// File path: assets/audio/rating_stars1.mp3 + String get ratingStars1 => 'assets/audio/rating_stars1.mp3'; + + /// File path: assets/audio/rating_stars2.mp3 + String get ratingStars2 => 'assets/audio/rating_stars2.mp3'; + + /// File path: assets/audio/rating_stars3.mp3 + String get ratingStars3 => 'assets/audio/rating_stars3.mp3'; + + /// File path: assets/audio/running_time.mp3 + String get runningTime => 'assets/audio/running_time.mp3'; + + /// File path: assets/audio/step1.mp3 + String get step1 => 'assets/audio/step1.mp3'; + + /// File path: assets/audio/trash_collected.mp3 + String get trashCollected => 'assets/audio/trash_collected.mp3'; + + /// File path: assets/audio/wrong_bin.mp3 + String get wrongBin => 'assets/audio/wrong_bin.mp3'; + /// List of all assets - List get values => [backgroundMusic, plasticBottle]; + List get values => [ + backgroundMusic, + bounceBack, + depositTrash1, + depositTrash2, + depositTrash3, + depositTrash4, + depositTrash5, + hintingArrow, + plasticBottle, + ratingStars0, + ratingStars1, + ratingStars2, + ratingStars3, + runningTime, + step1, + trashCollected, + wrongBin + ]; } class $AssetsImagesGen { @@ -776,14 +839,10 @@ class Assets { static const $AssetsImagesGen images = $AssetsImagesGen(); static const $AssetsRiveGen rive = $AssetsRiveGen(); static const $AssetsTilesGen tiles = $AssetsTilesGen(); - static const String trashyRoadTiledProject = - 'assets/trashy_road.tiled-project'; - static const String trashyRoadTiledSession = - 'assets/trashy_road.tiled-session'; + static const String trashyRoad = 'assets/trashy_road.tiled-project'; /// List of all assets - static List get values => - [trashyRoadTiledProject, trashyRoadTiledSession]; + static List get values => [trashyRoad]; } class AssetGenImage { diff --git a/packages/trashy_road/lib/l10n/arb/app_en.arb b/packages/trashy_road/lib/l10n/arb/app_en.arb new file mode 100644 index 00000000..62749688 --- /dev/null +++ b/packages/trashy_road/lib/l10n/arb/app_en.arb @@ -0,0 +1,19 @@ +{ + "@@locale": "en", + "great": "Great!", + "@great": { + "description": "Shown to congratulate the player in the Score Page." + }, + "nice": "Nice!", + "@nice": { + "description": "Shown to congratulate the player in the Score Page." + }, + "okay": "Okay", + "@okay": { + "description": "Shown to congratulate the player in the Score Page." + }, + "ohh": "Ohh", + "@ohh": { + "description": "Shown to congratulate the player in the Score Page." + } +} \ No newline at end of file diff --git a/packages/trashy_road/lib/l10n/arb/app_localizations.dart b/packages/trashy_road/lib/l10n/arb/app_localizations.dart new file mode 100644 index 00000000..421095f0 --- /dev/null +++ b/packages/trashy_road/lib/l10n/arb/app_localizations.dart @@ -0,0 +1,148 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_en.dart'; + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'arb/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations? of(BuildContext context) { + return Localizations.of(context, AppLocalizations); + } + + static const LocalizationsDelegate delegate = _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('en') + ]; + + /// Shown to congratulate the player in the Score Page. + /// + /// In en, this message translates to: + /// **'Great!'** + String get great; + + /// Shown to congratulate the player in the Score Page. + /// + /// In en, this message translates to: + /// **'Nice!'** + String get nice; + + /// Shown to congratulate the player in the Score Page. + /// + /// In en, this message translates to: + /// **'Okay'** + String get okay; + + /// Shown to congratulate the player in the Score Page. + /// + /// In en, this message translates to: + /// **'Ohh'** + String get ohh; +} + +class _AppLocalizationsDelegate extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => ['en'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + + + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': return AppLocalizationsEn(); + } + + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.' + ); +} diff --git a/packages/trashy_road/lib/l10n/arb/app_localizations_en.dart b/packages/trashy_road/lib/l10n/arb/app_localizations_en.dart new file mode 100644 index 00000000..3e8b8110 --- /dev/null +++ b/packages/trashy_road/lib/l10n/arb/app_localizations_en.dart @@ -0,0 +1,18 @@ +import 'app_localizations.dart'; + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get great => 'Great!'; + + @override + String get nice => 'Nice!'; + + @override + String get okay => 'Okay'; + + @override + String get ohh => 'Ohh'; +} diff --git a/packages/trashy_road/lib/l10n/l10n.dart b/packages/trashy_road/lib/l10n/l10n.dart new file mode 100644 index 00000000..5c7e291a --- /dev/null +++ b/packages/trashy_road/lib/l10n/l10n.dart @@ -0,0 +1,15 @@ +import 'package:flutter/widgets.dart'; +import 'package:trashy_road/l10n/l10n.dart'; + +export 'package:trashy_road/l10n/arb/app_localizations.dart'; + +/// Extension for [AppLocalizations] to access it from [BuildContext]. +/// +/// If the [AppLocalizations] is not found, generate it using: +/// ```sh +/// # Generate new AppLocalizations (from packages/trashy_road): +/// flutter gen-l10n +/// ``` +extension AppLocalizationsX on BuildContext { + AppLocalizations get l10n => AppLocalizations.of(this)!; +} diff --git a/packages/trashy_road/lib/main.dart b/packages/trashy_road/lib/main.dart index de92455a..d34d2272 100644 --- a/packages/trashy_road/lib/main.dart +++ b/packages/trashy_road/lib/main.dart @@ -3,6 +3,8 @@ import 'package:basura/basura.dart'; import 'package:flame/cache.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:trashy_road/l10n/l10n.dart'; +import 'package:trashy_road/src/audio/audio.dart'; import 'package:trashy_road/src/loading/loading.dart'; import 'package:trashy_road/src/maps/maps.dart'; @@ -31,12 +33,19 @@ class _MyApp extends StatelessWidget { BlocProvider( create: (context) => GameMapsBloc(), ), + BlocProvider( + create: (context) => AudioCubit( + audioCache: context.read().audio, + ), + ), ], child: BasuraTheme( data: BasuraThemeData.light(), child: MaterialApp( - title: 'Flutter Demo', + title: 'Trashy Town', debugShowCheckedModeBanner: false, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, diff --git a/packages/trashy_road/lib/src/audio/audio.dart b/packages/trashy_road/lib/src/audio/audio.dart new file mode 100644 index 00000000..c49f5c33 --- /dev/null +++ b/packages/trashy_road/lib/src/audio/audio.dart @@ -0,0 +1 @@ +export 'bloc/audio_cubit.dart'; diff --git a/packages/trashy_road/lib/src/audio/bloc/audio_cubit.dart b/packages/trashy_road/lib/src/audio/bloc/audio_cubit.dart new file mode 100644 index 00000000..c4c79c22 --- /dev/null +++ b/packages/trashy_road/lib/src/audio/bloc/audio_cubit.dart @@ -0,0 +1,140 @@ +import 'package:audioplayers/audioplayers.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flame_audio/bgm.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:trashy_road/gen/assets.gen.dart'; + +part 'audio_state.dart'; + +class AudioCubit extends Cubit { + AudioCubit({required AudioCache audioCache}) + : _backgroundMusic = Bgm(audioCache: audioCache), + _audioCache = audioCache, + super(const AudioState()); + + late final Map _players = { + GameSoundEffects.depositTrash1: AudioPlayer()..audioCache = _audioCache, + GameSoundEffects.depositTrash2: AudioPlayer()..audioCache = _audioCache, + GameSoundEffects.depositTrash3: AudioPlayer()..audioCache = _audioCache, + GameSoundEffects.depositTrash4: AudioPlayer()..audioCache = _audioCache, + GameSoundEffects.depositTrash5: AudioPlayer()..audioCache = _audioCache, + GameSoundEffects.hintingArrow: AudioPlayer()..audioCache = _audioCache, + GameSoundEffects.trashCollected: AudioPlayer()..audioCache = _audioCache, + GameSoundEffects.ratingStars0: AudioPlayer()..audioCache = _audioCache, + GameSoundEffects.ratingStars1: AudioPlayer()..audioCache = _audioCache, + GameSoundEffects.ratingStars2: AudioPlayer()..audioCache = _audioCache, + GameSoundEffects.ratingStars3: AudioPlayer()..audioCache = _audioCache, + GameSoundEffects.runningTime: AudioPlayer()..audioCache = _audioCache, + }; + + late final Map> _pools = { + GameSoundEffects.step1: AudioPool.create( + source: GameSoundEffects.step1.source, + maxPlayers: 6, + audioCache: _audioCache, + ), + GameSoundEffects.wrongBin: AudioPool.create( + source: GameSoundEffects.wrongBin.source, + maxPlayers: 3, + audioCache: _audioCache, + ), + }; + + final Bgm _backgroundMusic; + + final AudioCache _audioCache; + + Future _changeVolume(double volume) async { + await Future.wait( + [ + _backgroundMusic.audioPlayer.setVolume(volume), + ], + ); + + if (!isClosed) { + emit(state.copyWith(volume: volume)); + } + } + + /// Plays an effect sound. + /// + /// If there are no players available, this method will do nothing. + Future playEffect(GameAudioData audioData) async { + final hasPlayer = _players.containsKey(audioData); + if (hasPlayer) { + final player = _players[audioData]!; + if (player.state != PlayerState.playing) { + await player.play( + audioData.source, + volume: audioData.volume, + mode: PlayerMode.lowLatency, + ); + return; + } + } + + final hasPool = _pools.containsKey(audioData); + if (hasPool) { + final pool = await _pools[audioData]!; + await pool.start(volume: audioData.volume); + return; + } + } + + Future pauseEffect(GameAudioData audioData) async { + final hasPlayer = _players.containsKey(audioData); + if (hasPlayer) { + final player = _players[audioData]!; + if (player.state == PlayerState.playing) { + await player.pause(); + } + } + } + + Future resumeEffect(GameAudioData audioData) async { + final hasPlayer = _players.containsKey(audioData); + if (hasPlayer) { + final player = _players[audioData]!; + if (player.state == PlayerState.paused) { + await player.resume(); + } + } + } + + Future stopEffect(GameAudioData audioData) async { + final hasPlayer = _players.containsKey(audioData); + if (hasPlayer) { + final player = _players[audioData]!; + if (player.state == PlayerState.playing) { + await player.stop(); + } + } + } + + Future playBackgroundMusic(GameAudioData audioData) async { + // TODO(alestiago): Temporarily disabled the background music. + } + + Future pauseBackgroundMusic() async { + // TODO(alestiago): Temporarily disabled the background music. + } + + Future toggleVolume() async { + if (state.volume == 0) { + return _changeVolume(1); + } + return _changeVolume(0); + } + + @override + Future close() { + _players.forEach((key, value) async { + await value.dispose(); + }); + _pools.forEach((key, value) async { + await (await value).dispose(); + }); + _backgroundMusic.dispose(); + return super.close(); + } +} diff --git a/packages/trashy_road/lib/src/audio/bloc/audio_state.dart b/packages/trashy_road/lib/src/audio/bloc/audio_state.dart new file mode 100644 index 00000000..d43809ca --- /dev/null +++ b/packages/trashy_road/lib/src/audio/bloc/audio_state.dart @@ -0,0 +1,114 @@ +part of 'audio_cubit.dart'; + +class AudioState extends Equatable { + const AudioState({this.volume = 1}); + final double volume; + + AudioState copyWith({double? volume}) { + return AudioState(volume: volume ?? this.volume); + } + + @override + List get props => [volume]; +} + +class GameAudioData extends Equatable { + const GameAudioData._({ + required this.source, + required this.volume, + }); + + GameAudioData.fromPath( + String path, { + required double volume, + }) : this._( + source: AssetSource(path), + volume: volume, + ); + + final AssetSource source; + + final double volume; + + @override + List get props => [source, volume]; +} + +abstract class GameBackgroundMusic { + static final gameBackground = GameAudioData.fromPath( + Assets.audio.backgroundMusic, + volume: 0.2, + ); +} + +abstract class GameSoundEffects { + static final depositTrash1 = GameAudioData.fromPath( + Assets.audio.depositTrash1, + volume: 0.35, + ); + + static final depositTrash2 = GameAudioData.fromPath( + Assets.audio.depositTrash2, + volume: 0.35, + ); + + static final depositTrash3 = GameAudioData.fromPath( + Assets.audio.depositTrash3, + volume: 0.35, + ); + + static final depositTrash4 = GameAudioData.fromPath( + Assets.audio.depositTrash4, + volume: 0.35, + ); + + static final depositTrash5 = GameAudioData.fromPath( + Assets.audio.depositTrash5, + volume: 0.35, + ); + + static final hintingArrow = GameAudioData.fromPath( + Assets.audio.hintingArrow, + volume: 0.4, + ); + + static final trashCollected = GameAudioData.fromPath( + Assets.audio.trashCollected, + volume: 0.55, + ); + + static final ratingStars0 = GameAudioData.fromPath( + Assets.audio.ratingStars0, + volume: 0.35, + ); + + static final ratingStars1 = GameAudioData.fromPath( + Assets.audio.ratingStars1, + volume: 0.35, + ); + + static final ratingStars2 = GameAudioData.fromPath( + Assets.audio.ratingStars2, + volume: 0.35, + ); + + static final ratingStars3 = GameAudioData.fromPath( + Assets.audio.ratingStars3, + volume: 0.35, + ); + + static final wrongBin = GameAudioData.fromPath( + Assets.audio.wrongBin, + volume: 0.35, + ); + + static final step1 = GameAudioData.fromPath( + Assets.audio.step1, + volume: 0.35, + ); + + static final runningTime = GameAudioData.fromPath( + Assets.audio.runningTime, + volume: 0.3, + ); +} diff --git a/packages/trashy_road/lib/src/game/bloc/audio/audio_cubit.dart b/packages/trashy_road/lib/src/game/bloc/audio/audio_cubit.dart deleted file mode 100644 index 35b7e3e0..00000000 --- a/packages/trashy_road/lib/src/game/bloc/audio/audio_cubit.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:audioplayers/audioplayers.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flame_audio/bgm.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -part 'audio_state.dart'; - -class AudioCubit extends Cubit { - AudioCubit({required AudioCache audioCache}) - : effectPlayer = AudioPlayer()..audioCache = audioCache, - backgroundMusic = Bgm(audioCache: audioCache), - super(const AudioState()); - - @visibleForTesting - AudioCubit.test({ - required this.effectPlayer, - required this.backgroundMusic, - double volume = 1.0, - }) : super(AudioState(volume: volume)); - - final AudioPlayer effectPlayer; - - final Bgm backgroundMusic; - - Future _changeVolume(double volume) async { - await effectPlayer.setVolume(volume); - await backgroundMusic.audioPlayer.setVolume(volume); - if (!isClosed) { - emit(state.copyWith(volume: volume)); - } - } - - Future toggleVolume() async { - if (state.volume == 0) { - return _changeVolume(1); - } - return _changeVolume(0); - } - - @override - Future close() { - effectPlayer.dispose(); - backgroundMusic.dispose(); - return super.close(); - } -} diff --git a/packages/trashy_road/lib/src/game/bloc/audio/audio_state.dart b/packages/trashy_road/lib/src/game/bloc/audio/audio_state.dart deleted file mode 100644 index 4c71ec11..00000000 --- a/packages/trashy_road/lib/src/game/bloc/audio/audio_state.dart +++ /dev/null @@ -1,13 +0,0 @@ -part of 'audio_cubit.dart'; - -class AudioState extends Equatable { - const AudioState({this.volume = 1}); - final double volume; - - AudioState copyWith({double? volume}) { - return AudioState(volume: volume ?? this.volume); - } - - @override - List get props => [volume]; -} diff --git a/packages/trashy_road/lib/src/game/bloc/bloc.dart b/packages/trashy_road/lib/src/game/bloc/bloc.dart index f5d3ae13..57be67e1 100644 --- a/packages/trashy_road/lib/src/game/bloc/bloc.dart +++ b/packages/trashy_road/lib/src/game/bloc/bloc.dart @@ -1,2 +1 @@ -export 'audio/audio_cubit.dart'; export 'game/game_bloc.dart'; diff --git a/packages/trashy_road/lib/src/game/bloc/game/game_state.dart b/packages/trashy_road/lib/src/game/bloc/game/game_state.dart index b2a6927f..ae3f9fd6 100644 --- a/packages/trashy_road/lib/src/game/bloc/game/game_state.dart +++ b/packages/trashy_road/lib/src/game/bloc/game/game_state.dart @@ -61,7 +61,7 @@ class GameState extends Equatable { this.pausedDuration = Duration.zero, this.startedAt, this.pausedAt, - this.score = -1, + this.score, this.lostReason, }) : // TODO(alestiago): Remove magic string. @@ -79,7 +79,7 @@ class GameState extends Equatable { inventory: Inventory.empty(), pausedDuration: Duration.zero, collectedTrash: 0, - score: -1, + score: null, ); /// {@macro GameStatus} @@ -118,11 +118,11 @@ class GameState extends Equatable { /// The final score of the game. /// - /// If the game has not been completed, the score is `-1`. + /// If the game has not been completed, the score is `null`. /// /// The larger the score, the worse the player did. The score is calculated /// based on the amount of time that the player took to complete the game. - final int score; + final int? score; /// The reason why the game was loas. /// diff --git a/packages/trashy_road/lib/src/game/components/sprite/game_sprite_component.dart b/packages/trashy_road/lib/src/game/components/sprite/game_sprite_component.dart index d4c9d817..51df15fe 100644 --- a/packages/trashy_road/lib/src/game/components/sprite/game_sprite_component.dart +++ b/packages/trashy_road/lib/src/game/components/sprite/game_sprite_component.dart @@ -1,8 +1,10 @@ import 'dart:async'; import 'package:flame/components.dart'; +import 'package:trashy_road/src/game/game.dart'; -class GameSpriteComponent extends SpriteComponent with HasGameRef { +class GameSpriteComponent extends SpriteComponent + with HasGameReference { GameSpriteComponent.fromPath({ required String spritePath, super.position, diff --git a/packages/trashy_road/lib/src/game/debug_game.dart b/packages/trashy_road/lib/src/game/debug_game.dart index 3a2d169d..94154b24 100644 --- a/packages/trashy_road/lib/src/game/debug_game.dart +++ b/packages/trashy_road/lib/src/game/debug_game.dart @@ -12,7 +12,7 @@ class DebugTrashyRoadGame extends TrashyRoadGame { /// {@macro DebugTrashyRoadGame} DebugTrashyRoadGame({ required super.gameBloc, - required super.effectPlayer, + required super.audioBloc, required super.random, required super.resolution, super.images, diff --git a/packages/trashy_road/lib/src/game/entities/player/behaviors/player_collecting_trash_behavior.dart b/packages/trashy_road/lib/src/game/entities/player/behaviors/player_collecting_trash_behavior.dart index 843e969b..afe0262b 100644 --- a/packages/trashy_road/lib/src/game/entities/player/behaviors/player_collecting_trash_behavior.dart +++ b/packages/trashy_road/lib/src/game/entities/player/behaviors/player_collecting_trash_behavior.dart @@ -1,3 +1,4 @@ +import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame_behaviors/flame_behaviors.dart'; import 'package:flame_bloc/flame_bloc.dart'; @@ -18,6 +19,7 @@ class PlayerCollectingTrashBehavior extends CollisionBehavior } bloc.add(GameCollectedTrashEvent(item: other.trashType)); - other.removeFromParent(); + + other.collect(); } } diff --git a/packages/trashy_road/lib/src/game/entities/player/behaviors/player_hinting_behavior.dart b/packages/trashy_road/lib/src/game/entities/player/behaviors/player_hinting_behavior.dart index 6e8059a4..21b8b197 100644 --- a/packages/trashy_road/lib/src/game/entities/player/behaviors/player_hinting_behavior.dart +++ b/packages/trashy_road/lib/src/game/entities/player/behaviors/player_hinting_behavior.dart @@ -5,9 +5,11 @@ import 'package:flame/components.dart'; import 'package:flame/effects.dart'; import 'package:flame/extensions.dart'; import 'package:flame_behaviors/flame_behaviors.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flutter/animation.dart'; import 'package:trashy_road/game_settings.dart'; import 'package:trashy_road/gen/gen.dart'; +import 'package:trashy_road/src/audio/audio.dart'; import 'package:trashy_road/src/game/game.dart'; /// {@template PlayerHintingBehavior} @@ -17,7 +19,8 @@ import 'package:trashy_road/src/game/game.dart'; /// ([_hintDelay]) and a certain amount of time ([_hintInterval]) has passed /// since the last hint was shown. /// {@endtemplate} -class PlayerHintingBehavior extends Behavior with HasGameReference { +class PlayerHintingBehavior extends Behavior + with HasGameReference, FlameBlocReader { /// The minimum time between hints. static const _hintInterval = 5.0; @@ -108,6 +111,10 @@ class PlayerHintingBehavior extends Behavior with HasGameReference { return; } + if (bloc.state.status != GameStatus.playing) { + return; + } + _lastVisibleTrash = _isTrashVisible ? 0 : _lastVisibleTrash + dt; _lastHint += dt; @@ -213,8 +220,7 @@ enum _HintArrowDirection { } } -class _HintArrowSpriteComponent extends GameSpriteComponent - with HasGameReference { +class _HintArrowSpriteComponent extends GameSpriteComponent { _HintArrowSpriteComponent._({ required this.direction, required super.spritePath, @@ -253,6 +259,10 @@ class _HintArrowSpriteComponent extends GameSpriteComponent FutureOr onLoad() async { await super.onLoad(); + unawaited( + game.audioBloc.playEffect(GameSoundEffects.hintingArrow), + ); + final moveEffectController = EffectController(duration: 1); final fadeInEffectController = EffectController( diff --git a/packages/trashy_road/lib/src/game/entities/player/behaviors/player_moving_behavior.dart b/packages/trashy_road/lib/src/game/entities/player/behaviors/player_moving_behavior.dart index 11104195..885949c8 100644 --- a/packages/trashy_road/lib/src/game/entities/player/behaviors/player_moving_behavior.dart +++ b/packages/trashy_road/lib/src/game/entities/player/behaviors/player_moving_behavior.dart @@ -4,13 +4,17 @@ import 'package:flame_behaviors/flame_behaviors.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flutter/widgets.dart'; import 'package:trashy_road/game_settings.dart'; +import 'package:trashy_road/src/audio/audio.dart'; import 'package:trashy_road/src/game/game.dart'; enum Direction { up, down, left, right } /// A behavior that allows the player to move around the game. final class PlayerMovingBehavior extends Behavior - with FlameBlocReader, ParentIsA { + with + FlameBlocReader, + ParentIsA, + HasGameReference { /// The delay between player moves. static const moveDelay = Duration(milliseconds: 200); @@ -62,6 +66,8 @@ final class PlayerMovingBehavior extends Behavior _isMoving = false; parent.position.setFrom(_targetPosition); _previousPosition.setFrom(_targetPosition); + + game.audioBloc.playEffect(GameSoundEffects.step1); } } diff --git a/packages/trashy_road/lib/src/game/entities/trash/behaviors/behaviors.dart b/packages/trashy_road/lib/src/game/entities/trash/behaviors/behaviors.dart new file mode 100644 index 00000000..c15da7c5 --- /dev/null +++ b/packages/trashy_road/lib/src/game/entities/trash/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'trash_shaking_behavior.dart'; diff --git a/packages/trashy_road/lib/src/game/entities/trash/behaviors/trash_shaking_behavior.dart b/packages/trashy_road/lib/src/game/entities/trash/behaviors/trash_shaking_behavior.dart new file mode 100644 index 00000000..2fbd6c48 --- /dev/null +++ b/packages/trashy_road/lib/src/game/entities/trash/behaviors/trash_shaking_behavior.dart @@ -0,0 +1,44 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame_behaviors/flame_behaviors.dart'; +import 'package:flame_noise/flame_noise.dart'; +import 'package:trashy_road/src/game/game.dart'; + +final _random = Random(0); + +/// Shakes the [Trash] around every now and then. +/// +/// This is to catch the player's attention so they know that the [Trash] is +/// interactable and needs to be collected. +class TrashShakingBehavior extends Behavior { + final _shakeDistance = Vector2(10, 4); + + @override + FutureOr onLoad() async { + await super.onLoad(); + + add( + TimerComponent( + repeat: true, + period: 2 + _random.nextDouble() * 2, + onTick: () { + parent.add( + MoveEffect.by( + _shakeDistance, + NoiseEffectController( + noise: WhiteNoise( + seed: _random.nextInt(100), + frequency: 5, + ), + duration: 0.5, + ), + ), + ); + }, + ), + ); + } +} diff --git a/packages/trashy_road/lib/src/game/entities/trash/trash.dart b/packages/trashy_road/lib/src/game/entities/trash/trash.dart index f1a049ac..81fb20dd 100644 --- a/packages/trashy_road/lib/src/game/entities/trash/trash.dart +++ b/packages/trashy_road/lib/src/game/entities/trash/trash.dart @@ -2,14 +2,16 @@ import 'dart:math'; import 'package:flame/collisions.dart'; import 'package:flame/components.dart'; -import 'package:flame_audio/flame_audio.dart'; import 'package:flame_behaviors/flame_behaviors.dart'; import 'package:meta/meta.dart'; import 'package:tiled/tiled.dart'; import 'package:trashy_road/game_settings.dart'; import 'package:trashy_road/gen/assets.gen.dart'; +import 'package:trashy_road/src/audio/audio.dart'; import 'package:trashy_road/src/game/game.dart'; +export 'behaviors/behaviors.dart'; + /// The different types of [Trash]. enum TrashType { plastic._('plastic'), @@ -49,6 +51,7 @@ class Trash extends PositionedEntity drop: Vector2(0, -45), minDuration: 0.1, ), + TrashShakingBehavior(), PropagatingCollisionBehavior( RectangleHitbox( isSolid: true, @@ -114,20 +117,15 @@ class Trash extends PositionedEntity } } - @override - void removeFromParent() { - // TODO(alestiago): Play a sound according to what type of trash it is. - game.effectPlayer.play(AssetSource(Assets.audio.plasticBottle)); - - // TODO(alestiago): Consider whether or not to add the scale effect to the - // trash again. - + /// Collects the trash. + void collect() { + game.audioBloc.playEffect(GameSoundEffects.trashCollected); findBehavior() .children .whereType() .first .collisionType = CollisionType.inactive; - super.removeFromParent(); + removeFromParent(); } final TrashType trashType; @@ -135,17 +133,17 @@ class Trash extends PositionedEntity /// The different styles of plastic bottles. enum PlasticStyle { - /// {@template _PlasticStyle.one} + /// {@template _PlasticStyle.plasticBottleOne} /// A crushed plastic bottle that is laying on the ground with its lid facing /// east. /// {@endtemplate} - one, + plasticBottleOne, - /// {@template _PlasticStyle.two} + /// {@template _PlasticStyle.plasticBottleTwo} /// A crushed plastic bottle that is laying on the ground with its lid facing /// south-east. /// {@endtemplate} - two, + plasticBottleTwo, /// {@template _PlasticStyle.coldTakeAwayCup} /// A takeaway cup with a straw. @@ -245,11 +243,11 @@ class _PlasticSpriteGroup extends PositionComponent _PlasticSpriteGroup._({ required String spritePath, required String shadowPath, + super.scale, }) : super( // The `position` and `scale` have been eyeballed to match with the // appearance of the map. position: Vector2(0.5, 1.4)..toGameSize(), - scale: Vector2.all(0.8), anchor: Anchor.center, children: [ GameSpriteComponent.fromPath( @@ -268,10 +266,10 @@ class _PlasticSpriteGroup extends PositionComponent PlasticStyle style, ) { switch (style) { - case PlasticStyle.one: - return _PlasticSpriteGroup._styleOne(); - case PlasticStyle.two: - return _PlasticSpriteGroup._styleTwo(); + case PlasticStyle.plasticBottleOne: + return _PlasticSpriteGroup._plasticBottleOne(); + case PlasticStyle.plasticBottleTwo: + return _PlasticSpriteGroup._plasticBottleTwo(); case PlasticStyle.coldTakeAwayCup: return _PlasticSpriteGroup._coldTakeAwayCup(); case PlasticStyle.straw: @@ -281,34 +279,39 @@ class _PlasticSpriteGroup extends PositionComponent } } - /// {@macro _PlasticStyle.one} - factory _PlasticSpriteGroup._styleOne() => _PlasticSpriteGroup._( + /// {@macro _PlasticStyle.plasticBottleOne} + factory _PlasticSpriteGroup._plasticBottleOne() => _PlasticSpriteGroup._( spritePath: Assets.images.sprites.plasticBottle1.path, shadowPath: Assets.images.sprites.plasticBottle1Shadow.path, + scale: Vector2.all(0.5), ); - /// {@macro _PlasticBo_PlasticStylettleStyle.two} - factory _PlasticSpriteGroup._styleTwo() => _PlasticSpriteGroup._( + /// {@macro _PlasticBo_PlasticStylettleStyle.plasticBottleTwo} + factory _PlasticSpriteGroup._plasticBottleTwo() => _PlasticSpriteGroup._( spritePath: Assets.images.sprites.plasticBottle2.path, shadowPath: Assets.images.sprites.plasticBottle2Shadow.path, + scale: Vector2.all(0.5), ); /// {@macro _PlasticStyle.coldTakeAwayCup} factory _PlasticSpriteGroup._coldTakeAwayCup() => _PlasticSpriteGroup._( spritePath: Assets.images.sprites.takeawayCupCold.path, shadowPath: Assets.images.sprites.takeawayCupColdShadow.path, + scale: Vector2.all(0.8), ); /// {@macro _PlasticStyle.straw} factory _PlasticSpriteGroup._straw() => _PlasticSpriteGroup._( spritePath: Assets.images.sprites.plasticStraw.path, shadowPath: Assets.images.sprites.plasticStrawShadow.path, + scale: Vector2.all(0.8), ); /// {@macro _PlasticStyle.canHolder} factory _PlasticSpriteGroup._canHolder() => _PlasticSpriteGroup._( spritePath: Assets.images.sprites.canHolder.path, shadowPath: Assets.images.sprites.canHolderShadow.path, + scale: Vector2.all(0.8), ); } diff --git a/packages/trashy_road/lib/src/game/entities/trash_can/behaviors/trash_can_depositing_behavior.dart b/packages/trashy_road/lib/src/game/entities/trash_can/behaviors/trash_can_depositing_behavior.dart index 631108b3..91fc9a47 100644 --- a/packages/trashy_road/lib/src/game/entities/trash_can/behaviors/trash_can_depositing_behavior.dart +++ b/packages/trashy_road/lib/src/game/entities/trash_can/behaviors/trash_can_depositing_behavior.dart @@ -1,14 +1,33 @@ import 'dart:async'; +import 'dart:collection'; +import 'dart:math' as math; import 'package:flame/components.dart'; import 'package:flame_behaviors/flame_behaviors.dart'; import 'package:flame_bloc/flame_bloc.dart'; +import 'package:trashy_road/src/audio/audio.dart'; import 'package:trashy_road/src/game/game.dart'; +final _random = math.Random(0); + class TrashCanDepositingBehavior extends Behavior - with FlameBlocReader { + with + FlameBlocReader, + HasGameReference { /// The maximum amount of trash that the [TrashCan] can hold. - static const int _maximumCapacity = 3; + static const int _maximumCapacity = 999; + + /// The sound effects that are played when trash is deposited into the + /// [TrashCan]. + /// + /// The sound effect is chosen randomly when trash is deposited. + static final _depositSoundEffects = UnmodifiableSetView({ + GameSoundEffects.depositTrash1, + GameSoundEffects.depositTrash2, + GameSoundEffects.depositTrash3, + GameSoundEffects.depositTrash4, + GameSoundEffects.depositTrash5, + }); /// The current amount of trash that the [TrashCan] holds. int _capacity = 0; @@ -21,9 +40,6 @@ class TrashCanDepositingBehavior extends Behavior !parent.hasBehavior(), 'The parent can only have a single $TrashCanDepositingBehavior.', ); - - parent.add(_TrashCapacityTextComponent().._updateText(_capacity)); - parent.children.register<_TrashCapacityTextComponent>(); } /// Whether the [TrashCan] can deposit some of the [Player]'s trash. @@ -42,35 +58,19 @@ class TrashCanDepositingBehavior extends Behavior /// /// Does nothing if the [TrashCan] cannot deposit some trash. void deposit() { - if (!_canDeposit()) return; + if (!_canDeposit()) { + game.audioBloc.playEffect(GameSoundEffects.wrongBin); + return; + } + + game.audioBloc.playEffect( + _depositSoundEffects.elementAt( + _random.nextInt(_depositSoundEffects.length), + ), + ); _capacity++; bloc.add(GameDepositedTrashEvent(item: parent.trashType)); - - parent.children - .query<_TrashCapacityTextComponent>() - .first - ._updateText(_capacity); - parent.open(); } - - @override - void onRemove() { - parent.children - .query<_TrashCapacityTextComponent>() - .first - .removeFromParent(); - super.onRemove(); - } -} - -/// Displays the current capacity of the [TrashCan]. -class _TrashCapacityTextComponent extends TextComponent - with ParentIsA { - _TrashCapacityTextComponent(); - - void _updateText(int capacity) { - text = '$capacity/${TrashCanDepositingBehavior._maximumCapacity}'; - } } diff --git a/packages/trashy_road/lib/src/game/game.dart b/packages/trashy_road/lib/src/game/game.dart index b7101fca..6f3f1030 100644 --- a/packages/trashy_road/lib/src/game/game.dart +++ b/packages/trashy_road/lib/src/game/game.dart @@ -1,16 +1,17 @@ import 'dart:async'; import 'dart:math' hide Rectangle; -import 'package:audioplayers/audioplayers.dart'; import 'package:flame/cache.dart'; import 'package:flame/camera.dart'; import 'package:flame/components.dart'; import 'package:flame/events.dart'; +import 'package:flame/experimental.dart'; import 'package:flame/game.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_tiled/flame_tiled.dart'; import 'package:flutter/material.dart'; import 'package:trashy_road/game_settings.dart'; +import 'package:trashy_road/src/audio/audio.dart'; import 'package:trashy_road/src/game/game.dart'; export 'bloc/bloc.dart'; @@ -29,7 +30,7 @@ class TrashyRoadGame extends FlameGame DragCallbacks { TrashyRoadGame({ required GameBloc gameBloc, - required this.effectPlayer, + required this.audioBloc, required this.random, required this.resolution, Images? images, @@ -47,20 +48,27 @@ class TrashyRoadGame extends FlameGame /// {@macro GameBloc} final GameBloc _gameBloc; - /// Can play one audio at a time. - /// - /// Usually used to play very short sound effects. - final AudioPlayer effectPlayer; + /// {@macro AudioCubit} + final AudioCubit audioBloc; final Random random; late final Player _player; + TrashyRoadWorld? _trashyRoadWorld; + @override Color backgroundColor() { return Colors.transparent; } + @override + void onGameResize(Vector2 size) { + super.onGameResize(size); + _updateBounds(); + _updateZoom(); + } + @override FutureOr onLoad() async { await super.onLoad(); @@ -70,7 +78,8 @@ class TrashyRoadGame extends FlameGame GameSettings.gridDimensions, ); final tiled = TiledComponent(renderableTiledMap); - final trashyRoadWorld = TrashyRoadWorld.create(tiled: tiled); + final trashyRoadWorld = + _trashyRoadWorld = TrashyRoadWorld.create(tiled: tiled); children.register(); final blocProvider = FlameBlocProvider( @@ -87,15 +96,33 @@ class TrashyRoadGame extends FlameGame world.add(blocProvider); _player = trashyRoadWorld.tiled.children.whereType().first; - _player.children.register(); - camera.follow(_player); + _updateBounds(); } - @override - void onGameResize(Vector2 size) { - super.onGameResize(size); + void _updateBounds() { + final worldBounds = _trashyRoadWorld?.bounds; + if (worldBounds == null) return; + + final viewportHalf = camera.viewport.size / 2; + + final cameraBounds = Rectangle.fromPoints( + worldBounds.topLeft + viewportHalf, + worldBounds.bottomRight - viewportHalf, + ); + camera.setBounds(cameraBounds); + } + + void _updateZoom() { + final size = camera.viewport.size; camera.viewfinder.zoom = (size.x / resolution.width) + 0.2; + + final isPortrait = size.y > size.x; + if (isPortrait) { + // Increase the zoom on those mobile devices with a portrait aspect ratio + // to make the game look better, rather than too zoomed out. + camera.viewfinder.zoom += 0.4; + } } @override diff --git a/packages/trashy_road/lib/src/game/view/game_page.dart b/packages/trashy_road/lib/src/game/view/game_page.dart index b7ecffa2..1105a628 100644 --- a/packages/trashy_road/lib/src/game/view/game_page.dart +++ b/packages/trashy_road/lib/src/game/view/game_page.dart @@ -44,20 +44,11 @@ class GamePage extends StatelessWidget { @override Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => GameBloc( - identifier: _identifier, - map: _map, - ), - ), - BlocProvider( - create: (context) => AudioCubit( - audioCache: context.read().audio, - ), - ), - ], + return BlocProvider( + create: (context) => GameBloc( + identifier: _identifier, + map: _map, + ), child: const _GameView(), ); } @@ -97,25 +88,10 @@ class _GameView extends StatelessWidget { child: TopHud(), ), ), - if (isTutorial) - const Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: EdgeInsets.all(12), - child: InventoryHud(), - ), - ), - const Align( - alignment: Alignment.topCenter, - child: Padding( - padding: EdgeInsets.all(12), - child: TopHud(), - ), - ), if (isTutorial) const Align( alignment: Alignment(0, -0.6), - child: GameTutorial(), + child: TutorialHud(), ), ], ), @@ -130,15 +106,29 @@ class _GameCompletionListener extends BlocListener { listenWhen: (previous, current) => current.status == GameStatus.completed, listener: (context, state) { + assert( + state.score != null, + 'The game is completed, but the score is null.', + ); + final gameMapsBloc = context.read(); + final gameMap = gameMapsBloc.state.maps[state.identifier]; + final scoreRating = ScoreRating.fromSteps( + score: state.score, + steps: gameMap!.ratingSteps, + ); + context.read().add( GameMapCompletedEvent( identifier: state.identifier, - score: state.score, + score: state.score!, ), ); Navigator.push( context, - ScorePage.route(identifier: state.identifier), + ScorePage.route( + identifier: state.identifier, + scoreRating: scoreRating, + ), ); }, ); diff --git a/packages/trashy_road/lib/src/game/widgets/game_background_music_listener.dart b/packages/trashy_road/lib/src/game/widgets/game_background_music_listener.dart index 34b41285..e74bc54c 100644 --- a/packages/trashy_road/lib/src/game/widgets/game_background_music_listener.dart +++ b/packages/trashy_road/lib/src/game/widgets/game_background_music_listener.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:trashy_road/gen/assets.gen.dart'; +import 'package:trashy_road/src/audio/audio.dart'; import 'package:trashy_road/src/game/game.dart'; /// {@template GameBackgroundMusicListener} @@ -15,17 +15,13 @@ class GameBackgroundMusicListener extends StatelessWidget { final Widget child; void _playBackgroundMusic(BuildContext context) { - final audioBloc = context.read(); - if (audioBloc.backgroundMusic.isPlaying) return; - audioBloc.backgroundMusic.play( - Assets.audio.backgroundMusic, - volume: 0.25, - ); + context + .read() + .playBackgroundMusic(GameBackgroundMusic.gameBackground); } void _pauseBackgroundMusic(BuildContext context) { - final audioBloc = context.read(); - audioBloc.backgroundMusic.pause(); + context.read().pauseBackgroundMusic(); } @override @@ -61,5 +57,5 @@ bool _shouldPlayBackgroundMusic(GameState previous, GameState current) { } bool _shouldPauseBackgroundMusic(GameState previous, GameState current) { - return current.status == GameStatus.paused; + return !_shouldPlayBackgroundMusic(previous, current); } diff --git a/packages/trashy_road/lib/src/game/widgets/game_stopwatch.dart b/packages/trashy_road/lib/src/game/widgets/game_stopwatch.dart index ad2a6221..6fe20906 100644 --- a/packages/trashy_road/lib/src/game/widgets/game_stopwatch.dart +++ b/packages/trashy_road/lib/src/game/widgets/game_stopwatch.dart @@ -1,6 +1,7 @@ import 'package:basura/basura.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:trashy_road/src/audio/audio.dart'; import 'package:trashy_road/src/game/game.dart'; import 'package:trashy_road/src/maps/maps.dart'; @@ -20,6 +21,9 @@ class _GameStopwatchState extends State with SingleTickerProviderStateMixin { final _stopwatch = Stopwatch(); + bool _countingDown = false; + bool _timeIsUp = false; + late final _animation = AnimationController( vsync: this, duration: const Duration(seconds: 1), @@ -28,16 +32,29 @@ class _GameStopwatchState extends State void _stop() { _animation.stop(); _stopwatch.stop(); + + if (mounted && !_timeIsUp) { + context.read().pauseEffect(GameSoundEffects.runningTime); + } } void _start() { _animation.repeat(reverse: true); _stopwatch.start(); + + if (mounted && _countingDown) { + context.read().resumeEffect(GameSoundEffects.runningTime); + } } void _reset() { _animation.reset(); _stopwatch.reset(); + _countingDown = false; + + if (mounted) { + context.read().stopEffect(GameSoundEffects.runningTime); + } } void _onTick() { @@ -48,9 +65,16 @@ class _GameStopwatchState extends State final map = mapsBloc.state.maps[gameBloc.state.identifier]!; final completionSeconds = map.completionSeconds; - final timeIsUp = _stopwatch.elapsed.inSeconds >= completionSeconds; - if (timeIsUp && gameBloc.state.status == GameStatus.playing) { + final timeLeft = (completionSeconds * Duration.millisecondsPerSecond) - + _stopwatch.elapsed.inMilliseconds; + if (timeLeft < 10100 && !_countingDown) { + _countingDown = true; + context.read().playEffect(GameSoundEffects.runningTime); + } + + _timeIsUp = _stopwatch.elapsed.inSeconds >= completionSeconds; + if (_timeIsUp && gameBloc.state.status == GameStatus.playing) { gameBloc.add(const GameLostEvent(reason: GameLostReason.timeIsUp)); } } diff --git a/packages/trashy_road/lib/src/game/widgets/inventory_hud.dart b/packages/trashy_road/lib/src/game/widgets/inventory_hud.dart index ea9b1dc6..8699d41b 100644 --- a/packages/trashy_road/lib/src/game/widgets/inventory_hud.dart +++ b/packages/trashy_road/lib/src/game/widgets/inventory_hud.dart @@ -30,9 +30,10 @@ class InventoryHud extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 32), child: LayoutBuilder( builder: (context, constraints) { + const dimension = 58.5; final slotSize = Size.square( - ((constraints.maxWidth - 50) / Inventory.size) - .clamp(10.0, 50.0), + ((constraints.maxWidth - dimension) / Inventory.size) + .clamp(10.0, dimension), ); return Row( @@ -41,7 +42,13 @@ class InventoryHud extends StatelessWidget { Inventory.size, (index) => SizedBox.fromSize( size: slotSize, - child: _InventorySlot(index: index), + child: Padding( + padding: const EdgeInsets.all(4), + child: SizedBox.square( + dimension: dimension, + child: _InventorySlot(index: index), + ), + ), ), ), ); @@ -81,36 +88,30 @@ class _InventorySlot extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(4), - child: SizedBox.square( - dimension: 50, - child: BlocSelector( - selector: (state) { - if (state.inventory.items.length <= index) { - return null; - } - return state.inventory.items[index]; - }, - builder: (context, type) { - return Stack( - children: [ - Positioned.fill( - child: AnimatedOpacity( - duration: const Duration(milliseconds: 500), - opacity: type == null ? 1 : 0.1, - child: Assets.images.display.slotEmpty.image(), - ), - ), - if (type != null) - Positioned.fill( - child: _FilledInventorySlot.fromType(type), - ), - ], - ); - }, - ), - ), + return BlocSelector( + selector: (state) { + if (state.inventory.items.length <= index) { + return null; + } + return state.inventory.items[index]; + }, + builder: (context, type) { + return Stack( + children: [ + Positioned.fill( + child: AnimatedOpacity( + duration: const Duration(milliseconds: 500), + opacity: type == null ? 1 : 0.1, + child: Assets.images.display.slotEmpty.image(), + ), + ), + if (type != null) + Positioned.fill( + child: _FilledInventorySlot.fromType(type), + ), + ], + ); + }, ); } } diff --git a/packages/trashy_road/lib/src/game/widgets/trashy_road_game_widget.dart b/packages/trashy_road/lib/src/game/widgets/trashy_road_game_widget.dart index e45b2a52..549a1166 100644 --- a/packages/trashy_road/lib/src/game/widgets/trashy_road_game_widget.dart +++ b/packages/trashy_road/lib/src/game/widgets/trashy_road_game_widget.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:trashy_road/game_settings.dart'; +import 'package:trashy_road/src/audio/audio.dart'; import 'package:trashy_road/src/game/game.dart'; import 'package:trashy_road/src/loading/loading.dart'; @@ -35,14 +36,14 @@ class _TrashyRoadGameWidgetState extends State { ? DebugTrashyRoadGame( gameBloc: gameBloc, images: loadingBloc.images, - effectPlayer: audioBloc.effectPlayer, + audioBloc: audioBloc, resolution: resolution, random: _random, ) : TrashyRoadGame( gameBloc: gameBloc, images: loadingBloc.images, - effectPlayer: audioBloc.effectPlayer, + audioBloc: audioBloc, resolution: resolution, random: _random, ); diff --git a/packages/trashy_road/lib/src/game/widgets/game_tutorial.dart b/packages/trashy_road/lib/src/game/widgets/tutorial_hud.dart similarity index 93% rename from packages/trashy_road/lib/src/game/widgets/game_tutorial.dart rename to packages/trashy_road/lib/src/game/widgets/tutorial_hud.dart index 64ebea8b..3586688b 100644 --- a/packages/trashy_road/lib/src/game/widgets/game_tutorial.dart +++ b/packages/trashy_road/lib/src/game/widgets/tutorial_hud.dart @@ -18,18 +18,18 @@ enum _TutorialStatus { completed, } -/// {@template GameTutorial} +/// {@template TutorialHud} /// Displays tutorial information based on the current game state. /// {@endtemplate} -class GameTutorial extends StatefulWidget { - /// {@macro GameTutorial} - const GameTutorial({super.key}); +class TutorialHud extends StatefulWidget { + /// {@macro TutorialHud} + const TutorialHud({super.key}); @override - State createState() => _GameTutorialState(); + State createState() => _TutorialHudState(); } -class _GameTutorialState extends State { +class _TutorialHudState extends State { _TutorialStatus _tutorialStatus = _TutorialStatus.movement; @override diff --git a/packages/trashy_road/lib/src/game/widgets/widgets.dart b/packages/trashy_road/lib/src/game/widgets/widgets.dart index bdaa3156..1a6b9c91 100644 --- a/packages/trashy_road/lib/src/game/widgets/widgets.dart +++ b/packages/trashy_road/lib/src/game/widgets/widgets.dart @@ -1,10 +1,10 @@ export 'animated_star.dart'; export 'game_background_music_listener.dart'; export 'game_stopwatch.dart'; -export 'game_tutorial.dart'; export 'inventory_hud.dart'; export 'pause_button.dart'; export 'playing_hud_transition.dart'; export 'stopwatch_icon.dart'; export 'top_hud.dart'; export 'trashy_road_game_widget.dart'; +export 'tutorial_hud.dart'; diff --git a/packages/trashy_road/lib/src/loading/cubit/preload/preload_cubit.dart b/packages/trashy_road/lib/src/loading/cubit/preload/preload_cubit.dart index 1374eaed..35366d72 100644 --- a/packages/trashy_road/lib/src/loading/cubit/preload/preload_cubit.dart +++ b/packages/trashy_road/lib/src/loading/cubit/preload/preload_cubit.dart @@ -1,5 +1,6 @@ import 'package:audioplayers/audioplayers.dart'; import 'package:bloc/bloc.dart'; +import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:flame/cache.dart'; import 'package:flutter/widgets.dart'; @@ -29,36 +30,31 @@ class PreloadCubit extends Cubit { /// Load items sequentially allows display of what is being loaded Future loadSequentially() async { + final displayImages = + Assets.images.display.values.whereType().toList(); + final spritePaths = + Assets.images.sprites.values.map((image) => image.path).toList(); + final phases = [ - PreloadPhase( - 'audio', - () => audio.loadAll(Assets.audio.values), + ...PreloadPhase.sliced( + name: 'audio', + items: Assets.audio.values, + start: audio.loadAll, ), - PreloadPhase( - 'images', - () => imageProviderCache.loadAll( - [ - Assets.images.display.pauseIcon, - Assets.images.display.paperBackground, - Assets.images.display.replayIcon, - Assets.images.display.playIcon, - Assets.images.display.menuIcon, - Assets.images.display.nextIcon, - ], - ), + ...PreloadPhase.sliced( + name: 'images', + items: displayImages, + start: imageProviderCache.loadAll, ), - PreloadPhase( - 'game images', - () => images.loadAll([]), + ...PreloadPhase.sliced( + name: 'sprites', + items: spritePaths, + start: images.loadAll, ), - PreloadPhase( - 'maps', - () => tiled.loadAll( - [ - Assets.tiles.map1, - Assets.tiles.map2, - ], - ), + ...PreloadPhase.sliced( + name: 'maps', + items: [Assets.tiles.map1, Assets.tiles.map2], + start: tiled.loadAll, ), ]; @@ -81,4 +77,24 @@ class PreloadPhase { final String label; final ValueGetter> start; + + static Iterable sliced({ + required String name, + required Iterable items, + required Future Function(List items) start, + }) { + const sliceSize = 15; + final slices = items.slices(sliceSize).toList(); + final phases = []; + for (var phaseIndex = 0; phaseIndex < slices.length; phaseIndex++) { + final phase = slices[phaseIndex]; + phases.add( + PreloadPhase( + '$name ${phaseIndex + 1} of ${slices.length}', + () => start(phase), + ), + ); + } + return phases; + } } diff --git a/packages/trashy_road/lib/src/loading/view/loading_page.dart b/packages/trashy_road/lib/src/loading/view/loading_page.dart index 2247b760..5fe4294e 100644 --- a/packages/trashy_road/lib/src/loading/view/loading_page.dart +++ b/packages/trashy_road/lib/src/loading/view/loading_page.dart @@ -1,5 +1,6 @@ import 'package:basura/basura.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:trashy_road/src/loading/loading.dart'; import 'package:trashy_road/src/play/play.dart'; @@ -27,8 +28,18 @@ class _LoadingPageState extends State { listenWhen: (prevState, state) => !prevState.isComplete && state.isComplete, listener: (context, state) => _onPreloadComplete(context), - child: const Scaffold( - body: Center( + child: const DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xff5F97C4), + Color(0xff64A5CC), + ], + ), + ), + child: Center( child: _LoadingInternal(), ), ), @@ -41,7 +52,8 @@ class _LoadingInternal extends StatelessWidget { @override Widget build(BuildContext context) { - final primaryTextTheme = Theme.of(context).primaryTextTheme; + final basuraTheme = BasuraTheme.of(context); + final textStyle = basuraTheme.textTheme.button; return BlocBuilder( builder: (context, state) { @@ -51,18 +63,21 @@ class _LoadingInternal extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Padding( - padding: const EdgeInsets.symmetric(vertical: 8), + padding: const EdgeInsets.symmetric(vertical: 12), child: AnimatedProgressBar( progress: state.progress, - backgroundColor: BasuraColors.deepGreen, - foregroundColor: BasuraColors.white, + backgroundColor: BasuraColors.white, + foregroundColor: BasuraColors.black, ), ), - Text( - loadingMessage, - style: primaryTextTheme.bodySmall!.copyWith( - color: BasuraColors.lightGreen, - fontWeight: FontWeight.w900, + DefaultTextStyle( + style: textStyle, + child: Text( + loadingMessage, + style: textStyle.copyWith( + fontSize: 24, + color: BasuraColors.white, + ), ), ), ], diff --git a/packages/trashy_road/lib/src/loading/widgets/animated_progress_bar.dart b/packages/trashy_road/lib/src/loading/widgets/animated_progress_bar.dart index 104a1b72..b1ac5871 100644 --- a/packages/trashy_road/lib/src/loading/widgets/animated_progress_bar.dart +++ b/packages/trashy_road/lib/src/loading/widgets/animated_progress_bar.dart @@ -34,7 +34,7 @@ class AnimatedProgressBar extends StatelessWidget { Widget build(BuildContext context) { // Outer bar return ClipRRect( - borderRadius: BorderRadius.circular(2), + borderRadius: BorderRadius.circular(15), child: SizedBox( height: 16, width: 200, @@ -52,7 +52,7 @@ class AnimatedProgressBar extends StatelessWidget { alignment: Alignment.centerLeft, widthFactor: progress, child: ClipRRect( - borderRadius: BorderRadius.circular(1), + borderRadius: BorderRadius.circular(15), child: ColoredBox( color: foregroundColor, ), diff --git a/packages/trashy_road/lib/src/maps/bloc/game_maps_bloc.dart b/packages/trashy_road/lib/src/maps/bloc/game_maps_bloc.dart index 5322d927..faf1278d 100644 --- a/packages/trashy_road/lib/src/maps/bloc/game_maps_bloc.dart +++ b/packages/trashy_road/lib/src/maps/bloc/game_maps_bloc.dart @@ -25,7 +25,7 @@ class GameMapsBloc extends Bloc { Map? newMaps; final currentMap = state.maps[event.identifier]!; - final firstTimeCompleted = currentMap.score == -1; + final firstTimeCompleted = currentMap.score == null; if (firstTimeCompleted) { final nextMap = state.next(currentMap.identifier); @@ -37,7 +37,7 @@ class GameMapsBloc extends Bloc { } } - if (currentMap.score < event.score) { + if (firstTimeCompleted || (currentMap.score! > event.score)) { newMaps ??= Map.from(state.maps); final updatedMap = currentMap.copyWith(score: event.score); newMaps[event.identifier] = updatedMap; diff --git a/packages/trashy_road/lib/src/maps/bloc/game_maps_state.dart b/packages/trashy_road/lib/src/maps/bloc/game_maps_state.dart index ca52d543..70ec43ab 100644 --- a/packages/trashy_road/lib/src/maps/bloc/game_maps_state.dart +++ b/packages/trashy_road/lib/src/maps/bloc/game_maps_state.dart @@ -35,7 +35,7 @@ class GameMapsState extends Equatable { identifier: tutorialIdentifier, displayName: '1', path: Assets.tiles.map1, - score: -1, + score: null, ratingSteps: (15, 20, 30), locked: false, ), @@ -43,7 +43,7 @@ class GameMapsState extends Equatable { identifier: 'map2', displayName: '2', path: Assets.tiles.map2, - score: -1, + score: null, ratingSteps: (25, 50, 100), locked: true, ), @@ -96,9 +96,10 @@ enum ScoreRating { /// Creates a [ScoreRating] from the given [RatingSteps] and [score]. factory ScoreRating.fromSteps({ - required int score, + required int? score, required RatingSteps steps, }) { + if (score == null) return none; if (score < 0) return none; if (score <= steps.$1) return gold; if (score <= steps.$2) return silver; @@ -125,12 +126,13 @@ class GameMap extends Equatable { required this.displayName, required this.score, required this.ratingSteps, - required this.locked, + required bool locked, required this.path, - }) : scoreRating = ScoreRating.fromSteps( + }) : scoreRating = ScoreRating.fromSteps( score: score, steps: ratingSteps, - ); + ), + _locked = locked; /// The identifier of the map. final String identifier; @@ -140,8 +142,8 @@ class GameMap extends Equatable { /// The score the player has achieved on this map. /// - /// A score of -1 means the map has not been played yet. - final int score; + /// A score of `null` means the map has not been played yet. + final int? score; /// {@macro RatingSteps} final RatingSteps ratingSteps; @@ -149,10 +151,14 @@ class GameMap extends Equatable { /// {@macro ScoreRating} final ScoreRating scoreRating; + final bool _locked; + /// Whether the map is locked and cannot be played. /// /// A locked map is usually a map that is not yet available to the player. - final bool locked; + bool get locked { + return !(Uri.base.queryParameters['admin'] == 'true') && _locked; + } /// The path to the map file. final String path; diff --git a/packages/trashy_road/lib/src/score/view/score_page.dart b/packages/trashy_road/lib/src/score/view/score_page.dart index 51aaee4d..59bdbd90 100644 --- a/packages/trashy_road/lib/src/score/view/score_page.dart +++ b/packages/trashy_road/lib/src/score/view/score_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:trashy_road/gen/assets.gen.dart'; +import 'package:trashy_road/l10n/l10n.dart'; import 'package:trashy_road/src/game/game.dart'; import 'package:trashy_road/src/loading/loading.dart'; import 'package:trashy_road/src/maps/maps.dart'; @@ -11,17 +12,25 @@ import 'package:trashy_road/src/score/score.dart'; class ScorePage extends StatelessWidget { const ScorePage({ + required ScoreRating scoreRating, required String identifier, super.key, - }) : _identifier = identifier; + }) : _identifier = identifier, + _scoreRating = scoreRating; final String _identifier; + final ScoreRating _scoreRating; + static Route route({ required String identifier, + required ScoreRating scoreRating, }) { return _ScorePageRouteBuilder( - builder: (_) => ScorePage(identifier: identifier), + builder: (_) => ScorePage( + identifier: identifier, + scoreRating: scoreRating, + ), ); } @@ -64,12 +73,18 @@ class ScorePage extends StatelessWidget { @override Widget build(BuildContext context) { + final l10n = context.l10n; + + final title = switch (_scoreRating) { + ScoreRating.gold => l10n.great, + ScoreRating.silver => l10n.nice, + ScoreRating.bronze => l10n.okay, + ScoreRating.none => l10n.ohh, + }; + final screenSize = MediaQuery.sizeOf(context); final gameMapsBloc = context.read(); - - final map = gameMapsBloc.state.maps[_identifier]!; final nextMap = gameMapsBloc.state.next(_identifier); - final scoreRating = map.scoreRating; final basuraTheme = BasuraTheme.of(context); final textStyle = basuraTheme.textTheme.cardHeading.copyWith( @@ -98,11 +113,11 @@ class ScorePage extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Center( - child: AutoSizeText('Nice!', style: textStyle), + child: AutoSizeText(title, style: textStyle), ), SizedBox( height: 80, - child: AnimatedStarRating(rating: scoreRating.value), + child: AnimatedStarRating(rating: _scoreRating.value), ), ], ), diff --git a/packages/trashy_road/lib/src/score/widgets/animated_star_rating.dart b/packages/trashy_road/lib/src/score/widgets/animated_star_rating.dart index 2076863c..6a524c84 100644 --- a/packages/trashy_road/lib/src/score/widgets/animated_star_rating.dart +++ b/packages/trashy_road/lib/src/score/widgets/animated_star_rating.dart @@ -1,6 +1,8 @@ import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rive/rive.dart'; import 'package:trashy_road/gen/gen.dart'; +import 'package:trashy_road/src/audio/audio.dart'; /// An animated star rating. /// @@ -50,6 +52,19 @@ class _AnimatedStarRatingState extends State _animation.addListener(_onAnimationChanged); _animationController.forward(from: 0); + + if (mounted) _playSoundEffect(); + } + + void _playSoundEffect() { + final soundEffect = switch (widget._rating) { + 0 => GameSoundEffects.ratingStars0, + 1 => GameSoundEffects.ratingStars1, + 2 => GameSoundEffects.ratingStars2, + 3 => GameSoundEffects.ratingStars3, + _ => GameSoundEffects.ratingStars3, + }; + context.read().playEffect(soundEffect); } void _onAnimationChanged() { diff --git a/packages/trashy_road/pubspec.lock b/packages/trashy_road/pubspec.lock index e7d9f855..7e406f1d 100644 --- a/packages/trashy_road/pubspec.lock +++ b/packages/trashy_road/pubspec.lock @@ -320,6 +320,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + fast_noise: + dependency: transitive + description: + name: fast_noise + sha256: "2aa7bbde5599d2b6769750ee23536ec8a0096606fc9d10d29a3855e80715e1c6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" ffi: dependency: transitive description: @@ -376,6 +384,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.11.0" + flame_noise: + dependency: "direct main" + description: + name: flame_noise + sha256: e75afe58380156298deeca0f2b40b9e077fd4dea7b84a9b5c6e4536bf94a35e8 + url: "https://pub.dev" + source: hosted + version: "0.3.0" flame_test: dependency: "direct dev" description: @@ -429,6 +445,11 @@ packages: url: "https://pub.dev" source: hosted version: "5.4.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_svg: dependency: "direct main" description: @@ -495,6 +516,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" + source: hosted + version: "0.18.1" io: dependency: transitive description: diff --git a/packages/trashy_road/pubspec.yaml b/packages/trashy_road/pubspec.yaml index d63182d7..9648bca8 100644 --- a/packages/trashy_road/pubspec.yaml +++ b/packages/trashy_road/pubspec.yaml @@ -21,12 +21,16 @@ dependencies: flame_audio: ^2.10.0 flame_behaviors: ^1.1.0 flame_bloc: ^1.11.0 + flame_noise: ^0.3.0 flame_tiled: ^1.19.0 flutter: sdk: flutter flutter_bloc: ^8.1.3 flutter_gen: ^5.4.0 + flutter_localizations: + sdk: flutter flutter_svg: ^2.0.10+1 + intl: ^0.18.1 meta: ^1.11.0 path: ^1.8.3 rive: ^0.12.4 diff --git a/packages/trashy_road/test/src/game/bloc/audio/audio_cubit_test.dart b/packages/trashy_road/test/src/game/bloc/audio/audio_cubit_test.dart deleted file mode 100644 index 8b0f0cfa..00000000 --- a/packages/trashy_road/test/src/game/bloc/audio/audio_cubit_test.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:audioplayers/audioplayers.dart'; -import 'package:bloc_test/bloc_test.dart'; -import 'package:flame_audio/bgm.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:trashy_road/src/game/game.dart'; - -class _MockAudioCache extends Mock implements AudioCache {} - -class _MockAudioPlayer extends Mock implements AudioPlayer {} - -class _MockBgm extends Mock implements Bgm {} - -void main() { - group('AudioCubit', () { - TestWidgetsFlutterBinding.ensureInitialized(); - - late AudioCache audioCache; - late AudioPlayer effectPlayer; - late Bgm bgm; - late AudioPlayer bgmPlayer; - - setUp(() { - audioCache = _MockAudioCache(); - effectPlayer = _MockAudioPlayer(); - bgm = _MockBgm(); - bgmPlayer = _MockAudioPlayer(); - when(() => bgm.audioPlayer).thenReturn(bgmPlayer); - - when(effectPlayer.dispose).thenAnswer((_) => Future.value()); - when(bgmPlayer.dispose).thenAnswer((_) => Future.value()); - - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - const MethodChannel('xyz.luan/audioplayers'), - (_) => null, - ); - }); - - test('can be instantiated', () { - expect(AudioCubit(audioCache: audioCache), isA()); - }); - - blocTest( - 'toggleVolume mutes the volume when the volume is not 0', - setUp: () { - when(() => effectPlayer.setVolume(any())).thenAnswer((_) async {}); - when(() => bgmPlayer.setVolume(any())).thenAnswer((_) async {}); - }, - build: () => - AudioCubit.test(effectPlayer: effectPlayer, backgroundMusic: bgm), - act: (cubit) => cubit.toggleVolume(), - expect: () => [const AudioState(volume: 0)], - verify: (_) { - verify(() => effectPlayer.setVolume(any(that: equals(0)))).called(1); - verify(() => bgmPlayer.setVolume(any(that: equals(0)))).called(1); - }, - ); - - blocTest( - 'toggleVolume unmutes the volume when the volume is 0', - setUp: () { - when(() => effectPlayer.setVolume(any())).thenAnswer((_) async {}); - when(() => bgmPlayer.setVolume(any())).thenAnswer((_) async {}); - }, - build: () { - return AudioCubit.test( - effectPlayer: effectPlayer, - backgroundMusic: bgm, - volume: 0, - ); - }, - act: (cubit) => cubit.toggleVolume(), - expect: () => [const AudioState()], - verify: (_) { - verify(() => effectPlayer.setVolume(any(that: equals(1)))).called(1); - verify(() => bgmPlayer.setVolume(any(that: equals(1)))).called(1); - }, - ); - }); -} diff --git a/packages/trashy_road/test/src/maps/bloc/game_maps_state_test.dart b/packages/trashy_road/test/src/maps/bloc/game_maps_state_test.dart index f3e76627..a5549d18 100644 --- a/packages/trashy_road/test/src/maps/bloc/game_maps_state_test.dart +++ b/packages/trashy_road/test/src/maps/bloc/game_maps_state_test.dart @@ -45,6 +45,14 @@ void main() { expect(rating, ScoreRating.none); }, ); + + test( + '''returns ScoreRating.none when score is null''', + () { + final rating = ScoreRating.fromSteps(score: null, steps: (1, 2, 3)); + expect(rating, ScoreRating.none); + }, + ); }); }); }