teto service now caching data + TL match view

Also added weights constants for nerd stats
This commit is contained in:
dan63047 2023-06-21 22:17:39 +03:00
parent 5d5523ce06
commit ffbe76e5cc
6 changed files with 1006 additions and 87 deletions

View File

@ -5,6 +5,18 @@ import 'dart:developer' as developer;
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'dart:convert'; import 'dart:convert';
const double noTrRd = 60.9;
const double apmWeight = 1;
const double ppsWeight = 45;
const double vsWeight = 0.444;
const double appWeight = 185;
const double dssWeight = 175;
const double dspWeight = 450;
const double appdspWeight = 140;
const double vsapmWeight = 60;
const double cheeseWeight = 1.25;
const double gbeWeight = 315;
Duration doubleSecondsToDuration(double value) { Duration doubleSecondsToDuration(double value) {
value = value * 1000000; value = value * 1000000;
return Duration(microseconds: value.floor()); return Duration(microseconds: value.floor());
@ -578,42 +590,46 @@ class TetraLeagueAlphaRecord{
} }
class EndContextMulti { class EndContextMulti {
String? userId; late String userId;
String? username; late String username;
int? naturalOrder; late int naturalOrder;
int? inputs; late int inputs;
int? piecesPlaced; late int piecesPlaced;
Handling? handling; late Handling handling;
int? points; late int points;
int? wins; late int wins;
double? secondary; late double secondary;
List<double>? secondaryTracking; late List<double> secondaryTracking;
double? tertiary; late double tertiary;
List<double>? tertiaryTracking; late List<double> tertiaryTracking;
double? extra; late double extra;
List<double>? extraTracking; late List<double> extraTracking;
bool? success; late bool success;
late NerdStats nerdStats;
late EstTr estTr;
late Playstyle playstyle;
EndContextMulti( EndContextMulti(
{this.userId, {required this.userId,
this.naturalOrder, required this.username,
this.inputs, required this.naturalOrder,
this.piecesPlaced, required this.inputs,
this.handling, required this.piecesPlaced,
this.points, required this.handling,
this.wins, required this.points,
this.secondary, required this.wins,
this.secondaryTracking, required this.secondary,
this.tertiary, required this.secondaryTracking,
this.tertiaryTracking, required this.tertiary,
this.extra, required this.tertiaryTracking,
this.extraTracking, required this.extra,
this.success}); required this.extraTracking,
required this.success});
EndContextMulti.fromJson(Map<String, dynamic> json) { EndContextMulti.fromJson(Map<String, dynamic> json) {
userId = json['user']['_id']; userId = json['user']['_id'];
username = json['user']['username']; username = json['user']['username'];
handling = json['handling'] != null ? Handling.fromJson(json['handling']) : null; handling = Handling.fromJson(json['handling']);
success = json['success']; success = json['success'];
inputs = json['inputs']; inputs = json['inputs'];
piecesPlaced = json['piecesplaced']; piecesPlaced = json['piecesplaced'];
@ -626,15 +642,16 @@ class EndContextMulti {
tertiaryTracking = json['points']['tertiaryAvgTracking'].cast<double>(); tertiaryTracking = json['points']['tertiaryAvgTracking'].cast<double>();
extra = json['points']['extra']['vs']; extra = json['points']['extra']['vs'];
extraTracking = json['points']['extraAvgTracking']['aggregatestats___vsscore'].cast<double>(); extraTracking = json['points']['extraAvgTracking']['aggregatestats___vsscore'].cast<double>();
nerdStats = NerdStats(secondary, tertiary, extra);
estTr = EstTr(secondary, tertiary, extra, noTrRd, nerdStats.app, nerdStats.dss, nerdStats.dsp, nerdStats.gbe);
playstyle = Playstyle(secondary, tertiary, nerdStats.app, nerdStats.vsapm, nerdStats.dsp, nerdStats.gbe, estTr.srarea, estTr.statrank);
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{}; final Map<String, dynamic> data = <String, dynamic>{};
data['user']['_id'] = userId; data['user']['_id'] = userId;
data['user']['username'] = username; data['user']['username'] = username;
if (handling != null) { data['handling'] = handling.toJson();
data['handling'] = handling!.toJson();
}
data['success'] = success; data['success'] = success;
data['inputs'] = inputs; data['inputs'] = inputs;
data['piecesplaced'] = piecesPlaced; data['piecesplaced'] = piecesPlaced;

View File

@ -29,6 +29,8 @@ const String createTetrioUsersToTrack = '''
class TetrioService extends DB { class TetrioService extends DB {
Map<String, List<TetrioPlayer>> _players = {}; Map<String, List<TetrioPlayer>> _players = {};
final Map<String, TetrioPlayer> _playersCache = {};
final Map<String, TetraLeagueAlphaStream> _tlStreamsCache = {}; // i'm trying to respect oskware api It should look something like {"cached_until": TetrioPlayer}
static final TetrioService _shared = TetrioService._sharedInstance(); static final TetrioService _shared = TetrioService._sharedInstance();
factory TetrioService() => _shared; factory TetrioService() => _shared;
late final StreamController<Map<String, List<TetrioPlayer>>> _tetrioStreamController; late final StreamController<Map<String, List<TetrioPlayer>>> _tetrioStreamController;
@ -41,16 +43,16 @@ class TetrioService extends DB {
@override @override
Future<void> open() async { Future<void> open() async {
await super.open(); await super.open();
await _cachePlayers(); await _loadPlayers();
} }
Stream<Map<String, List<TetrioPlayer>>> get allPlayers => _tetrioStreamController.stream; Stream<Map<String, List<TetrioPlayer>>> get allPlayers => _tetrioStreamController.stream;
Future<void> _cachePlayers() async { Future<void> _loadPlayers() async {
final allPlayers = await getAllPlayers(); final allPlayers = await getAllPlayers();
_players = allPlayers.toList().first; // ??? _players = allPlayers.toList().first; // ???
_tetrioStreamController.add(_players); _tetrioStreamController.add(_players);
developer.log("_cachePlayers: $_players", name: "services/tetrio_crud"); developer.log("_loadPlayers: $_players", name: "services/tetrio_crud");
} }
Future<void> deletePlayer(String id) async { Future<void> deletePlayer(String id) async {
@ -72,6 +74,19 @@ class TetrioService extends DB {
} }
Future<TetraLeagueAlphaStream> getTLStream(String userID) async { Future<TetraLeagueAlphaStream> getTLStream(String userID) async {
try{
var cached = _tlStreamsCache.entries.firstWhere((element) => element.value.userId == userID);
if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){
developer.log("getTLStream: Stream $userID retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud");
return cached.value;
}else{
_tlStreamsCache.remove(cached.key);
developer.log("getTLStream: Cached stream $userID expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud");
}
}catch(e){
developer.log("getTLStream: Trying to retrieve stream $userID", name: "services/tetrio_crud");
}
var url = Uri.https('ch.tetr.io', 'api/streams/league_userrecent_${userID.toLowerCase().trim()}'); var url = Uri.https('ch.tetr.io', 'api/streams/league_userrecent_${userID.toLowerCase().trim()}');
final response = await http.get(url); final response = await http.get(url);
@ -83,13 +98,15 @@ class TetrioService extends DB {
// await ensureDbIsOpen(); // await ensureDbIsOpen();
// storeState(player); // storeState(player);
// } // }
developer.log("getTLStream: $userID stream retrieved and cached", name: "services/tetrio_crud");
_tlStreamsCache[jsonDecode(response.body)['cache']['cached_until'].toString()] = stream;
return stream; return stream;
} else { } else {
developer.log("getTLStream User dosen't exist", name: "services/tetrio_crud", error: response.body); developer.log("getTLStream User dosen't exist", name: "services/tetrio_crud", error: response.body);
throw Exception("User doesn't exist"); throw Exception("User doesn't exist");
} }
} else { } else {
developer.log("getTLStream Failed to fetch player", name: "services/tetrio_crud", error: response.statusCode); developer.log("getTLStream Failed to fetch stream", name: "services/tetrio_crud", error: response.statusCode);
throw Exception('Failed to fetch player'); throw Exception('Failed to fetch player');
} }
} }
@ -208,6 +225,19 @@ class TetrioService extends DB {
} }
Future<TetrioPlayer> fetchPlayer(String user, bool addToDB) async { Future<TetrioPlayer> fetchPlayer(String user, bool addToDB) async {
try{
var cached = _playersCache.entries.firstWhere((element) => element.value.userId == user || element.value.username == user);
if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){
developer.log("fetchPlayer: User $user retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud");
return cached.value;
}else{
_playersCache.remove(cached.key);
developer.log("fetchPlayer: Cached user $user expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud");
}
}catch(e){
developer.log("fetchPlayer: Trying to retrieve $user", name: "services/tetrio_crud");
}
var url = Uri.https('ch.tetr.io', 'api/users/${user.toLowerCase().trim()}'); var url = Uri.https('ch.tetr.io', 'api/users/${user.toLowerCase().trim()}');
final response = await http.get(url); final response = await http.get(url);
@ -219,13 +249,15 @@ class TetrioService extends DB {
await ensureDbIsOpen(); await ensureDbIsOpen();
storeState(player); storeState(player);
} }
developer.log("fetchPlayer: $user retrieved and cached", name: "services/tetrio_crud");
_playersCache[jsonDecode(response.body)['cache']['cached_until'].toString()] = player;
return player; return player;
} else { } else {
developer.log("fetchTetrioPlayer User dosen't exist", name: "services/tetrio_crud", error: response.body); developer.log("fetchPlayer User dosen't exist", name: "services/tetrio_crud", error: response.body);
throw Exception("User doesn't exist"); throw Exception("User doesn't exist");
} }
} else { } else {
developer.log("fetchTetrioPlayer Failed to fetch player", name: "services/tetrio_crud", error: response.statusCode); developer.log("fetchPlayer Failed to fetch player", name: "services/tetrio_crud", error: response.statusCode);
throw Exception('Failed to fetch player'); throw Exception('Failed to fetch player');
} }
} }

View File

@ -547,43 +547,43 @@ class CompareState extends State<CompareView> {
RadarEntry( RadarEntry(
value: theGreenSide! value: theGreenSide!
.tlSeason1.apm! * .tlSeason1.apm! *
1), apmWeight),
RadarEntry( RadarEntry(
value: theGreenSide! value: theGreenSide!
.tlSeason1.pps! * .tlSeason1.pps! *
45), ppsWeight),
RadarEntry( RadarEntry(
value: theGreenSide! value: theGreenSide!
.tlSeason1.vs! * .tlSeason1.vs! *
0.444), vsWeight),
RadarEntry( RadarEntry(
value: theGreenSide!.tlSeason1 value: theGreenSide!.tlSeason1
.nerdStats!.app * .nerdStats!.app *
185), appWeight),
RadarEntry( RadarEntry(
value: theGreenSide!.tlSeason1 value: theGreenSide!.tlSeason1
.nerdStats!.dss * .nerdStats!.dss *
175), dssWeight),
RadarEntry( RadarEntry(
value: theGreenSide!.tlSeason1 value: theGreenSide!.tlSeason1
.nerdStats!.dsp * .nerdStats!.dsp *
450), dspWeight),
RadarEntry( RadarEntry(
value: theGreenSide!.tlSeason1 value: theGreenSide!.tlSeason1
.nerdStats!.appdsp * .nerdStats!.appdsp *
140), appdspWeight),
RadarEntry( RadarEntry(
value: theGreenSide!.tlSeason1 value: theGreenSide!.tlSeason1
.nerdStats!.vsapm * .nerdStats!.vsapm *
60), vsapmWeight),
RadarEntry( RadarEntry(
value: theGreenSide!.tlSeason1 value: theGreenSide!.tlSeason1
.nerdStats!.cheese * .nerdStats!.cheese *
1.25), cheeseWeight),
RadarEntry( RadarEntry(
value: theGreenSide!.tlSeason1 value: theGreenSide!.tlSeason1
.nerdStats!.gbe * .nerdStats!.gbe *
315), gbeWeight),
], ],
), ),
RadarDataSet( RadarDataSet(

View File

@ -1,11 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'dart:developer' as developer;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:tetra_stats/data_objects/tetrio.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/services/crud_exceptions.dart'; import 'package:tetra_stats/services/crud_exceptions.dart';
import 'package:tetra_stats/views/tl_match_view.dart' show TlMatchResultView;
import 'package:tetra_stats/widgets/stat_sell_num.dart'; import 'package:tetra_stats/widgets/stat_sell_num.dart';
import 'package:tetra_stats/widgets/tl_thingy.dart'; import 'package:tetra_stats/widgets/tl_thingy.dart';
import 'package:tetra_stats/widgets/user_thingy.dart'; import 'package:tetra_stats/widgets/user_thingy.dart';
@ -81,7 +81,6 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
_getPreferences() _getPreferences()
.then((value) => changePlayer(prefs.getString("player") ?? "dan63047")); .then((value) => changePlayer(prefs.getString("player") ?? "dan63047"));
super.initState(); super.initState();
developer.log("Main view initialized", name: "main_view");
} }
@override @override
@ -89,7 +88,6 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
_tabController.dispose(); _tabController.dispose();
_scrollController.dispose(); _scrollController.dispose();
super.dispose(); super.dispose();
developer.log("Main view disposed", name: "main_view");
} }
Future<void> _getPreferences() async { Future<void> _getPreferences() async {
@ -155,10 +153,6 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
), ),
PopupMenuButton( PopupMenuButton(
itemBuilder: (BuildContext context) => <PopupMenuEntry>[ itemBuilder: (BuildContext context) => <PopupMenuEntry>[
// const PopupMenuItem(
// value: "/compare",
// child: Text('Compare'),
// ),
const PopupMenuItem( const PopupMenuItem(
value: "/states", value: "/states",
child: Text('Show stored data'), child: Text('Show stored data'),
@ -182,7 +176,6 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
child: FutureBuilder<TetrioPlayer>( child: FutureBuilder<TetrioPlayer>(
future: me, future: me,
builder: (context, snapshot) { builder: (context, snapshot) {
developer.log("builder ($context): $snapshot", name: "main_view");
switch (snapshot.connectionState) { switch (snapshot.connectionState) {
case ConnectionState.none: case ConnectionState.none:
return const Center( return const Center(
@ -225,10 +218,7 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
isScrollable: true, isScrollable: true,
tabs: myTabs, tabs: myTabs,
onTap: (int tabId) { onTap: (int tabId) {
setState(() { setState(() {});
developer.log("Tab changed to $tabId",
name: "main_view");
});
}, },
), ),
), ),
@ -350,8 +340,6 @@ class _NavDrawerState extends State<NavDrawer> {
leading: const Icon(Icons.home), leading: const Icon(Icons.home),
title: Text(homePlayerNickname), title: Text(homePlayerNickname),
onTap: () { onTap: () {
developer.log("Navigator changed player",
name: "main_view");
widget.changePlayer( widget.changePlayer(
prefs.getString("player") ?? "dan63047"); prefs.getString("player") ?? "dan63047");
Navigator.of(context).pop(); Navigator.of(context).pop();
@ -367,8 +355,6 @@ class _NavDrawerState extends State<NavDrawer> {
title: Text( title: Text(
allPlayers[keys[index]]?.last.username as String), allPlayers[keys[index]]?.last.username as String),
onTap: () { onTap: () {
developer.log("Navigator changed player",
name: "main_view");
widget.changePlayer(keys[index]); widget.changePlayer(keys[index]);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
@ -401,10 +387,7 @@ class _TLRecords extends StatelessWidget {
child: CircularProgressIndicator(color: Colors.white)); child: CircularProgressIndicator(color: Colors.white));
case ConnectionState.done: case ConnectionState.done:
if (snapshot.hasError) { if (snapshot.hasError) {
return Text(snapshot.error.toString(), return Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28));
style: TextStyle(
fontFamily: "Eurostile Round Extended",
fontSize: 28));
} else { } else {
return ListView( return ListView(
physics: const ClampingScrollPhysics(), physics: const ClampingScrollPhysics(),
@ -414,7 +397,7 @@ class _TLRecords extends StatelessWidget {
style: const TextStyle( style: const TextStyle(
fontFamily: "Eurostile Round Extended", fontFamily: "Eurostile Round Extended",
fontSize: 28,)), fontSize: 28,)),
title: Text("vs. ${value.endContext.firstWhere((element) => element.userId != userID).username!}"), title: Text("vs. ${value.endContext.firstWhere((element) => element.userId != userID).username}"),
subtitle: Text(dateFormat.format(value.timestamp!)), subtitle: Text(dateFormat.format(value.timestamp!)),
trailing: Column(mainAxisAlignment: MainAxisAlignment.end, trailing: Column(mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
@ -422,14 +405,14 @@ class _TLRecords extends StatelessWidget {
Text("${f2.format(value.endContext.firstWhere((element) => element.userId == userID).tertiary)} : ${f2.format(value.endContext.firstWhere((element) => element.userId != userID).tertiary)} PPS", style: TextStyle(height: 1.1)), Text("${f2.format(value.endContext.firstWhere((element) => element.userId == userID).tertiary)} : ${f2.format(value.endContext.firstWhere((element) => element.userId != userID).tertiary)} PPS", style: TextStyle(height: 1.1)),
Text("${f2.format(value.endContext.firstWhere((element) => element.userId == userID).extra)} : ${f2.format(value.endContext.firstWhere((element) => element.userId != userID).extra)} VS", style: TextStyle(height: 1.1)), Text("${f2.format(value.endContext.firstWhere((element) => element.userId == userID).extra)} : ${f2.format(value.endContext.firstWhere((element) => element.userId != userID).extra)} VS", style: TextStyle(height: 1.1)),
]), ]),
onTap: (){}, onTap: (){Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TlMatchResultView(record: value, initPlayerId: userID),
),
);},
)] )]
: [ : [const Text("No records",style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28))],
Text("No records",
style: TextStyle(
fontFamily: "Eurostile Round Extended",
fontSize: 28))
],
); );
} }
} }

View File

@ -0,0 +1,887 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
final DateFormat dateFormat = DateFormat.yMMMd().add_Hms();
class TlMatchResultView extends StatefulWidget {
final TetraLeagueAlphaRecord record;
final String initPlayerId;
const TlMatchResultView({Key? key, required this.record, required this.initPlayerId})
: super(key: key);
@override
State<StatefulWidget> createState() => TlMatchResultState();
}
class TlMatchResultState extends State<TlMatchResultView> {
late ScrollController _scrollController;
@override
void initState(){
_scrollController = ScrollController();
super.initState();
}
@override
Widget build(BuildContext context) {
bool bigScreen = MediaQuery.of(context).size.width > 768;
return Scaffold(
appBar: AppBar(
title: Text(
"${widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).username.toUpperCase()} vs. ${widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).username.toUpperCase()} in TL match ${dateFormat.format(widget.record.timestamp!)}"),
),
backgroundColor: Colors.black,
body: SafeArea(
child: NestedScrollView(
controller: _scrollController,
headerSliverBuilder: (context, value) {
return [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Colors.green, Colors.transparent],
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
stops: [0.0, 0.4],
)),
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
child: Column(children: [
Text(widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).username, style: const TextStyle(
fontFamily: "Eurostile Round Extended",
fontSize: 28)),
Text(widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).points.toString(), style: const TextStyle(
fontFamily: "Eurostile Round Extended",
fontSize: 42))
]),
),
),
),
const Padding(
padding: EdgeInsets.only(top: 16),
child: Text("VS"),
),
Expanded(
child: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Colors.red, Colors.transparent],
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
stops: [0.0, 0.4],
)),
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
child: Column(children: [
Text(widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).username, style: const TextStyle(
fontFamily: "Eurostile Round Extended",
fontSize: 28)),
Text(widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).points.toString(), style: const TextStyle(
fontFamily: "Eurostile Round Extended",
fontSize: 42))
]),
),
),
),
],
),
),
),
const SliverToBoxAdapter(
child: Divider(),
)
];
},
body: ListView(
children: [
Column(
children: [
CompareThingy(
label: "APM",
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondary,
redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondary,
fractionDigits: 2,
higherIsBetter: true,
),
CompareThingy(
label: "PPS",
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiary,
redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiary,
fractionDigits: 2,
higherIsBetter: true,
),
CompareThingy(
label: "VS",
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extra,
redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extra,
fractionDigits: 2,
higherIsBetter: true,
),
],
),
const Divider(),
Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text("Nerd Stats",
style: TextStyle(
fontFamily: "Eurostile Round Extended",
fontSize: bigScreen ? 42 : 28)),
),
CompareThingy(
label: "APP",
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.app,
redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.app,
fractionDigits: 3,
higherIsBetter: true,
),
CompareThingy(
label: "VS/APM",
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.vsapm,
redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.vsapm,
fractionDigits: 3,
higherIsBetter: true,
),
CompareThingy(
label: "DS/S",
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.dss,
redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.dss,
fractionDigits: 3,
higherIsBetter: true,
),
CompareThingy(
label: "DS/P",
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.dsp,
redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.dsp,
fractionDigits: 3,
higherIsBetter: true,
),
CompareThingy(
label: "APP + DS/P",
greenSide:
widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.appdsp,
redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.appdsp,
fractionDigits: 3,
higherIsBetter: true,
),
CompareThingy(
label: "Cheese",
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.cheese,
redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.cheese,
fractionDigits: 2,
higherIsBetter: true,
),
CompareThingy(
label: "Garbage Eff.",
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.gbe,
redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.gbe,
fractionDigits: 3,
higherIsBetter: true,
),
CompareThingy(
label: "Weighted APP",
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.nyaapp,
redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.nyaapp,
fractionDigits: 3,
higherIsBetter: true,
),
CompareThingy(
label: "Area",
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.area,
redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.area,
fractionDigits: 2,
higherIsBetter: true,
),
CompareThingy(
label: "Est. of TR",
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).estTr.esttr,
redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).estTr.esttr,
fractionDigits: 2,
higherIsBetter: true,
),
Wrap(
direction: Axis.horizontal,
alignment: WrapAlignment.spaceAround,
spacing: 25,
crossAxisAlignment: WrapCrossAlignment.start,
clipBehavior: Clip.hardEdge,
children: [
Padding(
padding:
const EdgeInsets.fromLTRB(20, 20, 20, 20),
child: SizedBox(
height: 300,
width: 300,
child: RadarChart(
RadarChartData(
radarShape: RadarShape.polygon,
tickCount: 4,
ticksTextStyle: const TextStyle(
color: Colors.transparent,
fontSize: 10),
radarBorderData: const BorderSide(
color: Colors.transparent, width: 1),
gridBorderData: const BorderSide(
color: Colors.white24, width: 1),
tickBorderData: const BorderSide(
color: Colors.transparent, width: 1),
getTitle: (index, angle) {
switch (index) {
case 0:
return RadarChartTitle(
text: 'APM',
angle: angle,
);
case 1:
return RadarChartTitle(
text: 'PPS',
angle: angle,
);
case 2:
return RadarChartTitle(
text: 'VS', angle: angle);
case 3:
return RadarChartTitle(
text: 'APP',
angle: angle + 180);
case 4:
return RadarChartTitle(
text: 'DS/S',
angle: angle + 180);
case 5:
return RadarChartTitle(
text: 'DS/P',
angle: angle + 180);
case 6:
return RadarChartTitle(
text: 'APP+DS/P',
angle: angle + 180);
case 7:
return RadarChartTitle(
text: 'VS/APM',
angle: angle + 180);
case 8:
return RadarChartTitle(
text: 'Cheese', angle: angle);
case 9:
return RadarChartTitle(
text: 'Gb Eff.', angle: angle);
default:
return const RadarChartTitle(
text: '');
}
},
dataSets: [
RadarDataSet(
fillColor: const Color.fromARGB(
115, 76, 175, 79),
borderColor: Colors.green,
dataEntries: [
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondary * apmWeight),
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiary * ppsWeight),
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extra * vsWeight),
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.app * appWeight),
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.dss * dssWeight),
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.dsp * dspWeight),
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.appdsp * appdspWeight),
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.vsapm * vsapmWeight),
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.cheese * cheeseWeight),
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.gbe * gbeWeight),
],
),
RadarDataSet(
fillColor: const Color.fromARGB(
115, 244, 67, 54),
borderColor: Colors.red,
dataEntries: [
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondary * apmWeight),
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiary * ppsWeight),
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extra * vsWeight),
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.app * appWeight),
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.dss * dssWeight),
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.dsp * dspWeight),
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.appdsp * appdspWeight),
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.vsapm * vsapmWeight),
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.cheese * cheeseWeight),
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.gbe * gbeWeight),
],
),
RadarDataSet(
fillColor: Colors.transparent,
borderColor: Colors.transparent,
dataEntries: [
const RadarEntry(value: 0),
const RadarEntry(value: 0),
const RadarEntry(value: 0),
const RadarEntry(value: 0),
const RadarEntry(value: 0),
const RadarEntry(value: 0),
const RadarEntry(value: 0),
const RadarEntry(value: 0),
const RadarEntry(value: 0),
const RadarEntry(value: 0),
],
)
],
),
swapAnimationDuration: const Duration(
milliseconds: 150), // Optional
swapAnimationCurve:
Curves.linear, // Optional
),
),
),
Padding(
padding:
const EdgeInsets.fromLTRB(20, 20, 20, 20),
child: SizedBox(
height: 300,
width: 300,
child: RadarChart(
RadarChartData(
radarShape: RadarShape.polygon,
tickCount: 4,
ticksTextStyle: const TextStyle(
color: Colors.transparent,
fontSize: 10),
radarBorderData: const BorderSide(
color: Colors.transparent, width: 1),
gridBorderData: const BorderSide(
color: Colors.white24, width: 1),
tickBorderData: const BorderSide(
color: Colors.transparent, width: 1),
getTitle: (index, angle) {
switch (index) {
case 0:
return RadarChartTitle(
text: 'Opener',
angle: angle,
);
case 1:
return RadarChartTitle(
text: 'Stride',
angle: angle,
);
case 2:
return RadarChartTitle(
text: 'Inf Ds',
angle: angle + 180);
case 3:
return RadarChartTitle(
text: 'Plonk', angle: angle);
default:
return const RadarChartTitle(
text: '');
}
},
dataSets: [
RadarDataSet(
fillColor: const Color.fromARGB(
115, 76, 175, 79),
borderColor: Colors.green,
dataEntries: [
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.opener),
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.stride),
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.infds),
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.plonk),
],
),
RadarDataSet(
fillColor: const Color.fromARGB(
115, 244, 67, 54),
borderColor: Colors.red,
dataEntries: [
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.opener),
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.stride),
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.infds),
RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.plonk),
],
),
RadarDataSet(
fillColor: Colors.transparent,
borderColor: Colors.transparent,
dataEntries: [
const RadarEntry(value: 0),
const RadarEntry(value: 0),
const RadarEntry(value: 0),
const RadarEntry(value: 0),
],
),
RadarDataSet(
fillColor: Colors.transparent,
borderColor: Colors.transparent,
dataEntries: [
const RadarEntry(value: 1),
const RadarEntry(value: 1),
const RadarEntry(value: 1),
const RadarEntry(value: 1),
],
)
],
),
swapAnimationDuration: const Duration(
milliseconds: 150), // Optional
swapAnimationCurve:
Curves.linear, // Optional
),
),
)
],
)
],
)
],
)
),
),
);
}
}
class CompareThingy extends StatelessWidget {
final num greenSide;
final num redSide;
final String label;
final bool higherIsBetter;
final int? fractionDigits;
const CompareThingy(
{super.key,
required this.greenSide,
required this.redSide,
required this.label,
required this.higherIsBetter,
this.fractionDigits});
String verdict(num greenSide, num redSide, int fraction) {
var f = NumberFormat("+#,###.##;-#,###.##");
f.maximumFractionDigits = fraction;
return f.format((greenSide - redSide));
}
@override
Widget build(BuildContext context) {
var f = NumberFormat("#,###.##");
f.maximumFractionDigits = fractionDigits ?? 0;
return Padding(
padding: const EdgeInsets.fromLTRB(16, 2, 16, 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: const [Colors.green, Colors.transparent],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
stops: [
0.0,
higherIsBetter
? greenSide > redSide
? 0.6
: 0
: greenSide < redSide
? 0.6
: 0
],
)),
child: Text(
f.format(greenSide),
style: const TextStyle(
fontSize: 22,
shadows: <Shadow>[
Shadow(
offset: Offset(0.0, 0.0),
blurRadius: 3.0,
color: Colors.black,
),
Shadow(
offset: Offset(0.0, 0.0),
blurRadius: 8.0,
color: Colors.black,
),
],
),
textAlign: TextAlign.start,
),
)),
Column(
children: [
Text(
label,
style: const TextStyle(fontSize: 22),
textAlign: TextAlign.center,
),
Text(
verdict(greenSide, redSide,
fractionDigits != null ? fractionDigits! + 2 : 0),
style: const TextStyle(fontSize: 16),
textAlign: TextAlign.center,
)
],
),
Expanded(
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: const [Colors.red, Colors.transparent],
begin: Alignment.centerRight,
end: Alignment.centerLeft,
stops: [
0.0,
higherIsBetter
? redSide > greenSide
? 0.6
: 0
: redSide < greenSide
? 0.6
: 0
],
)),
child: Text(
f.format(redSide),
style: const TextStyle(
fontSize: 22,
shadows: <Shadow>[
Shadow(
offset: Offset(0.0, 0.0),
blurRadius: 3.0,
color: Colors.black,
),
Shadow(
offset: Offset(0.0, 0.0),
blurRadius: 8.0,
color: Colors.black,
),
],
),
textAlign: TextAlign.end,
),
)),
],
),
);
}
}
class CompareBoolThingy extends StatelessWidget {
final bool greenSide;
final bool redSide;
final String label;
final bool trueIsBetter;
const CompareBoolThingy(
{super.key,
required this.greenSide,
required this.redSide,
required this.label,
required this.trueIsBetter});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 2, 16, 2),
child: Row(children: [
Expanded(
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: const [Colors.green, Colors.transparent],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
stops: [
0.0,
trueIsBetter
? greenSide
? 0.6
: 0
: !greenSide
? 0.6
: 0
],
)),
child: Text(
greenSide ? "Yes" : "No",
style: const TextStyle(
fontSize: 22,
shadows: <Shadow>[
Shadow(
offset: Offset(0.0, 0.0),
blurRadius: 3.0,
color: Colors.black,
),
Shadow(
offset: Offset(0.0, 0.0),
blurRadius: 8.0,
color: Colors.black,
),
],
),
textAlign: TextAlign.start,
),
)),
Column(
children: [
Text(
label,
style: const TextStyle(fontSize: 22),
textAlign: TextAlign.center,
),
const Text(
"---",
style: TextStyle(fontSize: 16),
textAlign: TextAlign.center,
)
],
),
Expanded(
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: const [Colors.red, Colors.transparent],
begin: Alignment.centerRight,
end: Alignment.centerLeft,
stops: [
0.0,
trueIsBetter
? redSide
? 0.6
: 0
: !redSide
? 0.6
: 0
],
)),
child: Text(
redSide ? "Yes" : "No",
style: const TextStyle(
fontSize: 22,
shadows: <Shadow>[
Shadow(
offset: Offset(0.0, 0.0),
blurRadius: 3.0,
color: Colors.black,
),
Shadow(
offset: Offset(0.0, 0.0),
blurRadius: 8.0,
color: Colors.black,
),
],
),
textAlign: TextAlign.end,
),
)),
]),
);
}
}
class CompareDurationThingy extends StatelessWidget {
final Duration greenSide;
final Duration redSide;
final String label;
final bool higherIsBetter;
const CompareDurationThingy(
{super.key,
required this.greenSide,
required this.redSide,
required this.label,
required this.higherIsBetter});
Duration verdict(Duration greenSide, Duration redSide) {
return greenSide - redSide;
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 2, 16, 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Text(
greenSide.toString(),
style: const TextStyle(
fontSize: 22,
),
textAlign: TextAlign.start,
)),
Column(
children: [
Text(
label,
style: const TextStyle(
fontSize: 22,
shadows: <Shadow>[
Shadow(
offset: Offset(0.0, 0.0),
blurRadius: 3.0,
color: Colors.black,
),
Shadow(
offset: Offset(0.0, 0.0),
blurRadius: 8.0,
color: Colors.black,
),
],
),
textAlign: TextAlign.center,
),
Text(
verdict(greenSide, redSide).toString(),
style: const TextStyle(fontSize: 16),
textAlign: TextAlign.center,
)
],
),
Expanded(
child: Text(
redSide.toString(),
style: const TextStyle(fontSize: 22),
textAlign: TextAlign.end,
)),
],
),
);
}
}
class CompareRegTimeThingy extends StatelessWidget {
final DateTime? greenSide;
final DateTime? redSide;
final String label;
final int? fractionDigits;
const CompareRegTimeThingy(
{super.key,
required this.greenSide,
required this.redSide,
required this.label,
this.fractionDigits});
String verdict(DateTime? greenSide, DateTime? redSide) {
var f = NumberFormat("#,### days later;#,### days before");
String result = "---";
if (greenSide != null && redSide != null)
result = f.format(greenSide.difference(redSide).inDays);
return result;
}
@override
Widget build(BuildContext context) {
DateFormat f = DateFormat.yMMMd();
return Padding(
padding: const EdgeInsets.fromLTRB(16, 2, 16, 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: const [Colors.green, Colors.transparent],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
stops: [
0.0,
greenSide == null
? 0.6
: redSide != null && greenSide!.isBefore(redSide!)
? 0.6
: 0
],
)),
child: Text(
greenSide != null ? f.format(greenSide!) : "From beginning",
style: const TextStyle(
fontSize: 22,
shadows: <Shadow>[
Shadow(
offset: Offset(0.0, 0.0),
blurRadius: 3.0,
color: Colors.black,
),
Shadow(
offset: Offset(0.0, 0.0),
blurRadius: 8.0,
color: Colors.black,
),
],
),
textAlign: TextAlign.start,
),
)),
Column(
children: [
Text(
label,
style: const TextStyle(fontSize: 22),
textAlign: TextAlign.center,
),
Text(
verdict(greenSide, redSide),
style: const TextStyle(fontSize: 16),
textAlign: TextAlign.center,
)
],
),
Expanded(
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: const [Colors.red, Colors.transparent],
begin: Alignment.centerRight,
end: Alignment.centerLeft,
stops: [
0.0,
redSide == null
? 0.6
: greenSide != null && redSide!.isBefore(greenSide!)
? 0.6
: 0
],
)),
child: Text(
redSide != null ? f.format(redSide!) : "From beginning",
style: const TextStyle(
fontSize: 22,
shadows: <Shadow>[
Shadow(
offset: Offset(0.0, 0.0),
blurRadius: 3.0,
color: Colors.black,
),
Shadow(
offset: Offset(0.0, 0.0),
blurRadius: 8.0,
color: Colors.black,
),
],
),
textAlign: TextAlign.end,
),
)),
],
),
);
}
}

View File

@ -199,16 +199,16 @@ class TLThingy extends StatelessWidget {
dataSets: [ dataSets: [
RadarDataSet( RadarDataSet(
dataEntries: [ dataEntries: [
RadarEntry(value: tl.apm! * 1), RadarEntry(value: tl.apm! * apmWeight),
RadarEntry(value: tl.pps! * 45), RadarEntry(value: tl.pps! * ppsWeight),
RadarEntry(value: tl.vs! * 0.444), RadarEntry(value: tl.vs! * vsWeight),
RadarEntry(value: tl.nerdStats!.app * 185), RadarEntry(value: tl.nerdStats!.app * appWeight),
RadarEntry(value: tl.nerdStats!.dss * 175), RadarEntry(value: tl.nerdStats!.dss * dssWeight),
RadarEntry(value: tl.nerdStats!.dsp * 450), RadarEntry(value: tl.nerdStats!.dsp * dspWeight),
RadarEntry(value: tl.nerdStats!.appdsp * 140), RadarEntry(value: tl.nerdStats!.appdsp * appdspWeight),
RadarEntry(value: tl.nerdStats!.vsapm * 60), RadarEntry(value: tl.nerdStats!.vsapm * vsWeight),
RadarEntry(value: tl.nerdStats!.cheese * 1.25), RadarEntry(value: tl.nerdStats!.cheese * cheeseWeight),
RadarEntry(value: tl.nerdStats!.gbe * 315), RadarEntry(value: tl.nerdStats!.gbe * gbeWeight),
], ],
), ),
RadarDataSet( RadarDataSet(