diff --git a/bemani/frontend/app.py b/bemani/frontend/app.py index 53990ef..518707d 100644 --- a/bemani/frontend/app.py +++ b/bemani/frontend/app.py @@ -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 = [] diff --git a/bemani/frontend/danevo/__init__.py b/bemani/frontend/danevo/__init__.py new file mode 100644 index 0000000..e26a13b --- /dev/null +++ b/bemani/frontend/danevo/__init__.py @@ -0,0 +1,8 @@ +from bemani.frontend.danevo.endpoints import danevo_pages +from bemani.frontend.danevo.cache import DanceEvolutionCache + + +__all__ = [ + "DanceEvolutionCache", + "danevo_pages", +] diff --git a/bemani/frontend/danevo/cache.py b/bemani/frontend/danevo/cache.py new file mode 100644 index 0000000..d0949ec --- /dev/null +++ b/bemani/frontend/danevo/cache.py @@ -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) diff --git a/bemani/frontend/danevo/danevo.py b/bemani/frontend/danevo/danevo.py new file mode 100644 index 0000000..0f247ef --- /dev/null +++ b/bemani/frontend/danevo/danevo.py @@ -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 diff --git a/bemani/frontend/danevo/endpoints.py b/bemani/frontend/danevo/endpoints.py new file mode 100644 index 0000000..edea869 --- /dev/null +++ b/bemani/frontend/danevo/endpoints.py @@ -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/") +@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//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/") +@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//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/") +@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//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, + } diff --git a/bemani/frontend/static/controllers/danevo/allplayers.react.js b/bemani/frontend/static/controllers/danevo/allplayers.react.js new file mode 100644 index 0000000..e3561a1 --- /dev/null +++ b/bemani/frontend/static/controllers/danevo/allplayers.react.js @@ -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 ( +
+
+ { player.name }; + }.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} + /> + + + ); + }, +}); + +ReactDOM.render( + React.createElement(all_players, null), + document.getElementById('content') +); diff --git a/bemani/frontend/static/controllers/danevo/player.react.js b/bemani/frontend/static/controllers/danevo/player.react.js new file mode 100644 index 0000000..53dd6b9 --- /dev/null +++ b/bemani/frontend/static/controllers/danevo/player.react.js @@ -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 ( +
+
+

{player.name}'s profile

+ {this.state.profiles.map(function(version) { + return ( +
+
+ {player.extid} + + + + + + + + {player.plays}回 + + + {player.player_class} + +
+ +
+ ); + } else { + return ( +
+
+ {this.state.profiles.map(function(version) { + return ( +
+
+ This player has no profile for {window.versions[this.state.version]}! +
+
+ ); + } + }, +}); + +ReactDOM.render( + React.createElement(profile_view, null), + document.getElementById('content') +); diff --git a/bemani/frontend/static/controllers/danevo/records.react.js b/bemani/frontend/static/controllers/danevo/records.react.js new file mode 100644 index 0000000..5f3a05c --- /dev/null +++ b/bemani/frontend/static/controllers/danevo/records.react.js @@ -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 ( +
+
+ Score + {this.props.score.points} + Grade + {this.props.score.grade} +
+
+ Combo + {this.props.score.combo <= 0 ? '-' : this.props.score.combo} + { this.props.score.full_combo ? full combo : null } +
+ { this.props.score.userid && window.shownames ? + : null + } +
+ ); + }, +}); + +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 --; + } else { + return {this.state.songs[songid].levels[chart]}; + } + }, + + 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 ( +
+ No records to display! +
+ ); + } + + var curpage = -1; + var curbutton = -1; + + return ( + <> +
+
+ + + + + + + + + + + + {songids.map(function(songid) { + var records = this.state.records[songid]; + if (!records) { + records = {}; + } + + var levels = this.state.songs[songid].levels; + return ( + + + + + + + + + ); + }.bind(this))} + +
Song / Artist / LevelLightStandardExtremeMasterStealth
+ +
+ Level {this.renderLevel(songid, 0)} +
+
0 ? "" : "nochart"}> + + 0 ? "" : "nochart"}> + + 0 ? "" : "nochart"}> + + 0 ? "" : "nochart"}> + + 0 ? "" : "nochart"}> + +
+
+ + ); + }, + + 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 ( + <> +
+ {window.valid_charts.map(function(chartname, index) { + return ( +
+ { 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 ( + <> +
+ {window.valid_charts.map(function(chartname, index) { + return ( +
+ { this.renderBySongIDList(songids, false) } + + ); + }, + + renderBySongIDList: function(songids, showplays) { + return ( +
+ + + + + + + + + + + + + {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 ( + + + + + + + + + ); + }.bind(this))} + + + + + + +
Song / Artist / LevelLightStandardExtremeMasterStealth
+ +
+ Level {this.renderLevel(songid, 0)} +
+ { showplays ?
#{index + 1} - {plays}{plays == 1 ? ' play' : ' plays'}
: null } +
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 + } +
+
+ ); + }, + + 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 ( +
+
+ { window.valid_sorts.map(function(sort, index) { + return ( +
+
+ {data} +
+
+ ); + }, +}); + +ReactDOM.render( + React.createElement(network_records, null), + document.getElementById('content') +); diff --git a/bemani/frontend/static/controllers/danevo/settings.react.js b/bemani/frontend/static/controllers/danevo/settings.react.js new file mode 100644 index 0000000..60d8753 --- /dev/null +++ b/bemani/frontend/static/controllers/danevo/settings.react.js @@ -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 ( + { + !this.state.editing_name ? + <> + {player.name} + + : +
+ (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" + /> + + +
+ }
+ ); + }, + + render: function() { + if (this.state.player[this.state.version]) { + var player = this.state.player[this.state.version]; + return ( +
+
+ {this.state.profiles.map(function(version) { + return ( +
+
+

User Profile

+ {this.renderName(player)} +
+
+ ); + } else { + return ( +
+
+ You have no profile for {window.versions[this.state.version]}! +
+
+ {this.state.profiles.map(function(version) { + return ( +
+
+ ); + } + }, +}); + +ReactDOM.render( + React.createElement(settings_view, null), + document.getElementById('content') +); diff --git a/bemani/frontend/static/controllers/danevo/topscores.react.js b/bemani/frontend/static/controllers/danevo/topscores.react.js new file mode 100644 index 0000000..0988343 --- /dev/null +++ b/bemani/frontend/static/controllers/danevo/topscores.react.js @@ -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 ( +
+
+
{window.name}
+
{window.artist}
+
{window.genre}
+
Level {window.levels[chart]}
+
+
+ {valid_charts.map(function(chart) { + return ( +
+
+ { + this.state.players[topscore.userid].name + } + ); + }.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." + /> + + + ); + }, +}); + +ReactDOM.render( + React.createElement(top_scores, null), + document.getElementById('content') +); diff --git a/bemani/frontend/static/themes/dark/color.css b/bemani/frontend/static/themes/dark/color.css index 21c279b..7ae390c 100644 --- a/bemani/frontend/static/themes/dark/color.css +++ b/bemani/frontend/static/themes/dark/color.css @@ -33,3 +33,7 @@ .sdvx.border { border-color: #710162; } + +.danevo.border { + border-color: #2e5eee; +} diff --git a/bemani/frontend/static/themes/default/color.css b/bemani/frontend/static/themes/default/color.css index 21c279b..7ae390c 100644 --- a/bemani/frontend/static/themes/default/color.css +++ b/bemani/frontend/static/themes/default/color.css @@ -33,3 +33,7 @@ .sdvx.border { border-color: #710162; } + +.danevo.border { + border-color: #2e5eee; +} diff --git a/bemani/utils/frontend.py b/bemani/utils/frontend.py index dcc66e4..710abf9 100644 --- a/bemani/utils/frontend.py +++ b/bemani/utils/frontend.py @@ -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: diff --git a/bemani/utils/scheduler.py b/bemani/utils/scheduler.py index 60f9cea..5a119c5 100644 --- a/bemani/utils/scheduler.py +++ b/bemani/utils/scheduler.py @@ -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: