mirror of
https://gitea.tendokyu.moe/pinapelz/Mirage.git
synced 2026-10-05 21:27:59 +03:00
FLOWER reflect beat and nostalgia userscripts
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
# Development
|
||||
Install dependencies:
|
||||
```bash
|
||||
pnpm install:all1
|
||||
pnpm install:all
|
||||
```
|
||||
|
||||
Create a `.env` file in `backend` based on `backend/.env.template`. Fill in the fields as required.
|
||||
|
||||
@@ -26,6 +26,16 @@ async function main() {
|
||||
formattedName: "MUSIC DIVER",
|
||||
description: "Taito's quadrant based drumming game",
|
||||
},
|
||||
{
|
||||
internalName: "nostalgia",
|
||||
formattedName: "NOSTALGIA",
|
||||
description: "A piano based touch music game from KONAMI",
|
||||
},
|
||||
{
|
||||
internalName: "reflecbeat",
|
||||
formattedName: "REFLEC BEAT",
|
||||
description: "A touchscreen rhythm game from KONAMI",
|
||||
}
|
||||
],
|
||||
});
|
||||
console.log("Initial seed data inserted");
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
// ==UserScript==
|
||||
// @name Mirage Nostalgia Score Scraper for FLOWER
|
||||
// @version 1.0
|
||||
// @description Scrapes scores from Flower's Nostalgia page and converts to Mirage
|
||||
// @author Meta-link
|
||||
// @match https://projectflower.eu/game/nostalgia/*
|
||||
// @grant none
|
||||
// ==/UserScript==
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
let mirage = {};
|
||||
const scores = [];
|
||||
|
||||
// Create progress bar container
|
||||
const progressContainer = document.createElement("div");
|
||||
progressContainer.style.position = "fixed";
|
||||
progressContainer.style.top = "50px";
|
||||
progressContainer.style.left = "50%";
|
||||
progressContainer.style.transform = "translateX(-50%)";
|
||||
progressContainer.style.width = "50%";
|
||||
progressContainer.style.background = "#eee";
|
||||
progressContainer.style.border = "1px solid #ccc";
|
||||
progressContainer.style.borderRadius = "4px";
|
||||
progressContainer.style.zIndex = 9999;
|
||||
progressContainer.style.display = "none";
|
||||
|
||||
const progressBar = document.createElement("div");
|
||||
progressBar.style.width = "0%";
|
||||
progressBar.style.height = "20px";
|
||||
progressBar.style.background = "#28a745";
|
||||
progressBar.style.borderRadius = "4px";
|
||||
progressContainer.appendChild(progressBar);
|
||||
document.body.appendChild(progressContainer);
|
||||
|
||||
function updateProgress(current, total) {
|
||||
progressContainer.style.display = "block";
|
||||
const percent = Math.round((current / total) * 100);
|
||||
progressBar.style.width = percent + "%";
|
||||
progressBar.textContent = `${current} / ${total} songs`;
|
||||
}
|
||||
|
||||
// Parse judgements from details row
|
||||
function parseJudgements(detailsRow) {
|
||||
const judgements = {};
|
||||
const cols = detailsRow.querySelectorAll("div.col-sm-2");
|
||||
cols.forEach(col => {
|
||||
const labelElem = col.querySelector("strong");
|
||||
if (!labelElem) return;
|
||||
|
||||
const label = labelElem.textContent.replace(/\s/g, "");
|
||||
const valueText = labelElem.nextSibling?.textContent?.trim() || col.textContent.replace(labelElem.textContent, "").trim();
|
||||
|
||||
if (label === "◆Just") judgements.marvelous = Number(valueText) || 0;
|
||||
else if (label === "Just") judgements.just = Number(valueText) || 0;
|
||||
else if (label === "Good") judgements.good = Number(valueText) || 0;
|
||||
else if (label === "Near") judgements.near = Number(valueText) || 0;
|
||||
else if (label === "Miss") judgements.miss = Number(valueText) || 0;
|
||||
else if (label === "Fast/Slow" || label === "Fast/Slow") {
|
||||
const parts = valueText.split("/").map(x => Number(x.trim()));
|
||||
judgements.fast = parts[0] || 0;
|
||||
judgements.slow = parts[1] || 0;
|
||||
}
|
||||
});
|
||||
return judgements;
|
||||
}
|
||||
|
||||
// Fetch artist from song page
|
||||
async function getArtistFromLink(link) {
|
||||
try {
|
||||
const res = await fetch(link);
|
||||
const text = await res.text();
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(text, "text/html");
|
||||
const ths = doc.querySelectorAll("th");
|
||||
for (let th of ths) {
|
||||
if (th.textContent.trim() === "Song Artist") {
|
||||
const td = th.nextElementSibling;
|
||||
if (td) return td.textContent.trim();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error fetching artist from", link, e);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// Parse the Nostalgia score table
|
||||
async function parseNostalgia() {
|
||||
const scoreLines = document.querySelectorAll(".table > tbody:nth-child(3) > tr");
|
||||
const songs = Array.from(scoreLines).filter(row => row.classList.contains("accordion-toggle"));
|
||||
const totalSongs = songs.length;
|
||||
|
||||
for (let i = 0; i < songs.length; i++) {
|
||||
const row = songs[i];
|
||||
|
||||
// Title and song link
|
||||
const songLinkElem = row.querySelector("td:nth-child(2) > a");
|
||||
const title = songLinkElem.querySelector("strong").textContent.trim();
|
||||
const songLink = songLinkElem.href;
|
||||
|
||||
// Fetch artist from song page
|
||||
const artist = await getArtistFromLink(songLink);
|
||||
|
||||
// Chart info: difficulty + level
|
||||
const chartCell = row.querySelector("td:nth-child(3)").textContent.trim();
|
||||
let difficulty = "Unknown";
|
||||
let level = 0;
|
||||
const match = chartCell.match(/([A-Za-z]+)\s*(\d+)/);
|
||||
if (match) {
|
||||
difficulty = match[1];
|
||||
level = Number(match[2]);
|
||||
}
|
||||
|
||||
// Score
|
||||
const score = parseInt(row.querySelector("td:nth-child(4) small").textContent.replaceAll(',', ''));
|
||||
|
||||
// Lamp
|
||||
const lamp = row.querySelector("td:nth-child(5) strong").textContent.trim();
|
||||
|
||||
// Time achieved
|
||||
const timeTxt = row.querySelector("td:nth-child(6) small").textContent.trim();
|
||||
const timeAchieved = new Date(timeTxt).getTime();
|
||||
|
||||
// Judgements
|
||||
const detailsRow = scoreLines[i * 2 + 1].querySelector("div[style*='padding: 5px']");
|
||||
const judgements = parseJudgements(detailsRow);
|
||||
|
||||
scores.push({
|
||||
title: title,
|
||||
artist: artist,
|
||||
difficulty: difficulty,
|
||||
level: level,
|
||||
score: score,
|
||||
lamp: lamp,
|
||||
timestamp: timeAchieved,
|
||||
judgements: judgements
|
||||
});
|
||||
|
||||
updateProgress(i + 1, totalSongs);
|
||||
}
|
||||
|
||||
progressContainer.style.display = "none";
|
||||
exportScores();
|
||||
}
|
||||
|
||||
// Export scores as JSON
|
||||
function exportScores() {
|
||||
mirage = {
|
||||
meta: {
|
||||
game: "nostalgia",
|
||||
playtype: "Single",
|
||||
service: "NOSTALGIA Flower Play History",
|
||||
},
|
||||
scores: scores,
|
||||
};
|
||||
|
||||
const blob = new Blob([JSON.stringify(mirage, null, 2)], {type: "application/json"});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "nostalgia_scores.json";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
alert(`Exported ${scores.length} scores!`);
|
||||
}
|
||||
|
||||
// Add export button
|
||||
const button = document.createElement("button");
|
||||
button.textContent = "Export Nostalgia Scores MIRAGE";
|
||||
button.style.position = "fixed";
|
||||
button.style.top = "10px";
|
||||
button.style.right = "10px";
|
||||
button.style.zIndex = 9999;
|
||||
button.style.padding = "8px 12px";
|
||||
button.style.background = "#007bff";
|
||||
button.style.color = "white";
|
||||
button.style.border = "none";
|
||||
button.style.borderRadius = "4px";
|
||||
button.style.cursor = "pointer";
|
||||
button.onclick = parseNostalgia;
|
||||
document.body.appendChild(button);
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,109 @@
|
||||
// ==UserScript==
|
||||
// @name REFLEC BEAT SCORE EXPORT
|
||||
// @namespace https://example.com/
|
||||
// @version 1.2
|
||||
// @description Export REFLEC BEAT scores including full judgements and timestamps as JSON
|
||||
// @match https://projectflower.eu/game/rb/profile/*
|
||||
// @grant none
|
||||
// ==/UserScript==
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Add export button
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = 'Mirage Export Scores JSON';
|
||||
btn.style.position = 'fixed';
|
||||
btn.style.top = '10px';
|
||||
btn.style.right = '10px';
|
||||
btn.style.zIndex = 9999;
|
||||
btn.style.padding = '8px 12px';
|
||||
btn.style.background = '#4CAF50';
|
||||
btn.style.color = 'white';
|
||||
btn.style.border = 'none';
|
||||
btn.style.borderRadius = '4px';
|
||||
btn.style.cursor = 'pointer';
|
||||
document.body.appendChild(btn);
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
const json = {
|
||||
meta: {
|
||||
game: "reflecbeat",
|
||||
playtype: "Single",
|
||||
service: "REFLEC BEAT Flower Play History"
|
||||
},
|
||||
scores: parseScoreLog()
|
||||
};
|
||||
|
||||
// Download JSON
|
||||
const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'reflecbeat_scores.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
|
||||
function parseScoreLog() {
|
||||
const rows = document.querySelectorAll('table.table tbody tr.accordion-toggle');
|
||||
const scores = [];
|
||||
|
||||
rows.forEach(row => {
|
||||
const titleElem = row.querySelector('td.pnmTitle a');
|
||||
const artistElem = row.querySelector('td.pnmTitle small');
|
||||
const difficultyElem = row.querySelector('td.gradeHARD, td.gradeMEDIUM, td.gradeEASY');
|
||||
const levelElem = difficultyElem?.querySelector('strong');
|
||||
const lampElem = row.querySelector('td.rb-rank strong'); // Rank / Lamp
|
||||
const scoreText = row.querySelector('td.rb-rank span')?.innerText || '';
|
||||
const scoreMatch = scoreText.replace(/,/g, '').match(/\d+/);
|
||||
const scoreNum = scoreMatch ? parseInt(scoreMatch[0]) : null;
|
||||
|
||||
// Extract timestamp from <small> in last column
|
||||
const timestampElem = row.querySelector('td.hidden-from-mobile small');
|
||||
let timestamp = null;
|
||||
if (timestampElem) {
|
||||
const dateStr = timestampElem.innerText.trim(); // e.g., "2025-06-27 5:23 PM"
|
||||
timestamp = new Date(dateStr).getTime(); // Unix ms
|
||||
}
|
||||
|
||||
// Get accordion ID
|
||||
const accordionId = row.getAttribute('data-target')?.replace('#', '');
|
||||
const accordion = document.getElementById(accordionId);
|
||||
|
||||
// Initialize metadata
|
||||
let lifeLeft = '', justReflec = 0, just = 0, great = 0, good = 0, miss = 0;
|
||||
|
||||
if (accordion) {
|
||||
const divs = accordion.querySelectorAll('div.col-sm-3, div.col-sm-2, div.col-sm-4');
|
||||
divs.forEach(div => {
|
||||
const label = div.querySelector('strong')?.innerText.trim();
|
||||
const value = div.childNodes[div.childNodes.length-1].nodeValue?.trim() || div.querySelector('a')?.innerText.trim() || '';
|
||||
switch(label) {
|
||||
case 'Life Left': lifeLeft = value; break;
|
||||
case 'Just Reflec': justReflec = parseInt(value) || 0; break;
|
||||
case 'Just': just = parseInt(value) || 0; break;
|
||||
case 'Great': great = parseInt(value) || 0; break;
|
||||
case 'Good': good = parseInt(value) || 0; break;
|
||||
case 'Miss': miss = parseInt(value) || 0; break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scores.push({
|
||||
title: titleElem?.innerText.trim() || '',
|
||||
artist: artistElem?.innerText.trim() || '',
|
||||
difficulty: difficultyElem?.innerText.replace(levelElem?.innerText, '').trim() || '',
|
||||
level: levelElem ? parseInt(levelElem.innerText.trim()) : null,
|
||||
score: scoreNum,
|
||||
lamp: lampElem?.innerText.trim() || '',
|
||||
lifeLeft: parseInt(lifeLeft) || null,
|
||||
timestamp, // Unix ms
|
||||
judgements: { justReflec, just, great, good, miss }
|
||||
});
|
||||
});
|
||||
|
||||
return scores;
|
||||
}
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user