1
0
mirror of synced 2024-11-12 01:00:46 +01:00

Jubeat WebUI update, adds support for emblem editing, stats and jubility breakdown.

This commit is contained in:
Subject38 2022-10-07 00:32:37 +00:00 committed by Jennifer Taylor
parent ef085ae99e
commit 14374ef2d3
11 changed files with 676 additions and 16 deletions

View File

@ -282,6 +282,7 @@ def viewplayer(userid: UserID) -> Response:
'refresh': url_for('jubeat_pages.listplayer', userid=userid),
'records': url_for('jubeat_pages.viewrecords', userid=userid),
'scores': url_for('jubeat_pages.viewscores', userid=userid),
'jubility': url_for('jubeat_pages.showjubility', userid=userid),
},
)
@ -298,28 +299,100 @@ def listplayer(userid: UserID) -> Dict[str, Any]:
}
@jubeat_pages.route('/players/<int:userid>/jubility')
@loginrequired
def showjubility(userid: UserID) -> Response:
frontend = JubeatFrontend(g.data, g.config, g.cache)
info = frontend.get_all_player_info([userid])[userid]
if not info:
abort(404)
latest_version = sorted(info.keys(), reverse=True)[0]
return render_react(
f'{info[latest_version]["name"]}\'s Jubility Breakdown',
'jubeat/jubility.react.js',
{
'playerid': userid,
'player': info,
'songs': frontend.get_all_songs(),
'versions': {version: name for (game, version, name) in frontend.all_games()},
},
{
'refresh': url_for('jubeat_pages.listplayer', userid=userid),
'individual_score': url_for('jubeat_pages.viewtopscores', musicid=-1),
'profile': url_for('jubeat_pages.viewplayer', userid=userid),
},
)
@jubeat_pages.route('/options')
@loginrequired
def viewsettings() -> Response:
frontend = JubeatFrontend(g.data, g.config, g.cache)
userid = g.userID
info = frontend.get_all_player_info([userid])[userid]
versions = sorted(
[version for (game, version, name) in frontend.all_games()],
reverse=True,
)
if not info:
abort(404)
all_emblems = frontend.get_all_items(versions)
return render_react(
'Jubeat Game Settings',
'jubeat/settings.react.js',
{
'player': info,
'versions': {version: name for (game, version, name) in frontend.all_games()},
'emblems': all_emblems,
},
{
'updatename': url_for('jubeat_pages.updatename'),
'updateemblem': url_for('jubeat_pages.updateemblem')
},
)
@jubeat_pages.route('/options/emblem/update', methods=['POST'])
@jsonify
@loginrequired
def updateemblem() -> Dict[str, Any]:
frontend = JubeatFrontend(g.data, g.config, g.cache)
version = int(request.get_json()['version'])
emblem = request.get_json()['emblem']
user = g.data.local.user.get_user(g.userID)
if user is None:
raise Exception('Unable to find user to update!')
# Grab profile and update emblem
profile = g.data.local.user.get_profile(GameConstants.JUBEAT, version, user.id)
if profile is None:
raise Exception('Unable to find profile to update!')
# Making emblem arr for update
emblem_arr = [
emblem['background'],
emblem['main'],
emblem['ornament'],
emblem['effect'],
emblem['speech_bubble'],
]
# Grab last dict from profile for updating emblem
last_dict = profile.get_dict('last')
last_dict.replace_int_array('emblem', 5, emblem_arr)
# Replace last dict that replaced int arr
profile.replace_dict('last', last_dict)
g.data.local.user.put_profile(GameConstants.JUBEAT, version, user.id, profile)
return {
'version': version,
'emblem': frontend.format_emblem(emblem_arr),
}
@jubeat_pages.route('/options/name/update', methods=['POST'])
@jsonify
@loginrequired

View File

