Merge branch 'v1-dev' into matching

matching
Azalea 2024-12-29 05:11:58 -05:00
commit 8a1e17ecd3
20 changed files with 382 additions and 676 deletions

View File

@ -1,552 +0,0 @@
<!-- Svelte 4.2.11 -->
<script lang="ts">
import {
UserBoxItemKind,
type AquaNetUser,
} from "../libs/generalTypes";
import { USER, USERBOX } from "../libs/sdk";
import { t, ts } from "../libs/i18n";
import { DATA_HOST, HAS_USERBOX_ASSETS } from "../libs/config";
import { fade, slide } from "svelte/transition";
import StatusOverlays from "./StatusOverlays.svelte";
import Icon from "@iconify/svelte";
let user: AquaNetUser;
let loading = true;
let error = "";
let submitting = "";
let changed: string[] = [];
// Things that can be changed in the userbox
const userBoxFields = [
{
key: "frame",
label: t("userbox.frame"),
userBoxKey: "frameId",
kind: UserBoxItemKind.frame,
},
{
key: "nameplate",
label: t("userbox.nameplate"),
userBoxKey: "nameplateId",
kind: UserBoxItemKind.nameplate,
},
{
key: "trophy",
label: t("userbox.trophy"),
userBoxKey: "trophyId",
kind: UserBoxItemKind.trophy,
},
{
key: "mapicon",
label: t("userbox.mapicon"),
userBoxKey: "mapIconId",
kind: UserBoxItemKind.mapicon,
},
{
key: "voice",
label: t("userbox.voice"),
userBoxKey: "voiceId",
kind: UserBoxItemKind.sysvoice,
},
{
key: "wear",
label: t("userbox.wear"),
userBoxKey: "avatarWear",
kind: UserBoxItemKind.avatar,
},
{
key: "head",
label: t("userbox.head"),
userBoxKey: "avatarHead",
kind: UserBoxItemKind.avatar,
},
{
key: "face",
label: t("userbox.face"),
userBoxKey: "avatarHead",
kind: UserBoxItemKind.avatar,
},
{
key: "skin",
label: t("userbox.skin"),
userBoxKey: "avatarSkin",
kind: UserBoxItemKind.avatar,
},
{
key: "item",
label: t("userbox.item"),
userBoxKey: "avatarItem",
kind: UserBoxItemKind.avatar,
},
{
key: "front",
label: t("userbox.front"),
userBoxKey: "avatarFront",
kind: UserBoxItemKind.avatar,
},
{
key: "back",
label: t("userbox.back"),
userBoxKey: "avatarBack",
kind: UserBoxItemKind.avatar,
},
] as const;
// The different types available (avatar is a special case, as it has multiple categories)
const userBoxItems = userBoxFields
.map((f) => f.kind)
.filter((v, i, a) => a.indexOf(v) === i);
// Available options for the current user on each field
let availableOptions = {
nameplate: [],
frame: [],
trophy: [],
mapicon: [],
voice: [],
wear: [],
head: [],
face: [],
skin: [],
item: [],
front: [],
back: [],
} as Record<string, { id: number; label: string }[]>;
// Current values of the userbox
let values = {
nameplate: undefined,
frame: undefined,
trophy: undefined,
mapicon: undefined,
voice: undefined,
wear: undefined,
head: undefined,
face: undefined,
skin: undefined,
item: undefined,
front: undefined,
back: undefined,
} as Record<string, number | undefined>;
function submit(obj: { field: string; value: string }) {
if (submitting) return
submitting = obj.field
USERBOX.setUserBox(obj)
.then(() => changed = changed.filter((c) => c !== obj.field))
.catch(e => error = e.message)
.finally(() => submitting = "")
}
async function fetchData() {
const currentValues = await USERBOX.getProfile().catch((e) => {
loading = false;
error = t("userbox.error.noprofile")
})
if (!currentValues) return
values = {
nameplate: currentValues.nameplateId,
frame: currentValues.frameId,
trophy: currentValues.trophyId,
mapicon: currentValues.mapIconId,
voice: currentValues.voiceId,
wear: currentValues.avatarWear,
head: currentValues.avatarHead,
face: currentValues.avatarFace,
skin: currentValues.avatarSkin,
item: currentValues.avatarItem,
front: currentValues.avatarFront,
back: currentValues.avatarBack,
};
const itemLabels = await USERBOX.getItemLabels().catch((e) => {
loading = false;
error = t("userbox.error.nodata")
return
});
if (!itemLabels) return;
await Promise.all(
userBoxItems.map(async (kind) => {
// Populate info about the items
return USERBOX.getUnlockedItems(kind).then((items) => {
switch (kind) {
case UserBoxItemKind.nameplate:
// Add the item id and the label to the available options
availableOptions.nameplate = items.map((i) => {
return {
id: i.itemId,
label: itemLabels.nameplate[i.itemId],
};
});
break;
case UserBoxItemKind.frame:
availableOptions.frame = items.map((i) => {
return {
id: i.itemId,
label: itemLabels.frame[i.itemId],
};
});
break;
case UserBoxItemKind.trophy:
availableOptions.trophy = items.map((i) => {
return {
id: i.itemId,
label: itemLabels.trophy[i.itemId],
};
});
break;
case UserBoxItemKind.mapicon:
availableOptions.mapicon = items.map((i) => {
return {
id: i.itemId,
label: itemLabels.mapicon[i.itemId],
};
});
break;
case UserBoxItemKind.sysvoice:
availableOptions.voice = items.map((i) => {
return {
id: i.itemId,
label: itemLabels.sysvoice[i.itemId],
};
});
break;
case UserBoxItemKind.avatar:
// Depending of the second number of the item id, we can know the kind of item
items.forEach((i) => {
const kind = i.itemId.toString().split("")[1];
switch (kind) {
case "1":
availableOptions.wear.push({
id: i.itemId,
label: itemLabels.avatar[i.itemId],
});
break;
case "2":
availableOptions.head.push({
id: i.itemId,
label: itemLabels.avatar[i.itemId],
});
break;
case "3":
availableOptions.face.push({
id: i.itemId,
label: itemLabels.avatar[i.itemId],
});
break;
case "4":
availableOptions.skin.push({
id: i.itemId,
label: itemLabels.avatar[i.itemId],
});
break;
case "5":
availableOptions.item.push({
id: i.itemId,
label: itemLabels.avatar[i.itemId],
});
break;
case "6":
availableOptions.front.push({
id: i.itemId,
label: itemLabels.avatar[i.itemId],
});
break;
case "7":
availableOptions.back.push({
id: i.itemId,
label: itemLabels.avatar[i.itemId],
});
break;
}
});
break;
}
});
}),
);
loading = false;
}
const kindMap: Record<string, string> =
{ nameplate: "nameplateId", frame: "frameId", trophy: "trophyId", mapicon: "mapIconId", voice: "voiceId" }
const categories = ["wear", "head", "face", "skin", "item", "front", "back"]
function generateBodyFromKind(
kind: "frame" | "nameplate" | "trophy" | "mapicon" | "voice" | "wear" | "head" | "face" | "skin" | "item" | "front" | "back",
value: number,
) {
if (kind in kindMap) return { field: kindMap[kind], value: `${value}` }
return { field: "avatarItem", value: `${categories.indexOf(kind) + 1}:${value}` }
}
USER.me().then(u => {
if (!u) throw new Error(t("userbox.error.noprofile"))
user = u
fetchData()
}).catch((e) => { loading = false; error = e.message });
</script>
{#if !loading && !error}
<div class="fields">
{#each userBoxFields as { key, label, kind }, i (key)}
<div class="field">
<label for={key}>{label}</label>
<div>
<select bind:value={values[key]} id={key} on:change={() => changed = [...changed, key]}>
{#each availableOptions[key] as option}
<option value={option.id}>{option.label || `${key} ${option.id.toString()}`}</option>
{/each}
</select>
{#if changed.includes(key)}
<button
transition:slide={{ axis: "x" }}
on:click={() => {
const newValue = values[key];
if (newValue === undefined) return;
submit(generateBodyFromKind(key, newValue));
}}
>
{#if submitting === key}
<Icon icon="line-md:loading-twotone-loop" />
{:else}
{t("settings.profile.save")}
{/if}
</button>
{/if}
</div>
</div>
{/each}
</div>
{#if HAS_USERBOX_ASSETS}
<div class="preview">
<h2>{t("userbox.preview.ui")}</h2>
<!-- Frame -->
{#if values.frame}
<img
src={`${DATA_HOST}/d/chu3/frame/${values.frame}.png`}
alt="Preview"
/>
{/if}
<div class="secondrow">
<!-- Map Icon -->
{#if values.mapicon}
<div class="mapicon">
<img
src={`${DATA_HOST}/d/chu3/mapicon/${values.mapicon}.png`}
alt="Preview"
/>
</div>
{/if}
<!-- System voice -->
{#if values.voice}
<div>
<img
src={`${DATA_HOST}/d/chu3/systemVoice/${values.voice}.png`}
alt="Preview"
/>
</div>
{/if}
</div>
<h2>{t("userbox.preview.nameplate")}</h2>
<!-- Nameplate -->
{#if values.nameplate}
<div class="nameplate">
<img
src={`${DATA_HOST}/d/chu3/nameplate/${values.nameplate}.png`}
alt="Preview"
/>
<p class="trophy">
{availableOptions.trophy.find((x) => x.id === values.trophy)
?.label}
</p>
<div class="username">
<p>
{user.displayName}
</p>
</div>
</div>
{/if}
<h2>{t("userbox.preview.avatar")}</h2>
<div class="avatar">
<div>
<img
src={`${DATA_HOST}/d/chu3/avatarAccessory/${values.wear}.png`}
alt="Preview"
/>
</div>
<div>
<img
src={`${DATA_HOST}/d/chu3/avatarAccessory/${values.head}.png`}
alt="Preview"
/>
</div>
<div>
<img
src={`${DATA_HOST}/d/chu3/avatarAccessory/${values.face}.png`}
alt="Preview"
/>
</div>
<div>
<img
src={`${DATA_HOST}/d/chu3/avatarAccessory/${values.skin}.png`}
alt="Preview"
/>
</div>
<div>
<img
src={`${DATA_HOST}/d/chu3/avatarAccessory/${values.item}.png`}
alt="Preview"
/>
</div>
<div>
<img
src={`${DATA_HOST}/d/chu3/avatarAccessory/${values.front}.png`}
alt="Preview"
/>
</div>
<div>
<img
src={`${DATA_HOST}/d/chu3/avatarAccessory/${values.back}.png`}
alt="Preview"
/>
</div>
</div>
</div>
{/if}
{/if}
<StatusOverlays {error} {loading} />
<style lang="sass">
@use "../vars"
img
width: 100%
height: auto
.preview
display: flex
flex-direction: column
gap: 1rem
width: 50%
@media (max-width: vars.$w-max)
width: 100%
.secondrow
display: flex
gap: 1rem
justify-content: space-between
div
width: 40%
flex-grow:0
.avatar
display: grid
grid-template-columns: repeat(3, 1fr)
grid-template-rows: repeat(3, 1fr)
gap: 1rem
div
border: 1px solid white
aspect-ratio:1
overflow: hidden
display: flex
align-items: center
justify-content: center
border-radius: 0.4rem
img
width: auto
height: 100%
.nameplate
position: relative
width: 400px
> .trophy
position: absolute
top: 10px
left: 150px
width: 220px
background-color: #fff
color: black
border-radius: 0.4rem
text-align: center
font-weight:500
box-shadow: 0 0 5px rgba(0, 0, 0, 0.5)
font-size: .8rem
> .username
position: absolute
top: 50px
left: 150px
width: 220px
height: 50px
background-color: #fff
color: black
border-radius: 0.2rem
box-shadow: 0 0 5px rgba(0, 0, 0, 0.5)
display: flex
> p
margin: .2rem .5rem
font-size: 1.2rem
.mapicon
height: 100px
display: flex
justify-content: center
img
width: auto
height: 100%
.fields
display: flex
flex-direction: column
gap: 12px
width: 100%
flex-grow: 0
label
display: flex
flex-direction: column
select
width: 100%
.field
display: flex
flex-direction: column
width: 100%
label
max-width: max-content
> div:not(.bool)
display: flex
align-items: center
gap: 1rem
margin-top: 0.5rem
> select
flex: 1
</style>

View File

@ -0,0 +1,205 @@
<!-- Svelte 4.2.11 -->
<script lang="ts">
import {
type AquaNetUser,
type UserBox,
type UserItem,
} from "../../libs/generalTypes";
import { DATA, USER, USERBOX } from "../../libs/sdk";
import { t, ts } from "../../libs/i18n";
import { DATA_HOST, FADE_IN, FADE_OUT, HAS_USERBOX_ASSETS } from "../../libs/config";
import { fade, slide } from "svelte/transition";
import StatusOverlays from "../StatusOverlays.svelte";
import Icon from "@iconify/svelte";
import GameSettingFields from "./GameSettingFields.svelte";
import { filter } from "d3";
import { coverNotFound } from "../../libs/ui";
let user: AquaNetUser
let [loading, error, submitting, preview] = [true, "", "", ""]
let changed: string[] = [];
// Available (unlocked) options for each kind of item
// In allItems: 'namePlate', 'frame', 'trophy', 'mapIcon', 'systemVoice', 'avatarAccessory'
let allItems: Record<string, Record<string, { name: string }>> = {}
let iKinds = { namePlate: 1, frame: 2, trophy: 3, mapIcon: 8, systemVoice: 9, avatarAccessory: 11 }
// In userbox: 'nameplateId', 'frameId', 'trophyId', 'mapIconId', 'voiceId', 'avatar{Wear/Head/Face/Skin/Item/Front/Back}'
let userbox: UserBox
let avatarKinds = ['Wear', 'Head', 'Face', 'Skin', 'Item', 'Front', 'Back']
// iKey should match allItems keys, and ubKey should match userbox keys
let userItems: { iKey: string, ubKey: keyof UserBox, items: UserItem[] }[] = []
// Submit changes
function submit(field: keyof UserBox) {
let obj = { field, value: userbox[field] }
if (submitting) return
submitting = obj.field
USERBOX.setUserBox(obj)
.then(() => changed = changed.filter((c) => c !== obj.field))
.catch(e => error = e.message)
.finally(() => submitting = "")
}
// Fetch data from the server
async function fetchData() {
const profile = await USERBOX.getProfile().catch(_ => {
loading = false
error = t("userbox.error.nodata")
})
if (!profile) return
userbox = profile.user
userItems = Object.entries(iKinds).flatMap(([iKey, iKind]) => {
if (iKey != 'avatarAccessory') {
let ubKey = `${iKey}Id`
if (ubKey == 'namePlateId') ubKey = 'nameplateId'
if (ubKey == 'systemVoiceId') ubKey = 'voiceId'
return [{ iKey, ubKey: ubKey as keyof UserBox,
items: profile.items.filter(x => x.itemKind === iKind)
}]
}
return avatarKinds.map((aKind, i) => {
let items = profile.items.filter(x => x.itemKind === iKind && Math.floor(x.itemId / 100000) % 10 === i + 1)
return { iKey, ubKey: `avatar${aKind}` as keyof UserBox, items }
})
})
allItems = await DATA.allItems('chu3').catch(_ => {
loading = false
error = t("userbox.error.nodata")
}) as typeof allItems
console.log("User Items", userItems)
console.log("All items", allItems)
console.log("Userbox", userbox)
loading = false
}
USER.me().then(u => {
if (!u) throw new Error(t("userbox.error.nodata"))
user = u
return fetchData()
}).catch((e) => { loading = false; error = e.message });
</script>
<StatusOverlays {error} loading={loading || !!submitting} />
{#if !loading && !error}
<div out:fade={FADE_OUT} in:fade={FADE_IN}>
<h2>{t("userbox.header.general")}</h2>
<GameSettingFields game="chu3"/>
<h2>{t("userbox.header.userbox")}</h2>
<div class="fields">
{#each userItems as { iKey, ubKey, items }, i}
<div class="field">
<label for={ubKey}>{ts(`userbox.${ubKey}`)}</label>
<div>
<select bind:value={userbox[ubKey]} id={ubKey} on:change={() => changed = [...changed, ubKey]}>
{#each items as option}
<option value={option.itemId}>{allItems[iKey][option.itemId]?.name || `(unknown ${option.itemId})`}</option>
{/each}
</select>
{#if changed.includes(ubKey)}
<button transition:slide={{axis: "x"}} on:click={() => submit(ubKey)} disabled={!!submitting}>
{t("settings.profile.save")}
</button>
{/if}
</div>
</div>
{/each}
</div>
{#if HAS_USERBOX_ASSETS}
<h2>{t("userbox.header.preview")}</h2>
<p class="notice">{t("userbox.preview.notice")}</p>
<input bind:value={preview} placeholder={t("userbox.preview.url")}/>
{#if preview}
<div class="preview">
{#each userItems.filter(v => v.iKey != 'trophy' && v.iKey != 'systemVoice') as { iKey, ubKey, items }, i}
<div>
<span>{ts(`userbox.${ubKey}`)}</span>
<img src={`${preview}/${iKey}/${userbox[ubKey].toString().padStart(8, '0')}.png`} alt="" on:error={coverNotFound} />
</div>
{/each}
</div>
{/if}
{/if}
</div>
{/if}
<style lang="sass">
@use "../../vars"
input
width: 100%
h2
margin-bottom: 0.5rem
p.notice
opacity: 0.6
margin-top: 0
.preview
margin-top: 32px
display: flex
flex-wrap: wrap
justify-content: space-between
gap: 32px
> div
position: relative
width: 100px
height: 100px
overflow: hidden
background: vars.$ov-lighter
border-radius: vars.$border-radius
span
position: absolute
bottom: 0
width: 100%
text-align: center
z-index: 10
background: rgba(0, 0, 0, 0.2)
backdrop-filter: blur(2px)
img
position: absolute
inset: 0
width: 100%
height: 100%
object-fit: contain
.fields
display: flex
flex-direction: column
gap: 12px
width: 100%
flex-grow: 0
label
display: flex
flex-direction: column
select
width: 100%
.field
display: flex
flex-direction: column
width: 100%
label
max-width: max-content
> div:not(.bool)
display: flex
align-items: center
gap: 1rem
margin-top: 0.5rem
> select
flex: 1
</style>

View File

@ -1,10 +1,10 @@
<script lang="ts">
import { slide } from "svelte/transition";
import { SETTING } from "../libs/sdk";
import type { GameOption } from "../libs/generalTypes";
import { ts } from "../libs/i18n";
import StatusOverlays from "./StatusOverlays.svelte";
import InputWithButton from "./ui/InputWithButton.svelte";
import { SETTING } from "../../libs/sdk";
import type { GameOption } from "../../libs/generalTypes";
import { ts } from "../../libs/i18n";
import StatusOverlays from "../StatusOverlays.svelte";
import InputWithButton from "../ui/InputWithButton.svelte";
export let game: string;
let gameFields: GameOption[] = []

View File

@ -1,9 +1,9 @@
<script>
import { fade } from "svelte/transition";
import { FADE_IN, FADE_OUT } from "../libs/config";
import { FADE_IN, FADE_OUT } from "../../libs/config";
import GameSettingFields from "./GameSettingFields.svelte";
import { ts } from "../libs/i18n";
import useLocalStorage from "../libs/hooks/useLocalStorage.svelte";
import { ts } from "../../libs/i18n";
import useLocalStorage from "../../libs/hooks/useLocalStorage.svelte";
const rounding = useLocalStorage("rounding", true);
</script>

View File

@ -1,10 +1,10 @@
<script lang="ts">
import { slide, fade } from "svelte/transition";
import { FADE_IN, FADE_OUT } from "../libs/config";
import { t } from "../libs/i18n.js";
import { FADE_IN, FADE_OUT } from "../../libs/config";
import { t } from "../../libs/i18n.js";
import Icon from "@iconify/svelte";
import StatusOverlays from "./StatusOverlays.svelte";
import { GAME } from "../libs/sdk";
import StatusOverlays from "../StatusOverlays.svelte";
import { GAME } from "../../libs/sdk";
const profileFields = [
['name', t('settings.mai2.name')],

View File

@ -1,6 +1,6 @@
<script>
import { fade } from "svelte/transition";
import { FADE_IN, FADE_OUT } from "../libs/config";
import { FADE_IN, FADE_OUT } from "../../libs/config";
import GameSettingFields from "./GameSettingFields.svelte";
</script>

View File

@ -133,33 +133,15 @@ export interface GameOption {
changed?: boolean
}
export interface UserItem { itemKind: number, itemId: number, stock: number }
export interface UserBox {
userName:string,
level:number,
exp:string,
point:number,
totalPoint:number,
playerRating:number,
highestRating:number,
nameplateId:number,
frameId:number,
characterId:number,
trophyId:number,
totalMapNum:number,
totalHiScore: number,
totalBasicHighScore:number,
totalAdvancedHighScore:number,
totalExpertHighScore:number,
totalMasterHighScore:number,
totalUltimaHighScore:number,
friendCount:number,
firstPlayDate:Date,
lastPlayDate:Date,
courseClass:number,
overPowerPoint:number,
overPowerRate:number,
mapIconId:number,
voiceId:number,
userName: string,
nameplateId: number,
frameId: number,
characterId: number,
trophyId: number,
mapIconId: number,
voiceId: number,
avatarWear: number,
avatarHead: number,
avatarFace: number,
@ -168,16 +150,3 @@ export interface UserBox {
avatarFront: number,
avatarBack: number,
}
// Assign a number to each kind of user box item with an enum
export enum UserBoxItemKind {
nameplate = 1,
frame = 2,
trophy = 3,
mapicon = 8,
sysvoice = 9,
avatar = 11,
}
// Define type only with the keys
export type UserBoxItemKindStr = keyof typeof UserBoxItemKind

View File

@ -138,8 +138,10 @@ export const EN_REF_SETTINGS = {
'settings.fields.waccaInfiniteWp.desc': 'Set WP to 999999',
'settings.fields.waccaAlwaysVip.name': 'Wacca: Always VIP',
'settings.fields.waccaAlwaysVip.desc': 'Set VIP expiration date to 2077-01-01',
'settings.fields.chusanTeamName.name': 'Chunithm Team Name',
'settings.fields.chusanTeamName.name': 'Chuni: Team Name',
'settings.fields.chusanTeamName.desc': 'Customize the text displayed on the top of your profile.',
'settings.fields.chusanInfinitePenguins.name': 'Chuni: Infinite Penguins',
'settings.fields.chusanInfinitePenguins.desc': 'Set penguin statues for character level prompting to 999.',
'settings.fields.rounding.name': 'Score Rounding',
'settings.fields.rounding.desc': 'Round the score to one decimal place',
'settings.fields.optOutOfLeaderboard.name': 'Opt Out of Leaderboard',
@ -159,26 +161,24 @@ export const EN_REF_SETTINGS = {
}
export const EN_REF_USERBOX = {
'userbox.tabs.chusan':'Chuni',
'userbox.tabs.maimai':'Mai (WIP)',
'userbox.tabs.ongeki':'Ongeki (WIP)',
'userbox.nameplate': 'Nameplate',
'userbox.frame': 'Frame',
'userbox.trophy': 'Trophy (Title)',
'userbox.mapicon': 'Map Icon',
'userbox.voice':'System Voice',
'userbox.wear':'Avatar Wear',
'userbox.head':'Avatar Head',
'userbox.face':'Avatar Face',
'userbox.skin':'Avatar Skin',
'userbox.item':'Avatar Item',
'userbox.front':'Avatar Front',
'userbox.back':'Avatar Back',
'userbox.preview.avatar':'Avatar Preview',
'userbox.preview.nameplate':'Nameplate Preview',
'userbox.preview.ui':'Interface Preview',
'userbox.error.noprofile':'No profile was found for this game',
'userbox.error.nodata':'No data was found for this game',
'userbox.header.general': 'General Settings',
'userbox.header.userbox': 'UserBox Settings',
'userbox.header.preview': 'UserBox Preview',
'userbox.nameplateId': 'Nameplate',
'userbox.frameId': 'Frame',
'userbox.trophyId': 'Trophy (Title)',
'userbox.mapIconId': 'Map Icon',
'userbox.voiceId': 'System Voice',
'userbox.avatarWear': 'Avatar Wear',
'userbox.avatarHead': 'Avatar Head',
'userbox.avatarFace': 'Avatar Face',
'userbox.avatarSkin': 'Avatar Skin',
'userbox.avatarItem': 'Avatar Item',
'userbox.avatarFront': 'Avatar Front',
'userbox.avatarBack': 'Avatar Back',
'userbox.preview.notice': 'To honor the copyright, we cannot host the images of the userbox items. However, if someone else is willing to provide the images, you can enter their URL here and it will be displayed.',
'userbox.preview.url': 'Image URL',
'userbox.error.nodata': 'Chuni data not found',
}
export const EN_REF = { ...EN_REF_USER, ...EN_REF_Welcome, ...EN_REF_GENERAL,

View File

@ -4,6 +4,7 @@ import {
EN_REF_LEADERBOARD,
EN_REF_SETTINGS,
EN_REF_USER,
EN_REF_USERBOX,
type EN_REF_Welcome
} from "./en_ref";
@ -147,8 +148,10 @@ const zhSettings: typeof EN_REF_SETTINGS = {
'settings.fields.waccaInfiniteWp.desc': '将 WP 设置为 999999',
'settings.fields.waccaAlwaysVip.name': 'Wacca: 永久会员',
'settings.fields.waccaAlwaysVip.desc': '将 VIP 到期时间设置为 2077-01-01',
'settings.fields.chusanTeamName.name': '中二队名',
'settings.fields.chusanTeamName.name': '中二: ',
'settings.fields.chusanTeamName.desc': '自定义显示在个人资料顶部的文本。',
'settings.fields.chusanInfinitePenguins.name': '中二: 无限企鹅',
'settings.fields.chusanInfinitePenguins.desc': '将角色界限突破的企鹅雕像数量设置为 999。',
'settings.fields.rounding.name': '分数舍入',
'settings.fields.rounding.desc': '把分数四舍五入到一位小数',
'settings.fields.optOutOfLeaderboard.name': '不参与排行榜',
@ -167,5 +170,26 @@ const zhSettings: typeof EN_REF_SETTINGS = {
'settings.export': '导出玩家数据',
}
export const zhUserbox: typeof EN_REF_USERBOX = {
'userbox.header.general': '游戏设置',
'userbox.header.userbox': 'UserBox 设置',
'userbox.header.preview': 'UserBox 预览',
'userbox.nameplateId': '名牌',
'userbox.frameId': '边框',
'userbox.trophyId': '称号',
'userbox.mapIconId': '地图图标',
'userbox.voiceId': '系统语音',
'userbox.avatarWear': '企鹅服饰',
'userbox.avatarHead': '企鹅头饰',
'userbox.avatarFace': '企鹅面部',
'userbox.avatarSkin': '企鹅皮肤',
'userbox.avatarItem': '企鹅物品',
'userbox.avatarFront': '企鹅前景',
'userbox.avatarBack': '企鹅背景',
'userbox.preview.notice': '「生存战略」:为了尊重版权,我们不会提供游戏内物品的图片。但是如果你认识其他愿意提供图床的人,在这里输入 URL 就可以显示出预览。',
'userbox.preview.url': '图床 URL',
'userbox.error.nodata': '未找到中二数据',
};
export const ZH = { ...zhUser, ...zhWelcome, ...zhGeneral,
...zhLeaderboard, ...zhHome, ...zhSettings }
...zhLeaderboard, ...zhHome, ...zhSettings, ...zhUserbox }

View File

@ -8,7 +8,7 @@ import type {
TrendEntry,
AquaNetUser, GameOption,
UserBox,
UserBoxItemKind
UserItem
} from './generalTypes'
import type { GameName } from './scoring'
@ -262,13 +262,8 @@ export const USER = {
}
export const USERBOX = {
getProfile: (): Promise<UserBox> =>
getProfile: (): Promise<{ user: UserBox, items: UserItem[] }> =>
get('/api/v2/game/chu3/user-box', {}),
getUnlockedItems: (itemId: UserBoxItemKind): Promise<{ itemKind: number, itemId: number, stock: number, isValid: boolean }[]> =>
get(`/api/v2/game/chu3/user-box-item-by-kind`,{ itemId }),
getItemLabels: () => get(`/api/v2/game/chu3/user-box-all-items`, {}).then(it =>
Object.fromEntries(Object.entries(it).map(([key, value]) =>
[key, Object.fromEntries((value as any[]).map(it => [it.id, it.name]))]))),
setUserBox: (d: { field: string, value: number | string }) =>
post(`/api/v2/game/chu3/user-detail-set`, d),
}
@ -305,7 +300,9 @@ export const GAME = {
export const DATA = {
allMusic: (game: GameName): Promise<AllMusic> =>
fetch(`${DATA_HOST}/d/${game}/00/all-music.json`).then(it => it.json())
fetch(`${DATA_HOST}/d/${game}/00/all-music.json`).then(it => it.json()),
allItems: (game: GameName): Promise<Record<string, Record<string, any>>> =>
fetch(`${DATA_HOST}/d/${game}/00/all-items.json`).then(it => it.json()),
}
export const SETTING = {

View File

@ -9,10 +9,10 @@
import { pfp } from "../../libs/ui";
import { t, ts } from "../../libs/i18n";
import { FADE_IN, FADE_OUT } from "../../libs/config";
import UserBox from "../../components/UserBox.svelte";
import Mai2Settings from "../../components/Mai2Settings.svelte";
import WaccaSettings from "../../components/WaccaSettings.svelte";
import GeneralGameSettings from "../../components/GeneralGameSettings.svelte";
import UserBox from "../../components/settings/ChuniSettings.svelte";
import Mai2Settings from "../../components/settings/Mai2Settings.svelte";
import WaccaSettings from "../../components/settings/WaccaSettings.svelte";
import GeneralGameSettings from "../../components/settings/GeneralGameSettings.svelte";
USER.ensureLoggedIn()

View File

@ -1,19 +1,21 @@
# Chusan dev notes
## Item types
| ItemKind | Name | Single / Multiple | Notes |
|----------|-------------------|-------------------|-------------------------------|
| 1 | Nameplate | Single | - |
| 2 | Frame | Single | - |
| 3 | Trophy | Single | - |
| 4 | Skill | Multiple | Stock is level of skill |
| 5 | Ticket | Multiple | - |
| 6 | Present | Multiple? | - |
| 7 | Music (Unlock) | Single | - |
| 8 | Map Icon | Single | - |
| 9 | System Voice | Single | - |
| 10 | Symbol Chat | Single | - |
| 11 | Avatar Accessory | Single |Part can determined by category|
| ItemKind | Name | Single / Multiple | Notes |
|----------|------------------|-------------------|---------------------------------|
| 1 | Nameplate | Single | - |
| 2 | Frame | Single | - |
| 3 | Trophy | Single | - |
| 4 | Skill | Multiple | Stock is level of skill |
| 5 | Ticket | Multiple | - |
| 6 | Present | Multiple? | - |
| 7 | Music (Unlock) | Single | - |
| 8 | Map Icon | Single | - |
| 9 | System Voice | Single | - |
| 10 | Symbol Chat | Single | - |
| 11 | Avatar Accessory | Single | Part can determined by category |
Note: Chuni penguin statues are tickets (ok that sounds dumb)
## Avatar accessory category
| Category ID | Part name |

View File

@ -21,7 +21,6 @@ import java.security.MessageDigest
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.util.*
import java.util.concurrent.locks.Lock

View File

@ -3,7 +3,10 @@ package icu.samnyan.aqua.net
import ext.*
import icu.samnyan.aqua.net.db.AquaUserServices
import icu.samnyan.aqua.net.utils.SUCCESS
import icu.samnyan.aqua.sega.general.service.CardService
import icu.samnyan.aqua.sega.chusan.model.Chu3Repos
import icu.samnyan.aqua.sega.general.model.sensitiveInfo
import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos
import jakarta.transaction.Transactional
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration
@ -18,12 +21,13 @@ class BotProps {
}
@RestController
@ConditionalOnProperty("aqua-net.frontier.enabled", havingValue = "true")
@ConditionalOnProperty("aqua-net.bot.enabled", havingValue = "true")
@API("/api/v2/bot")
class BotController(
val cardService: CardService,
val us: AquaUserServices,
val props: BotProps
val props: BotProps,
val chu3Db: Chu3Repos,
val mai2Db: Mai2Repos,
) {
fun Str.checkSecret() {
if (this != props.secret) 403 - "Invalid Secret"
@ -31,14 +35,55 @@ class BotController(
@PostMapping("/ranking-ban")
@Doc("Ban a user from the leaderboard", "Success status")
suspend fun rankingBan(@RP secret: Str, @RP username: Str) {
suspend fun rankingBan(@RP secret: Str, @RP username: Str): Any {
secret.checkSecret()
us.cardByName(username) { card ->
return us.cardByName(username) { card ->
card.rankingBanned = true
cardService.cardRepo.save(card)
us.cardRepo.save(card)
SUCCESS
}
}
@Transactional
@PostMapping("/debug-user-profile")
@Doc("Obtain debug information for a user card", "User card details")
fun debugUserProfile(@RP secret: Str, @RP cardId: Str): Any {
secret.checkSecret()
// 1. Check if the card exist
var cards = listOfNotNull(
us.cardRepo.findByExtId(cardId.long)(),
us.cardRepo.findByLuid(cardId)(),
us.cardRepo.findById(cardId.long)(),
).toMutableList()
cards += cards.flatMap {
(it.aquaUser?.cards ?: emptyList()) + listOfNotNull(it.aquaUser?.ghostCard)
}
cards = cards.distinctBy { it.id }.toMutableList()
return cards.map { card ->
// Find all games played by this card
val chu3 = chu3Db.userData.findByCard_ExtId(card.extId)()
val mai2 = mai2Db.userData.findByCard_ExtId(card.extId)()
val gamesDict = listOfNotNull(chu3, mai2).map {
// Find the keychip owner
val keychip = it.lastClientId
val keychipOwner = keychip?.let { us.userRepo.findByKeychip(it) }
mapOf(
"userData" to it,
"keychip" to keychip,
"keychipOwner" to keychipOwner,
"keychipOwnerCards" to keychipOwner?.cards?.map { it.sensitiveInfo() }
)
}
mapOf(
"card" to card.sensitiveInfo(),
"games" to gamesDict
)
}
}
}

View File

@ -32,8 +32,11 @@ class AquaGameOptions(
@SettingField("wacca")
var waccaAlwaysVip: Boolean = false,
@SettingField("general")
@SettingField("chu3")
var chusanTeamName: String = "",
@SettingField("chu3")
var chusanInfinitePenguins: Boolean = false,
)
interface AquaGameOptionsRepo : JpaRepository<AquaGameOptions, Long>

View File

@ -30,15 +30,13 @@ class Chusan(
"trophyId" to { u, v -> u.trophyId = v.int },
"mapIconId" to { u, v -> u.mapIconId = v.int },
"voiceId" to { u, v -> u.voiceId = v.int },
"avatarItem" to { u, v -> v.split(':', limit=2).map { it.int }.let { (cat, data) -> when (cat) {
1 -> u.avatarWear = data
2 -> u.avatarHead = data
3 -> u.avatarFace = data
4 -> u.avatarSkin = data
5 -> u.avatarItem = data
6 -> u.avatarFront = data
7 -> u.avatarBack = data
} } }
"avatarWear" to { u, v -> u.avatarWear = v.int },
"avatarHead" to { u, v -> u.avatarHead = v.int },
"avatarFace" to { u, v -> u.avatarFace = v.int },
"avatarSkin" to { u, v -> u.avatarSkin = v.int },
"avatarItem" to { u, v -> u.avatarItem = v.int },
"avatarFront" to { u, v -> u.avatarFront = v.int },
"avatarBack" to { u, v -> u.avatarBack = v.int },
) }
override suspend fun userSummary(@RP username: Str, @RP token: String?) = us.cardByName(username) { card ->
@ -60,11 +58,9 @@ class Chusan(
// UserBox related APIs
@API("user-box")
fun userBox(@RP token: String) = us.jwt.auth(token) {
userDataRepo.findByCard(it.ghostCard) ?: (404 - "Game data not found") }
@API("user-box-item-by-kind")
fun userBoxItem(@RP token: String, @RP itemId: Int) = us.jwt.auth(token) {
rp.userItem.findAllByUser_Card_ExtIdAndItemKind(it.ghostCard.extId, itemId) }
val u = userDataRepo.findByCard(it.ghostCard) ?: (404 - "Game data not found")
mapOf("user" to u, "items" to rp.userItem.findAllByUser(u))
}
@API("user-box-all-items")
fun userBoxAllItems() = allItems

View File

@ -6,6 +6,7 @@ import icu.samnyan.aqua.sega.chusan.model.response.data.MatchingMemberInfo
import icu.samnyan.aqua.sega.chusan.model.response.data.MatchingWaitState
import icu.samnyan.aqua.sega.chusan.model.response.data.UserEmoney
import icu.samnyan.aqua.sega.chusan.model.userdata.UserCharge
import icu.samnyan.aqua.sega.chusan.model.userdata.UserItem
import icu.samnyan.aqua.sega.chusan.model.userdata.UserMusicDetail
import icu.samnyan.aqua.sega.general.model.response.UserRecentRating
import java.time.format.DateTimeFormatter
@ -122,13 +123,26 @@ val chusanInit: ChusanController.() -> Unit = {
}
}
// Check dev/chusan_dev_notes for more item information
val penguins = ls(8000, 8010, 8020, 8030)
"GetUserItem".pagedWithKind("userItemList") {
val rawIndex = data["nextIndex"]!!.long
val kind = parsing { (rawIndex / 10000000000L).int }
data["nextIndex"] = rawIndex % 10000000000L
mapOf("itemKind" to kind) grabs {
// TODO: All unlock
db.userItem.findAllByUser_Card_ExtIdAndItemKind(uid, kind)
val items = db.userItem.findAllByUser_Card_ExtIdAndItemKind(uid, kind).toMutableList()
// Check game options
db.userData.findByCard_ExtId(uid)()?.card?.aquaUser?.gameOptions?.let {
if (it.chusanInfinitePenguins && kind == 5) {
items.removeAll { it.itemId in penguins }
items.addAll(penguins.map { UserItem(kind, it, 999, true) })
}
}
items
} postProcess {
val ni = it["nextIndex"]!!.long
if (ni != -1L) it["nextIndex"] = ni + (kind * 10000000000L)

View File

@ -90,6 +90,7 @@ interface Chu3UserGeneralDataRepo : Chu3UserLinked<UserGeneralData> {
}
interface Chu3UserItemRepo : Chu3UserLinked<UserItem> {
fun findAllByUser(user: Chu3UserData): List<UserItem>
fun findTopByUserAndItemIdAndItemKindOrderByIdDesc(user: Chu3UserData, itemId: Int, itemKind: Int): Optional<UserItem>
fun findByUserAndItemIdAndItemKind(user: Chu3UserData, itemId: Int, itemKind: Int): UserItem?

View File

@ -48,3 +48,5 @@ class Card(
@Suppress("unused") // Used by serialization
val isLinked get() = aquaUser != null
}
fun Card.sensitiveInfo() = mapOf("id" to id, "extId" to extId, "luid" to luid)

View File

@ -0,0 +1 @@
ALTER TABLE aqua_game_options ADD chusan_infinite_penguins BIT(1) NOT NULL DEFAULT 0;