Initial implementation of DanEvo frontend, only including records, all players, profiles, top scores and personal settings.

This commit is contained in:
Jennifer Taylor 2025-09-29 02:47:20 +00:00
parent 8b60aff379
commit 13bebc392a
14 changed files with 1525 additions and 2 deletions

View File

@ -412,6 +412,47 @@ def navigation() -> Dict[str, Any]:
},
)
if GameConstants.DANCE_EVOLUTION in g.config.support:
# Dance Evolution pages
danevo_entries = []
if len([p for p in profiles if p[0] == GameConstants.DANCE_EVOLUTION]) > 0:
danevo_entries.extend(
[
{
"label": "Game Options",
"uri": url_for("danevo_pages.viewsettings"),
},
{
"label": "Personal Profile",
"uri": url_for("danevo_pages.viewplayer", userid=g.userID),
},
{
"label": "Personal Records",
"uri": url_for("danevo_pages.viewrecords", userid=g.userID),
},
]
)
danevo_entries.extend(
[
{
"label": "Global Records",
"uri": url_for("danevo_pages.viewnetworkrecords"),
},
{
"label": "All Players",
"uri": url_for("danevo_pages.viewplayers"),
},
]
)
pages.append(
{
"label": "Dance Evolution",
"entries": danevo_entries,
"base_uri": app.blueprints["danevo_pages"].url_prefix,
"gamecode": GameConstants.DANCE_EVOLUTION.value,
},
)
if GameConstants.DDR in g.config.support:
# DDR pages
ddr_entries = []

View File

@ -0,0 +1,8 @@
from bemani.frontend.danevo.endpoints import danevo_pages
from bemani.frontend.danevo.cache import DanceEvolutionCache
__all__ = [
"DanceEvolutionCache",
"danevo_pages",
]

View File

@ -0,0 +1,10 @@
from bemani.common import cache
from bemani.data import Config, Data
from bemani.frontend.danevo.danevo import DanceEvolutionFrontend
class DanceEvolutionCache:
@classmethod
def preload(cls, data: Data, config: Config) -> None:
frontend = DanceEvolutionFrontend(data, config, cache)
frontend.get_all_songs(force_db_load=True)

View File

@ -0,0 +1,88 @@
# vim: set fileencoding=utf-8
from typing import Any, Dict, Iterator, List, Tuple
from bemani.backend.danevo import DanceEvolutionFactory, DanceEvolutionBase
from bemani.common import Profile, ValidatedDict, GameConstants
from bemani.data import Attempt, Score, Song, UserID
from bemani.frontend.base import FrontendBase
class DanceEvolutionFrontend(FrontendBase):
game: GameConstants = GameConstants.DANCE_EVOLUTION
valid_charts: List[int] = [
DanceEvolutionBase.CHART_TYPE_LIGHT,
DanceEvolutionBase.CHART_TYPE_STANDARD,
DanceEvolutionBase.CHART_TYPE_EXTREME,
DanceEvolutionBase.CHART_TYPE_STEALTH,
DanceEvolutionBase.CHART_TYPE_MASTER,
# Only included so that we can grab the play count for this song.
DanceEvolutionBase.CHART_TYPE_PLAYTRACKING,
]
valid_rival_types: List[str] = []
def all_games(self) -> Iterator[Tuple[GameConstants, int, str]]:
yield from DanceEvolutionFactory.all_games()
def get_all_songs(self, force_db_load: bool = False) -> Dict[int, Dict[str, Any]]:
def is_valid(data: Dict[str, Any]) -> bool:
if "levels" not in data:
return False
levels = data["levels"]
if not isinstance(levels, list):
return False
for x in levels:
if x == 0:
return False
return True
songs = super().get_all_songs(force_db_load)
filtered_songs = {sid: contents for sid, contents in songs.items() if is_valid(contents)}
return filtered_songs
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")
formatted_score["full_combo"] = score.data.get_bool("full_combo")
formatted_score["medal"] = score.data.get_int("grade")
formatted_score["grade"] = {
DanceEvolutionBase.GRADE_FAILED: "FAILED",
DanceEvolutionBase.GRADE_E: "E",
DanceEvolutionBase.GRADE_D: "D",
DanceEvolutionBase.GRADE_C: "C",
DanceEvolutionBase.GRADE_B: "B",
DanceEvolutionBase.GRADE_A: "A",
DanceEvolutionBase.GRADE_AA: "AA",
DanceEvolutionBase.GRADE_AAA: "AAA",
}.get(score.data.get_int("grade"), "NO PLAY")
return formatted_score
def format_attempt(self, userid: UserID, attempt: Attempt) -> Dict[str, Any]:
raise NotImplementedError("Dance Evolution does not have attempts!")
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["player_class"] = profile.get_int("class")
return formatted_profile
def format_song(self, song: Song) -> Dict[str, Any]:
levels = [0, 0, 0, 0, 0]
levels[song.chart] = song.data.get_int("level")
formatted_song = super().format_song(song)
formatted_song["levels"] = levels
formatted_song["bpm_min"] = song.data.get_int("bpm_min")
formatted_song["bpm_max"] = song.data.get_int("bpm_max")
return formatted_song
def merge_song(self, existing: Dict[str, Any], new: Song) -> Dict[str, Any]:
if new.chart == DanceEvolutionBase.CHART_TYPE_PLAYTRACKING:
return existing
new_song = super().merge_song(existing, new)
if existing["levels"][new.chart] == 0:
new_song["levels"][new.chart] = new.data.get_int("level")
return new_song

View File

@ -0,0 +1,280 @@
# vim: set fileencoding=utf-8
import re
from typing import Any, Dict
from flask import Blueprint, request, Response, url_for, abort
from bemani.common import GameConstants
from bemani.data import UserID
from bemani.frontend.app import loginrequired, jsonify, render_react
from bemani.frontend.danevo.danevo import DanceEvolutionFrontend
from bemani.frontend.templates import templates_location
from bemani.frontend.static import static_location
from bemani.frontend.types import g
danevo_pages = Blueprint(
"danevo_pages",
__name__,
url_prefix=f"/{GameConstants.DANCE_EVOLUTION.value}",
template_folder=templates_location,
static_folder=static_location,
)
@danevo_pages.route("/records")
@loginrequired
def viewnetworkrecords() -> Response:
frontend = DanceEvolutionFrontend(g.data, g.config, g.cache)
network_records = frontend.get_network_records()
versions = {version: name for (game, version, name) in frontend.all_games()}
return render_react(
"Global Dance Evolution Records",
"danevo/records.react.js",
{
"records": network_records["records"],
"songs": frontend.get_all_songs(),
"players": network_records["players"],
"versions": versions,
"shownames": True,
"showpersonalsort": False,
"filterempty": False,
},
{
"refresh": url_for("danevo_pages.listnetworkrecords"),
"player": url_for("danevo_pages.viewplayer", userid=-1),
"individual_score": url_for("danevo_pages.viewtopscores", musicid=-1),
},
)
@danevo_pages.route("/records/list")
@jsonify
@loginrequired
def listnetworkrecords() -> Dict[str, Any]:
frontend = DanceEvolutionFrontend(g.data, g.config, g.cache)
return frontend.get_network_records()
@danevo_pages.route("/records/<int:userid>")
@loginrequired
def viewrecords(userid: UserID) -> Response:
frontend = DanceEvolutionFrontend(g.data, g.config, g.cache)
info = frontend.get_latest_player_info([userid]).get(userid)
if info is None:
abort(404)
versions = {version: name for (game, version, name) in frontend.all_games()}
return render_react(
f'{info["name"]}\'s Dance Evolution Records',
"danevo/records.react.js",
{
"records": frontend.get_records(userid),
"songs": frontend.get_all_songs(),
"players": {},
"versions": versions,
"shownames": False,
"showpersonalsort": True,
"filterempty": True,
},
{
"refresh": url_for("danevo_pages.listrecords", userid=userid),
"player": url_for("danevo_pages.viewplayer", userid=-1),
"individual_score": url_for("danevo_pages.viewtopscores", musicid=-1),
},
)
@danevo_pages.route("/records/<int:userid>/list")
@jsonify
@loginrequired
def listrecords(userid: UserID) -> Dict[str, Any]:
frontend = DanceEvolutionFrontend(g.data, g.config, g.cache)
return {
"records": frontend.get_records(userid),
"players": {},
}
@danevo_pages.route("/topscores/<int:musicid>")
@loginrequired
def viewtopscores(musicid: int) -> Response:
# We just want to find the latest mix that this song exists in
frontend = DanceEvolutionFrontend(g.data, g.config, g.cache)
versions = sorted(
[version for (game, version, name) in frontend.all_games()],
reverse=True,
)
name = None
artist = None
genre = None
levels = [0, 0, 0, 0, 0]
for version in versions:
for chart in [0, 1, 2, 3, 4]:
details = g.data.local.music.get_song(GameConstants.DANCE_EVOLUTION, version, musicid, chart)
if details is not None:
if name is None:
name = details.name
if artist is None:
artist = details.artist
if genre is None:
genre = details.genre
if levels[chart] == 0:
levels[chart] = details.data.get_int("level")
if name is None or not [x for x in levels if x > 0]:
# Not a real song!
abort(404)
top_scores = frontend.get_top_scores(musicid)
return render_react(
f"Top Dance Evolution Scores for {artist} - {name}",
"danevo/topscores.react.js",
{
"name": name,
"artist": artist,
"genre": genre,
"levels": levels,
"players": top_scores["players"],
"topscores": top_scores["topscores"],
},
{
"refresh": url_for("danevo_pages.listtopscores", musicid=musicid),
"player": url_for("danevo_pages.viewplayer", userid=-1),
},
)
@danevo_pages.route("/topscores/<int:musicid>/list")
@jsonify
@loginrequired
def listtopscores(musicid: int) -> Dict[str, Any]:
frontend = DanceEvolutionFrontend(g.data, g.config, g.cache)
return frontend.get_top_scores(musicid)
@danevo_pages.route("/players")
@loginrequired
def viewplayers() -> Response:
frontend = DanceEvolutionFrontend(g.data, g.config, g.cache)
return render_react(
"All Dance Evolution Players",
"danevo/allplayers.react.js",
{"players": frontend.get_all_players()},
{
"refresh": url_for("danevo_pages.listplayers"),
"player": url_for("danevo_pages.viewplayer", userid=-1),
},
)
@danevo_pages.route("/players/list")
@jsonify
@loginrequired
def listplayers() -> Dict[str, Any]:
frontend = DanceEvolutionFrontend(g.data, g.config, g.cache)
return {
"players": frontend.get_all_players(),
}
@danevo_pages.route("/players/<int:userid>")
@loginrequired
def viewplayer(userid: UserID) -> Response:
frontend = DanceEvolutionFrontend(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 Dance Evolution Profile',
"danevo/player.react.js",
{
"playerid": userid,
"own_profile": userid == g.userID,
"player": info,
"versions": {version: name for (game, version, name) in frontend.all_games()},
},
{
"refresh": url_for("danevo_pages.listplayer", userid=userid),
"records": url_for("danevo_pages.viewrecords", userid=userid),
},
)
@danevo_pages.route("/players/<int:userid>/list")
@jsonify
@loginrequired
def listplayer(userid: UserID) -> Dict[str, Any]:
frontend = DanceEvolutionFrontend(g.data, g.config, g.cache)
info = frontend.get_all_player_info([userid])[userid]
return {
"player": info,
}
@danevo_pages.route("/options")
@loginrequired
def viewsettings() -> Response:
frontend = DanceEvolutionFrontend(g.data, g.config, g.cache)
userid = g.userID
info = frontend.get_all_player_info([userid])[userid]
if not info:
abort(404)
return render_react(
"Dance Evolution Game Settings",
"danevo/settings.react.js",
{
"player": info,
"versions": {version: name for (game, version, name) in frontend.all_games()},
},
{
"updatename": url_for("danevo_pages.updatename"),
},
)
@danevo_pages.route("/options/name/update", methods=["POST"])
@jsonify
@loginrequired
def updatename() -> Dict[str, Any]:
version = int(request.get_json()["version"])
name = request.get_json()["name"]
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 name
profile = g.data.local.user.get_profile(GameConstants.DANCE_EVOLUTION, version, user.id)
if profile is None:
raise Exception("Unable to find profile to update!")
if len(name) == 0 or len(name) > 10:
raise Exception("Invalid profile name!")
if (
re.match(
"^["
+ "\uff20-\uff3a"
+ "\uff41-\uff5a" # widetext A-Z and @
+ "\uff10-\uff19" # widetext a-z
+ "\uff0c\uff0e\uff3f" # widetext 0-9
+ "\u3041-\u308d\u308f\u3092\u3093" # widetext ,._
+ "\u30a1-\u30ed\u30ef\u30f2\u30f3\u30fc" # hiragana
+ "\u2605\u266a" # allowed symbols
+ "]*$", # katakana
name,
)
is None
):
raise Exception("Invalid profile name!")
profile.replace_str("name", name)
g.data.local.user.put_profile(GameConstants.DANCE_EVOLUTION, version, user.id, profile)
# Return that we updated
return {
"version": version,
"name": name,
}

