Clean up all score page logic, centralize table formatting and parsing, add more game metadata
Some checks are pending
Build / build (push) Waiting to run

This commit is contained in:
Trenton Zimmer 2025-11-12 08:13:06 -05:00
parent e4a5b21779
commit 9bf57adc7d
12 changed files with 252 additions and 339 deletions

View File

@ -1,4 +1,4 @@
VITE_APP_VERSION="3.0.32"
VITE_APP_VERSION="3.0.33"
VITE_API_URL="http://localhost:8000/"
VITE_API_KEY="your_api_key_should_be_here"
VITE_ASSET_PATH="/assets"

View File

@ -1,4 +1,4 @@
VITE_APP_VERSION="3.0.32"
VITE_APP_VERSION="3.0.33"
VITE_API_URL="https://restfulsleep.phaseii.network"
VITE_API_KEY="your_api_key_should_be_here"
VITE_ASSET_PATH="https://cdn.phaseii.network/file/PhaseII/web-assets"

View File

@ -31,5 +31,6 @@
"3.0.29": ["- (Major) Finish arcade PASELI support", "- (Minor) Clean up arcade page, add button for opening owner", "- (Bugfix) Fix table upper curved edges"],
"3.0.30": ["- (Major) Rewrite auth flow at backend and frontend", "- (Minor) Add auth to all api calls", "- (Bugfix) Fix bad user auth bug", "- (Minor) Add more greetings"],
"3.0.31": ["- (Major) Change all date formatting to a sortable format", "- (Minor) Add news archive page", "- (Bugfix) Add real news limiting", "- (Shrimp) Add more shrimp"],
"3.0.32": ["- (Major) Change phase \"Attempt\" to \"Score\"", "- (Bugfix) Fix pop'n music version sorting", "- (Bugfix) Fix Gitadora chart data formatting issues", "- (Bugfix) Filter personal records to songs with scores"]
"3.0.32": ["- (Major) Change phase \"Attempt\" to \"Score\"", "- (Bugfix) Fix pop'n music version sorting", "- (Bugfix) Fix Gitadora chart data formatting issues", "- (Bugfix) Filter personal records to songs with scores"],
"3.0.33": ["- (Major) Condense all score tables to one parser and format", "- (Major) Add more game metadata", "- (Bugfix) Fix difficulties showing as `NaN`", "- (Beta) Add a developmental device plugin for card effect"]
}

View File

