Files
zkldi_Tachi/database-seeds/scripts/rerunners/add-sdvx-clear-tierlist.js
T

556 lines
18 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const { Command } = require("commander");
const { parse } = require("csv-parse/sync");
const fs = require("fs");
const { ReadCollection, MutateCollection } = require("../util");
// The tier lists are available at
// https://docs.google.com/spreadsheets/d/1cFltguBvPplBem-x1STHnG3k4TZzFfyNEZ-RwsQszoo/edit
// Download a given level's tierlist as CSV and use -f <CSV filename>.
// The level number will be picked up from the filename.
const SUPER_INDIV_DIFFERENCE = "超個人差";
const TIERS = {
// 16 tiers are really weird for no good reason.
16: {
"16逆詐称(16F)": {
text: "16F",
value: 16.0,
},
"16弱(16EとF)": {
text: "16E",
value: 16.1,
},
"16中(16CとD)": {
text: "16C/D",
value: 16.3,
},
"16強(16AとB)": {
text: "16A/B",
value: 16.5,
},
"16強+(16A以上S未満)": {
text: "16A+",
value: 16.7,
},
"16詐称(16S)": {
text: "16S",
value: 16.9,
},
},
17: {
"F-": {
text: "17F-",
value: 17.0,
},
F: {
text: "17F",
value: 17.1,
},
E: {
text: "17E",
value: 17.2,
},
D: {
text: "17D",
value: 17.3,
},
"C": {
text: "17C",
value: 17.4,
},
B: {
text: "17B",
value: 17.5,
},
"B+": {
text: "17B+",
value: 17.6,
},
A: {
text: "17A",
value: 17.7,
},
"A+": {
text: "17A+",
value: 17.8,
},
// There is no S, don't ask me why.
},
18: {
F: {
text: "18F",
value: 18.0,
},
E: {
text: "18E",
value: 18.1,
},
D: {
text: "18D",
value: 18.3,
},
C: {
text: "18C",
value: 18.5,
},
B: {
text: "18B",
value: 18.6,
},
A: {
text: "18A",
value: 18.7,
},
"A+": {
text: "18A+",
value: 18.8,
},
S: {
text: "18S",
value: 18.9,
},
SS: {
// Literally just Joyeuse lmao
text: "18SS",
value: 19.5, //if this were a score tier list this would be in the 20s
},
},
19: {
F: {
text: "19F",
value: 19.0,
},
E: {
text: "19E",
value: 19.1,
},
D: {
text: "19D",
value: 19.3,
},
C: {
text: "19C",
value: 19.4,
},
B: {
text: "19B",
value: 19.5,
},
A: {
text: "19A",
value: 19.7,
},
"A+": {
text: "19A+",
value: 19.9,
},
S: {
text: "19S",
value: 20.0,
},
},
20: {
B: {
text: "20B",
value: 20.1,
},
A: {
text: "20A",
value: 20.3,
},
"A+": {
text: "20A+",
value: 20.5,
},
S: {
text: "20S",
value: 20.5, // These are literally no harder than anything in A+ clear wise
},
},
};
const MANUAL_TITLE_MAP = {
// 16s
"jack-the-Ripper♦": "Jack-the-Ripper◆",
"50th Memorial Songs-二人の時-": "50th Memorial Songs -二人の時 ~under the cherry blossoms~-",
// "Help me, CODYYYYYY!!" is a special title that only applies to the GRV... why, sdvx, why
"Help me,CODYYYYYY!!": "Help me, ERINNNNNN!! - SH Style -",
"Togather Going My Way": "Together Going My Way", // lol
"コンベア速度Maxしゃいにん☆廻転ズシ": 'コンベア速度Max!? しゃいにん☆廻転ズシ"Sushi&Peace"',
"Venomous Firefry": "Venomous Firefly", // you fry them up real good
"Genesis At Oasis(Matsudo)": "Genesis At Oasis (Hirayasu Matsudo Remix)",
"FIRST:DREAM": "FIRST:DREAMS",
"Flaa Behaivor": "Flaa Behavior",
FlwoerNation: "FlowerNation",
"けもののおうじゃ めうめう": "けもののおうじゃ🐾めうめう",
すきなことでいいです: "すきなことだけでいいです",
"U.N.オーエンは彼女なのか(TO-HOlic)": "U.N.オーエンは彼女なのか?(TO-HOlic mix)",
"Elemental Creation(Kamome mix)": "Elemental Creation (kamome sano Remix)",
"Engraved Mark -Gow's ill!-": "Engraved Mark-Gow's ill! RMX-",
"Make Majic": "Make Magic", // asdf
"PHYCHO+HEROES": "PSYCHO+HEROES",
"夏色DIARY -SD'VmiX-": "夏色DIARY -Summer Dazzlin' Vacation miX-",
"ユニバーページ(i-word Mix)": "ユニバーページ(i-world Mix)",
// In their defence I make this typo all the time
"50th Memorial Songs -Begining Story-": "50th Memorial Songs -Beginning Story-",
// This is NOT "Game Over". This is an EG chart and isn't in this
// repo yet but better to avoid confusion down the line.
"GAME ØVER": "G4ME ØVEЯ",
"Narcissus At Oasis 影虎。style": "Narcissus At Oasis -影虎。 style-",
"POSSESSION (Aoi Q.E.DRMX)": "POSSESSION (Aoi Q.E.D. RMX)",
"Narcissus At Oasis (Freezer)": "Narcissus At Oasis (Freezer Remix)",
// There are two prefix-matches for this, so we need to be explicit.
RPG: "RPG/アニメ「映画クレヨンしんちゃん バカうまっ!B級グルメサバイバル!!」より",
Applique: "Appliqué",
"Daydream Cafê(Euro Hopping Mix)": "Daydream café (Euro Hopping Mix)",
"Thank you for playing music": "Thank you for your playing music", // LMFAOOO
"赤より紅い夢-Aya2g Tech Dance Rmx-": "赤より紅い夢-Aya2g Tech Dance Remix-",
"動く、動く(Electro Remix)": '動く、動く("A&M Chillin\' " Electro Remix)',
幻想郷DENPASTICグリーティング: "幻想郷DEMPASTICグリーティング",
"雪月花 (Shiron)": "雪月花 (Shiron & Sound Artz Remix)",
"闇夜舞踏会 -緋碧と蝶のための-masquerade-": "闇夜舞踏会 -緋碧と蝶のためのmasquerade-",
"少年は空を辿る Prog Piano Rmx": "少年は空を辿る Prog Piano Remix",
"EOS INFINITE EDIT": "EOS -INFINITE EDIT-",
"Now Loading...": "Now loading…",
"しゅわスパ大作戦☆(カシオれ!くーにゃん)": "しゅわスパ大作戦☆ (カシオれ!くーにゃんリミックス)",
"双翼 Black Wings - SDVX Edit. -": "双翼 - Black Wings - SDVX Edit. -",
"VALKYRIE ASAULT": "VALKYRIE ASSAULT",
"SuperMiracleEmsemble": "SuperMiracleEnsemble",
"仔羊のナヴァラン・クリシェを添えて": "~仔羊のナヴァラン・クリシェを添えて~",
// 17s
"Emperors divide": "Emperor's Divide",
"TENKAICHI ULTIMATE MEDLEY": "TENKAICHI ULTIMATE BOSSRUSH MEDLEY",
"Believe (y)our Wings{V:VID RAYS}": "Believe (y)our Wings {V:IVID RAYS}",
"Help me ERINNNNNN!! -Cranky remix-": "Help me, ERINNNNNN!! -Cranky remix-",
"おーまい!らぶりー!すうぃーてぃー!だーりん!":
"おーまい!らぶりー!すうぃーてぃ!だーりん!",
"サヨナラ・ヘヴン(かめりあ`sRMX)": "サヨナラ・ヘヴン (かめりあ's NEKOMATAelectroRMX)",
超超光速スピードスターかなで: "超☆超☆光☆速☆出☆前☆最☆速!!! スピード★スター★かなで", // understandable
熱情のザパデアート: "熱情のサパデアード",
"ゆりゆらららゆるゆり大事件 (yuzenリミ)": "ゆりゆららららゆるゆり大事件(yuzen remix)",
// I am not changing the parser to account for missing brackets, and this would
// need an override anyways. Included the second one in case they fix it later.
"Bule Forest (Prog Key Remix)[MXM": "Blue Forest (Prog Keys Remix)",
"Bule Forest (Prog Key Remix)": "Blue Forest (Prog Keys Remix)",
チルノとまりおのパーフェクト算数教室: "チルノとまりおのパーフェクトさんすう教室", // I guess???
物凄いスペースシャトルでこいしが物凄いうた: "物凄いスペースシャトルでこいしが物凄いうた", // also not 100% sure
"Iridescent Crouds": "Iridescent Clouds", // there's so many people I can't see!
"感情の摩天楼~Arr.Demetori": "感情の魔天楼 ~ Arr.Demetori",
".59 -BOOTH REMIX-": ".59 -BOOTH BOOST REMIX-",
"One In A Billion(Hedonist Rimix)": "One In A Billion(Hedonist Remix)",
"cloche(といぼっくすうぃんぐ りみっくす)": "cloche(といぼっくすうぃんぐ みっくす)",
"Sacrifce Escape: 不条理の模倣による感情と代償":
"Sacrifice Escape: 不条理の模倣による感情と代償",
"The Sampling Paradise(P*Light)": "The Sampling Paradise (P*Light Remix)",
"イゴモヨスのブヨブヨ・スケッチ": "イゴモヨス=オムルのテーマによるブヨブヨ・スケッチの試み",
"ABSOLUTE(ismk passionate mix)": "ABSOLUTE(ismK passionate remix)",
"泥の分際で私だけの大切を奪おうなんて" : "泥の分際で私だけの大切を奪おうだなんて",
"Rhapsody ⚙︎f Triumph": "Rhapsody ⚙f Triumph", // There is some weird non-printing character here fml
// 18s
"*Erm,~ ShockWAVE Syndrome...?": "* Erm, could it be a Spatiotemporal ShockWAVE Syndrome...?",
Idora: "Idola", // I should just normalize out R/L...
"KAC 2013 MEDLEY Empress Side": "KAC 2013 ULTIMATE MEDLEY -HISTORIA SOUND VOLTEX- Empress Side",
"KAC 2013 MEDLEY Emperor Side": "KAC 2013 ULTIMATE MEDLEY -HISTORIA SOUND VOLTEX- Emperor Side",
"She is my wife ミツル子Remixちゃん": "She is my wife すーぱーアイドル☆ミツル子Remixちゃん",
AΩ: "ΑΩ", // they use latin A instead of alpha
"Electric Injuly": "Electric Injury", // holy shit dude
"Unicorn tail Dustnoxxxx RMX": "Unicorn tail Dustboxxxx RMX",
"混乱少女そふらんちゃん!!": "混乱少女♥そふらんちゃん!!",
"消失(Hommarju Rremix)": "消失(Hommarju Remix)", // ur supposed to roll the Rr
"Sakura Reflection(P*Light Remix)": "Sakura Reflection (P*Light Slayer Remix)",
"怪盗Fの台本 ~消えたダイヤの謎~": "怪盗Fの台本~消えたダイヤの謎~", // F
"アルティメットトゥルース_-Phantasm-": "アルティメットトゥルース -Phantasm-",
"G4ME OVEЯ": "G4ME ØVEЯ", // This is also in 16s spelled differently :////
// 19s
// See Blue Forest
"Cross Fire[MXM": "Cross Fire",
};
function validTiers(levelNum) {
return Object.keys(TIERS[levelNum]).concat([SUPER_INDIV_DIFFERENCE]);
}
function normalizeTitle(title) {
return title
.toLowerCase()
.replace(/ /g, "")
.replace(/ /g, "")
.replace(/ /g, "")
.replace(/:/g, ":")
.replace(/(/g, "(")
.replace(/)/g, ")")
.replace(/!/g, "!")
.replace(/?/g, "?")
.replace(/`/g, "'")
.replace(/’/g, "'")
.replace(/~/g, "~");
}
function findSong(songs, title) {
// There are two songs called "Life is [Bb]eautiful". Yes, really.
// I CANNOT be assed to search both songs by level or case-sensitive.
if (title === "Life is beautiful") {
return songs.find((song) => song.id === 1264);
}
const song = songs.find(
(song) =>
normalizeTitle(song.title) === normalizeTitle(title) ||
song.title === MANUAL_TITLE_MAP[title]
);
if (song) {
return song;
}
// Only do prefix match _after_ trying normal match, since there are some
// correct song titles that are also prefixes of other song titles
// (e.g. チルノのパーフェクトさんすう教室 and チルノのパーフェクトさんすう教室 ⑨周年バージョン
// or Elemental Creation and Elemental Creation (kamome sano Remix)).
const prefixSong = songs.find((song) =>
normalizeTitle(song.title).startsWith(normalizeTitle(title))
);
if (prefixSong) {
console.log(`Prefix-matched ${title} to ${prefixSong.title}.`);
}
return prefixSong;
}
// levelNum: a number 16-20
// csvData: raw CSV data from the sheet, in a nested list
// headerRow: the index (0-indexed) of the row with the tier names
// leftOffset: the index of the first column with tierlist info
// simple: do not check for double columns or other weird things (19-20)
function addTiers(levelNum, csvData, headerRow, leftOffset, simple) {
const songs = ReadCollection("songs-sdvx.json");
MutateCollection("charts-sdvx.json", (charts) => {
let col = leftOffset;
let row = headerRow;
tierLoop: while (col < csvData[headerRow].length) {
let tierName = csvData[row][col];
if (levelNum === 17 && tierName.match(/ +曲数:[0-9]+/)) {
// For 17, apparently they include the number of songs in each tier.
// e.g. "A 曲数:54"
// So we cut this out.
tierName = tierName.split(" ")[0]
}
if (tierName === "" && col > leftOffset) {
// This might be a double column (two columns for the same tier),
// so check the cell to the left.
tierName = csvData[row][col - 1];
}
if (!validTiers(levelNum).includes(tierName)) {
// We're probably just done.
console.log(`"${tierName}" does not match a known tier, so we should be finished.`);
break;
}
console.log(`Processing tier ${tierName} at [${row}, ${col}]`);
row++;
const superDiff = tierName === SUPER_INDIV_DIFFERENCE;
const baseTier = !superDiff
? TIERS[levelNum][tierName]
: {
text: SUPER_INDIV_DIFFERENCE,
value: levelNum,
};
while (row < csvData.length) {
let chartString = csvData[row++][col].trim();
if (chartString === "") {
break;
}
const tier = {
...baseTier,
individualDifference: superDiff,
};
// A few overrides.
// 【Believe (y)our Wings{V:VID RAYS}】[MXM]
chartString = chartString.replace(/^【(.+)】(\[[A-Z]{3}\])$/, "【$1$2】");
// Star is outside brackets bc fuck you
if (chartString === "※【Opium and Purple haze[GRV]】") {
tier.individualDifference = true;
chartString = "Opium and Purple haze[GRV]";
}
if (superDiff) {
// Extract a range to display (e.g. "18A+ - 18D").
// The two ~ characters are different and both are in use.
let rangeMatch = chartString.match(/^(.+)\((\d{2}.+)[~〜~](\d{2}.+)\)$/);
if (!rangeMatch) {
// 19s
// Yes, there really is no closing paren.
rangeMatch = chartString.match(/^【(.+)\((19.+)[〜~](19.+)】$/);
}
// 16s don't have ranges, so if we can't find it, no sweat.
if (rangeMatch) {
chartString = rangeMatch[1];
tier.text = `${rangeMatch[2]} - ${rangeMatch[3]}`;
} else {
console.log(
`No range found for individual difference chart ${chartString}.`
);
}
}
// We don't actually store this but need to get rid of the brackets.
const sightreadKillerMatch = chartString.match(/^【(.*?) ?】$/);
if (sightreadKillerMatch) {
chartString = sightreadKillerMatch[1];
}
const individualDifferenceMatch = chartString.match(/^※(.*)$/);
if (individualDifferenceMatch) {
tier.individualDifference = true;
chartString = individualDifferenceMatch[1];
}
// Not every entry has the diff, but we need it to distinguish between a few charts
// (e.g. KHAMEN BREAK) that have two diffs at the same level.
const [_, title, difficulty] = chartString.match(/^(.*?)(?:\[([A-Z]{3})\])?$/);
if (
difficulty &&
!["NOV", "ADV", "EXH", "MXM", "INF", "GRV", "HVN", "VVD"].includes(difficulty)
) {
console.log(`Unknown difficulty ${difficulty} for ${title}.`);
}
const song = findSong(songs, title);
if (!song) {
console.log(`Unable to find song matching ${title}`);
continue;
}
let chart = charts.find(
(chart) =>
chart.songID === song.id &&
chart.levelNum === levelNum &&
chart.difficulty === difficulty
);
if (!chart) {
// Sometimes the difficulty is missing, or straight up wrong (e.g. MXM instead of VVD) so just try without.
chart = charts.find(
(chart) => chart.songID === song.id && chart.levelNum === levelNum
);
}
if (!chart) {
console.log("Can't find chart:");
console.log(`tierlist title ${title} matches song ${song.title} (${song.id})`);
console.log(`at [${difficulty}] ${levelNum}`);
continue;
}
if (
"clear" in chart.tierlistInfo &&
chart.tierlistInfo.clear.value !== tier.value
) {
console.log(`Overwriting tier for ${song.title} [${chart.difficulty}]`);
console.log(
`Old tier ${chart.tierlistInfo.clear.text} (${chart.tierlistInfo.clear.value})`
);
console.log(`New tier ${tier.text} (${tier.value})`);
}
chart.tierlistInfo.clear = tier;
}
if (simple || row >= csvData.length) {
row = headerRow;
col++;
continue tierLoop;
}
while (csvData[row][col] === "") {
row++;
if (row >= csvData.length) {
// Next column.
row = headerRow;
col++;
continue tierLoop;
}
}
// If we get here, we have found something at the bottom of the column.
const cell = csvData[row][col];
if (cell === tierName) {
// This signifies the end of the column.
row = headerRow;
col++;
continue tierLoop;
}
if (validTiers(levelNum).includes(cell)) {
console.log(`Found a second tier in column ${col} at row ${row}.`);
// There is another tier, here, continue without resetting col.
continue tierLoop;
}
// Otherwise, not sure what we found, but just move on to the next column.
row = headerRow;
col++;
}
const missingTiers = charts.filter(
(chart) => chart.levelNum === levelNum && !("clear" in chart.tierlistInfo)
);
if (missingTiers.length > 0) {
console.log(`The following lv${levelNum} charts are still missing a tier:`);
for (const chart of missingTiers) {
const song = songs.find((song) => song.id === chart.songID);
console.log(
`${song.id}: ${song.title} [${chart.difficulty}] (displayVersion: ${song.data.displayVersion})`
);
}
}
return charts;
});
}
const program = new Command();
program.option("-f, --file <CSV File>");
program.parse(process.argv);
const options = program.opts();
const csvData = parse(fs.readFileSync(options.file), {});
const levelIndicator = options.file.match(/ - (Lv.*).csv$/);
if (!levelIndicator) {
console.log("Unrecognized filename.");
}
switch (levelIndicator[1]) {
case "Lv16":
addTiers(16, csvData, 2, 1, false);
break;
case "Lv17":
addTiers(17, csvData, 2, 0, false);
break;
case "Lv18":
addTiers(18, csvData, 1, 0, false);
break;
case "Lv19,20":
addTiers(20, csvData, 1, 1, true);
// Locate start of 19s
let header19 = -1;
for (rowIdx in csvData) {
if (csvData[rowIdx][0] === "Lv19") {
header19 = rowIdx;
break;
}
}
addTiers(19, csvData, header19, 1, true);
break;
default:
console.log("Unrecognized level");
break;
}