View File

@ -0,0 +1,97 @@
/*** @jsx React.DOM */
var all_players = createReactClass({
getInitialState: function(props) {
return {
players: window.players,
};
},
componentDidMount: function() {
this.refreshPlayers();
},
refreshPlayers: function() {
AJAX.get(
Link.get('refresh'),
function(response) {
this.setState({
players: response.players,
});
// Refresh every 30 seconds
setTimeout(this.refreshPlayers, 30000);
}.bind(this)
);
},
render: function() {
return (
<div>
<div className="section">
<Table
className="list players"
columns={[
{
name: 'Name',
render: function(userid) {
var player = this.state.players[userid];
return <a href={Link.get('player', userid)}>{ player.name }</a>;
}.bind(this),
sort: function(aid, bid) {
var a = this.state.players[aid];
var b = this.state.players[bid];
return a.name.localeCompare(b.name);
}.bind(this),
},
{
name: 'Dance Evolution ID',
render: function(userid) {
var player = this.state.players[userid];
return player.extid;
}.bind(this),
sort: function(aid, bid) {
var a = this.state.players[aid];
var b = this.state.players[bid];
return a.extid.localeCompare(b.extid);
}.bind(this),
},
{
name: 'Total Rounds',
render: function(userid) {
var player = this.state.players[userid];
return player.plays;
}.bind(this),
sort: function(aid, bid) {
var a = this.state.players[aid];
var b = this.state.players[bid];
return a.plays - b.plays;
}.bind(this),
reverse: true,
},
{
name: 'Class',
render: function(userid) {
var player = this.state.players[userid];
return player.player_class;
}.bind(this),
sort: function(aid, bid) {
var a = this.state.players[aid];
var b = this.state.players[bid];
return a.player_class - b.player_class;
}.bind(this),
},
]}
rows={Object.keys(this.state.players)}
paginate={10}
/>
</div>
</div>
);
},
});
ReactDOM.render(
React.createElement(all_players, null),
document.getElementById('content')
);

View File

@ -0,0 +1,113 @@
/*** @jsx React.DOM */
var valid_versions = Object.keys(window.versions);
var pagenav = new History(valid_versions);
var profile_view = createReactClass({
getInitialState: function(props) {
var profiles = Object.keys(window.player);
return {
player: window.player,
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)
);
},
render: function() {
if (this.state.player[this.state.version]) {
var player = this.state.player[this.state.version];
return (
<div>
<div className="section danevo-nav">
<h3>{player.name}'s profile</h3>
{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))}
</div>
<div className="section">
<LabelledSection label="User ID">{player.extid}</LabelledSection>
<LabelledSection label="Profile Created">
<Timestamp timestamp={player.first_play_time}/>
</LabelledSection>
<LabelledSection label="Last Played">
<Timestamp timestamp={player.last_play_time}/>
</LabelledSection>
<LabelledSection label="Total Rounds">
{player.plays}
</LabelledSection>
<LabelledSection label="Class">
{player.player_class}
</LabelledSection>
</div>
<div className="section">
<a href={Link.get('records')}>{ window.own_profile ?
<span>view your records</span> :
<span>view {player.name}'s records</span>
}</a>
</div>
</div>
);
} else {
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))}
</div>
<div className="section">
This player has no profile for {window.versions[this.state.version]}!
</div>
</div>
);
}
},
});
ReactDOM.render(
React.createElement(profile_view, null),
document.getElementById('content')
);

View File

