diff --git a/lib/end_game/view/end_game_page.dart b/lib/end_game/view/end_game_page.dart index 5dde9b6..6dbc81a 100644 --- a/lib/end_game/view/end_game_page.dart +++ b/lib/end_game/view/end_game_page.dart @@ -3,7 +3,10 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:gamepads/gamepads.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:lightrunners/firebase/score_calculator.dart'; +import 'package:lightrunners/firebase/scores.dart'; import 'package:lightrunners/game/game.dart'; +import 'package:lightrunners/game/player.dart'; import 'package:lightrunners/title/view/title_page.dart'; import 'package:lightrunners/ui/ui.dart'; import 'package:lightrunners/utils/gamepad_navigator.dart'; @@ -11,16 +14,16 @@ import 'package:lightrunners/widgets/widgets.dart'; class EndGamePage extends StatefulWidget { const EndGamePage({ - required this.scores, + required this.points, super.key, }); - final Map scores; + final Map points; - static Route route(Map scores) { + static Route route(Map points) { return MaterialPageRoute( maintainState: false, - builder: (_) => EndGamePage(scores: scores), + builder: (_) => EndGamePage(points: points), ); } @@ -32,11 +35,20 @@ class _EndGamePageState extends State { late StreamSubscription _gamepadSubscription; late GamepadNavigator _gamepadNavigator; + // TODO(any): display loading indicator on screen + bool updatingFirebase = true; + @override void initState() { super.initState(); + _updateFirebase(); _gamepadNavigator = GamepadNavigator( - onAny: () => Navigator.of(context).pushReplacement(TitlePage.route()), + onAny: () { + if (updatingFirebase) { + return; + } + Navigator.of(context).pushReplacement(TitlePage.route()); + }, ); _gamepadSubscription = Gamepads.events.listen(_gamepadNavigator.handle); } @@ -48,11 +60,25 @@ class _EndGamePageState extends State { super.dispose(); } + Future _updateFirebase() async { + final scores = ScoreCalculator.computeScores(widget.points); + final futures = scores.entries + .where((entry) => entry.key.playerId != null) + .map((entry) { + return Scores.updateScore( + playerId: entry.key.playerId!, + score: entry.value, + ); + }); + await Future.wait(futures); + setState(() => updatingFirebase = false); + } + @override Widget build(BuildContext context) { final fontFamily = GoogleFonts.bungee().fontFamily; - final scores = widget.scores.entries.toList() + final scores = widget.points.entries.toList() ..sort((a, b) => b.value.compareTo(a.value)); const baseSize = 64; @@ -88,7 +114,7 @@ class _EndGamePageState extends State { child: Column( children: [ Image.asset( - 'assets/images/ships/${shipSprites[GamePalette.shipValues.indexOf(scores[i].key)]}', + 'assets/images/ships/${shipSprites[GamePalette.shipValues.indexOf(scores[i].key.color)]}', width: baseSize.toDouble(), ), const SizedBox(height: 16), @@ -97,7 +123,7 @@ class _EndGamePageState extends State { style: TextStyle( fontFamily: fontFamily, fontSize: 32, - color: scores[i].key, + color: scores[i].key.color, ), ), const SizedBox(height: 16), @@ -105,7 +131,7 @@ class _EndGamePageState extends State { padding: const EdgeInsets.all(16), width: 180, decoration: BoxDecoration( - color: scores[i].key, + color: scores[i].key.color, backgroundBlendMode: BlendMode.colorBurn, boxShadow: [ BoxShadow( diff --git a/lib/firebase/score.dart b/lib/firebase/score.dart new file mode 100644 index 0000000..72887f5 --- /dev/null +++ b/lib/firebase/score.dart @@ -0,0 +1,20 @@ +import 'package:firedart/firestore/models.dart'; + +class Score { + final int playerId; + final String email; + final int score; + + Score({ + required this.playerId, + required this.email, + required this.score, + }); + + Score.fromDocument(Document document) + : this( + playerId: document.map['playerId'] as int, + email: document.map['email'] as String, + score: document.map['score'] as int, + ); +} diff --git a/lib/firebase/score_calculator.dart b/lib/firebase/score_calculator.dart new file mode 100644 index 0000000..84e458a --- /dev/null +++ b/lib/firebase/score_calculator.dart @@ -0,0 +1,47 @@ +import 'package:collection/collection.dart'; +import 'package:lightrunners/game/player.dart'; + +class _PlayerScore { + final Player player; + int score = 0; + + _PlayerScore(this.player); +} + +class ScoreCalculator { + ScoreCalculator._(); + + static Map computeScores(Map points) { + final scores = points.entries + .toList() + .sortedBy((entry) => entry.value) + .map((entry) => _PlayerScore(entry.key)); + + final numPlayers = points.length; + final totalScore = points.values.reduce((a, b) => a + b); + + var jackpot = numPlayers; + if (jackpot == 0) { + return {}; + } + + scores.first.score = 1; + for (final playerScore in scores) { + final point = points[playerScore.player]!; + final ratio = point / totalScore; + + final score = (ratio * jackpot).ceil().clamp(0, jackpot); + playerScore.score += score; + jackpot -= score; + if (jackpot == 0) { + break; + } + } + + return Map.fromEntries( + scores + .map((score) => MapEntry(score.player, score.score)) + .where((element) => element.value > 0), + ); + } +} diff --git a/lib/firebase/scores.dart b/lib/firebase/scores.dart new file mode 100644 index 0000000..c662dba --- /dev/null +++ b/lib/firebase/scores.dart @@ -0,0 +1,37 @@ +import 'package:firedart/firedart.dart'; +import 'package:lightrunners/firebase/score.dart'; + +class Scores { + static late Firestore firestore; + + Scores._(); + + static Future init() async { + // NOTE: This will not work. support was removed from the library + const projectId = 'lightrunners-e89a9'; + Firestore.initialize(projectId); + firestore = Firestore.instance; + } + + static Future> topScores() async { + final page = await firestore + .collection('scores') + .orderBy('score', descending: true) + .limit(10) + .get(); + return page.map(Score.fromDocument).toList(); + } + + static Future updateScore({ + required int playerId, + required int score, + }) async { + final document = + firestore.collection('scores').document(playerId.toString()); + if (await document.exists) { + await document.update({'score': score}); + } else { + print('Error: Score not found for player id $playerId.'); + } + } +} diff --git a/lib/game/components/ship.dart b/lib/game/components/ship.dart index d84cc58..032107a 100644 --- a/lib/game/components/ship.dart +++ b/lib/game/components/ship.dart @@ -9,6 +9,7 @@ import 'package:flame/geometry.dart'; import 'package:flutter/services.dart'; import 'package:gamepads/gamepads.dart'; import 'package:lightrunners/game/game.dart'; +import 'package:lightrunners/game/player.dart'; import 'package:lightrunners/ui/ui.dart'; import 'package:lightrunners/utils/gamepad_map.dart'; import 'package:lightrunners/utils/input_handler_utils.dart'; @@ -86,12 +87,16 @@ class Ship extends SpriteComponent static final _random = Random(); Ship(this.playerNumber, this.gamepadId) - : moveJoystick = _makeJoystick(gamepadId, leftXAxis, leftYAxis), + : player = Player( + color: _shipColors[playerNumber].color, + ), + moveJoystick = _makeJoystick(gamepadId, leftXAxis, leftYAxis), super(size: Vector2(40, 80), anchor: Anchor.center) { paint = _shipColors[playerNumber]; spritePath = shipSprites[playerNumber]; } + final Player player; final int playerNumber; final String? gamepadId; // null means keyboard int score = 0; diff --git a/lib/game/lightrunners_game.dart b/lib/game/lightrunners_game.dart index e33efed..73d9499 100644 --- a/lib/game/lightrunners_game.dart +++ b/lib/game/lightrunners_game.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math'; import 'dart:ui'; import 'package:flame/camera.dart'; @@ -10,6 +11,7 @@ import 'package:lightrunners/game/components/game_border.dart'; import 'package:lightrunners/game/components/score_panel.dart'; import 'package:lightrunners/game/components/spotlight.dart'; import 'package:lightrunners/game/game.dart'; +import 'package:lightrunners/game/player.dart'; import 'package:lightrunners/utils/constants.dart'; import 'package:lightrunners/utils/utils.dart'; @@ -22,7 +24,7 @@ class LightRunnersGame extends FlameGame late final Map ships; StreamSubscription? _subscription; - final void Function(Map) onEndGame; + final void Function(Map) onEndGame; LightRunnersGame({required this.players, required this.onEndGame}); @@ -55,7 +57,7 @@ class LightRunnersGame extends FlameGame onTimeUp: () { countDown.removeFromParent(); onEndGame({ - for (final ship in ships.values) ship.paint.color: ship.score, + for (final ship in ships.values) ship.player: ship.score, }); }, ), @@ -73,6 +75,11 @@ class LightRunnersGame extends FlameGame ships[''] = Ship(0, null); } + // TODO(any): correctly configure player numbers from the app + for (final ship in ships.values) { + ship.player.playerId = Random().nextInt(999); + } + _subscription = Gamepads.events.listen((event) { ships[event.gamepadId]?.onGamepadEvent(event); }); diff --git a/lib/game/player.dart b/lib/game/player.dart new file mode 100644 index 0000000..c13ad7c --- /dev/null +++ b/lib/game/player.dart @@ -0,0 +1,10 @@ +import 'package:flame/extensions.dart'; + +class Player { + int? playerId; + final Color color; + + Player({ + required this.color, + }); +} diff --git a/lib/leaderboard/view/leaderboard_page.dart b/lib/leaderboard/view/leaderboard_page.dart index 02e758c..c1d99a6 100644 --- a/lib/leaderboard/view/leaderboard_page.dart +++ b/lib/leaderboard/view/leaderboard_page.dart @@ -4,6 +4,8 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:gamepads/gamepads.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:lightrunners/firebase/score.dart'; +import 'package:lightrunners/firebase/scores.dart'; import 'package:lightrunners/title/title.dart'; import 'package:lightrunners/utils/gamepad_navigator.dart'; import 'package:lightrunners/widgets/widgets.dart'; @@ -13,13 +15,6 @@ const _maxCharactersScoreBoard = 26; const _lightrunnersInfoBlob = '''Light Runners was made with 💙 by Blue Fire and Invertase for Fluttercon 2023 '''; -final _leaderboard = [ - (name: 'Erick', score: 100), - (name: 'Luan', score: 90), - (name: 'Renan', score: 80), - (name: 'Spydon', score: 70), - (name: 'Wolfenrain', score: 60), -]; class LeaderboardPage extends StatefulWidget { const LeaderboardPage({ @@ -41,6 +36,9 @@ class _LeaderboardPageState extends State { late StreamSubscription _gamepadSubscription; late GamepadNavigator _gamepadNavigator; + bool loading = true; + List scores = []; + @override void initState() { super.initState(); @@ -48,6 +46,15 @@ class _LeaderboardPageState extends State { onAny: () => Navigator.of(context).pushReplacement(TitlePage.route()), ); _gamepadSubscription = Gamepads.events.listen(_gamepadNavigator.handle); + _updateScores(); + } + + Future _updateScores() async { + final scores = await Scores.topScores(); + setState(() { + loading = false; + this.scores = scores; + }); } @override @@ -92,16 +99,19 @@ class _LeaderboardPageState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - for (final entry in _leaderboard) - Text( - _toScoreboardLine(entry), - style: TextStyle( - fontFamily: major, - fontWeight: FontWeight.bold, - fontSize: 38, - color: Colors.white, - ), - ) + if (loading) + const CircularProgressIndicator() + else + for (final score in scores) + Text( + _toScoreboardLine(score), + style: TextStyle( + fontFamily: major, + fontWeight: FontWeight.bold, + fontSize: 38, + color: Colors.white, + ), + ) ], ), ), @@ -111,14 +121,16 @@ class _LeaderboardPageState extends State { ); } - String _toScoreboardLine(({String name, int score}) entry) { - final name = entry.name.substring( + String _toScoreboardLine(Score record) { + final name = record.email.substring( 0, - min(entry.name.length, _maxCharactersScoreBoard - _maxScoreDigits - 1), + min(record.email.length, _maxCharactersScoreBoard - _maxScoreDigits - 1), ); final maxScore = pow(10, _maxScoreDigits) - 1; - final score = - entry.score.clamp(0, maxScore).toString().padLeft(_maxScoreDigits, '0'); + final score = record.score + .clamp(0, maxScore) + .toString() + .padLeft(_maxScoreDigits, '0'); final dotDotDot = '.' * (_maxCharactersScoreBoard - name.length - score.length); diff --git a/lib/main.dart b/lib/main.dart index cc07549..0c2aadd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,14 @@ import 'package:flame/flame.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:lightrunners/firebase/scores.dart'; import 'package:lightrunners/shaders/shaders.dart'; import 'package:lightrunners/title/title.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Flame.device.fullScreen(); + Scores.init(); runApp( CRTShader( enabled: true, diff --git a/pubspec.lock b/pubspec.lock index 0d195d4..f694c90 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.6.0" + archive: + dependency: transitive + description: + name: archive + sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" + url: "https://pub.dev" + source: hosted + version: "3.3.7" args: dependency: transitive description: @@ -169,6 +177,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.4" + firedart: + dependency: "direct main" + description: + name: firedart + sha256: "74a67f7582acecb164674cd2d640e44806dfca1b868cc7474177392125a0cbbf" + url: "https://pub.dev" + source: hosted + version: "0.9.5" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" flame: dependency: "direct main" description: @@ -298,6 +322,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.4" + googleapis_auth: + dependency: transitive + description: + name: googleapis_auth + sha256: af7c3a3edf9d0de2e1e0a77e994fae0a581c525fa7012af4fa0d4a52ed9484da + url: "https://pub.dev" + source: hosted + version: "1.4.1" + grpc: + dependency: transitive + description: + name: grpc + sha256: "4e9bff1ebb4ff370cf471b1ab85007b408ade867dcc34b551eee7ee7da573002" + url: "https://pub.dev" + source: hosted + version: "3.2.2" http: dependency: transitive description: @@ -306,6 +346,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.13.5" + http2: + dependency: transitive + description: + name: http2 + sha256: "38db0c4aa9f1cd238a5d2e86aa0cc7cc91c77e0c6c94ba64bbe85e4ff732a952" + url: "https://pub.dev" + source: hosted + version: "2.2.0" http_multi_server: dependency: transitive description: @@ -487,6 +535,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + url: "https://pub.dev" + source: hosted + version: "3.7.3" pool: dependency: transitive description: @@ -503,6 +559,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.4" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "4034a02b7e231e7e60bff30a8ac13a7347abfdac0798595fae0b90a3f0afe759" + url: "https://pub.dev" + source: hosted + version: "3.0.0" pub_semver: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 951340e..b116fc7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,6 +9,7 @@ environment: dependencies: collection: ^1.17.0 + firedart: ^0.9.5 flame: ^1.8.0 flame_audio: ^2.0.3 flutter: