Ver código fonte

Make rank progress bar work

Clément Wolf 6 meses atrás
pai
commit
348ce06628
9 arquivos alterados com 464 adições e 126 exclusões
  1. 2 1
      .eslintrc.json
  2. 257 0
      countries.js
  3. 49 67
      elo.js
  4. 78 0
      elo_cache.js
  5. 3 0
      index.js
  6. 1 0
      user.js
  7. 69 58
      views/user.eta
  8. 3 0
      website_api.js
  9. 2 0
      website_ssr.js

+ 2 - 1
.eslintrc.json

@@ -18,5 +18,6 @@
     "new-cap": "off",
     "no-invalid-this": "off",
     "no-extend-native": "off"
-  }
+  },
+  "ignorePatterns": ["**/*.min.js"]
 }

+ 257 - 0
countries.js

@@ -0,0 +1,257 @@
+// Stolen from osu-web
+const countries = {
+  'A1': 'Anonymous Proxy',
+  'A2': 'Satellite Provider',
+  'AD': 'Andorra',
+  'AE': 'United Arab Emirates',
+  'AF': 'Afghanistan',
+  'AG': 'Antigua and Barbuda',
+  'AI': 'Anguilla',
+  'AL': 'Albania',
+  'AM': 'Armenia',
+  'AN': 'Netherlands Antilles',
+  'AO': 'Angola',
+  'AP': 'Asia/Pacific Region',
+  'AQ': 'Antarctica',
+  'AR': 'Argentina',
+  'AS': 'American Samoa',
+  'AT': 'Austria',
+  'AU': 'Australia',
+  'AW': 'Aruba',
+  'AX': 'Aland Islands',
+  'AZ': 'Azerbaijan',
+  'BA': 'Bosnia and Herzegovina',
+  'BB': 'Barbados',
+  'BD': 'Bangladesh',
+  'BE': 'Belgium',
+  'BF': 'Burkina Faso',
+  'BG': 'Bulgaria',
+  'BH': 'Bahrain',
+  'BI': 'Burundi',
+  'BJ': 'Benin',
+  'BL': 'Saint Barthelemy',
+  'BM': 'Bermuda',
+  'BN': 'Brunei',
+  'BO': 'Bolivia',
+  'BR': 'Brazil',
+  'BS': 'Bahamas',
+  'BT': 'Bhutan',
+  'BV': 'Bouvet Island',
+  'BW': 'Botswana',
+  'BY': 'Belarus',
+  'BZ': 'Belize',
+  'CA': 'Canada',
+  'CC': 'Cocos (Keeling) Islands',
+  'CD': 'Congo, The Democratic Republic of the',
+  'CF': 'Central African Republic',
+  'CG': 'Congo',
+  'CH': 'Switzerland',
+  'CI': 'Cote D\'Ivoire',
+  'CK': 'Cook Islands',
+  'CL': 'Chile',
+  'CM': 'Cameroon',
+  'CN': 'China',
+  'CO': 'Colombia',
+  'CR': 'Costa Rica',
+  'CU': 'Cuba',
+  'CV': 'Cabo Verde',
+  'CX': 'Christmas Island',
+  'CY': 'Cyprus',
+  'CZ': 'Czechia',
+  'DE': 'Germany',
+  'DJ': 'Djibouti',
+  'DK': 'Denmark',
+  'DM': 'Dominica',
+  'DO': 'Dominican Republic',
+  'DZ': 'Algeria',
+  'EC': 'Ecuador',
+  'EE': 'Estonia',
+  'EG': 'Egypt',
+  'EH': 'Western Sahara',
+  'ER': 'Eritrea',
+  'ES': 'Spain',
+  'ET': 'Ethiopia',
+  'EU': 'Europe',
+  'FI': 'Finland',
+  'FJ': 'Fiji',
+  'FK': 'Falkland Islands (Malvinas)',
+  'FM': 'Micronesia, Federated States of',
+  'FO': 'Faroe Islands',
+  'FR': 'France',
+  'FX': 'France, Metropolitan',
+  'GA': 'Gabon',
+  'GB': 'United Kingdom',
+  'GD': 'Grenada',
+  'GE': 'Georgia',
+  'GF': 'French Guiana',
+  'GG': 'Guernsey',
+  'GH': 'Ghana',
+  'GI': 'Gibraltar',
+  'GL': 'Greenland',
+  'GM': 'Gambia',
+  'GN': 'Guinea',
+  'GP': 'Guadeloupe',
+  'GQ': 'Equatorial Guinea',
+  'GR': 'Greece',
+  'GS': 'South Georgia and the South Sandwich Islands',
+  'GT': 'Guatemala',
+  'GU': 'Guam',
+  'GW': 'Guinea-Bissau',
+  'GY': 'Guyana',
+  'HK': 'Hong Kong',
+  'HM': 'Heard Island and McDonald Islands',
+  'HN': 'Honduras',
+  'HR': 'Croatia',
+  'HT': 'Haiti',
+  'HU': 'Hungary',
+  'ID': 'Indonesia',
+  'IE': 'Ireland',
+  'IL': 'Israel',
+  'IM': 'Isle of Man',
+  'IN': 'India',
+  'IO': 'British Indian Ocean Territory',
+  'IQ': 'Iraq',
+  'IR': 'Iran, Islamic Republic of',
+  'IS': 'Iceland',
+  'IT': 'Italy',
+  'JE': 'Jersey',
+  'JM': 'Jamaica',
+  'JO': 'Jordan',
+  'JP': 'Japan',
+  'KE': 'Kenya',
+  'KG': 'Kyrgyzstan',
+  'KH': 'Cambodia',
+  'KI': 'Kiribati',
+  'KM': 'Comoros',
+  'KN': 'Saint Kitts and Nevis',
+  'KP': 'Korea, Democratic People\'s Republic of',
+  'KR': 'South Korea',
+  'KW': 'Kuwait',
+  'KY': 'Cayman Islands',
+  'KZ': 'Kazakhstan',
+  'LA': 'Lao People\'s Democratic Republic',
+  'LB': 'Lebanon',
+  'LC': 'Saint Lucia',
+  'LI': 'Liechtenstein',
+  'LK': 'Sri Lanka',
+  'LR': 'Liberia',
+  'LS': 'Lesotho',
+  'LT': 'Lithuania',
+  'LU': 'Luxembourg',
+  'LV': 'Latvia',
+  'LY': 'Libya',
+  'MA': 'Morocco',
+  'MC': 'Monaco',
+  'MD': 'Moldova',
+  'ME': 'Montenegro',
+  'MF': 'Saint Martin',
+  'MG': 'Madagascar',
+  'MH': 'Marshall Islands',
+  'MK': 'North Macedonia',
+  'ML': 'Mali',
+  'MM': 'Myanmar',
+  'MN': 'Mongolia',
+  'MO': 'Macau',
+  'MP': 'Northern Mariana Islands',
+  'MQ': 'Martinique',
+  'MR': 'Mauritania',
+  'MS': 'Montserrat',
+  'MT': 'Malta',
+  'MU': 'Mauritius',
+  'MV': 'Maldives',
+  'MW': 'Malawi',
+  'MX': 'Mexico',
+  'MY': 'Malaysia',
+  'MZ': 'Mozambique',
+  'NA': 'Namibia',
+  'NC': 'New Caledonia',
+  'NE': 'Niger',
+  'NF': 'Norfolk Island',
+  'NG': 'Nigeria',
+  'NI': 'Nicaragua',
+  'NL': 'Netherlands',
+  'NO': 'Norway',
+  'NP': 'Nepal',
+  'NR': 'Nauru',
+  'NU': 'Niue',
+  'NZ': 'New Zealand',
+  'O1': 'Other',
+  'OM': 'Oman',
+  'PA': 'Panama',
+  'PE': 'Peru',
+  'PF': 'French Polynesia',
+  'PG': 'Papua New Guinea',
+  'PH': 'Philippines',
+  'PK': 'Pakistan',
+  'PL': 'Poland',
+  'PM': 'Saint Pierre and Miquelon',
+  'PN': 'Pitcairn',
+  'PR': 'Puerto Rico',
+  'PS': 'Palestine, State of',
+  'PT': 'Portugal',
+  'PW': 'Palau',
+  'PY': 'Paraguay',
+  'QA': 'Qatar',
+  'RE': 'Reunion',
+  'RO': 'Romania',
+  'RS': 'Serbia',
+  'RU': 'Russian Federation',
+  'RW': 'Rwanda',
+  'SA': 'Saudi Arabia',
+  'SB': 'Solomon Islands',
+  'SC': 'Seychelles',
+  'SD': 'Sudan',
+  'SE': 'Sweden',
+  'SG': 'Singapore',
+  'SH': 'Saint Helena',
+  'SI': 'Slovenia',
+  'SJ': 'Svalbard and Jan Mayen',
+  'SK': 'Slovakia',
+  'SL': 'Sierra Leone',
+  'SM': 'San Marino',
+  'SN': 'Senegal',
+  'SO': 'Somalia',
+  'SR': 'Suriname',
+  'ST': 'Sao Tome and Principe',
+  'SV': 'El Salvador',
+  'SY': 'Syrian Arab Republic',
+  'SZ': 'Eswatini',
+  'TC': 'Turks and Caicos Islands',
+  'TD': 'Chad',
+  'TF': 'French Southern Territories',
+  'TG': 'Togo',
+  'TH': 'Thailand',
+  'TJ': 'Tajikistan',
+  'TK': 'Tokelau',
+  'TL': 'Timor-Leste',
+  'TM': 'Turkmenistan',
+  'TN': 'Tunisia',
+  'TO': 'Tonga',
+  'TR': 'Türkiye',
+  'TT': 'Trinidad and Tobago',
+  'TV': 'Tuvalu',
+  'TW': 'Taiwan',
+  'TZ': 'Tanzania, United Republic of',
+  'UA': 'Ukraine',
+  'UG': 'Uganda',
+  'UM': 'United States Minor Outlying Islands',
+  'US': 'United States',
+  'UY': 'Uruguay',
+  'UZ': 'Uzbekistan',
+  'VA': 'Holy See (Vatican City State)',
+  'VC': 'Saint Vincent and the Grenadines',
+  'VE': 'Venezuela',
+  'VG': 'Virgin Islands, British',
+  'VI': 'Virgin Islands, U.S.',
+  'VN': 'Vietnam',
+  'VU': 'Vanuatu',
+  'WF': 'Wallis and Futuna',
+  'WS': 'Samoa',
+  'YE': 'Yemen',
+  'YT': 'Mayotte',
+  'ZA': 'South Africa',
+  'ZM': 'Zambia',
+  'ZW': 'Zimbabwe',
+};
+
+export default countries;