@ -0,0 +1,529 @@
/*** @jsx React.DOM */
var valid_sorts = ['series', 'name', 'popularity'];
var valid_charts = ['Light', 'Standard', 'Extreme', 'Master', 'Stealth'];
var valid_mixes = Object.keys(window.versions);
var valid_subsorts = [valid_mixes, false, false, valid_charts, valid_charts];
if (window.showpersonalsort) {
valid_sorts.push('score');
valid_sorts.push('grade');
}
var pagenav = new History(valid_sorts, valid_subsorts);
var sort_names = {
'series': 'Series',
'name': 'Song Name',
'popularity': 'Popularity',
'score': 'Score',
'grade': 'Grade',
};
var HighScore = createReactClass({
render: function() {
if (!this.props.score) {
return null;
}
return (
<div className="score">
<div>
<span className="label">Score</span>
<span className="score">{this.props.score.points}</span>
<span className="label">Grade</span>
<span className="score">{this.props.score.grade}</span>
</div>
<div>
<span className="label">Combo</span>
<span className="score">{this.props.score.combo <= 0 ? '-' : this.props.score.combo}</span>
{ this.props.score.full_combo ? <span className="label">full combo</span> : null }
</div>
{ this.props.score.userid && window.shownames ?
<div><a href={Link.get('player', this.props.score.userid)}>{
this.props.players[this.props.score.userid].name
}</a></div> : null
}
</div>
);
},
});
var network_records = createReactClass({
sortRecords: function(records) {
var sorted_records = {};
records.forEach(function(record) {
if (!(record.songid in sorted_records)) {
sorted_records[record.songid] = {}
}
sorted_records[record.songid][record.chart] = record;
});
return sorted_records;
},
getInitialState: function(props) {
return {
songs: window.songs,
records: this.sortRecords(window.records),
players: window.players,
versions: window.versions,
sort: pagenav.getInitialState('series', '1'),
subtab: this.getSubIndex('series', pagenav.getInitialSubState('series', '1')),
offset: 0,
limit: 10,
};
},
getSubIndex: function(sort, subsort) {
var subtab = 0;
window.valid_sorts.forEach(function(potential, index) {
if (window.valid_subsorts[index]) {
window.valid_subsorts[index].forEach(function(subpotential, subindex) {
if (subpotential == subsort) {
subtab = subindex;
}
}.bind(this));
}
}.bind(this));
return subtab;
},
componentDidMount: function() {
pagenav.onChange(function(sort, subsort) {
var subtab = this.getSubIndex(sort, subsort);
this.setState({sort: sort, offset: 0, subtab: subtab});
}.bind(this));
this.refreshRecords();
},
refreshRecords: function() {
AJAX.get(
Link.get('refresh'),
function(response) {
this.setState({
records: this.sortRecords(response.records),
players: response.players,
});
// Refresh every 15 seconds
setTimeout(this.refreshRecords, 15000);
}.bind(this)
);
},
renderLevel: function(songid, chart) {
if (this.state.songs[songid].levels[chart] == 0) {
return <span className="level">--</span>;
} else {
return <span className="level">{this.state.songs[songid].levels[chart]}</span>;
}
},
getPlays: function(record) {
if (!record) { return 0; }
var plays = 0;
// Play counts are only storted in the play statistics chart.
if (record[5]) { plays += record[5].plays; }
return plays;
},
renderBySeries: function() {
var songids = Object.keys(this.state.songs);
if (window.filterempty) {
songids = songids.filter(function(songid) {
return this.getPlays(this.state.records[songid]) > 0;
}.bind(this));
}
if (songids.length == 0) {
return (
<div>
No records to display!
</div>
);
}
var curpage = -1;
var curbutton = -1;
return (
<>
<div className="section" key="contents">
<table className="list records">
<thead>
<tr>
<th className="subheader">Song / Artist / Level</th>
<th className="subheader">Light</th>
<th className="subheader">Standard</th>
<th className="subheader">Extreme</th>
<th className="subheader">Master</th>
<th className="subheader">Stealth</th>
</tr>
</thead>
<tbody>
{songids.map(function(songid) {
var records = this.state.records[songid];
if (!records) {
records = {};
}
var levels = this.state.songs[songid].levels;
return (
<tr key={songid.toString()}>
<td className="center">
<div>
<a href={Link.get('individual_score', songid)}>
<div className="songname">{ this.state.songs[songid].name }</div>
<div className="songartist">{ this.state.songs[songid].artist }</div>
<div className="songgenre">{ this.state.songs[songid].genre }</div>
</a>
</div>
<div className="songlevels">
Level {this.renderLevel(songid, 0)}
</div>
</td>
<td className={levels[0] > 0 ? "" : "nochart"}>
<HighScore
players={this.state.players}
songid={songid}
chart={0}
score={records[0]}
/>
</td>
<td className={levels[1] > 0 ? "" : "nochart"}>
<HighScore
players={this.state.players}
songid={songid}
chart={1}
score={records[1]}
/>
</td>
<td className={levels[2] > 0 ? "" : "nochart"}>
<HighScore
players={this.state.players}
songid={songid}
chart={2}
score={records[2]}
/>
</td>
<td className={levels[4] > 0 ? "" : "nochart"}>
<HighScore
players={this.state.players}
songid={songid}
chart={4}
score={records[4]}
/>
</td>
<td className={levels[3] > 0 ? "" : "nochart"}>
<HighScore
players={this.state.players}
songid={songid}
chart={3}
score={records[3]}
/>
</td>
</tr>
);
}.bind(this))}
</tbody>
</table>
</div>
</>
);
},
renderByName: function() {
var songids = Object.keys(this.state.songs).sort(function(a, b) {
var an = this.state.songs[a].name;
var bn = this.state.songs[b].name;
var c = an.localeCompare(bn);
if (c == 0) {
return parseInt(a) - parseInt(b)
} else {
return c;
}
}.bind(this));
if (window.filterempty) {
songids = songids.filter(function(songid) {
return this.getPlays(this.state.records[songid]) > 0;
}.bind(this));
}
return this.renderBySongIDList(songids, false);
},
renderByPopularity: function() {
var songids = Object.keys(this.state.songs).sort(function(a, b) {
var ap = this.getPlays(this.state.records[a]);
var bp = this.getPlays(this.state.records[b]);
if (bp == ap) {
return parseInt(a) - parseInt(b)
} else {
return bp - ap;
}
}.bind(this));
if (window.filterempty) {
songids = songids.filter(function(songid) {
return this.getPlays(this.state.records[songid]) > 0;
}.bind(this));
}
return this.renderBySongIDList(songids, true);
},
renderByScore: function() {
var songids = Object.keys(this.state.songs).sort(function(a, b) {
// Grab records for this song
var ar = this.state.records[a];
var br = this.state.records[b];
var ac = null;
var bc = null;
var as = 0;
var bs = 0;
// Fill in record for current chart only if it exists
if (ar) { ac = ar[this.state.subtab]; }
if (br) { bc = br[this.state.subtab]; }
if (ac) { as = ac.points; }
if (bc) { bs = bc.points; }
if (bs == as) {
return parseInt(a) - parseInt(b);
} else {
return bs - as;
}
}.bind(this));
if (window.filterempty) {
songids = songids.filter(function(songid) {
return this.getPlays(this.state.records[songid]) > 0;
}.bind(this));
}
return (
<>
<div className="section">
{window.valid_charts.map(function(chartname, index) {
return (
<Nav
title={ chartname }
active={ this.state.subtab == index }
onClick={function(event) {
if (this.state.subtab == index) { return; }
this.setState({subtab: index, offset: 0});
pagenav.navigate(this.state.sort, window.valid_charts[index]);
}.bind(this)}
/>
);
}.bind(this))}
</div>
{ this.renderBySongIDList(songids, false) }
</>
);
},
renderByClearGrade: function() {
var songids = Object.keys(this.state.songs).sort(function(a, b) {
// Grab records for this song
var ar = this.state.records[a];
var br = this.state.records[b];
var ac = null;
var bc = null;
var al = 0;
var bl = 0;
// Fill in record for current chart only if it exists
if (ar) { ac = ar[this.state.subtab]; }
if (br) { bc = br[this.state.subtab]; }
if (ac) { al = ac.medal; }
if (bc) { bl = bc.medal; }
if (al == bl) {
return parseInt(a) - parseInt(b)
} else {
return bl - al;
}
}.bind(this));
if (window.filterempty) {
songids = songids.filter(function(songid) {
return this.getPlays(this.state.records[songid]) > 0;
}.bind(this));
}
return (
<>
<div className="section">
{window.valid_charts.map(function(chartname, index) {
return (
<Nav
title={ chartname }
active={ this.state.subtab == index }
onClick={function(event) {
if (this.state.subtab == index) { return; }
this.setState({subtab: index, offset: 0});
pagenav.navigate(this.state.sort, window.valid_charts[index]);
}.bind(this)}
/>
);
}.bind(this))}
</div>
{ this.renderBySongIDList(songids, false) }
</>
);
},
renderBySongIDList: function(songids, showplays) {
return (
<div className="section">
<table className="list records">
<thead>
<tr>
<th className="subheader">Song / Artist / Level</th>
<th className="subheader">Light</th>
<th className="subheader">Standard</th>
<th className="subheader">Extreme</th>
<th className="subheader">Master</th>
<th className="subheader">Stealth</th>
</tr>
</thead>
<tbody>
{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 levels = this.state.songs[songid].levels;
return (
<tr key={songid.toString()}>
<td className="center">
<div>
<a href={Link.get('individual_score', songid)}>
<div className="songname">{ this.state.songs[songid].name }</div>
<div className="songartist">{ this.state.songs[songid].artist }</div>
<div className="songgenre">{ this.state.songs[songid].genre }</div>
</a>
</div>
<div className="songlevels">
Level {this.renderLevel(songid, 0)}
</div>
{ showplays ? <div className="songplays">#{index + 1} - {plays}{plays == 1 ? ' play' : ' plays'}</div> : null }
</td>
<td className={levels[0] > 0 ? "" : "nochart"}>
<HighScore
players={this.state.players}
songid={songid}
chart={0}
score={records[0]}
/>
</td>
<td className={levels[1] > 0 ? "" : "nochart"}>
<HighScore
players={this.state.players}
songid={songid}
chart={1}
score={records[1]}
/>
</td>
<td className={levels[2] > 0 ? "" : "nochart"}>
<HighScore
players={this.state.players}
songid={songid}
chart={2}
score={records[2]}
/>
</td>
<td className={levels[4] > 0 ? "" : "nochart"}>
<HighScore
players={this.state.players}
songid={songid}
chart={4}
score={records[4]}
/>
</td>
<td className={levels[3] > 0 ? "" : "nochart"}>
<HighScore
players={this.state.players}
songid={songid}
chart={3}
score={records[3]}
/>
</td>
</tr>
);
}.bind(this))}
</tbody>
<tfoot>
<tr>
<td colSpan={5}>
{ this.state.offset > 0 ?
<Prev onClick={function(event) {
var page = this.state.offset - this.state.limit;
if (page < 0) { page = 0; }
this.setState({offset: page});
}.bind(this)}/> : null
}
{ (this.state.offset + this.state.limit) < songids.length ?
<Next style={ {float: 'right'} } onClick={function(event) {
var page = this.state.offset + this.state.limit;
if (page >= songids.length) { return }
this.setState({offset: page});
}.bind(this)}/> :
null
}
</td>
</tr>
</tfoot>
</table>
</div>
);
},
render: function() {
var data = null;
if (this.state.sort == 'series') {
data = this.renderBySeries();
} else if (this.state.sort == 'popularity') {
data = this.renderByPopularity();
} else if (this.state.sort == 'name') {
data = this.renderByName();
} else if (this.state.sort == 'score') {
data = this.renderByScore();
} else if (this.state.sort == 'grade') {
data = this.renderByClearGrade();
}
return (
<div>
<div className="section">
{ window.valid_sorts.map(function(sort, index) {
return (
<Nav
title={"Records Sorted by " + window.sort_names[sort]}
active={this.state.sort == sort}
onClick={function(event) {
if (this.state.sort == sort) { return; }
this.setState({sort: sort, offset: 0, subtab: 0});
pagenav.navigate(sort, window.valid_subsorts[index][0]);
}.bind(this)}
/>
);
}.bind(this)) }
</div>
<div className="section">
{data}
</div>
</div>
);
},
});
ReactDOM.render(
React.createElement(network_records, null),
document.getElementById('content')
);

View File

@ -0,0 +1,195 @@
/*** @jsx React.DOM */
var valid_versions = Object.keys(window.versions);
var pagenav = new History(valid_versions);
var settings_view = createReactClass({
getInitialState: function(props) {
var profiles = Object.keys(window.player);
var version = pagenav.getInitialState(profiles[profiles.length - 1]);
return {
player: window.player,
profiles: profiles,
version: version,
new_name: window.player[version].name,
editing_name: false,
};
},
componentDidMount: function() {
pagenav.onChange(function(version) {
this.setState({version: version});
}.bind(this));
},
componentDidUpdate: function() {
if (this.focus_element && this.focus_element != this.already_focused) {
this.focus_element.focus();
this.already_focused = this.focus_element;
}
},
saveName: function(event) {
AJAX.post(
Link.get('updatename'),
{
version: this.state.version,
name: this.state.new_name,
},
function(response) {
var player = this.state.player;
player[response.version].name = response.name;
this.setState({
player: player,
new_name: this.state.player[response.version].name,
editing_name: false,
});
}.bind(this)
);
event.preventDefault();
},
renderName: function(player) {
return (
<LabelledSection vertical={true} label="Name">{
!this.state.editing_name ?
<>
<span>{player.name}</span>
<Edit
onClick={function(event) {
this.setState({editing_name: true});
}.bind(this)}
/>
</> :
<form className="inline" onSubmit={this.saveName}>
<input
type="text"
className="inline"
maxlength="10"
size="12"
autofocus="true"
ref={c => (this.focus_element = c)}
value={this.state.new_name}
onChange={function(event) {
var rawvalue = event.target.value;
var value = "";
// Nasty conversion to change typing into wide text
for (var i = 0; i < rawvalue.length; i++) {
var c = rawvalue.charCodeAt(i);
if (c >= '0'.charCodeAt(0) && c <= '9'.charCodeAt(0)) {
c = 0xFF10 + (c - '0'.charCodeAt(0));
} else if(c >= 'A'.charCodeAt(0) && c <= 'Z'.charCodeAt(0)) {
c = 0xFF21 + (c - 'A'.charCodeAt(0));
} else if(c >= 'a'.charCodeAt(0) && c <= 'z'.charCodeAt(0)) {
c = 0xFF41 + (c - 'a'.charCodeAt(0));
} else if(c == '@'.charCodeAt(0)) {
c = 0xFF20;
} else if(c == ','.charCodeAt(0)) {
c = 0xFF0C;
} else if(c == '.'.charCodeAt(0)) {
c = 0xFF0E;
} else if(c == '_'.charCodeAt(0)) {
c = 0xFF3F;
}
value = value + String.fromCharCode(c);
}
var nameRegex = new RegExp(
"^[" +
"\uFF20-\uFF3A" + // widetext A-Z and @
"\uFF41-\uFF5A" + // widetext a-z
"\uFF10-\uFF19" + // widetext 0-9
"\uFF0C\uFF0E\uFF3F" + // widetext ,._
"\u3041-\u308D\u308F\u3092\u3093" + // hiragana
"\u30A1-\u30ED\u30EF\u30F2\u30F3\u30FC" + // katakana
"\u2605\u266A" + // allowed symbols
"]*$"
);
if (value.length <= 10 && nameRegex.test(value)) {
this.setState({new_name: value});
}
}.bind(this)}
name="name"
/>
<input
type="submit"
value="save"
/>
<input
type="button"
value="cancel"
onClick={function(event) {
this.setState({
new_name: this.state.player[this.state.version].name,
editing_name: false,
});
}.bind(this)}
/>
</form>
}</LabelledSection>
);
},
render: function() {
if (this.state.player[this.state.version]) {
var player = this.state.player[this.state.version];
return (
<div>
<div className="section danevo-nav">
{this.state.profiles.map(function(version) {
return (
<Nav
title={window.versions[version]}
active={this.state.version == version}
onClick={function(event) {
if (this.state.editing_name) { return; }
if (this.state.version == version) { return; }
this.setState({
version: version,
new_name: this.state.player[version].name,
});
pagenav.navigate(version);
}.bind(this)}
/>
);
}.bind(this))}
</div>
<div className="section">
<h3>User Profile</h3>
{this.renderName(player)}
</div>
</div>
);
} else {
return (
<div>
<div className="section">
You have no profile for {window.versions[this.state.version]}!
</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))}
</div>
</div>
);
}
},
});
ReactDOM.render(
React.createElement(settings_view, null),
document.getElementById('content')
);

