From 9feae496670235701c3966467ce6ea6941c13463 Mon Sep 17 00:00:00 2001 From: Jennifer Taylor Date: Sat, 8 Oct 2022 01:32:59 +0000 Subject: [PATCH] Massive overhaul to Jubeat frontend, including the following: - Clan jubility breakdown - Displaying music rate and stats identically across all score pages - Ability to choose what jubility (legacy or new) to display and sort on all players. - Various tweaks and small quality of life improvements. --- bemani/frontend/jubeat/jubeat.py | 55 +++- .../static/components/slider.react.js | 27 ++ .../controllers/jubeat/allplayers.react.js | 49 +++- .../controllers/jubeat/jubility.react.js | 95 +++++- .../static/controllers/jubeat/player.react.js | 44 ++- .../controllers/jubeat/records.react.js | 273 +++++++++--------- .../static/controllers/jubeat/scores.react.js | 26 +- .../controllers/jubeat/topscores.react.js | 49 ++-- bemani/frontend/static/link.js | 6 +- .../frontend/static/themes/dark/section.css | 7 - bemani/frontend/static/themes/dark/site.css | 45 +++ .../static/themes/default/section.css | 7 - .../frontend/static/themes/default/site.css | 45 +++ 13 files changed, 503 insertions(+), 225 deletions(-) create mode 100644 bemani/frontend/static/components/slider.react.js diff --git a/bemani/frontend/jubeat/jubeat.py b/bemani/frontend/jubeat/jubeat.py index 2762d3f..22e1f2f 100644 --- a/bemani/frontend/jubeat/jubeat.py +++ b/bemani/frontend/jubeat/jubeat.py @@ -146,13 +146,54 @@ class JubeatFrontend(FrontendBase): formatted_profile = super().format_profile(profile, playstats) formatted_profile['plays'] = playstats.get_int('total_plays') formatted_profile['emblem'] = self.format_emblem(profile.get_dict('last').get_int_array('emblem', 5)) - formatted_profile['jubility'] = profile.get_int('jubility') - formatted_profile['pick_up_jubility'] = profile.get_float('pick_up_jubility') - # Only reason this is a dictionary of dictionaries is because ValidatedDict doesn't support a list of dictionaries. - # Probably intentionally lol. Just listify the pickup/common charts. - formatted_profile['pick_up_chart'] = list(profile.get_dict('pick_up_chart').values()) - formatted_profile['common_jubility'] = profile.get_float('common_jubility') - formatted_profile['common_chart'] = list(profile.get_dict('common_chart').values()) + formatted_profile['jubility'] = ( + profile.get_int('jubility') + if profile.version not in {VersionConstants.JUBEAT_PROP, VersionConstants.JUBEAT_QUBELL, VersionConstants.JUBEAT_FESTO} + else 0 + ) + formatted_profile['pick_up_jubility'] = ( + profile.get_float('pick_up_jubility') + if profile.version == VersionConstants.JUBEAT_FESTO + else 0 + ) + formatted_profile['common_jubility'] = ( + profile.get_float('common_jubility') + if profile.version == VersionConstants.JUBEAT_FESTO + else 0 + ) + if profile.version == VersionConstants.JUBEAT_FESTO: + # Only reason this is a dictionary of dictionaries is because ValidatedDict doesn't support a list of dictionaries. + # Probably intentionally lol. Just listify the pickup/common charts. + formatted_profile['pick_up_chart'] = list(profile.get_dict('pick_up_chart').values()) + formatted_profile['common_chart'] = list(profile.get_dict('common_chart').values()) + elif profile.version == VersionConstants.JUBEAT_CLAN: + # Look up achievements which is where jubility was stored. This is a bit of a hack + # due to the fact that this could be formatting remote profiles, but then they should + # have no achievements. + userid = self.data.local.user.from_refid(profile.game, profile.version, profile.refid) + if userid is not None: + achievements = self.data.local.user.get_achievements(profile.game, profile.version, userid) + else: + achievements = [] + + jubeat_entries: List[ValidatedDict] = [] + for achievement in achievements: + if achievement.type != 'jubility': + continue + + # Figure out for each song, what's the highest value jubility and + # keep that. + bestentry = ValidatedDict() + for chart in [0, 1, 2]: + entry = achievement.data.get_dict(str(chart)) + if entry.get_int("value") >= bestentry.get_int("value"): + bestentry = entry.clone() + bestentry.replace_int("songid", achievement.id) + bestentry.replace_int("chart", chart) + jubeat_entries.append(bestentry) + jubeat_entries = sorted(jubeat_entries, key=lambda entry: entry.get_int("value"), reverse=True)[:30] + formatted_profile['chart'] = jubeat_entries + formatted_profile['ex_count'] = profile.get_int('ex_cnt') formatted_profile['fc_count'] = profile.get_int('fc_cnt') return formatted_profile diff --git a/bemani/frontend/static/components/slider.react.js b/bemani/frontend/static/components/slider.react.js new file mode 100644 index 0000000..053901d --- /dev/null +++ b/bemani/frontend/static/components/slider.react.js @@ -0,0 +1,27 @@ +/** @jsx React.DOM */ + +var Slider = React.createClass({ + render: function() { + return ( +
{ this.props.value ? + + + {this.props.on} + : + + {this.props.off} + + + }
+ ); + }, +}); diff --git a/bemani/frontend/static/controllers/jubeat/allplayers.react.js b/bemani/frontend/static/controllers/jubeat/allplayers.react.js index 79217a5..f120f87 100644 --- a/bemani/frontend/static/controllers/jubeat/allplayers.react.js +++ b/bemani/frontend/static/controllers/jubeat/allplayers.react.js @@ -5,6 +5,7 @@ var all_players = React.createClass({ getInitialState: function(props) { return { players: window.players, + jubility: true, }; }, @@ -57,7 +58,7 @@ var all_players = React.createClass({ }.bind(this), }, { - name: 'Play Count', + name: 'Total Rounds', render: function(userid) { var player = this.state.players[userid]; return player.plays; @@ -70,28 +71,50 @@ var all_players = React.createClass({ reverse: true, }, { - name: 'Jubility', + name: + Jubility + + + , render: function(userid) { var player = this.state.players[userid]; - if (player.common_jubility != 0 || player.pick_up_jubility != 0) { + if (this.state.jubility && (player.common_jubility != 0 || player.pick_up_jubility != 0)) { return (player.common_jubility + player.pick_up_jubility).toFixed(1); - } else if (player.jubility != 0) { + } else if (!this.state.jubility && player.jubility != 0) { return player.jubility / 100 } else { - return 0 + return ""; } }.bind(this), sort: function(aid, bid) { var a = this.state.players[aid]; var b = this.state.players[bid]; - if (a.common_jubility != 0 || a.pick_up_jubility != 0) - var ajub = a.common_jubility+a.pick_up_jubility; - else - var ajub = a.jubility / 100; - if (b.common_jubility != 0 || b.pick_up_jubility != 0) - var bjub = b.common_jubility+b.pick_up_jubility; - else - var bjub = b.jubility / 100; + if (this.state.jubility) { + if (a.common_jubility != 0 || a.pick_up_jubility != 0) + var ajub = a.common_jubility+a.pick_up_jubility; + else + var ajub = 0; + if (b.common_jubility != 0 || b.pick_up_jubility != 0) + var bjub = b.common_jubility+b.pick_up_jubility; + else + var bjub = 0; + } else { + if (a.jubility != 0) + var ajub = a.jubility / 100; + else + var ajub = 0; + if (b.jubility != 0) + var bjub = b.jubility / 100; + else + var bjub = 0; + } return ajub-bjub; }.bind(this), reverse: true, diff --git a/bemani/frontend/static/controllers/jubeat/jubility.react.js b/bemani/frontend/static/controllers/jubeat/jubility.react.js index eac3ba7..b9fbd8b 100644 --- a/bemani/frontend/static/controllers/jubeat/jubility.react.js +++ b/bemani/frontend/static/controllers/jubeat/jubility.react.js @@ -37,18 +37,87 @@ var jubility_view = React.createClass({ ); }, + convertChart: function(chart) { + switch(chart) { + case 0: + return 'Basic'; + case 1: + return 'Advanced'; + case 2: + return 'Extreme'; + case 3: + return 'Hard Mode Basic'; + case 4: + return 'Hard Mode Advanced'; + case 5: + return 'Hard Mode Extreme'; + default: + return 'u broke it'; + } + }, + renderJubilityBreakdown: function(player) { - return ( -
- {this.renderJubilityTable(player, true)} - {this.renderJubilityTable(player, false)} + if (this.state.version == 13) // festo + return ( +
+ {this.renderFestoJubilityTable(player, true)} + {this.renderFestoJubilityTable(player, false)} +
+ ); + if (this.state.version == 12) // clan + return ( +
+ {this.renderClanJubilityTable(player)} +
+ ); + + return null; + }, + + renderClanJubilityTable: function(player) { + if (typeof player.chart === 'undefined' || player.chart.length == 0) { + return null; + } + return( +
+

+ Chart breakdown +

+

Individual song jubility gets averaged to calculate player total jubility.

+ +
{ this.state.songs[entry.songid].name }
+ + ); + }.bind(this), + }, + { + name: 'Hard Mode', + render: function(entry) { return entry.hard_mode ? 'Yes' : 'No'; } + }, + { + name: 'Jubility', + render: function(entry) { return (entry.value / 100.0).toFixed(2); }, + sort: function(a, b) { + return a.value - b.value; + }, + reverse: true, + }, + ]} + defaultsort='Jubility' + rows={player.chart} + /> ); }, - renderJubilityTable: function(player, pickup) { - if (this.state.version != 13) // festo - return null; + renderFestoJubilityTable: function(player, pickup) { if (pickup == true) jubilityChart = player.pick_up_chart; else @@ -63,6 +132,7 @@ var jubility_view = React.createClass({ {pickup == true ? Pick up chart breakdown : Common chart breakdown}

+

Individual song jubility gets added to calculate total jubility.

+
{ this.state.songs[entry.music_id].name }
); @@ -149,9 +219,6 @@ var jubility_view = React.createClass({ render: function() { if (this.state.player[this.state.version]) { var player = this.state.player[this.state.version]; - var filteredVersion = Object.values(this.state.profiles).map(function(version) { - return Object.values(window.versions)[version-1] - }); var item = Object.keys(window.versions).map(function(k){ return window.versions[k] }) @@ -160,7 +227,7 @@ var jubility_view = React.createClass({

- ← Back To Profile + ← Back To Profile

@@ -168,6 +235,10 @@ var jubility_view = React.createClass({

{player.name}'s jubility

{this.state.profiles.map(function(version) { + if (version < 12) { + // No breakdown here, no point in displaying. + return null; + } return (

- + + @@ -456,123 +469,125 @@ var network_records = React.createClass({ renderBySongIDList: function(songids, showplays) { return ( -
{ this.state.versions[songid] }
{ + !paginate ? this.state.versions[songid] : "Song / Artist / Difficulties" + } Basic Advanced Extreme
- - - - - - - - - - - - - {songids.map(function(songid, index) { - if (index < this.state.offset || index >= this.state.offset + this.state.limit) { - return null; - } - - var records = this.state.records[songid]; - if (!records) { - records = {}; - } - - var plays = this.getPlays(records); - var difficulties = this.state.songs[songid].difficulties; - return ( - - - - - - - - - - ); - }.bind(this))} - - - - + + + + + + + + + ); + }.bind(this))} + + + + + + +
SongBasicAdvancedExtremeHard Mode BasicHard Mode AdvancedHard Mode Extreme
- -
- {this.renderDifficulty(songid, 0)} - / - {this.renderDifficulty(songid, 1)} - / - {this.renderDifficulty(songid, 2)} -
- { showplays ?
#{index + 1} - {plays}{plays == 1 ? ' play' : ' plays'}
: null } -
0 ? "" : "nochart"}> - - 0 ? "" : "nochart"}> - - 0 ? "" : "nochart"}> - - 0 ? "" : "nochart"}> - - 0 ? "" : "nochart"}> - - 0 ? "" : "nochart"}> - -
- { this.state.offset > 0 ? - : null +
+ + + + + + + + + + + + + + {songids.map(function(songid, index) { + if (index < this.state.offset || index >= this.state.offset + this.state.limit) { + return null; } - { (this.state.offset + this.state.limit) < songids.length ? - = songids.length) { return } - this.setState({offset: page}); - }.bind(this)}/> : - null + + var records = this.state.records[songid]; + if (!records) { + records = {}; } - - - -
Song / Artist / DifficultiesBasicAdvancedExtremeHard Mode BasicHard Mode AdvancedHard Mode Extreme
+ + var plays = this.getPlays(records); + var difficulties = this.state.songs[songid].difficulties; + return ( +
+ +
+ {this.renderDifficulty(songid, 0)} + / + {this.renderDifficulty(songid, 1)} + / + {this.renderDifficulty(songid, 2)} +
+ { showplays ?
#{index + 1} - {plays}{plays == 1 ? ' play' : ' plays'}
: null } +
0 ? "" : "nochart"}> + + 0 ? "" : "nochart"}> + + 0 ? "" : "nochart"}> + + 0 ? "" : "nochart"}> + + 0 ? "" : "nochart"}> + + 0 ? "" : "nochart"}> + +
+ { this.state.offset > 0 ? + : null + } + { (this.state.offset + this.state.limit) < songids.length ? + = songids.length) { return } + this.setState({offset: page}); + }.bind(this)}/> : + null + } +
+
); }, diff --git a/bemani/frontend/static/controllers/jubeat/scores.react.js b/bemani/frontend/static/controllers/jubeat/scores.react.js index c430dc5..5fa7bfd 100644 --- a/bemani/frontend/static/controllers/jubeat/scores.react.js +++ b/bemani/frontend/static/controllers/jubeat/scores.react.js @@ -52,6 +52,13 @@ var network_scores = React.createClass({ }, renderScore: function(score) { + has_stats = ( + score.stats.perfect > 0 || + score.stats.great > 0 || + score.stats.good > 0 || + score.stats.poor > 0 || + score.stats.miss > 0 + ); return (
@@ -59,14 +66,12 @@ var network_scores = React.createClass({ {score.points} Combo {score.combo < 0 ? '-' : score.combo} - Music Rate - {score.music_rate < 0 ? '-' : score.music_rate} + {score.music_rate > 0 ? + Music Rate + {score.music_rate <= 0 ? '-' : score.music_rate}% + : null}
-
- {score.status} -
- Stats: -
+ {has_stats ?
{score.stats.perfect} / {score.stats.great} @@ -76,6 +81,9 @@ var network_scores = React.createClass({ {score.stats.poor} / {score.stats.miss} +
: null} +
+ {score.status}
); @@ -89,8 +97,8 @@ var network_scores = React.createClass({ { window.shownames ? Name : null } Timestamp - Song - Chart + Song / Artist + Difficulty Score diff --git a/bemani/frontend/static/controllers/jubeat/topscores.react.js b/bemani/frontend/static/controllers/jubeat/topscores.react.js index 9f3f84a..b9fd95e 100644 --- a/bemani/frontend/static/controllers/jubeat/topscores.react.js +++ b/bemani/frontend/static/controllers/jubeat/topscores.react.js @@ -122,27 +122,42 @@ var top_scores = React.createClass({ }, { name: 'Combo', - render: function(topscore) { return topscore.combo > 0 ? topscore.combo : '-'; }, + render: function(topscore) { return topscore.combo > 0 ? topscore.combo : ''; }, + sort: function(a, b) { + return a.combo - b.combo; + }, + reverse: true, }, { - name: 'Perfect', - render: function(topscore) { return topscore.stats.perfect } + name: 'Music Rate', + render: function(topscore) { return topscore.music_rate > 0 ? topscore.music_rate + "%" : ''; }, + sort: function(a, b) { + return a.music_rate - b.music_rate; + }, + reverse: true, }, { - name: 'Great', - render: function(topscore) { return topscore.stats.great } - }, - { - name: 'Good', - render: function(topscore) { return topscore.stats.good } - }, - { - name: 'Poor', - render: function(topscore) { return topscore.stats.poor } - }, - { - name: 'Miss', - render: function(topscore) { return topscore.stats.miss } + name: 'Judgement Stats', + render: function(topscore) { + has_stats = ( + topscore.stats.perfect > 0 || + topscore.stats.great > 0 || + topscore.stats.good > 0 || + topscore.stats.poor > 0 || + topscore.stats.miss > 0 + ); + return has_stats ?
+ {topscore.stats.perfect} + / + {topscore.stats.great} + / + {topscore.stats.good} + / + {topscore.stats.poor} + / + {topscore.stats.miss} +
: null; + } }, ]} defaultsort='Score' diff --git a/bemani/frontend/static/link.js b/bemani/frontend/static/link.js index 1ac7e4f..29f8dc8 100644 --- a/bemani/frontend/static/link.js +++ b/bemani/frontend/static/link.js @@ -5,7 +5,11 @@ var Link = { get: function(name, param, anchor) { var uri = window.uris[name]; if (!param || !uri) { - return uri; + if (!anchor) { + return uri; + } else { + return uri + '#' + anchor.toString(); + } } else if (!anchor) { return uri.replace("/-1", "/" + param.toString()); } else { diff --git a/bemani/frontend/static/themes/dark/section.css b/bemani/frontend/static/themes/dark/section.css index 9b7b55c..853aaa5 100644 --- a/bemani/frontend/static/themes/dark/section.css +++ b/bemani/frontend/static/themes/dark/section.css @@ -54,13 +54,6 @@ div.labelledsection.filled { border: 1px dashed #dddddd; } -span.separator { - font-size: large; - font-weight: bold; - padding-left: 5px; - padding-right: 5px; -} - div.labelledsection.iidx.themeoption select { width: 200px; } diff --git a/bemani/frontend/static/themes/dark/site.css b/bemani/frontend/static/themes/dark/site.css index cd8d1cb..06a155d 100644 --- a/bemani/frontend/static/themes/dark/site.css +++ b/bemani/frontend/static/themes/dark/site.css @@ -273,3 +273,48 @@ span.checkbox { padding-right: 5px; cursor: default; } + +div.slider { + display: inline-block; + border-radius: 10px; +} + +div.slider.on { + background: #2196F3; + border: 1px solid #0a6fc2; +} + +div.slider.off { + background: #cccccc; + border: 1px solid #a0a0a0; +} + +div.slider span.label { + width: 70px; + font-weight: normal; + font-size: small; + height: 18px; + display: inline-block; +} + +div.slider span.label.on { + text-align: center; +} + +div.slider span.label.off { + text-align: center; +} + +div.slider span.ball { + position: absolute; + display: inline-block; + border-radius: 8px; + background: white; + width: 14px; + height: 14px; + margin: 2px; +} + +div.slider span.ball.off { + margin-left: -16px; +} diff --git a/bemani/frontend/static/themes/default/section.css b/bemani/frontend/static/themes/default/section.css index 9b7b55c..853aaa5 100644 --- a/bemani/frontend/static/themes/default/section.css +++ b/bemani/frontend/static/themes/default/section.css @@ -54,13 +54,6 @@ div.labelledsection.filled { border: 1px dashed #dddddd; } -span.separator { - font-size: large; - font-weight: bold; - padding-left: 5px; - padding-right: 5px; -} - div.labelledsection.iidx.themeoption select { width: 200px; } diff --git a/bemani/frontend/static/themes/default/site.css b/bemani/frontend/static/themes/default/site.css index 4e32055..8ad34f3 100644 --- a/bemani/frontend/static/themes/default/site.css +++ b/bemani/frontend/static/themes/default/site.css @@ -267,3 +267,48 @@ span.checkbox { padding-right: 5px; cursor: default; } + +div.slider { + display: inline-block; + border-radius: 10px; +} + +div.slider.on { + background: #2196F3; + border: 1px solid #0a6fc2; +} + +div.slider.off { + background: #cccccc; + border: 1px solid #a0a0a0; +} + +div.slider span.label { + width: 70px; + font-weight: normal; + font-size: small; + height: 18px; + display: inline-block; +} + +div.slider span.label.on { + text-align: center; +} + +div.slider span.label.off { + text-align: center; +} + +div.slider span.ball { + position: absolute; + display: inline-block; + border-radius: 8px; + background: white; + width: 14px; + height: 14px; + margin: 2px; +} + +div.slider span.ball.off { + margin-left: -16px; +}