@ -1,5 +1,5 @@
# vim: set fileencoding=utf-8
from typing import Any, Dict, Iterator, List, Tuple
from typing import Any, Dict, Iterator, List, Optional, Tuple
from bemani.backend.jubeat import JubeatFactory, JubeatBase
from bemani.common import Profile, ValidatedDict, GameConstants, VersionConstants
@ -42,6 +42,58 @@ class JubeatFrontend(FrontendBase):
if version in mapping:
yield (game, mapping[version], name)
def get_duplicate_id(self, musicid: int, chart: int) -> Optional[Tuple[int, int]]:
# In qubell and clan omnimix, PPAP and Bonjour the world are placed
# at this arbitrary songid since they weren't assigned one originally
# In jubeat festo, these songs were given proper songids so we need to account for this
legacy_to_modern_map = {
71000001: 70000124, # PPAP
71000002: 70000154, # Bonjour the world
50000020: 80000037, # 千本桜 was removed and then revived in clan
60000063: 70000100, # Khamen break sdvx had the first id for prop(never released officially)
}
oldid = legacy_to_modern_map.get(musicid)
oldchart = chart
if oldid is not None:
return (oldid, oldchart)
else:
return None
def get_all_items(self, versions: list) -> Dict[str, List[Dict[str, Any]]]:
result = {}
for version in versions:
emblem = self.__format_jubeat_extras(version)
result[version] = emblem['emblems']
return result
def __format_jubeat_extras(self, version: int) -> Dict[str, List[Dict[str, Any]]]:
# Gotta look up the unlock catalog
items = self.data.local.game.get_items(self.game, version)
# Format it depending on the version
if version in {
VersionConstants.JUBEAT_PROP,
VersionConstants.JUBEAT_QUBELL,
VersionConstants.JUBEAT_CLAN,
VersionConstants.JUBEAT_FESTO,
}:
return {
"emblems": [
{
"index": str(item.id),
"song": item.data.get_int("music_id"),
"layer": item.data.get_int("layer"),
"evolved": item.data.get_int("evolved"),
"rarity": item.data.get_int("rarity"),
"name": item.data.get_str("name"),
}
for item in items
if item.type == "emblem"
],
}
else:
return {"emblems": []}
def format_score(self, userid: UserID, score: Score) -> Dict[str, Any]:
formatted_score = super().format_score(userid, score)
formatted_score['combo'] = score.data.get_int('combo', -1)
@ -57,6 +109,9 @@ class JubeatFrontend(FrontendBase):
JubeatBase.PLAY_MEDAL_NEARLY_EXCELLENT: "NEARLY EXCELLENT",
JubeatBase.PLAY_MEDAL_EXCELLENT: "EXCELLENT",
}.get(score.data.get_int('medal'), 'NO PLAY')
formatted_score['music_rate'] = score.data.get_int('music_rate', 0) / 10
formatted_score['clear_cnt'] = score.data.get_int('clear_count', 0)
formatted_score['stats'] = score.data.get_dict('stats')
return formatted_score
def format_attempt(self, userid: UserID, attempt: Attempt) -> Dict[str, Any]:
@ -74,11 +129,32 @@ class JubeatFrontend(FrontendBase):
JubeatBase.PLAY_MEDAL_NEARLY_EXCELLENT: "NEARLY EXCELLENT",
JubeatBase.PLAY_MEDAL_EXCELLENT: "EXCELLENT",
}.get(attempt.data.get_int('medal'), 'NO PLAY')
formatted_attempt['music_rate'] = attempt.data.get_int('music_rate', 0) / 10
formatted_attempt['stats'] = attempt.data.get_dict('stats')
return formatted_attempt
def format_emblem(self, emblem: list) -> Dict[str, Any]:
return {
'background': emblem[0],
'main': emblem[1],
'ornament': emblem[2],
'effect': emblem[3],
'speech_bubble': emblem[4],
}
def format_profile(self, profile: Profile, playstats: ValidatedDict) -> Dict[str, Any]:
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['ex_count'] = profile.get_int('ex_cnt')
formatted_profile['fc_count'] = profile.get_int('fc_cnt')
return formatted_profile
def format_song(self, song: Song) -> Dict[str, Any]:

View File

@ -69,6 +69,33 @@ var all_players = React.createClass({
}.bind(this),
reverse: true,
},
{
name: 'Jubility',
render: function(userid) {
var player = this.state.players[userid];
if (player.common_jubility != 0 || player.pick_up_jubility != 0) {
return (player.common_jubility + player.pick_up_jubility).toFixed(1);
} else if (player.jubility != 0) {
return player.jubility / 100
} else {
return 0
}
}.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;
return ajub-bjub;
}.bind(this),
reverse: true,
},
]}
rows={Object.keys(this.state.players)}
paginate={10}

View File

