Add Jubeat title support, add Jubeat BTA base support
Some checks failed
Build / build (push) Has been cancelled

This commit is contained in:
Trenton Zimmer 2025-11-17 15:15:42 -05:00
parent 7a258bb8ea
commit 860603a194
13 changed files with 69917 additions and 6 deletions

View File

@ -1,4 +1,4 @@
VITE_APP_VERSION="3.0.33"
VITE_APP_VERSION="3.0.34"
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.33"
VITE_APP_VERSION="3.0.34"
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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -32,5 +32,6 @@
"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.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"]
"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"],
"3.0.34": ["- (Major) Add support for Jubeat titles", "- (Major) Add base BTA support"]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -33,6 +33,7 @@ function colorText() {
const thisGame = getGameInfo(props.game);
const version = ref(props.version);
const AkanameSettings = ref([]);
const TitlePartSettings = ref([]);
const fullyLoaded = ref(false);
async function loadAkanameSettings() {
@ -50,9 +51,26 @@ async function loadAkanameSettings() {
}
}
async function loadTitlePartSettings() {
try {
const response = await axios.get(
`/data-sources/jubeat-title/${version.value}.json`,
);
if (response.data) {
TitlePartSettings.value = response.data;
}
} catch (error) {
console.error("Error loading TitleParts settings:", error.message);
} finally {
fullyLoaded.value = true;
}
}
onMounted(async () => {
if (thisGame.id === GameConstants.SDVX) {
await loadAkanameSettings();
} else if (thisGame.id === GameConstants.JUBEAT) {
await loadTitlePartSettings();
} else {
fullyLoaded.value = true;
}
@ -65,20 +83,22 @@ onMounted(async () => {
class="md:flex md:space-x-12 md:justify-center md:items-center grid grid-cols-1 text-center"
>
<UserEmblem
v-if="game == 'jubeat' && version >= 10 && profile.last?.emblem"
v-if="
game == GameConstants.JUBEAT && version >= 10 && profile.last?.emblem
"
:version="version"
:profile="profile"
class="place-self-center pb-6 md:pb-0"
/>
<UserQpro
v-if="game == 'iidx' && version >= 20 && profile.qpro"
v-if="game == GameConstants.IIDX && version >= 20 && profile.qpro"
:version="version"
:profile="profile"
class="place-self-center md:mt-10 mb-10 md:mb-0"
/>
<div class="drop-shadow-2xl">
<p
v-if="profile.title"
v-if="profile.title && game != GameConstants.JUBEAT"
:class="colorText()"
class="text-2xl tracking-widest font-light -mb-1"
>
@ -96,6 +116,17 @@ onMounted(async () => {
AkanameSettings.find((item) => item.id === profile.akaname)?.label
}}
</p>
<p
v-if="
(profile.title || profile.parts) && game == GameConstants.JUBEAT
"
class="text-2xl tracking-widest font-light my-1"
>
{{
TitlePartSettings.find((item) => item.id === profile.title)?.label
}}
</p>
</template>
<p class="text-xl font-mono">{{ dashCode(profile.extid) }}</p>
</div>

View File

@ -0,0 +1,140 @@
<script setup>
import axios from "axios";
import { watch, ref } from "vue";
import { useRouter } from "vue-router";
import { PhSpinnerBall } from "@phosphor-icons/vue";
import BaseButton from "@/components/BaseButton.vue";
import BaseIcon from "@/components/BaseIcon.vue";
import CardBox from "@/components/CardBox.vue";
import FormField from "@/components/FormField.vue";
import FormControl from "@/components/FormControl.vue";
import PillTag from "@/components/PillTag.vue";
import { GameConstants } from "@/constants";
import { APIUpdateProfile } from "@/stores/api/profile";
const props = defineProps({
profile: {
type: Object,
default: null,
},
version: {
type: Number,
default: 6,
},
});
const router = useRouter();
const userProfile = ref(JSON.parse(JSON.stringify(props.profile)));
const version = ref(props.version);
const TitleKey = ref(0);
const newTitle = ref(userProfile.value?.title ?? 0);
const TitleSettings = ref([]);
const isModified = ref(false);
const loading = ref(false);
watch(
() => props.version,
() => {
userProfile.value = JSON.parse(JSON.stringify(props.profile));
loadTitleSettings();
newTitle.value = userProfile.value?.title;
isModified.value = false;
},
);
watch(
() => props.version,
() => {
version.value = props.version;
},
);
watch(
newTitle,
() => {
TitleKey.value++;
isModified.value = !dataEquals(newTitle.value, props.profile.akaname);
},
{ deep: true },
);
loadTitleSettings();
function loadTitleSettings() {
axios
.get(`/data-sources/jubeat-title/${version.value}.json`)
.then((r) => {
if (r.data) {
TitleSettings.value = r.data;
}
})
.catch((error) => {
console.log(error.message);
});
}
function dataEquals(data1, data2) {
return JSON.stringify({ data1 }) === JSON.stringify({ data2 });
}
async function updateProfile() {
loading.value = true;
var newProfile = JSON.parse(JSON.stringify(props.profile));
newProfile.title = newTitle.value;
const partId = TitleSettings.value.find(
(item) => item.id === newTitle.value,
)?.part;
newProfile.parts = partId;
const profileStatus = await APIUpdateProfile(
GameConstants.JUBEAT,
props.version,
{ title: newTitle.value, parts: partId },
);
if (profileStatus.status != "error") {
router.go();
}
}
function revert() {
newTitle.value = userProfile.value?.title ?? 0;
isModified.value = false;
}
</script>
<template>
<CardBox class="mt-6">
<PillTag color="info" label="Title / Title Part" class="mb-2" />
<div class="grid md:grid-cols-2 space-y-6 align-center">
<form>
<FormField label="Title" help="Sets your profile's title">
<FormControl v-model="newTitle" :options="TitleSettings" />
</FormField>
</form>
</div>
<template #footer>
<div class="space-x-2 pb-4">
<BaseButton
v-if="isModified"
color="success"
label="Save"
@click="updateProfile()"
/>
<BaseButton
v-if="isModified"
color="danger"
label="Revert"
@click="revert()"
/>
<BaseIcon
v-if="loading"
:icon="PhSpinnerBall"
color="text-yellow-500"
class="animate animate-spin"
/>
</div>
</template>
</CardBox>
</template>

View File

@ -188,6 +188,7 @@ export class VersionConstants {
static JUBEAT_CLAN = 12;
static JUBEAT_FESTO = 13;
static JUBEAT_AVE = 14;
static JUBEAT_BEYOND = 15;
static LOVEPLUS_ARCADE = 1;
static LOVEPLUS_CC = 2;
@ -1650,6 +1651,11 @@ export const gameData = [
label: "Avenue",
maxRivals: 3,
},
{
id: VersionConstants.JUBEAT_BEYOND,
label: "Beyond the Avenue",
maxRivals: 3,
},
],
},
{

View File

@ -27,6 +27,7 @@ import {
} from "@/constants/values";
import { getGameInfo } from "@/constants";
import { getGameOptions } from "@/constants/options";
import TitlePartCardBox from "@/components/Cards/TitlePartCardBox.vue";
const $route = useRoute();
const $router = useRouter();
@ -246,6 +247,11 @@ async function updateProfile() {
:profile="myProfile"
:version="versionForm.currentVersion"
/>
<TitlePartCardBox
v-if="gameID == 'jubeat' && versionForm.currentVersion >= 13"
:profile="myProfile"
:version="versionForm.currentVersion"
/>
<QproCardBox
v-if="
(gameID == 'iidx' || gameID == 'iidxclass') &&

96
tools/title_parser.py Normal file
View File

@ -0,0 +1,96 @@
import sys
import os
import re
import json
import xml.etree.ElementTree as ET
args = sys.argv
if len(args) != 3:
print('<title_parser> <achievement_info.xml> <title_parts_info.xml>\nWrites a resulting JSON file to the same path.')
sys.exit()
achFilePath = args[1]
if not os.path.exists(achFilePath):
print(f'The path {achFilePath} does not exist!')
sys.exit()
partsFilePath = args[2]
if not os.path.exists(partsFilePath):
print(f'The path {partsFilePath} does not exist!')
sys.exit()
if not achFilePath.endswith('.xml'):
print('You must supply an XML file.')
sys.exit()
if not partsFilePath.endswith('.xml'):
print('You must supply an XML file.')
sys.exit()
def parseParts(xmlFile: str) -> dict:
root = ET.fromstring(xmlFile)
parts = {}
for part in root.findall('body/data'):
partId = part.find('parts_id')
if not partId.text:
continue
partJIS = part.find('parts')
if partJIS == None:
continue
partStr = bytes.fromhex(partJIS.text).decode('shift-jis')
parts[int(partId.text)] = partStr
return parts
def parseAch(xmlFile: str, parts: dict) -> list[dict]:
root = ET.fromstring(xmlFile)
titles = []
for title in root.findall('body/data'):
title_id = title.find('id', None)
if not title_id.text:
continue
index = title.find('index').text
if not index:
continue
partStr = ""
part_id = 0
is_need_parts = title.find('is_need_parts').text
if int(is_need_parts):
parts_id = title.find('parts_id')
partStr = parts[int(parts_id.text)]
part_id = int(parts_id.text)
partJIS = title.find('template')
if partJIS == None:
continue
titleStr = bytes.fromhex(partJIS.text).decode('shift-jis')
titleStr = titleStr.replace('%s', partStr)
conditionJIS = title.find('condition')
if conditionJIS == None:
continue
conditionStr = bytes.fromhex(conditionJIS.text).decode('shift-jis')
parsedData = {}
parsedData['label'] = titleStr
parsedData['condition'] = conditionStr
parsedData['index'] = int(title_id.text)
parsedData['part'] = int(part_id)
parsedData['id'] = int(index)
titles.append(parsedData)
return titles
achData = []
with open(partsFilePath, 'r', encoding='utf-8') as inFile:
parts = parseParts(inFile.read())
with open(achFilePath, 'r', encoding='utf-8') as inFile:
achData = parseAch(inFile.read(), parts)
with open(achFilePath.replace('.xml', '.json'), 'w', encoding='utf-8') as outFile:
outFile.write(json.dumps(achData, indent=4))
print(f'Converted {len(achData)} parts\nyippie!')