@ -361,8 +361,9 @@ export const gameData = [
noRivals: true,
useUnicode: true,
scoreHeaders: [
{ text: "Combos", value: "data.combo" },
{ text: "Medal", value: "data.medal" },
{ text: "Clear Gauge", value: "data.clear_gauge", width: 120 },
{ text: "Combos", value: "data.combo", width: 120 },
{ text: "Medal", value: "data.medal", width: 120 },
],
chartTable: {
0: "LIGHT",
@ -441,7 +442,10 @@ export const gameData = [
icon: null,
cardBG: null,
noRivals: true,
scoreHeaders: [{ text: "Combos", value: "data.combo" }],
scoreHeaders: [
{ text: "Combos", value: "data.combo", width: 120 },
{ text: "Halo", value: "data.param", width: 120 },
],
chartTable: {
0: "1A",
1: "2A",

View File

@ -0,0 +1,13 @@
export function shouldRenderChart(difficulty, chartTable, chartKey) {
const invalidDifficulties = [0, -1, "-1", null];
const hasValidDifficulty = !invalidDifficulties.includes(difficulty);
const hasChartInTable = !!chartTable?.[chartKey];
return hasValidDifficulty && hasChartInTable;
}
export function formatDifficulty(difficulty, difficultyDenom = 1) {
if (isNaN(difficulty / difficultyDenom)) {
return difficulty;
}
return difficulty / difficultyDenom;
}

View File

@ -0,0 +1,143 @@
import { formatSortableDate } from "@/constants/date";
import { formatDifficulty } from "@/constants/scoreDataFilters";
export function scoreHeaders(thisGame) {
const headers = [
{ text: "Player", value: "username", width: 120 },
{ text: "New PB", value: "newRecord", width: 100 },
{ text: "Timestamp", value: "timestamp", width: 140 },
{ text: "Song", value: "song.name", width: 180 },
{ text: "Artist", value: "song.artist", width: 150 },
{ text: "Chart", value: "song.chart", width: 100 },
{ text: "Grade", value: "data.rank", width: 80 },
{ text: "Score", value: "points", width: 120 },
];
if (thisGame.scoreHeaders) {
for (var header of thisGame.scoreHeaders) {
headers.push(header);
}
}
return headers;
}
export function personalScoreHeaders(thisGame) {
const headers = [
{ text: "Timestamp", value: "timestamp", width: 140 },
{ text: "New PB", value: "newRecord", width: 100 },
{ text: "Song", value: "song.name", width: 180 },
{ text: "Artist", value: "song.artist", width: 180 },
{ text: "Chart", value: "song.chart", width: 120 },
{ text: "Grade", value: "data.rank", width: 80 },
{ text: "Score", value: "points", width: 120 },
];
if (thisGame.scoreHeaders) {
for (var header of thisGame.scoreHeaders) {
headers.push(header);
}
}
return headers;
}
export function formatScoreTable(thisGame, scores) {
var formattedItems = [];
for (var item of scores) {
if (item.newRecord) {
item.newRecord = "✅";
} else {
item.newRecord = "";
}
if (item.timestamp) {
item.timestamp = formatSortableDate(item.timestamp);
}
if (item.points != undefined) {
item.points = item.points
.toString()
.replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ",");
}
if (item.data?.stats?.score != undefined) {
item.exscore = item.points.toString();
item.points = item.data?.stats?.score
.toString()
.replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ",");
}
if (item.song?.chart != undefined && thisGame.chartTable) {
item.song.chart = `${thisGame.chartTable[item.song?.chart]} - ${formatDifficulty(
item.song.data?.difficulty,
thisGame.difficultyDenom,
)}`;
}
if (item.data?.halo != undefined && thisGame.haloTable) {
item.data.halo = thisGame.haloTable[item.data?.halo];
}
if (item.data?.medal != undefined && thisGame.medalTable) {
item.data.medal = thisGame.medalTable[item.data?.medal];
}
if (item.data?.clear_status != undefined && thisGame.medalTable) {
item.data.medal = thisGame.medalTable[item.data?.clear_status];
}
if (item.data?.rank != undefined && thisGame.rankTable) {
item.data.rank = thisGame.rankTable[item.data?.rank];
}
if (item.data?.result_rank != undefined && thisGame.rankTable) {
item.data.rank = thisGame.rankTable[item.data?.result_rank];
}
if (item.data?.grade != undefined && thisGame.rankTable) {
item.data.rank = thisGame.rankTable[item.data?.grade];
}
if (item.data?.skill_perc > 0) {
item.data.skill_perc = `${item.data?.skill_perc / 100}%`;
} else {
item.data.skill_perc = "0%";
}
if (item.data?.skill_points) {
item.data.skill_points = item.data?.skill_points / 10;
}
if (item.data?.perc > 0) {
item.data.perc = `${item.data?.perc / 100}%`;
} else {
item.data.perc = "0%";
}
if (item.data?.new_skill) {
item.data.new_skill = item.data?.new_skill / 10;
}
if (item.data?.music_rate) {
item.data.music_rate = item.data?.music_rate / 10;
}
if (item.data?.excellent) {
item.medal = "EX FC";
} else if (item.data?.fullcombo) {
item.medal = "FC";
} else if (item.data?.clear) {
item.medal = "CLEARED";
} else {
item.medal = "FAILED";
}
if (item.data?.clear_gauge !== undefined) {
item.data.clear_gauge = `${item.data?.clear_gauge / 10}%`;
}
formattedItems.push(item);
}
return formattedItems;
}

View File

@ -12,6 +12,11 @@ import GameHeader from "@/components/Cards/GameHeader.vue";
import { APIGetRecordData } from "@/stores/api/music";
import { getGameInfo } from "@/constants";
import {
shouldRenderChart,
formatDifficulty,
} from "@/constants/scoreDataFilters";
const $route = useRoute();
const $router = useRouter();
var gameId = $route.params.game;
@ -99,14 +104,22 @@ const filteredSongs = computed(() => {
<template v-for="chart of song.charts" :key="chart.db_id">
<div
v-if="
chart.data?.difficulty != 0 &&
thisGame.chartTable[chart.chart]
shouldRenderChart(
chart.data?.difficulty,
thisGame.chartTable,
chart.chart,
)
"
class="bg-gray-900 dark:bg-gray-700 p-4 rounded-lg"
>
<h2 class="text-md md:text-lg">
{{ thisGame.chartTable[chart.chart] }} -
{{ chart.data?.difficulty / (thisGame.difficultyDenom ?? 1) }}
{{
formatDifficulty(
chart.data?.difficulty,
thisGame.difficultyDenom,
)
}}
</h2>
{{
chart.record

View File

@ -10,7 +10,7 @@ import GeneralTable from "@/components/GeneralTable.vue";
import CardBox from "@/components/CardBox.vue";
import GameHeader from "@/components/Cards/GameHeader.vue";
import { getGameInfo } from "@/constants";
import { formatSortableDate } from "@/constants/date";
import { formatScoreTable, scoreHeaders } from "@/constants/table/scores";
const $route = useRoute();
const $router = useRouter();
@ -32,128 +32,15 @@ if (!thisGame) {
}
const scores = ref([]);
const headers = [
{ text: "Player", value: "username", width: 120 },
{ text: "New PB", value: "newRecord", width: 100 },
{ text: "Timestamp", value: "timestamp", width: 140 },
{ text: "Song", value: "song.name", width: 180 },
{ text: "Artist", value: "song.artist", width: 150 },
{ text: "Chart", value: "song.chart", width: 100 },
{ text: "Grade", value: "data.rank", width: 80 },
{ text: "Score", value: "points", width: 120 },
];
if (thisGame.scoreHeaders) {
for (var header of thisGame.scoreHeaders) {
headers.push(header);
}
}
// headers.push({ text: "Type", value: "type", width: 80 });
onMounted(async () => {
try {
const data = await mainStore.getAttemptData(gameID);
scores.value = formatScores(data);
scores.value = formatScoreTable(thisGame, data);
} catch (error) {
console.error("Failed to fetch score data:", error);
}
});
function formatScores(scores) {
var formattedItems = [];
for (var item of scores) {
if (item.newRecord) {
item.newRecord = "✅";
} else {
item.newRecord = "";
}
if (item.timestamp) {
item.timestamp = formatSortableDate(item.timestamp);
}
if (item.points != undefined) {
item.points = item.points
.toString()
.replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ",");
}
if (item.data?.stats?.score != undefined) {
item.exscore = item.points.toString();
item.points = item.data?.stats?.score
.toString()
.replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ",");
}
if (item.song?.chart != undefined && thisGame.chartTable) {
item.song.chart = thisGame.chartTable[item.song?.chart];
}
if (item.data?.halo != undefined && thisGame.haloTable) {
item.data.halo = thisGame.haloTable[item.data?.halo];
}
if (item.data?.medal != undefined && thisGame.medalTable) {
item.data.medal = thisGame.medalTable[item.data?.medal];
}
if (item.data?.clear_status != undefined && thisGame.medalTable) {
item.data.medal = thisGame.medalTable[item.data?.clear_status];
}
if (item.data?.rank != undefined && thisGame.rankTable) {
item.data.rank = thisGame.rankTable[item.data?.rank];
}
if (item.data?.result_rank != undefined && thisGame.rankTable) {
item.data.rank = thisGame.rankTable[item.data?.result_rank];
}
if (item.data?.grade != undefined && thisGame.rankTable) {
item.data.rank = thisGame.rankTable[item.data?.grade];
}
if (item.data?.skill_perc > 0) {
item.data.skill_perc = `${item.data?.skill_perc / 100}%`;
} else {
item.data.skill_perc = "0%";
}
if (item.data?.skill_points) {
item.data.skill_points = item.data?.skill_points / 10;
}
if (item.data?.perc > 0) {
item.data.perc = `${item.data?.perc / 100}%`;
} else {
item.data.perc = "0%";
}
if (item.data?.new_skill) {
item.data.new_skill = item.data?.new_skill / 10;
}
if (item.data?.music_rate) {
item.data.music_rate = item.data?.music_rate / 10;
}
if (item.data?.excellent) {
item.medal = "EX FC";
} else if (item.data?.fullcombo) {
item.medal = "FC";
} else if (item.data?.clear) {
item.medal = "CLEARED";
} else {
item.medal = "FAILED";
}
formattedItems.push(item);
}
return formattedItems;
}
const navigateToSong = (item) => {
const songId = item.song.id;
$router.push(`/games/${gameID}/song/${songId}`);
@ -175,7 +62,7 @@ const navigateToSong = (item) => {
<CardBox has-table>
<GeneralTable
:headers="headers"
:headers="scoreHeaders(thisGame)"
:items="scores"
@row-clicked="navigateToSong"
/>

View File

@ -13,6 +13,11 @@ import GameHeader from "@/components/Cards/GameHeader.vue";
import { APIGetRecordData } from "@/stores/api/music";
import { APIGetProfile } from "@/stores/api/profile";
import { getGameInfo } from "@/constants";
import {
shouldRenderChart,
formatDifficulty,
} from "@/constants/scoreDataFilters";
const $route = useRoute();
const $router = useRouter();
var gameId = $route.params.game;
@ -156,15 +161,21 @@ const songsWithRecords = computed(() => {
<template v-if="chart.record">
<div
v-if="
chart.data?.difficulty != 0 &&
thisGame.chartTable[chart.chart]
shouldRenderChart(
chart.data?.difficulty,
thisGame.chartTable,
chart.chart,
)
"
class="bg-gray-900 dark:bg-gray-700 p-4 rounded-lg"
>
<h2 class="text-md lg:text-lg">
{{ thisGame.chartTable[chart.chart] }} -
{{
chart.data?.difficulty / (thisGame.difficultyDenom ?? 1)
formatDifficulty(
chart.data?.difficulty,
thisGame.difficultyDenom,
)
}}
</h2>
{{

View File

@ -10,10 +10,13 @@ import BaseButton from "@/components/BaseButton.vue";
import GeneralTable from "@/components/GeneralTable.vue";
import CardBox from "@/components/CardBox.vue";
import GameHeader from "@/components/Cards/GameHeader.vue";
import { formatSortableDate } from "@/constants/date";
import { APIGetProfile } from "@/stores/api/profile";
import { getGameInfo } from "@/constants";
import {
formatScoreTable,
personalScoreHeaders,
} from "@/constants/table/scores";
const $route = useRoute();
const $router = useRouter();
@ -39,28 +42,10 @@ if (!thisGame) {
const myProfile = ref(null);
const scores = ref([]);
const headers = [
{ text: "Timestamp", value: "timestamp", width: 140 },
{ text: "New PB", value: "newRecord", width: 100 },
{ text: "Song", value: "song.name", width: 180 },
{ text: "Artist", value: "song.artist", width: 180 },
{ text: "Chart", value: "song.chart", width: 100 },
{ text: "Grade", value: "data.rank", width: 80 },
{ text: "Score", value: "points", width: 120 },
];
if (thisGame.scoreHeaders) {
for (var header of thisGame.scoreHeaders) {
headers.push(header);
}
}
// headers.push({ text: "Type", value: "type", width: 80 });
onMounted(async () => {
try {
const data = await mainStore.getAttemptData(gameID, profileUserId);
scores.value = formatScores(data);
scores.value = formatScoreTable(thisGame, data);
} catch (error) {
console.error("Failed to fetch score data:", error);
}
@ -78,99 +63,6 @@ async function loadProfile() {
}
}
function formatScores(scores) {
var formattedItems = [];
for (var item of scores) {
if (item.newRecord) {
item.newRecord = "✅";
} else {
item.newRecord = "";
}
if (item.timestamp) {
item.timestamp = formatSortableDate(item.timestamp);
}
if (item.points != undefined) {
item.points = item.points
.toString()
.replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ",");
}
if (item.data?.stats?.score != undefined) {
item.exscore = item.points.toString();
item.points = item.data?.stats?.score
.toString()
.replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ",");
}
if (item.song?.chart != undefined && thisGame.chartTable) {
item.song.chart = thisGame.chartTable[item.song?.chart];
}
if (item.data?.halo != undefined && thisGame.haloTable) {
item.data.halo = thisGame.haloTable[item.data?.halo];
}
if (item.data?.medal != undefined && thisGame.medalTable) {
item.data.medal = thisGame.medalTable[item.data?.medal];
}
if (item.data?.clear_status != undefined && thisGame.medalTable) {
item.data.medal = thisGame.medalTable[item.data?.clear_status];
}
if (item.data?.rank != undefined && thisGame.rankTable) {
item.data.rank = thisGame.rankTable[item.data?.rank];
}
if (item.data?.result_rank != undefined && thisGame.rankTable) {
item.data.rank = thisGame.rankTable[item.data?.result_rank];
}
if (item.data?.grade != undefined && thisGame.rankTable) {
item.data.rank = thisGame.rankTable[item.data?.grade];
}
if (item.data?.skill_perc > 0) {
item.data.skill_perc = `${item.data?.skill_perc / 100}%`;
} else {
item.data.skill_perc = "0%";
}
if (item.data?.skill_points) {
item.data.skill_points = item.data?.skill_points / 10;
}
if (item.data?.perc > 0) {
item.data.perc = `${item.data?.perc / 100}%`;
} else {
item.data.perc = "0%";
}
if (item.data?.new_skill) {
item.data.new_skill = item.data?.new_skill / 10;
}
if (item.data?.music_rate) {
item.data.music_rate = item.data?.music_rate / 10;
}
if (item.data?.excellent) {
item.medal = "EX FC";
} else if (item.data?.fullcombo) {
item.medal = "FC";
} else if (item.data?.clear) {
item.medal = "CLEARED";
} else {
item.medal = "FAILED";
}
formattedItems.push(item);
}
return formattedItems;
}
const navigateToSong = (item) => {
const songId = item.song.id;
$router.push(`/games/${gameID}/song/${songId}`);
@ -208,7 +100,7 @@ const navigateToSong = (item) => {
<CardBox has-table>
<GeneralTable
:headers="headers"
:headers="personalScoreHeaders(thisGame)"
:items="scores"
@row-clicked="navigateToSong"
/>

View File

@ -14,7 +14,8 @@ import GameHeader from "@/components/Cards/GameHeader.vue";
import { APIGetTopScore } from "@/stores/api/music";
import { getGameInfo } from "@/constants";
import { formatSortableDate } from "@/constants/date";
import { formatDifficulty } from "@/constants/scoreDataFilters";
import { formatScoreTable } from "@/constants/table/scores";
const $route = useRoute();
const $router = useRouter();
var gameId = $route.params.game;
@ -69,9 +70,10 @@ const chartOptions = computed(() => {
)
// eslint-disable-next-line no-unused-vars
.map((chart, index) => {
const label = `${thisGame.chartTable[chart.chart]} - ${
chart.data?.difficulty / (thisGame.difficultyDenom ?? 1)
}`;
const label = `${thisGame.chartTable[chart.chart]} - ${formatDifficulty(
chart.data?.difficulty,
thisGame.difficultyDenom,
)}`;
return { id: chart.chart, label };
})
);
@ -82,97 +84,9 @@ const selectedChartRecords = computed(() => {
const chart = JSON.parse(
JSON.stringify(songData.value.charts[chartSelector.currentChart]),
);
return formatScores(chart?.records ?? []);
return formatScoreTable(thisGame, chart?.records ?? []);
});
function formatScores(scores) {
var formattedItems = [];
for (var rawItem of scores) {
const item = { ...rawItem };
if (item.timestamp) {
item.timestamp = formatSortableDate(item.timestamp);
}
if (item.points != undefined) {
item.points = item.points
.toString()
.replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ",");
}
if (item.data?.stats?.score != undefined) {
item.exscore = item.points.toString();
item.points = item.data?.stats?.score
.toString()
.replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ",");
}
if (item.song?.chart != undefined && thisGame.chartTable) {
item.song.chart = thisGame.chartTable[item.song?.chart];
}
if (item.data?.halo != undefined && thisGame.haloTable) {
item.data.halo = thisGame.haloTable[item.data?.halo];
}
if (item.data?.medal != undefined && thisGame.medalTable) {
item.data.medal = thisGame.medalTable[item.data?.medal];
}
if (item.data?.clear_status != undefined && thisGame.medalTable) {
item.data.medal = thisGame.medalTable[item.data?.clear_status];
}
if (item.data?.rank != undefined && thisGame.rankTable) {
item.data.rank = thisGame.rankTable[item.data?.rank];
}
if (item.data?.result_rank != undefined && thisGame.rankTable) {
item.data.rank = thisGame.rankTable[item.data?.result_rank];
}
if (item.data?.grade != undefined && thisGame.rankTable) {
item.data.rank = thisGame.rankTable[item.data?.grade];
}
if (item.data?.skill_perc > 0) {
item.data.skill_perc = `${item.data?.skill_perc / 100}%`;
} else {
item.data.skill_perc = "0%";
}
if (item.data?.skill_points) {
item.data.skill_points = item.data?.skill_points / 10;
}
if (item.data?.perc > 0) {
item.data.perc = `${item.data?.perc / 100}%`;
} else {
item.data.perc = "0%";
}
if (item.data?.new_skill) {
item.data.new_skill = item.data?.new_skill / 10;
}
if (item.data?.music_rate) {
item.data.music_rate = item.data?.music_rate / 10;
}
if (item.data?.excellent) {
item.medal = "EX FC";
} else if (item.data?.fullcombo) {
item.medal = "FC";
} else if (item.data?.clear) {
item.medal = "CLEARED";
} else {
item.medal = "FAILED";
}
formattedItems.push(item);
}
return formattedItems;
}
const navigateToProfile = (item) => {
const userId = item.userId;
$router.push(`/games/${gameId}/profiles/${userId}`);
@ -200,9 +114,10 @@ const navigateToProfile = (item) => {
thisGame.chartTable[chart.chart]
"
color="info"
:label="`${thisGame.chartTable[chart.chart]} - ${
chart.data?.difficulty / (thisGame.difficultyDenom ?? 1)
}`"
:label="`${thisGame.chartTable[chart.chart]} - ${formatDifficulty(
chart.data?.difficulty,
thisGame.difficultyDenom,
)}`"
/>
</template>
</div>

View File

@ -1,5 +1,5 @@
<script setup>
import { ref, onMounted } from "vue";
import { ref, onMounted, onUnmounted } from "vue";
import { PhFileImage } from "@phosphor-icons/vue";
import SectionMain from "@/components/SectionMain.vue";
import CardBox from "@/components/CardBox.vue";
@ -29,6 +29,22 @@ async function loadVideos() {
onMounted(async () => {
await loadVideos();
// 🌐 Mobile tilt support
if (window.DeviceOrientationEvent) {
// Request permission on iOS
if (typeof DeviceOrientationEvent.requestPermission === "function") {
try {
const response = await DeviceOrientationEvent.requestPermission();
if (response !== "granted") return;
} catch (e) {
console.warn("Device orientation permission denied {1}", e);
return;
}
}
window.addEventListener("deviceorientation", handleDeviceTilt);
}
});
function filterContent(data) {
@ -79,6 +95,24 @@ function resetTransform(id) {
shine.style.opacity = "0";
}
}
function handleDeviceTilt(event) {
const { beta, gamma } = event; // beta: x-axis (front/back), gamma: y-axis (left/right)
if (beta === null || gamma === null) return;
// Clamp values for smooth motion
const rotateX = Math.max(Math.min(beta - 45, 15), -15); // limit between -15° to +15°
const rotateY = Math.max(Math.min(gamma, 15), -15);
const cards = document.querySelectorAll(".tilt-card");
cards.forEach((card) => {
card.style.transform = `rotateX(${rotateX / 3}deg) rotateY(${rotateY / 3}deg) scale(1.02)`;
});
}
onUnmounted(() => {
window.removeEventListener("deviceorientation", handleDeviceTilt);
});
</script>
<template>