@ -0,0 +1,228 @@
/*** @jsx React.DOM */
var valid_versions = Object.keys(window.versions);
var pagenav = new History(valid_versions);
var jubility_view = React.createClass({
getInitialState: function(props) {
var profiles = Object.keys(window.player);
return {
player: window.player,
songs: window.songs,
profiles: profiles,
version: pagenav.getInitialState(profiles[profiles.length - 1]),
};
},
componentDidMount: function() {
pagenav.onChange(function(version) {
this.setState({version: version});
}.bind(this));
this.refreshProfile();
},
refreshProfile: function() {
AJAX.get(
Link.get('refresh'),
function(response) {
var profiles = Object.keys(response.player);
this.setState({
player: response.player,
profiles: profiles,
});
setTimeout(this.refreshProfile, 5000);
}.bind(this)
);
},
renderJubilityBreakdown: function(player) {
return (
<div className='row'>
{this.renderJubilityTable(player, true)}
{this.renderJubilityTable(player, false)}
</div>
);
},
renderJubilityTable: function(player, pickup) {
if (this.state.version != 13) // festo
return null;
if (pickup == true)
jubilityChart = player.pick_up_chart;
else
jubilityChart = player.common_chart;
if (typeof jubilityChart === 'undefined' || jubilityChart.length == 0) {
return null;
}
return(
<div className='col-6 col-12-medium'>
<p>
<b>
{pickup == true ? <b>Pick up chart breakdown</b> : <b>Common chart breakdown</b>}
</b>
</p>
<Table
className='list jubility'
columns={[
{
name: 'Song',
render: function(entry) {
return (
<a href={Link.get('individual_score', entry.music_id)}>
<div>{ this.state.songs[entry.music_id].name }</div>
</a>
);
}.bind(this),
},
{
name: 'Hard Mode',
render: function(entry) { return entry.seq >= 3 ? 'Yes' : 'No'; }
},
{
name: 'Music Rate',
render: function(entry) { return entry.music_rate.toFixed(1) + '%'; },
sort: function(a, b) {
return a.music_rate - b.music_rate;
},
reverse: true,
},
{
name: 'Jubility',
render: function(entry) { return entry.value.toFixed(1); },
sort: function(a, b) {
return a.value - b.value;
},
reverse: true,
},
]}
defaultsort='Jubility'
rows={jubilityChart}
/>
</div>
);
},
renderJubility: function(player) {
return(
// version == prop ( No Jubility )
this.state.version == 10 ?
<div>
<p>This version of jubeat doesn't support Jubility</p>
</div>
:
// version == qubell ( No Jubility )
this.state.version == 11 ?
<div>
<p>This version of jubeat doesn't support Jubility</p>
</div>
:
// version == festo
this.state.version == 13 ?
<div>
<LabelledSection label='Jubility'>
{(player.common_jubility+player.pick_up_jubility).toFixed(1)}
</LabelledSection>
<LabelledSection label='Common Jubility'>
{player.common_jubility.toFixed(1)}
</LabelledSection>
<LabelledSection label='Pick up Jubility'>
{player.pick_up_jubility.toFixed(1)}
</LabelledSection>
</div>
:
// Default which version >= Saucer except qubell and festo
this.state.version >= 8 ?
<div>
<LabelledSection label='Jubility'>
{player.jubility / 100}
</LabelledSection>
</div>
:
<div>
<p>This version of jubeat doesn't support Jubility</p>
</div>
)
},
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]
})
return (
<div>
<section>
<p>
<b>
<a href={Link.get('profile')}>&larr; Back To Profile</a>
</b>
</p>
</section>
<section>
<h3>{player.name}'s jubility</h3>
<p>
{this.state.profiles.map(function(version) {
return (
<Nav
title={window.versions[version]}
active={this.state.version == version}
onClick={function(event) {
if (this.state.version == version) { return; }
this.setState({
version: version,
});
pagenav.navigate(version);
}.bind(this)}
/>
);
}.bind(this))}
</p>
</section>
<section>
{this.renderJubility(player)}
</section>
<section>
{this.renderJubilityBreakdown(player)}
</section>
</div>
);
} else {
var item = Object.keys(window.versions).map(function(k){
return window.versions[k]
})
return (
<div>
<section>
<p>
<SelectVersion
name='version'
value={ item.indexOf(item[this.state.version - 1]) }
versions={ item }
onChange={function(event) {
var version = item.indexOf(item[event]) + 1
if (this.state.version == version) { return; }
this.setState({version: version});
pagenav.navigate(version);
}.bind(this)}
/>
</p>
</section>
<section>
<p>This player has no profile for {window.versions[this.state.version]}!</p>
</section>
</div>
);
}
},
});
ReactDOM.render(
React.createElement(jubility_view, null),
document.getElementById('content')
);

View File

@ -36,9 +36,50 @@ var profile_view = React.createClass({
);
},
renderJubility: function(player) {
return(
// version == prop ( No Jubility )
this.state.version == 10 ?
null
:
// version == qubell ( No Jubility )
this.state.version == 11 ?
null
:
// version == festo
this.state.version == 13 ?
<div>
<LabelledSection label="Jubility">
{(player.common_jubility+player.pick_up_jubility).toFixed(1)}
</LabelledSection>
<p>
<b>
<a href={Link.get('jubility')}>{ window.own_profile ?
<span>Your Jubility Breakdown &rarr;</span> :
<span>{player.name}'s Jubility Breakdown &rarr;</span>
}</a>
</b>
</p>
</div>
:
// Default which version >= Saucer except qubell and festo
this.state.version >= 8 ?
<div>
<LabelledSection label="Jubility">
{player.jubility / 100}
</LabelledSection>
</div>
:
null
)
},
render: function() {
if (this.state.player[this.state.version]) {
var player = this.state.player[this.state.version];
var item = Object.keys(window.versions).map(function(k){
return window.versions[k]
})
return (
<div>
<div className="section">
@ -68,6 +109,13 @@ var profile_view = React.createClass({
<LabelledSection label="Total Plays">
{player.plays}
</LabelledSection>
<LabelledSection label="EXCELLENTs">
{player.ex_count}
</LabelledSection>
<LabelledSection label="FULL COMBOs">
{player.fc_count}
</LabelledSection>
{this.renderJubility(player)}
</div>
<div className="section">
<a href={Link.get('records')}>{ window.own_profile ?
@ -83,22 +131,23 @@ var profile_view = React.createClass({
</div>
);
} else {
var item = Object.keys(window.versions).map(function(k){
return window.versions[k]
})
return (
<div>
<div className="section">
{this.state.profiles.map(function(version) {
return (
<Nav
title={window.versions[version]}
active={this.state.version == version}
onClick={function(event) {
if (this.state.version == version) { return; }
this.setState({version: version});
pagenav.navigate(version);
}.bind(this)}
/>
);
}.bind(this))}
<SelectVersion
name="version"
value={ item.indexOf(item[this.state.version - 1]) }
versions={ item }
onChange={function(event) {
var version = item.indexOf(item[event]) + 1
if (this.state.version == version) { return; }
this.setState({version: version});
pagenav.navigate(version);
}.bind(this)}
/>
</div>
<div className="section">
This player has no profile for {window.versions[this.state.version]}!

View File

@ -35,6 +35,18 @@ var HighScore = React.createClass({
</div>
<div>
<span className="status">{this.props.score.status}</span>
<br/>
<span className="bolder">Stats:</span>
<br/>
{this.props.score.stats.perfect}
<span> / </span>
{this.props.score.stats.great}
<span> / </span>
{this.props.score.stats.good}
<span> / </span>
{this.props.score.stats.poor}
<span> / </span>
{this.props.score.stats.miss}
</div>
{ this.props.score.userid && window.shownames ?
<div><a href={Link.get('player', this.props.score.userid)}>{

View File

@ -64,6 +64,18 @@ var network_scores = React.createClass({
</div>
<div>
<span className="status">{score.status}</span>
<br/>
<span className="bolder">Stats:</span>
<br/>
{score.stats.perfect}
<span> / </span>
{score.stats.great}
<span> / </span>
{score.stats.good}
<span> / </span>
{score.stats.poor}
<span> / </span>
{score.stats.miss}
</div>
</div>
);

View File

@ -3,6 +3,22 @@
var valid_versions = Object.keys(window.versions);
var pagenav = new History(valid_versions);
var valid_emblem_options = [
'background',
'main',
'ornament',
'effect',
'speech_bubble',
]
var emblem_option_names = {
'main': 'Main',
'background': 'Background',
'ornament': 'Ornament',
'effect': 'Effect',
'speech_bubble': 'Speech Bubble',
}
var settings_view = React.createClass({
getInitialState: function(props) {
@ -14,6 +30,9 @@ var settings_view = React.createClass({
version: version,
new_name: window.player[version].name,
editing_name: false,
emblem_changed: {},
emblem_saving: {},
emblem_saved: {},
};
},
@ -30,6 +49,42 @@ var settings_view = React.createClass({
}
},
setEmblemChanged: function(val) {
this.state.emblem_changed[this.state.version] = val;
return this.state.emblem_changed
},
setEmblemSaving: function(val) {
this.state.emblem_saving[this.state.version] = val;
return this.state.emblem_saving
},
setEmblemSaved: function(val) {
this.state.emblem_saved[this.state.version] = val;
return this.state.emblem_saved
},
saveEmblem: function(event) {
this.setState({ emblem_saving: this.setEmblemSaving(true), emblem_saved: this.setEmblemSaved(false) })
AJAX.post(
Link.get('updateemblem'),
{
version: this.state.version,
emblem: this.state.player[this.state.version].emblem,
},
function(response) {
var player = this.state.player
player[response.version].emblem = response.emblem
this.setState({
player: player,
emblem_saving: this.setEmblemSaving(false),
emblem_saved: this.setEmblemSaved(true),
emblem_changed: this.setEmblemChanged(false),
})
}.bind(this)
)
},
saveName: function(event) {
AJAX.post(
Link.get('updatename'),
@ -99,6 +154,67 @@ var settings_view = React.createClass({
);
},
renderEmblem: function(player) {
return (
<div className="section">
<h3>Emblem</h3>
{
valid_emblem_options.map(function(emblem_option) {
var player = this.state.player[this.state.version]
var layer = valid_emblem_options.indexOf(emblem_option) + 1
var items = window.emblems[this.state.version].filter(function (emblem) {
return emblem.layer == layer
});
var results = {};
items
.map(function(item) { return { 'index': item.index, 'name': `${item.name} (★${item.rarity})` } })
.forEach (value => results[value.index] = value.name);
if (layer != 2) {
results[0] = "None"
}
return(
<LabelledSection
className="jubeat emblemoption"
vertical={true}
label={emblem_option_names[emblem_option]}
>
<SelectInt
name={emblem_option}
value={player.emblem[emblem_option]}
choices={results}
onChange={function(choice) {
var player = this.state.player;
player[this.state.version].emblem[emblem_option] = choice;
this.setState({
player: player,
emblem_changed: this.setEmblemChanged(true),
})
}.bind(this)}
/>
</LabelledSection>
)
}.bind(this))
}
<input
type="submit"
value="save"
disabled={!this.state.emblem_changed[this.state.version]}
onClick={function(event) {
this.saveEmblem(event);
}.bind(this)}
/>
{ this.state.emblem_saving[this.state.version] ?
<img className="loading" src={Link.get('static', 'loading-16.gif')} /> :
null
}
{ this.state.emblem_saved[this.state.version] ?
<span>&#x2713;</span> :
null
}
</div>
)
},
render: function() {
if (this.state.player[this.state.version]) {
var player = this.state.player[this.state.version];
@ -127,6 +243,9 @@ var settings_view = React.createClass({
<h3>User Profile</h3>
{this.renderName(player)}
</div>
{
this.state.version > 9 ? this.renderEmblem(player) : null
}
</div>
);
} else {

View File

@ -124,6 +124,26 @@ var top_scores = React.createClass({
name: 'Combo',
render: function(topscore) { return topscore.combo > 0 ? topscore.combo : '-'; },
},
{
name: 'Perfect',
render: function(topscore) { return topscore.stats.perfect }
},
{
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 }
},
]}
defaultsort='Score'
rows={this.state.topscores[chart]}

View File

@ -68,7 +68,29 @@ table.records, table.attempts, table.topscores, table.players, table.events {
width: 100%;
}
table.records a, table.attempts a, table.topscores a, table.players a {
table.jubility {
width: 100%;
width: calc(100% - 15px);
float: left;
margin: 5px;
}
.row {
display: flex;
}
.column {
flex: 50%;
padding: 5px;
}
@media screen and (max-width: 600px) {
.row {
display: inline;
}
}
table.records a, table.attempts a, table.topscores a, table.players a, table.jubility a {
text-decoration: none;
}

View File

@ -68,7 +68,29 @@ table.records, table.attempts, table.topscores, table.players, table.events {
width: 100%;
}
table.records a, table.attempts a, table.topscores a, table.players a {
table.jubility {
width: 100%;
width: calc(100% - 15px);
float: left;
margin: 5px;
}
.row {
display: flex;
}
.column {
flex: 50%;
padding: 5px;
}
@media screen and (max-width: 600px) {
.row {
display: inline;
}
}
table.records a, table.attempts a, table.topscores a, table.players a, table.jubility a {
text-decoration: none;
}