New p1nkl0bst3r's API endpoint + TL progress bar design

For some reason TL progress bar behaves differently on android
This commit is contained in:
dan63047 2024-05-03 01:26:12 +03:00
parent 6195705dcb
commit 9aa67686da
6 changed files with 127 additions and 35 deletions

View File

@ -756,16 +756,15 @@ class EstTr {
srarea = (_apm * 0) + (_pps * 135) + (_vs * 0) + (_app * 290) + (_dss * 0) + (_dsp * 700) + (_gbe * 0);
statrank = 11.2 * atan((srarea - 93) / 130) + 1;
if (statrank <= 0) statrank = 0.001;
estglicko = (4.0867 * srarea + 186.68);
//estglicko = (4.0867 * srarea + 186.68);
double ntemp = _pps*(150+(((_vs/_apm) - 1.66)*35))+_app*290+_dsp*700;
estglicko = 0.000013*pow(ntemp, 3) - 0.0196 *pow(ntemp, 2) + (12.645*ntemp)-1005.4;
esttr = 25000 /
(
1 + pow(10, (
(
(
1500-(
0.000013*pow(ntemp, 3) - 0.0196 *pow(ntemp, 2) + (12.645*ntemp)-1005.4
)
1500-estglicko
)*pi
)/sqrt(
(

View File

@ -77,6 +77,7 @@ class TetrioService extends DB {
final Map<String, PlayerLeaderboardPosition> _lbPositions = {};
final Map<String, List<News>> _newsCache = {};
final Map<String, Map<String, double?>> _topTRcache = {};
final Map<String, List<Map<String, double>>> _cutoffsCache = {};
final Map<String, TetraLeagueAlphaStream> _tlStreamsCache = {};
/// Thing, that sends every request to the API endpoints
final client = kDebugMode ? UserAgentClient("Kagari-chan loves osk (Tetra Stats dev build)", http.Client()) : UserAgentClient("Tetra Stats v${packageInfo.version} (dm @dan63047 if someone abuse that software)", http.Client());
@ -297,6 +298,67 @@ class TetrioService extends DB {
// Sidenote: as you can see, fetch functions looks and works pretty much same way, as described above,
// so i'm going to document only unique differences between them
Future<List<Map<String, double>>> fetchCutoffs() async {
try{
var cached = _cutoffsCache.entries.first;
if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){ // if not expired
developer.log("fetchCutoffs: Cutoffs retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud");
return cached.value;
}else{ // if cache expired
_topTRcache.remove(cached.key);
developer.log("fetchCutoffs: Cutoffs expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud");
}
}catch(e){ // actually going to obtain
developer.log("fetchCutoffs: Trying to retrieve Cutoffs", name: "services/tetrio_crud");
}
Uri url;
if (kIsWeb) {
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "PeakTR"});
} else {
url = Uri.https('api.p1nkl0bst3r.xyz', 'rankcutoff', {"users": null});
}
try{
final response = await client.get(url);
switch (response.statusCode) {
case 200:
Map<String, dynamic> rawData = jsonDecode(response.body);
Map<String, dynamic> data = rawData["cutoffs"] as Map<String, dynamic>;
Map<String, double> trCutoffs = {};
Map<String, double> glickoCutoffs = {};
for (String rank in data.keys){
trCutoffs[rank] = data[rank]["rating"];
glickoCutoffs[rank] = data[rank]["glicko"];
}
_cutoffsCache[(rawData["ts"] + 300000).toString()] = [trCutoffs, glickoCutoffs];
return [trCutoffs, glickoCutoffs];
case 404:
developer.log("fetchCutoffs: Cutoffs are gone", name: "services/tetrio_crud", error: response.statusCode);
return [];
// if not 200 or 404 - throw a unique for each code exception
case 403:
throw P1nkl0bst3rForbidden();
case 429:
throw P1nkl0bst3rTooManyRequests();
case 418:
throw TetrioOskwareBridgeProblem();
case 500:
case 502:
case 503:
case 504:
throw P1nkl0bst3rInternalProblem();
default:
developer.log("fetchCutoffs: Failed to fetch top Cutoffs", name: "services/tetrio_crud", error: response.statusCode);
throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason");
}
} on http.ClientException catch (e, s) { // If local http client fails
developer.log("$e, $s");
throw http.ClientException(e.message, e.uri); // just assuming, that our end user don't have acess to the internet
}
}
/// Retrieves Tetra League history from p1nkl0bst3r api for a player with given [id]. Returns a list of states
/// (state = instance of [TetrioPlayer] at some point of time). Can throw an exception if fails to retrieve data.
Future<List<TetrioPlayer>> fetchAndsaveTLHistory(String id) async {

View File

@ -2,6 +2,7 @@ import 'package:intl/intl.dart';
import 'package:tetra_stats/gen/strings.g.dart';
final NumberFormat comparef = NumberFormat("+#,###.###;-#,###.###")..maximumFractionDigits = 3;
final NumberFormat comparef2 = NumberFormat("+#,###.##;-#,###.##")..maximumFractionDigits = 2;
final NumberFormat intf = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0);
final NumberFormat f4 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 4);
final NumberFormat f3 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 3);

View File

@ -182,12 +182,13 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
teto.fetchTLStream(_searchFor),
teto.fetchRecords(_searchFor),
teto.fetchNews(_searchFor),
prefs.getBool("showPositions") != true ? teto.fetchCutoffs() : Future.delayed(Duration.zero, ()=>[]),
if (me.tlSeason1.gamesPlayed > 9) teto.fetchTopTR(_searchFor) // can retrieve this only if player has TR
]);
tlStream = requests[0] as TetraLeagueAlphaStream;
records = requests[1] as Map<String, dynamic>;
news = requests[2] as List<News>;
topTR = requests.elementAtOrNull(3) as double?; // No TR - no Top TR
topTR = requests.elementAtOrNull(4) as double?; // No TR - no Top TR
meAmongEveryone = teto.getCachedLeaderboardPositions(me.userId);
if (prefs.getBool("showPositions") == true){
@ -198,15 +199,17 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
meAmongEveryone = await compute(everyone!.getLeaderboardPosition, me);
if (meAmongEveryone != null) teto.cacheLeaderboardPositions(me.userId, meAmongEveryone!);
}
}
Map<String, double> cutoffs = prefs.getBool("showPositions") == true ? everyone!.cutoffs : requests[3][0];
Map<String, double> cutoffsGlicko = prefs.getBool("showPositions") == true ? everyone!.cutoffsGlicko : requests[3][1];
if (me.tlSeason1.gamesPlayed > 9) {
thatRankCutoff = everyone!.cutoffs[me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank];
thatRankGlickoCutoff = everyone!.cutoffsGlicko[me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank];
nextRankCutoff = everyone!.cutoffs[ranks.elementAtOrNull(ranks.indexOf(me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank)+1)];
nextRankGlickoCutoff = everyone!.cutoffsGlicko[ranks.elementAtOrNull(ranks.indexOf(me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank)+1)];
thatRankCutoff = cutoffs[me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank];
thatRankGlickoCutoff = cutoffsGlicko[me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank];
nextRankCutoff = cutoffs[ranks.elementAtOrNull(ranks.indexOf(me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank)+1)];
nextRankGlickoCutoff = cutoffsGlicko[ranks.elementAtOrNull(ranks.indexOf(me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank)+1)];
nextRankCutoff = nextRankCutoff??25000;
nextRankGlickoCutoff = nextRankGlickoCutoff??double.infinity;
}
}
if (everyone != null && me.tlSeason1.gamesPlayed > 9) rankAverages = everyone?.averages[me.tlSeason1.percentileRank]?[0];