View File

@ -0,0 +1,151 @@
/*** @jsx React.DOM */
var valid_charts = ['Light', 'Standard', 'Extreme', 'Master', 'Stealth'];
var pagenav = new History(valid_charts);
var top_scores = createReactClass({
sortTopScores: function(topscores) {
var newscores = [[], [], [], [], []];
topscores.map(function(score) {
// Skip over the play stats dummy chart.
if (score.chart != 5) {
newscores[score.chart].push(score);
}
}.bind(this));
return newscores;
},
getInitialState: function(props) {
return {
topscores: this.sortTopScores(window.topscores),
players: window.players,
chart: pagenav.getInitialState(valid_charts[0]),
};
},
componentDidMount: function() {
pagenav.onChange(function(chart) {
this.setState({chart: chart});
}.bind(this));
this.refreshScores();
},
refreshScores: function() {
AJAX.get(
Link.get('refresh'),
function(response) {
this.setState({
topscores: this.sortTopScores(response.topscores),
players: response.players,
});
// Refresh every 15 seconds
setTimeout(this.refreshScores, 15000);
}.bind(this)
);
},
convertChart: function(chart) {
switch(chart) {
case 'Light':
return 0;
case 'Standard':
return 1;
case 'Extreme':
return 2;
case 'Stealth':
return 3;
case 'Master':
return 4;
default:
return null;
}
},
render: function() {
var chart = this.convertChart(this.state.chart);
return (
<div>
<div className="section">
<div className="songname">{window.name}</div>
<div className="songartist">{window.artist}</div>
<div className="songgenre">{window.genre}</div>
<div className="songlevels">Level {window.levels[chart]}</div>
</div>
<div className="section">
{valid_charts.map(function(chart) {
return (
<Nav
title={chart}
active={this.state.chart == chart}
onClick={function(event) {
if (this.state.chart == chart) { return; }
this.setState({chart: chart});
pagenav.navigate(chart);
}.bind(this)}
/>
);
}.bind(this))}
</div>
<div className="section">
<Table
className="list topscores"
columns={[
{
name: 'Name',
render: function(topscore) {
return (
<a href={Link.get('player', topscore.userid)}>{
this.state.players[topscore.userid].name
}</a>
);
}.bind(this),
sort: function(a, b) {
var an = this.state.players[a.userid].name;
var bn = this.state.players[b.userid].name;
return an.localeCompare(bn);
}.bind(this),
},
{
name: 'Score',
render: function(topscore) { return topscore.points; },
sort: function(a, b) {
return a.points - b.points;
},
reverse: true,
},
{
name: 'Grade',
render: function(topscore) { return topscore.grade; },
},
{
name: 'Combo',
render: function(topscore) {
return (
(topscore.combo >= 0 ? topscore.combo : '-') +
(topscore.full_combo ? ' (full combo)' : '')
);
},
sort: function(a, b) {
return a.combo - b.combo;
},
reverse: true,
},
]}
defaultsort='Score'
rows={this.state.topscores[chart]}
key={chart}
paginate={10}
emptymessage="There are no scores for this chart."
/>
</div>
</div>
);
},
});
ReactDOM.render(
React.createElement(top_scores, null),
document.getElementById('content')
);