+ 49 - 67
elo.js

@@ -3,25 +3,9 @@ import db from './database.js';
 import Config from './util/config.js';
 import {get_user_by_id} from './user.js';
 import {gen_url} from './util/helpers.js';
+import {get_division_from_elo, get_rankup_progress} from './elo_cache.js';
 
 
-const RANK_DIVISIONS = [
-  'Cardboard',
-  'Wood',
-  'Wood+',
-  'Bronze',
-  'Bronze+',
-  'Silver',
-  'Silver+',
-  'Gold',
-  'Gold+',
-  'Platinum',
-  'Platinum+',
-  'Diamond',
-  'Diamond+',
-  'Legendary',
-];
-
 const get_rating_stmt = db.prepare(`SELECT * FROM rating WHERE user_id = ? AND mode = ?`);
 const update_rating_stmt = db.prepare(`
   UPDATE rating SET
@@ -161,34 +145,36 @@ async function save_game_and_update_rating(lobby, game) {
     return;
   }
 
+  const RANK_DIVISIONS = [
+    'Unranked',
+    'Cardboard',
+    'Wood',
+    'Wood+',
+    'Bronze',
+    'Bronze+',
+    'Silver',
+    'Silver+',
+    'Gold',
+    'Gold+',
+    'Platinum',
+    'Platinum+',
+    'Diamond',
+    'Diamond+',
+    'Legendary',
+    'The One',
+  ];
   const rank_changes = [];
-  const division_to_index = (text) => {
-    if (text == 'Unranked') {
-      return -1;
-    } else if (text == 'The One') {
-      return RANK_DIVISIONS.length;
-    } else {
-      return RANK_DIVISIONS.indexOf(text);
-    }
-  };
-
-  const active_users = get_active_users(game.mode_int);
   for (const score of game.scores) {
     const player = await get_user_by_id(score.user_id);
     if (player.ratings[game.mode_int].total_scores < Config.games_needed_for_rank) continue;
 
-    const better_users = better_users_stmt.get(
-        Config.games_needed_for_rank, game.mode_int,
-        Config.games_needed_for_rank, game.mode_int,
-        player.user_id,
-    );
-    const ratio = 1.0 - (better_users.nb / active_users);
+    // TODO: handle "The One" (get_division_from_elo doesn't get it)
     const old_rank_text = player.ratings[game.mode_int].division;
-    const new_rank_text = get_rank_text(ratio);
+    const new_rank_text = get_division_from_elo(score.rating.elo, game.mode_int);
 
     if (old_rank_text != new_rank_text) {
       const user_page = gen_url(game.mode_int, `/u/${player.user_id}/`);
-      const direction = (division_to_index(new_rank_text) > division_to_index(old_rank_text)) ? '▲' : '▼';
+      const direction = (RANK_DIVISIONS.indexOf(new_rank_text) > RANK_DIVISIONS.indexOf(old_rank_text)) ? '▲' : '▼';
       rank_changes.push(`${player.username} [${user_page} ${direction} ${new_rank_text} ]`);
       db.prepare(`UPDATE rating SET division = ? WHERE user_id = ? AND mode = ?`).run(new_rank_text, player.user_id, game.mode_int);
     }
@@ -210,34 +196,12 @@ async function save_game_and_update_rating(lobby, game) {
 }
 
 
-function get_rank_text(rank_float) {
-  if (rank_float == 1.0) {
-    return 'The One';
-  }
-
-  // Epic rank distribution algorithm
-  for (let i = 0; i < RANK_DIVISIONS.length; i++) {
-    // Turn current 'Cardboard' rank into a value between 0 and 1
-    const rank_nb = (i + 1) / RANK_DIVISIONS.length;
-
-    // To make climbing ranks more satisfying, we make lower ranks more common.
-    // Visual representation: https://graphtoy.com/?f1(x,t)=1-((cos(x%5E0.8*%F0%9D%9C%8B)/2)+0.5)&v1=true&f2(x,t)=&v2=true&f3(x,t)=&v3=false&f4(x,t)=&v4=false&f5(x,t)=&v5=false&f6(x,t)=&v6=false&grid=true&coords=0.3918011117299855,0.3722110561434862,1.0068654346588846
-    const cutoff = 1 - ((Math.cos(Math.pow(rank_nb, 0.8) * Math.PI) / 2) + 0.5);
-    if (rank_float < cutoff) {
-      return RANK_DIVISIONS[i];
-    }
-  }
-
-  // Ok, floating point errors, who cares
-  return RANK_DIVISIONS[RANK_DIVISIONS.length - 1];
-}
-
-
 function get_active_users(ruleset) {
   return nb_active_users[ruleset];
 }
 
 
+// TODO: optimize
 function get_user_rank(user_id, ruleset) {
   if (!user_id) return null;
 
@@ -247,24 +211,42 @@ function get_user_rank(user_id, ruleset) {
     return null;
   }
 
-  const active_users = get_active_users(ruleset);
   const better_users = better_users_stmt.get(
       Config.games_needed_for_rank, ruleset,
       Config.games_needed_for_rank, ruleset,
       user_id,
   );
-  const ratio = 1.0 - (better_users.nb / active_users);
   const is_ranked = rating.total_scores >= Config.games_needed_for_rank;
 
-  return {
-    elo: is_ranked ? rating.elo : '???',
-    rank_nb: is_ranked ? (better_users.nb + 1) : '???',
-    text: is_ranked ? get_rank_text(ratio) : 'Unranked',
-    ratio: ratio,
-    total_nb: active_users,
+  const info = {
+    elo: '???',
+    rank_nb: '???',
+    text: 'Unranked',
     total_scores: rating.total_scores,
     is_ranked: is_ranked,
   };
+
+  if (is_ranked) {
+    info.elo = rating.elo;
+    info.rank_nb = (better_users.nb + 1);
+
+    if (better_users.nb == 0) {
+      // TODO: this is jank - should rely on get_rankup_progress instead
+      info.text = 'The One';
+      info.rankup = {
+        floor_name: 'The One',
+        floor_elo: rating.elo,
+        curr_elo: rating.elo,
+        ceil_name: 'The One',
+        ceil_elo: rating.elo,
+      };
+    } else {
+      info.text = get_division_from_elo(rating.elo, ruleset);
+      info.rankup = get_rankup_progress(rating.elo, ruleset);
+    }
+  }
+
+  return info;
 }
 
 

+ 78 - 0
elo_cache.js

@@ -0,0 +1,78 @@
+import db from './database.js';
+import Config from './util/config.js';
+
+
+const division_tresholds = [[], [], [], []];
+const RANK_DIVISIONS = [
+  'Cardboard',
+  'Wood',
+  'Wood+',
+  'Bronze',
+  'Bronze+',
+  'Silver',
+  'Silver+',
+  'Gold',
+  'Gold+',
+  'Platinum',
+  'Platinum+',
+  'Diamond',
+  'Diamond+',
+  'Legendary',
+];
+
+
+function update_division_tresholds() {
+  console.time('update_division_tresholds');
+  for (let j = 0; j < 4; j++) {
+    // Really stupid query but that's the fastest way I found...
+    const elos = db.prepare('SELECT elo FROM rating WHERE total_scores >= ? AND mode = ? ORDER BY elo ASC').all(Config.games_needed_for_rank, j);
+
+    for (let i = 0; i < RANK_DIVISIONS.length; i++) {
+      // Turn current 'Cardboard' rank into a value between 0 and 1
+      const rank_nb = (i + 1) / RANK_DIVISIONS.length;
+
+      // To make climbing divisions more satisfying, we make lower divisions more common.
+      // Visual representation: https://graphtoy.com/?f1(x,t)=1-((cos(x%5E0.8*%F0%9D%9C%8B)/2)+0.5)&v1=true&f2(x,t)=&v2=true&f3(x,t)=&v3=false&f4(x,t)=&v4=false&f5(x,t)=&v5=false&f6(x,t)=&v6=false&grid=true&coords=0.3918011117299855,0.3722110561434862,1.0068654346588846
+      const cutoff = 1 - ((Math.cos(Math.pow(rank_nb, 0.8) * Math.PI) / 2) + 0.5);
+
+      // Get elo at this cutoff point for each gamemode
+      const offset = Math.floor(elos.length * cutoff) - 1;
+      division_tresholds[j][i] = elos[offset].elo;
+    }
+  }
+  console.timeEnd('update_division_tresholds');
+
+  setTimeout(update_division_tresholds, 3600 * 1000);
+}
+
+
+function get_division_from_elo(elo, mode) {
+  for (let i = 0; i < RANK_DIVISIONS.length; i++) {
+    if (elo < division_tresholds[mode][i]) {
+      return RANK_DIVISIONS[i];
+    }
+  }
+}
+
+function get_rankup_progress(elo, mode) {
+  for (let i = 0; i < RANK_DIVISIONS.length; i++) {
+    if (elo > division_tresholds[mode][i]) continue;
+    return {
+      floor_name: RANK_DIVISIONS[i],
+      floor_elo: i == 0 ? 0 : division_tresholds[mode][i - 1],
+      curr_elo: elo,
+      ceil_name: RANK_DIVISIONS[i + 1],
+      ceil_elo: division_tresholds[mode][i],
+    };
+  }
+
+  return {
+    floor_name: RANK_DIVISIONS[RANK_DIVISIONS.length - 1],
+    floor_elo: division_tresholds[mode][RANK_DIVISIONS.length - 1],
+    curr_elo: elo,
+    ceil_name: 'The One',
+    ceil_elo: 3000, // TODO
+  };
+}
+
+export {get_division_from_elo, get_rankup_progress, update_division_tresholds};

+ 3 - 0
index.js

@@ -9,6 +9,7 @@ import {listen as website_listen} from './website.js';
 import {init_lobby} from './ranked.js';
 import Config from './util/config.js';
 import {capture_sentry_exception} from './util/helpers.js';
+import {update_division_tresholds} from './elo_cache.js';
 
 
 async function rejoin_lobbies() {
@@ -91,6 +92,8 @@ async function main() {
     });
   }
 
+  update_division_tresholds();
+
   bancho.on('pm', (msg) => {
     for (const cmd of commands) {
       const match = cmd.regex.exec(msg.message);

+ 1 - 0
user.js

@@ -16,6 +16,7 @@ async function init_user(user_id) {
   const user_data = {
     username: 'New user',
     country_code: 'Unknown',
+    country_name: 'Unknown country',
     last_visit: new Date(0).toISOString(),
   };
   const user = db.prepare(`

+ 69 - 58
views/user.eta

@@ -53,68 +53,79 @@
         <path fill="none" stroke="url('#gradient')" d="${d}" stroke-width="4" />
       </svg>`;
   }
+
+  let rankup_percent = (it.rank.elo - it.rank.rankup.floor_elo) / (it.rank.rankup.ceil_elo - it.rank.rankup.floor_elo);
+  rankup_percent = Math.round(rankup_percent * 100);
 %>
 
-<div class="flex justify-center">
-  <img class="block h-16 rounded-full" src="https://s.ppy.sh/a/<%= it.user.user_id %>" />
-  <div class="ml-4">
-    <h1 class="ml-2"><%~ it.flag %><%= ' ' + it.user.username %></h1>
-    <a class="subheading ml-3" target="_blank" href="https://osu.ppy.sh/users/<%= it.user.user_id %>">osu! profile</a>
-  </div>
-</div>
-<div class="max-w-2xl m-auto">
-  <% if(it.rank.is_ranked) { %>
-  <div class="flex m-auto p-2">
-    <%~ gen_chart([1500, 1600, 1800, 1750, 1400, 1500, 1600, 1500, 1500, 1600, 1800, 1750, 1400, 1500, 1600, 1500]) %>
-    <div class="inline-flex flex-col p-2 m-4 whitespace-nowrap">
-      <span class="text-lg">Rank #<%= it.rank.rank_nb %></span>
-      <span class="text-lg"><%= fancy_elo(it.rank.elo) + ' ' %>elo</span>
-      <span class="text-lg"><%= it.rank.total_scores + ' ' %>games played</span>
+<div class="flex flex-wrap gap-6 justify-center">
+  <div class="w-80">
+    <div class="flex">
+      <img class="block h-24 rounded-full" src="https://s.ppy.sh/a/<%= it.user.user_id %>" />
+      <div class="ml-6 mt-3">
+        <h1><%= it.user.username %></h1>
+        <h3 class="text-lg"><%~ it.flag + ' ' + it.country_name %></h3>
+      </div>
+    </div>
+    <% if(it.rank.is_ranked) { %>
+    <div class="text-center mt-4">
+      <span class="text-xl <%= division_to_class[it.rank.text] %>"><%= it.rank.text %></span>
+      <% if(it.rank.rankup.floor_name != 'The One') { %>
+      <div class="progress bg-gray-300" style="width:100%">
+        <div class="bar bg-orange-600" style="width: <%= rankup_percent %>%; height: 20px"></div>
+      </div>
+      <%= Math.ceil(it.rank.rankup.ceil_elo - it.rank.elo) + ' elo to ' + it.rank.rankup.ceil_name %>
+      <% } %>
+    </div>
+    <div class="p-2">
+      <%~ gen_chart([1500, 1600, 1800, 1750, 1400, 1500, 1600, 1500, 1500, 1600, 1800, 1750, 1400, 1500, 1600, 1500]) %>
+    </div>
+    <% } %>
+    <div class="mt-4">
+      <ul>
+        <li><span class="text-lg">Rank #<%= it.rank.rank_nb %></span></li>
+        <li><span class="text-lg"><%= fancy_elo(it.rank.elo) + ' ' %>elo</span></li>
+        <li><span class="text-lg"><%= it.rank.total_scores + ' ' %>games played</span></li>
+        <li><a target="_blank" href="https://osu.ppy.sh/users/<%= it.user.user_id %>">osu! profile</a></li>
+      </ul>
     </div>
   </div>
-  <% } %>
-</div>
-<div class="text-center">
-  <span class="text-xl <%= division_to_class[it.rank.text] %>"><%= it.rank.text %></span>
-  <div class="progress bg-gray-300" style="width:100%">
-    <div class="bar bg-orange-600" style="width: 70%; height: 20px"></div>
-  </div>
-  654 elo to Legendary
-</div>
 
-<div class="max-w-5xl m-auto">
-  <h2>Match History</h2>
-  <table class="match-history block my-4">
-    <thead class="flex flex-col">
-      <tr class="inline-flex justify-between">
-        <td class="map grow w-40 p-1.5 text-center font-bold">Map</td>
-        <td class="w-40 p-1.5 text-center font-bold">Placement</td>
-        <td class="w-40 p-1.5 text-center font-bold">Elo change</td>
-        <td class="w-40 p-1.5 text-center font-bold">Time</td>
-      </tr>
-    </thead>
-    <tbody class="flex flex-col map-list">
-      <% it.matches.matches.forEach((match) => {
-        let match_elo = Math.round(match.elo_diff);
-        if(match_elo > 0) match_elo = '+' + match_elo;
-      %>
-      <tr class="inline-flex justify-between">
-        <td class="map grow">
-          <a href="https://osu.ppy.sh/beatmapsets/<%= match.map.set_id %>#<%= it.osu_ruleset %>/<%= match.map.map_id %>"><%= match.map.name %></a>
-        </td>
-        <td><%= match.placement %>/<%= match.nb_players %></td>
-        <td class="<%= match.elo_diff >= 0 ? 'text-green-500' : 'text-red-500' %>"><%= match_elo %></td>
-        <td data-tms="<%= match.tms %>"><%= match.time %></td>
-      </tr>
-      <% }) %>
-    </tbody>
-  </table>
+  <div class="max-w-5xl">
+    <h2>Match History</h2>
+    <table class="match-history block my-4">
+      <thead class="flex flex-col">
+        <tr class="inline-flex justify-between">
+          <td class="map grow w-40 p-1.5 text-center font-bold">Map</td>
+          <td class="w-40 p-1.5 text-center font-bold">Placement</td>
+          <td class="w-40 p-1.5 text-center font-bold">Elo change</td>
+          <td class="w-40 p-1.5 text-center font-bold">Time</td>
+        </tr>
+      </thead>
+      <tbody class="flex flex-col map-list">
+        <% it.matches.matches.forEach((match) => {
+          let match_elo = Math.round(match.elo_diff);
+          if(match_elo > 0) match_elo = '+' + match_elo;
+        %>
+        <tr class="inline-flex justify-between">
+          <td class="map grow">
+            <a href="https://osu.ppy.sh/beatmapsets/<%= match.map.set_id %>#<%= it.osu_ruleset %>/<%= match.map.map_id %>"><%= match.map.name %></a>
+          </td>
+          <td><%= match.placement %>/<%= match.nb_players %></td>
+          <td class="<%= match.elo_diff >= 0 ? 'text-green-500' : 'text-red-500' %>"><%= match_elo %></td>
+          <td data-tms="<%= match.tms %>"><%= match.time %></td>
+        </tr>
+        <% }) %>
+      </tbody>
+    </table>
+
+    <%~
+      include('pagination.eta', {
+        page_num: it.matches.page,
+        max_pages: it.matches.max_pages,
+        url_formatter: (num) => `/u/${it.user.user_id}/page-${num}/`
+      })
+    %>
+  </div>
 
-  <%~
-    include('pagination.eta', {
-      page_num: it.matches.page,
-      max_pages: it.matches.max_pages,
-      url_formatter: (num) => `/u/${it.user.user_id}/page-${num}/`
-    })
-  %>
 </div>

+ 3 - 0
website_api.js

@@ -11,6 +11,7 @@ dayjs.extend(relativeTime);
 
 import Config from './util/config.js';
 import bancho from './bancho.js';
+import countries from './countries.js';
 import db from './database.js';
 import {get_user_rank, get_active_users} from './elo.js';
 import {init_lobby, get_new_title} from './ranked.js';
@@ -210,6 +211,7 @@ async function get_leaderboard_page(mode, page_num) {
     data.players.push({
       user_id: user.user_id,
       country_code: user.country_code,
+      country_name: countries[user.country_code] || 'Unknown country',
       username: user.username,
       ranking: ranking,
       elo: Math.round(user.elo),
@@ -233,6 +235,7 @@ async function get_user_profile(user_id, mode) {
   return {
     username: user.username,
     country_code: user.country_code,
+    country_name: countries[user.country_code] || 'Unknown country',
     user_id: user.user_id,
     ranks: ranks,
   };

+ 2 - 0
website_ssr.js

@@ -3,6 +3,7 @@ import {Eta} from 'eta';
 import twemoji from 'twemoji';
 
 import Config from './util/config.js';
+import countries from './countries.js';
 import {get_user_by_id} from './user.js';
 import {get_user_rank} from './elo.js';
 import {get_user_matches, get_leaderboard_page} from './website_api.js';
@@ -86,6 +87,7 @@ async function render_user_profile(req, res, page_num) {
   return res.end(eta.render('user.eta', {
     user, rank, matches,
     flag: country_code_to_flag_html(user.country_code),
+    country_name: countries[user.country_code] || 'Unknown country',
     osu_ruleset: osu_rulesets[mode],
   }));
 }