View File

@ -33,10 +33,49 @@ class TLProgress extends StatelessWidget{
Widget build(BuildContext context) {
final glickoForWin = rate(tlData.glicko!, tlData.rd!, 0.06, [[tlData.glicko!, tlData.rd!, 1]], {})[0]-tlData.glicko!;
return Padding(
padding: const EdgeInsets.all(8.0),
padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: MediaQuery.of(context).size.width,
height: 48,
child: Stack(
alignment: AlignmentDirectional.bottomCenter,
fit: StackFit.expand,
children: [
Positioned(left: 0,
child: RichText(
textAlign: TextAlign.left,
text: TextSpan(
style: TextStyle(color: Colors.white, fontFamily: "Eurostile Round", fontSize: 12),
children: [
if (tlData.prevAt > 0) TextSpan(text: "${f0.format(tlData.prevAt)}"),
if (tlData.prevAt > 0 && previousRankTRcutoff != null) TextSpan(text: "\n"),
if (previousRankTRcutoff != null) TextSpan(text: "${intf.format(previousRankTRcutoff)} (${comparef2.format(previousRankTRcutoff!-tlData.rating)}) TR"),
if ((tlData.prevAt > 0 || previousRankTRcutoff != null) && previousGlickoCutoff != null) TextSpan(text: "\n"),
if (previousGlickoCutoff != null) TextSpan(text: "~${f2.format((tlData.glicko!-previousGlickoCutoff!)/glickoForWin)} defeats")
]
)
),
),
Positioned(right: 0,
child: RichText(
textAlign: TextAlign.right,
text: TextSpan(
style: TextStyle(color: Colors.white, fontFamily: "Eurostile Round", fontSize: 12),
children: [
if (tlData.nextAt > 0) TextSpan(text: "${f0.format(tlData.nextAt)}"),
if (tlData.nextAt > 0 && nextRankTRcutoff != null) TextSpan(text: "\n"),
if (nextRankTRcutoff != null) TextSpan(text: "${intf.format(nextRankTRcutoff)} (${comparef2.format(nextRankTRcutoff!-tlData.rating)}) TR"),
if ((tlData.nextAt > 0 || nextRankTRcutoff != null) && nextRankGlickoCutoff != null) TextSpan(text: "\n"),
if (nextRankGlickoCutoff != null) TextSpan(text: "~${f2.format((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin)} victories")
]
)
),
)
],),
),
SfLinearGauge(
minimum: 0,
maximum: 1,
@ -49,34 +88,22 @@ class TLProgress extends StatelessWidget{
],
markerPointers: [
LinearShapePointer(value: (previousRankTRcutoff != null && nextRankTRcutoff != null) ? getBarTR(tlData.rating)! : getBarPosition(), position: LinearElementPosition.cross, shapeType: LinearShapePointerType.diamond, color: Colors.white, height: 20),
LinearWidgetPointer(offset: 4, position: LinearElementPosition.outside, value: (previousRankTRcutoff != null && nextRankTRcutoff != null) ? getBarTR(tlData.rating)! : getBarPosition(), child: Text("${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(tlData.standing)}"))
if (tlData.standing != -1) LinearWidgetPointer(offset: 4, position: LinearElementPosition.outside, value: (previousRankTRcutoff != null && nextRankTRcutoff != null) ? getBarTR(tlData.rating)! : getBarPosition(), child: Text("${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(tlData.standing)}", style: TextStyle(fontSize: 14),))
],
isMirrored: true,
showTicks: true,
axisLabelStyle: TextStyle(),
onGenerateLabels: () => [
LinearAxisLabel(text: "${tlData.prevAt > 0 ? "${f0.format(tlData.prevAt)}" : ""}\n ${intf.format(previousRankTRcutoff)} TR", value: 0),
LinearAxisLabel(text: "${tlData.nextAt > 0 ? "${f0.format(tlData.nextAt)}" : ""}\n ${intf.format(nextRankTRcutoff)} TR", value: 1),
],
// onGenerateLabels: () => [
// LinearAxisLabel(text: "${tlData.prevAt > 0 ? "${f0.format(tlData.prevAt)}" : ""}\n ${intf.format(previousRankTRcutoff)} TR", value: 0),
// LinearAxisLabel(text: "${tlData.nextAt > 0 ? "${f0.format(tlData.nextAt)}" : ""}\n ${intf.format(nextRankTRcutoff)} TR", value: 1),
// ],
// labelFormatterCallback: (value) {
// if (value == "0") return "${f0.format(previousRankPosition)}\n 26,700 TR";
// else return f0.format(nextRankPosition);
// },
showLabels: true
showLabels: false
)
]
),
Container(
width: MediaQuery.of(context).size.width,
height: 20,
child: Stack(
fit: StackFit.expand,
children: [
Positioned(child: Text("${f2.format(tlData.rating-previousRankTRcutoff!)} (${f2.format((tlData.glicko!-previousGlickoCutoff!)/glickoForWin)} losses)"), left: 0,),
Positioned(child: Text("${f2.format(nextRankTRcutoff!-tlData.rating)} (${f2.format((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin)} wins)"), right: 0,)
],),
)
],
)
);
}
}

View File

@ -31,7 +31,7 @@ class TLThingy extends StatefulWidget {
final double? nextRankCutoff;
final double? nextRankCutoffGlicko;
final double? nextRankTarget;
const TLThingy({super.key, required this.tl, required this.userID, required this.states, this.showTitle = true, this.bot=false, this.guest=false, this.topTR, this.lbPositions, this.averages, this.nextRankCutoff = 25000, this.thatRankCutoff = 0, this.thatRankCutoffGlicko = 0, this.nextRankCutoffGlicko = double.infinity, this.nextRankTarget = 25000, this.thatRankTarget = 0});
const TLThingy({super.key, required this.tl, required this.userID, required this.states, this.showTitle = true, this.bot=false, this.guest=false, this.topTR, this.lbPositions, this.averages, this.nextRankCutoff, this.thatRankCutoff, this.thatRankCutoffGlicko, this.nextRankCutoffGlicko, this.nextRankTarget, this.thatRankTarget});
@override
State<TLThingy> createState() => _TLThingyState();
@ -342,9 +342,9 @@ class _TLThingyState extends State<TLThingy> {
if (oldTl?.estTr?.esttr != null) TextSpan(text: comparef.format(currentTl.estTr!.esttr - oldTl!.estTr!.esttr), style: TextStyle(
color: oldTl!.estTr!.esttr > currentTl.estTr!.esttr ? Colors.redAccent : Colors.greenAccent
),),
if (oldTl?.estTr?.esttr != null || widget.lbPositions?.estTr != null) const TextSpan(text: ""),
if (oldTl?.estTr?.esttr != null) const TextSpan(text: ""),
if (widget.lbPositions?.estTr != null) TextSpan(text: widget.lbPositions!.estTr!.position >= 1000 ? "${t.top} ${f2.format(widget.lbPositions!.estTr!.percentage*100)}%" : "${widget.lbPositions!.estTr!.position}", style: TextStyle(color: getColorOfRank(widget.lbPositions!.estTr!.position))),
if (widget.lbPositions?.estTr != null) const TextSpan(text: ""),
if (widget.lbPositions?.estTr != null || oldTl?.estTr?.esttr != null) const TextSpan(text: ""),
TextSpan(text: "Glicko: ${f2.format(currentTl.estTr!.estglicko)}")
]
),