diff --git a/README.md b/README.md index 9c656d3..4a049ac 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,9 @@ - ~~Tetra League matches history~~ - ~~Tetra League historic charts for tracked players~~ - ~~Better UI with delta and hints for stats~~ *v0.2.0, we are here* -- ~~Ability to compare player with APM-PPS-VS stats~~ *dev build are here* -- Ability to fetch Tetra League leaderboard -- Average stats for ranks +- ~~Ability to compare player with APM-PPS-VS stats~~ +- ~~Ability to fetch Tetra League leaderboard~~ +- ~~Average stats for ranks~~ *dev build are here* - Ability to compare player with avgRank - UI Animations - i18n, EN and RU locales diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index da75d04..768f28f 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -13,6 +13,25 @@ const double appdspWeight = 140; const double vsapmWeight = 60; const double cheeseWeight = 1.25; const double gbeWeight = 315; +const Map ranksCutoffs = { + "x": 0.01, + "u": 0.05, + "ss": 0.11, + "s+": 0.17, + "s": 0.23, + "s-": 0.3, + "a+": 0.38, + "a": 0.46, + "a-": 0.54, + "b+": 0.62, + "b": 0.7, + "b-": 0.78, + "c+": 0.84, + "c": 0.9, + "c-": 0.95, + "d+": 0.975, + "d": 1 +}; Duration doubleSecondsToDuration(double value) { value = value * 1000000; @@ -719,7 +738,7 @@ class TetraLeagueAlpha { nextRank = json['next_rank']; nextAt = json['next_at']; percentileRank = json['percentile_rank']; - nerdStats = (apm != null && pps != null && apm != null) ? NerdStats(apm!, pps!, vs!) : null; + nerdStats = (apm != null && pps != null && vs != null) ? NerdStats(apm!, pps!, vs!) : null; estTr = (nerdStats != null) ? EstTr(apm!, pps!, vs!, (rd != null) ? rd! : 69, nerdStats!.app, nerdStats!.dss, nerdStats!.dsp, nerdStats!.gbe) : null; playstyle = (nerdStats != null) ? Playstyle(apm!, pps!, nerdStats!.app, nerdStats!.vsapm, nerdStats!.dsp, nerdStats!.gbe, estTr!.srarea, estTr!.statrank) : null; @@ -845,6 +864,55 @@ class TetrioPlayersLeaderboard { TetrioPlayersLeaderboard(this.type, this.leaderboard); + List getAverageOfRank(String rank){ + List filtredLeaderboard = List.from(leaderboard); + filtredLeaderboard.removeWhere((element) => element.rank != rank); + if (filtredLeaderboard.isEmpty) throw Exception("Invalid rank"); + double avgAPM = 0, avgPPS = 0, avgVS = 0, avgTR = 0, avgGlicko = 0, avgRD = 0, lowestTR = 25000; + int avgGamesPlayed = 0, avgGamesWon = 0, totalGamesPlayed = 0, totalGamesWon = 0; + for (var entry in filtredLeaderboard){ + avgAPM += entry.apm; + avgPPS += entry.pps; + avgVS += entry.vs; + avgTR += entry.rating; + avgGlicko += entry.glicko; + avgRD += entry.rd; + totalGamesPlayed += entry.gamesPlayed; + totalGamesWon += entry.gamesWon; + if (entry.rating < lowestTR) lowestTR = entry.rating; + } + avgAPM /= filtredLeaderboard.length; + avgPPS /= filtredLeaderboard.length; + avgVS /= filtredLeaderboard.length; + avgTR /= filtredLeaderboard.length; + avgGlicko /= filtredLeaderboard.length; + avgRD /= filtredLeaderboard.length; + avgGamesPlayed = (totalGamesPlayed / filtredLeaderboard.length).floor(); + avgGamesWon = (totalGamesWon / filtredLeaderboard.length).floor(); + return [TetraLeagueAlpha(apm: avgAPM, pps: avgPPS, vs: avgVS, glicko: avgGlicko, rd: avgRD, gamesPlayed: avgGamesPlayed, gamesWon: avgGamesWon, bestRank: rank, decaying: false, rating: avgTR, rank: rank, percentileRank: rank, percentile: 0, standing: -1, standingLocal: -1, nextAt: -1, prevAt: -1), + {"totalGamesPlayed": totalGamesPlayed, "totalGamesWon": totalGamesWon, "players": filtredLeaderboard.length, "lowestTR": lowestTR, "toEnterTR": leaderboard[(leaderboard.length * ranksCutoffs[rank]!).floor()-1].rating}]; + } + + Map> get averages => { + 'x': getAverageOfRank("x"), + 'u': getAverageOfRank("u"), + 'ss': getAverageOfRank("ss"), + 's+': getAverageOfRank("s+"), + 's': getAverageOfRank("s"), + 's-': getAverageOfRank("s-"), + 'a+': getAverageOfRank("a+"), + 'a': getAverageOfRank("a"), + 'a-': getAverageOfRank("a-"), + 'b+': getAverageOfRank("b+"), + 'b': getAverageOfRank("b"), + 'b-': getAverageOfRank("b-"), + 'c+': getAverageOfRank("c+"), + 'c': getAverageOfRank("c"), + 'c-': getAverageOfRank("c-"), + 'd+': getAverageOfRank("d+"), + 'd': getAverageOfRank("d") + }; + TetrioPlayersLeaderboard.fromJson(List json, String t, DateTime ts) { type = t; timestamp = ts; diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 047642a..ce70bb0 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -1,10 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:developer' as developer; -import 'dart:io'; -import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; -import 'package:path_provider/path_provider.dart'; import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/services/sqlite_db_controller.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; diff --git a/lib/views/compare_view.dart b/lib/views/compare_view.dart index 98aa956..30eb882 100644 --- a/lib/views/compare_view.dart +++ b/lib/views/compare_view.dart @@ -820,7 +820,7 @@ class CompareState extends State { ) ], ) - ] : [Text("Please, enter username, user ID, or APM-PPS-VS values (divider doesn't matter) to both of fields")], + ] : [const Text("Please, enter username, user ID, or APM-PPS-VS values (divider doesn't matter) to both of fields")], ) ), ), diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index d9403a0..88d32e0 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -31,7 +31,8 @@ final NumberFormat f4 = NumberFormat.decimalPatternDigits(decimalDigits: 4); final DateFormat dateFormat = DateFormat.yMMMd().add_Hms(); class MainView extends StatefulWidget { - const MainView({Key? key}) : super(key: key); + final String? player; + const MainView({Key? key, this.player}) : super(key: key); String get title => "Tetra Stats: $_titleNickname"; @@ -89,8 +90,12 @@ class _MainState extends State with SingleTickerProviderStateMixin { teto.open(); _scrollController = ScrollController(); _tabController = TabController(length: 6, vsync: this); - _getPreferences() + if (widget.player != null){ + changePlayer(widget.player!); + }else{ + _getPreferences() .then((value) => changePlayer(prefs.getString("player") ?? "dan63047")); + } super.initState(); } @@ -178,7 +183,7 @@ class _MainState extends State with SingleTickerProviderStateMixin { @override Widget build(BuildContext context) { return Scaffold( - drawer: NavDrawer(changePlayer), + drawer: widget.player == null ? NavDrawer(changePlayer) : null, appBar: AppBar( title: !_searchBoolean ? Text( @@ -222,6 +227,10 @@ class _MainState extends State with SingleTickerProviderStateMixin { ), PopupMenuButton( itemBuilder: (BuildContext context) => [ + const PopupMenuItem( + value: "refresh", + child: Text('Refresh'), + ), const PopupMenuItem( value: "/states", child: Text('Show stored data'), @@ -236,7 +245,7 @@ class _MainState extends State with SingleTickerProviderStateMixin { ), ], onSelected: (value) { - if (value == "tll") {teto.fetchTLLeaderboard(); + if (value == "refresh") {changePlayer(_searchFor); return;} Navigator.pushNamed(context, value); }, @@ -542,7 +551,7 @@ class _HistoryChartThigy extends StatelessWidget{ height: MediaQuery.of(context).size.height - 100, child: Stack( children: [ - Padding( padding: bigScreen ? const EdgeInsets.fromLTRB(40, 40, 40, 48) : const EdgeInsets.fromLTRB(0, 40, 0, 48) , + Padding( padding: bigScreen ? const EdgeInsets.fromLTRB(40, 40, 40, 48) : const EdgeInsets.fromLTRB(0, 40, 16, 48) , child: LineChart( LineChartData( lineBarsData: [LineChartBarData(spots: data)], diff --git a/lib/views/ranks_averages_view.dart b/lib/views/ranks_averages_view.dart new file mode 100644 index 0000000..8005e33 --- /dev/null +++ b/lib/views/ranks_averages_view.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:tetra_stats/views/tl_leaderboard_view.dart'; + +class RankAveragesView extends StatefulWidget { + const RankAveragesView({Key? key}) : super(key: key); + + @override + State createState() => RanksAverages(); +} + +class RanksAverages extends State { + Map> averages = {}; + + + + @override + void initState() { + teto.fetchTLLeaderboard().then((value) {averages = value.averages; setState(() { + });}); + super.initState(); + } + + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Ranks averages"), + ), + backgroundColor: Colors.black, + body: SafeArea( + child: ListView.builder( + itemCount: averages.length, + itemBuilder: (context, index){ + bool bigScreen = MediaQuery.of(context).size.width > 768; + List keys = averages.keys.toList(); + return ListTile( + leading: Image.asset("res/tetrio_tl_alpha_ranks/${keys[index]}.png", height: 48), + title: Text("${averages[keys[index]]?[1]["players"]} players", style: const TextStyle(fontFamily: "Eurostile Round Extended")), + subtitle: Text("${f2.format(averages[keys[index]]?[0].apm)} APM, ${f2.format(averages[keys[index]]?[0].pps)} PPS, ${f2.format(averages[keys[index]]?[0].vs)} VS, ${f2.format(averages[keys[index]]?[0].nerdStats.app)} APP, ${f2.format(averages[keys[index]]?[0].nerdStats.vsapm)} VS/APM"), + trailing: Text("${f2.format(averages[keys[index]]?[1]["toEnterTR"])} TR", style: bigScreen ? const TextStyle(fontSize: 28) : null)); + }) + ), + ); + } +} diff --git a/lib/views/tl_leaderboard_view.dart b/lib/views/tl_leaderboard_view.dart index 1b326c1..93d62e8 100644 --- a/lib/views/tl_leaderboard_view.dart +++ b/lib/views/tl_leaderboard_view.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/services/tetrio_crud.dart'; -import 'package:tetra_stats/views/states_view.dart'; +import 'package:tetra_stats/views/main_view.dart'; +import 'package:tetra_stats/views/ranks_averages_view.dart'; final TetrioService teto = TetrioService(); @@ -22,6 +22,21 @@ class TLLeaderboardState extends State { return Scaffold( appBar: AppBar( title: const Text("Tetra League Leaderboard"), + actions: [ + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const RankAveragesView(), + maintainState: false, + ), + ); + }, + icon: const Icon(Icons.compress), + tooltip: "Averages", + ), + ], ), backgroundColor: Colors.black, body: SafeArea( @@ -39,9 +54,9 @@ class TLLeaderboardState extends State { headerSliverBuilder: (context, value) { String howManyPlayers(int numberOfPlayers) => Intl.plural( numberOfPlayers, - zero: 'Empty list. Press "Track" button in previous view to add current player here', - one: 'There is only one player', - other: 'There are $numberOfPlayers players', + zero: 'Empty list. Looks like something is wrong...', + one: 'There is only one player... What?', + other: 'There are $numberOfPlayers ranked players.', name: 'howManyPeople', args: [numberOfPlayers], desc: 'Description of how many people are seen in a place.', @@ -62,25 +77,27 @@ class TLLeaderboardState extends State { body: ListView.builder( itemCount: allPlayers!.length, itemBuilder: (context, index) { + bool bigScreen = MediaQuery.of(context).size.width > 768; return ListTile( - leading: Text((index+1).toString(), style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)), - title: Text("${allPlayers[index].username}", style: const TextStyle(fontFamily: "Eurostile Round Extended")), + leading: Text((index+1).toString(), style: bigScreen ? const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28) : null), + title: Text(allPlayers[index].username, style: const TextStyle(fontFamily: "Eurostile Round Extended")), subtitle: Text( "${f2.format(allPlayers[index].apm)} APM, ${f2.format(allPlayers[index].pps)} PPS, ${f2.format(allPlayers[index].vs)} VS, ${f2.format(allPlayers[index].app)} APP, ${f2.format(allPlayers[index].vsapm)} VS/APM"), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ - Text("${f2.format(allPlayers[index].rating)} TR", style: const TextStyle(fontSize: 28)), - Image.asset("res/tetrio_tl_alpha_ranks/${allPlayers[index].rank}.png", height: 48), + Text("${f2.format(allPlayers[index].rating)} TR", style: bigScreen ? const TextStyle(fontSize: 28) : null), + Image.asset("res/tetrio_tl_alpha_ranks/${allPlayers[index].rank}.png", height: bigScreen ? 48 : 16), ], ), onTap: () { - // Navigator.push( - // context, - // MaterialPageRoute( - // builder: (context) => StatesView(states: allPlayers!), - // ), - // ); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MainView(player: allPlayers[index].userId), + maintainState: false, + ), + ); }, ); })); diff --git a/pubspec.yaml b/pubspec.yaml index 1447080..893c402 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -159,6 +159,9 @@ flutter: - res/tetrio_badges/tawsignite_expert.png - res/tetrio_badges/tawslg.png - res/tetrio_badges/tetralympic_masters.png + - res/tetrio_badges/thaitour_1.png + - res/tetrio_badges/thaitour_2.png + - res/tetrio_badges/thaitour_3.png - res/tetrio_badges/ttsdtc_1.png - res/tetrio_badges/ttsdtc_2.png - res/tetrio_badges/ttsdtc_3.png diff --git a/res/tetrio_badges/thaitour_1.png b/res/tetrio_badges/thaitour_1.png new file mode 100644 index 0000000..903568d Binary files /dev/null and b/res/tetrio_badges/thaitour_1.png differ diff --git a/res/tetrio_badges/thaitour_2.png b/res/tetrio_badges/thaitour_2.png new file mode 100644 index 0000000..f3ffccd Binary files /dev/null and b/res/tetrio_badges/thaitour_2.png differ diff --git a/res/tetrio_badges/thaitour_3.png b/res/tetrio_badges/thaitour_3.png new file mode 100644 index 0000000..cec402b Binary files /dev/null and b/res/tetrio_badges/thaitour_3.png differ