Ranks averages + Improved leaderboard + bugfix
This commit is contained in:
parent
9fb74f051c
commit
3fb7b1fabb
|
@ -15,9 +15,9 @@
|
||||||
- ~~Tetra League matches history~~
|
- ~~Tetra League matches history~~
|
||||||
- ~~Tetra League historic charts for tracked players~~
|
- ~~Tetra League historic charts for tracked players~~
|
||||||
- ~~Better UI with delta and hints for stats~~ *v0.2.0, we are here*
|
- ~~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 compare player with APM-PPS-VS stats~~
|
||||||
- Ability to fetch Tetra League leaderboard
|
- ~~Ability to fetch Tetra League leaderboard~~
|
||||||
- Average stats for ranks
|
- ~~Average stats for ranks~~ *dev build are here*
|
||||||
- Ability to compare player with avgRank
|
- Ability to compare player with avgRank
|
||||||
- UI Animations
|
- UI Animations
|
||||||
- i18n, EN and RU locales
|
- i18n, EN and RU locales
|
||||||
|
|
|
@ -13,6 +13,25 @@ const double appdspWeight = 140;
|
||||||
const double vsapmWeight = 60;
|
const double vsapmWeight = 60;
|
||||||
const double cheeseWeight = 1.25;
|
const double cheeseWeight = 1.25;
|
||||||
const double gbeWeight = 315;
|
const double gbeWeight = 315;
|
||||||
|
const Map<String, double> 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) {
|
Duration doubleSecondsToDuration(double value) {
|
||||||
value = value * 1000000;
|
value = value * 1000000;
|
||||||
|
@ -719,7 +738,7 @@ class TetraLeagueAlpha {
|
||||||
nextRank = json['next_rank'];
|
nextRank = json['next_rank'];
|
||||||
nextAt = json['next_at'];
|
nextAt = json['next_at'];
|
||||||
percentileRank = json['percentile_rank'];
|
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;
|
estTr = (nerdStats != null) ? EstTr(apm!, pps!, vs!, (rd != null) ? rd! : 69, nerdStats!.app, nerdStats!.dss, nerdStats!.dsp, nerdStats!.gbe) : null;
|
||||||
playstyle =
|
playstyle =
|
||||||
(nerdStats != null) ? Playstyle(apm!, pps!, nerdStats!.app, nerdStats!.vsapm, nerdStats!.dsp, nerdStats!.gbe, estTr!.srarea, estTr!.statrank) : null;
|
(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);
|
TetrioPlayersLeaderboard(this.type, this.leaderboard);
|
||||||
|
|
||||||
|
List<dynamic> getAverageOfRank(String rank){
|
||||||
|
List<TetrioPlayerFromLeaderboard> 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<String, List<dynamic>> 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<dynamic> json, String t, DateTime ts) {
|
TetrioPlayersLeaderboard.fromJson(List<dynamic> json, String t, DateTime ts) {
|
||||||
type = t;
|
type = t;
|
||||||
timestamp = ts;
|
timestamp = ts;
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:developer' as developer;
|
import 'dart:developer' as developer;
|
||||||
import 'dart:io';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
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/crud_exceptions.dart';
|
||||||
import 'package:tetra_stats/services/sqlite_db_controller.dart';
|
import 'package:tetra_stats/services/sqlite_db_controller.dart';
|
||||||
import 'package:tetra_stats/data_objects/tetrio.dart';
|
import 'package:tetra_stats/data_objects/tetrio.dart';
|
||||||
|
|
|
@ -820,7 +820,7 @@ class CompareState extends State<CompareView> {
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
] : [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")],
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -31,7 +31,8 @@ final NumberFormat f4 = NumberFormat.decimalPatternDigits(decimalDigits: 4);
|
||||||
final DateFormat dateFormat = DateFormat.yMMMd().add_Hms();
|
final DateFormat dateFormat = DateFormat.yMMMd().add_Hms();
|
||||||
|
|
||||||
class MainView extends StatefulWidget {
|
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";
|
String get title => "Tetra Stats: $_titleNickname";
|
||||||
|
|
||||||
|
@ -89,8 +90,12 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
|
||||||
teto.open();
|
teto.open();
|
||||||
_scrollController = ScrollController();
|
_scrollController = ScrollController();
|
||||||
_tabController = TabController(length: 6, vsync: this);
|
_tabController = TabController(length: 6, vsync: this);
|
||||||
|
if (widget.player != null){
|
||||||
|
changePlayer(widget.player!);
|
||||||
|
}else{
|
||||||
_getPreferences()
|
_getPreferences()
|
||||||
.then((value) => changePlayer(prefs.getString("player") ?? "dan63047"));
|
.then((value) => changePlayer(prefs.getString("player") ?? "dan63047"));
|
||||||
|
}
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,7 +183,7 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
drawer: NavDrawer(changePlayer),
|
drawer: widget.player == null ? NavDrawer(changePlayer) : null,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: !_searchBoolean
|
title: !_searchBoolean
|
||||||
? Text(
|
? Text(
|
||||||
|
@ -222,6 +227,10 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
|
||||||
),
|
),
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
|
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
|
||||||
|
const PopupMenuItem(
|
||||||
|
value: "refresh",
|
||||||
|
child: Text('Refresh'),
|
||||||
|
),
|
||||||
const PopupMenuItem(
|
const PopupMenuItem(
|
||||||
value: "/states",
|
value: "/states",
|
||||||
child: Text('Show stored data'),
|
child: Text('Show stored data'),
|
||||||
|
@ -236,7 +245,7 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
onSelected: (value) {
|
onSelected: (value) {
|
||||||
if (value == "tll") {teto.fetchTLLeaderboard();
|
if (value == "refresh") {changePlayer(_searchFor);
|
||||||
return;}
|
return;}
|
||||||
Navigator.pushNamed(context, value);
|
Navigator.pushNamed(context, value);
|
||||||
},
|
},
|
||||||
|
@ -542,7 +551,7 @@ class _HistoryChartThigy extends StatelessWidget{
|
||||||
height: MediaQuery.of(context).size.height - 100,
|
height: MediaQuery.of(context).size.height - 100,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
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(
|
child: LineChart(
|
||||||
LineChartData(
|
LineChartData(
|
||||||
lineBarsData: [LineChartBarData(spots: data)],
|
lineBarsData: [LineChartBarData(spots: data)],
|
||||||
|
|
|
@ -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<StatefulWidget> createState() => RanksAverages();
|
||||||
|
}
|
||||||
|
|
||||||
|
class RanksAverages extends State<RankAveragesView> {
|
||||||
|
Map<String, List<dynamic>> 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<String> 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));
|
||||||
|
})
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.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/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();
|
final TetrioService teto = TetrioService();
|
||||||
|
|
||||||
|
@ -22,6 +22,21 @@ class TLLeaderboardState extends State<TLLeaderboardView> {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text("Tetra League Leaderboard"),
|
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,
|
backgroundColor: Colors.black,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
|
@ -39,9 +54,9 @@ class TLLeaderboardState extends State<TLLeaderboardView> {
|
||||||
headerSliverBuilder: (context, value) {
|
headerSliverBuilder: (context, value) {
|
||||||
String howManyPlayers(int numberOfPlayers) => Intl.plural(
|
String howManyPlayers(int numberOfPlayers) => Intl.plural(
|
||||||
numberOfPlayers,
|
numberOfPlayers,
|
||||||
zero: 'Empty list. Press "Track" button in previous view to add current player here',
|
zero: 'Empty list. Looks like something is wrong...',
|
||||||
one: 'There is only one player',
|
one: 'There is only one player... What?',
|
||||||
other: 'There are $numberOfPlayers players',
|
other: 'There are $numberOfPlayers ranked players.',
|
||||||
name: 'howManyPeople',
|
name: 'howManyPeople',
|
||||||
args: [numberOfPlayers],
|
args: [numberOfPlayers],
|
||||||
desc: 'Description of how many people are seen in a place.',
|
desc: 'Description of how many people are seen in a place.',
|
||||||
|
@ -62,25 +77,27 @@ class TLLeaderboardState extends State<TLLeaderboardView> {
|
||||||
body: ListView.builder(
|
body: ListView.builder(
|
||||||
itemCount: allPlayers!.length,
|
itemCount: allPlayers!.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
|
bool bigScreen = MediaQuery.of(context).size.width > 768;
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: Text((index+1).toString(), style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)),
|
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")),
|
title: Text(allPlayers[index].username, style: const TextStyle(fontFamily: "Eurostile Round Extended")),
|
||||||
subtitle: Text(
|
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"),
|
"${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(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text("${f2.format(allPlayers[index].rating)} TR", style: const TextStyle(fontSize: 28)),
|
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: 48),
|
Image.asset("res/tetrio_tl_alpha_ranks/${allPlayers[index].rank}.png", height: bigScreen ? 48 : 16),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// Navigator.push(
|
Navigator.push(
|
||||||
// context,
|
context,
|
||||||
// MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
// builder: (context) => StatesView(states: allPlayers!),
|
builder: (context) => MainView(player: allPlayers[index].userId),
|
||||||
// ),
|
maintainState: false,
|
||||||
// );
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -159,6 +159,9 @@ flutter:
|
||||||
- res/tetrio_badges/tawsignite_expert.png
|
- res/tetrio_badges/tawsignite_expert.png
|
||||||
- res/tetrio_badges/tawslg.png
|
- res/tetrio_badges/tawslg.png
|
||||||
- res/tetrio_badges/tetralympic_masters.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_1.png
|
||||||
- res/tetrio_badges/ttsdtc_2.png
|
- res/tetrio_badges/ttsdtc_2.png
|
||||||
- res/tetrio_badges/ttsdtc_3.png
|
- res/tetrio_badges/ttsdtc_3.png
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
Loading…
Reference in New Issue