View File

@ -33,3 +33,7 @@
.sdvx.border {
border-color: #710162;
}
.danevo.border {
border-color: #2e5eee;
}

View File

@ -33,3 +33,7 @@
.sdvx.border {
border-color: #710162;
}
.danevo.border {
border-color: #2e5eee;
}

View File

@ -16,6 +16,7 @@ from bemani.frontend.ddr import ddr_pages
from bemani.frontend.sdvx import sdvx_pages
from bemani.frontend.reflec import reflec_pages
from bemani.frontend.museca import museca_pages
from bemani.frontend.danevo import danevo_pages
from bemani.utils.config import (
load_config as base_load_config,
instantiate_cache as base_instantiate_cache,
@ -47,7 +48,8 @@ def register_blueprints() -> None:
app.register_blueprint(reflec_pages)
if GameConstants.MUSECA in config.support:
app.register_blueprint(museca_pages)
# TODO: DanEvo frontend here.
if GameConstants.DANCE_EVOLUTION in config.support:
app.register_blueprint(danevo_pages)
def register_games() -> None:

View File

@ -20,6 +20,7 @@ from bemani.frontend.ddr import DDRCache
from bemani.frontend.sdvx import SoundVoltexCache
from bemani.frontend.reflec import ReflecBeatCache
from bemani.frontend.museca import MusecaCache
from bemani.frontend.danevo import DanceEvolutionCache
from bemani.common import GameConstants, Time
from bemani.data import Config, Data
from bemani.utils.config import load_config, instantiate_cache
@ -60,7 +61,7 @@ def run_scheduled_work(config: Config) -> None:
enabled_caches.append(MusecaCache)
if GameConstants.DANCE_EVOLUTION in config.support:
enabled_factories.append(DanceEvolutionFactory)
# TODO: Frontend cache here.
enabled_caches.append(DanceEvolutionCache)
# First, run any backend scheduled work
for factory in enabled_factories: