mirror of https://github.com/hykilpikonna/AquaDX
Merge branch 'v1-dev' into matching
commit
8a1e17ecd3
|
@ -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>
|
|
|
@ -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>
|
|
@ -1,10 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { slide } from "svelte/transition";
|
import { slide } from "svelte/transition";
|
||||||
import { SETTING } from "../libs/sdk";
|
import { SETTING } from "../../libs/sdk";
|
||||||
import type { GameOption } from "../libs/generalTypes";
|
import type { GameOption } from "../../libs/generalTypes";
|
||||||
import { ts } from "../libs/i18n";
|
import { ts } from "../../libs/i18n";
|
||||||
import StatusOverlays from "./StatusOverlays.svelte";
|
import StatusOverlays from "../StatusOverlays.svelte";
|
||||||
import InputWithButton from "./ui/InputWithButton.svelte";
|
import InputWithButton from "../ui/InputWithButton.svelte";
|
||||||
|
|
||||||
export let game: string;
|
export let game: string;
|
||||||
let gameFields: GameOption[] = []
|
let gameFields: GameOption[] = []
|
|
@ -1,9 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { fade } from "svelte/transition";
|
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 GameSettingFields from "./GameSettingFields.svelte";
|
||||||
import { ts } from "../libs/i18n";
|
import { ts } from "../../libs/i18n";
|
||||||
import useLocalStorage from "../libs/hooks/useLocalStorage.svelte";
|
import useLocalStorage from "../../libs/hooks/useLocalStorage.svelte";
|
||||||
|
|
||||||
const rounding = useLocalStorage("rounding", true);
|
const rounding = useLocalStorage("rounding", true);
|
||||||
</script>
|
</script>
|
|
@ -1,10 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { slide, fade } from "svelte/transition";
|
import { slide, fade } from "svelte/transition";
|
||||||
import { FADE_IN, FADE_OUT } from "../libs/config";
|
import { FADE_IN, FADE_OUT } from "../../libs/config";
|
||||||
import { t } from "../libs/i18n.js";
|
import { t } from "../../libs/i18n.js";
|
||||||
import Icon from "@iconify/svelte";
|
import Icon from "@iconify/svelte";
|
||||||
import StatusOverlays from "./StatusOverlays.svelte";
|
import StatusOverlays from "../StatusOverlays.svelte";
|
||||||
import { GAME } from "../libs/sdk";
|
import { GAME } from "../../libs/sdk";
|
||||||
|
|
||||||
const profileFields = [
|
const profileFields = [
|
||||||
['name', t('settings.mai2.name')],
|
['name', t('settings.mai2.name')],
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { fade } from "svelte/transition";
|
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 GameSettingFields from "./GameSettingFields.svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -133,33 +133,15 @@ export interface GameOption {
|
||||||
changed?: boolean
|
changed?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserItem { itemKind: number, itemId: number, stock: number }
|
||||||
export interface UserBox {
|
export interface UserBox {
|
||||||
userName:string,
|
userName: string,
|
||||||
level:number,
|
nameplateId: number,
|
||||||
exp:string,
|
frameId: number,
|
||||||
point:number,
|
characterId: number,
|
||||||
totalPoint:number,
|
trophyId: number,
|
||||||
playerRating:number,
|
mapIconId: number,
|
||||||
highestRating:number,
|
voiceId: 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,
|
|
||||||
avatarWear: number,
|
avatarWear: number,
|
||||||
avatarHead: number,
|
avatarHead: number,
|
||||||
avatarFace: number,
|
avatarFace: number,
|
||||||
|
@ -168,16 +150,3 @@ export interface UserBox {
|
||||||
avatarFront: number,
|
avatarFront: number,
|
||||||
avatarBack: 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
|
|
||||||
|
|
|
@ -138,8 +138,10 @@ export const EN_REF_SETTINGS = {
|
||||||
'settings.fields.waccaInfiniteWp.desc': 'Set WP to 999999',
|
'settings.fields.waccaInfiniteWp.desc': 'Set WP to 999999',
|
||||||
'settings.fields.waccaAlwaysVip.name': 'Wacca: Always VIP',
|
'settings.fields.waccaAlwaysVip.name': 'Wacca: Always VIP',
|
||||||
'settings.fields.waccaAlwaysVip.desc': 'Set VIP expiration date to 2077-01-01',
|
'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.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.name': 'Score Rounding',
|
||||||
'settings.fields.rounding.desc': 'Round the score to one decimal place',
|
'settings.fields.rounding.desc': 'Round the score to one decimal place',
|
||||||
'settings.fields.optOutOfLeaderboard.name': 'Opt Out of Leaderboard',
|
'settings.fields.optOutOfLeaderboard.name': 'Opt Out of Leaderboard',
|
||||||
|
@ -159,26 +161,24 @@ export const EN_REF_SETTINGS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EN_REF_USERBOX = {
|
export const EN_REF_USERBOX = {
|
||||||
'userbox.tabs.chusan':'Chuni',
|
'userbox.header.general': 'General Settings',
|
||||||
'userbox.tabs.maimai':'Mai (WIP)',
|
'userbox.header.userbox': 'UserBox Settings',
|
||||||
'userbox.tabs.ongeki':'Ongeki (WIP)',
|
'userbox.header.preview': 'UserBox Preview',
|
||||||
'userbox.nameplate': 'Nameplate',
|
'userbox.nameplateId': 'Nameplate',
|
||||||
'userbox.frame': 'Frame',
|
'userbox.frameId': 'Frame',
|
||||||
'userbox.trophy': 'Trophy (Title)',
|
'userbox.trophyId': 'Trophy (Title)',
|
||||||
'userbox.mapicon': 'Map Icon',
|
'userbox.mapIconId': 'Map Icon',
|
||||||
'userbox.voice':'System Voice',
|
'userbox.voiceId': 'System Voice',
|
||||||
'userbox.wear':'Avatar Wear',
|
'userbox.avatarWear': 'Avatar Wear',
|
||||||
'userbox.head':'Avatar Head',
|
'userbox.avatarHead': 'Avatar Head',
|
||||||
'userbox.face':'Avatar Face',
|
'userbox.avatarFace': 'Avatar Face',
|
||||||
'userbox.skin':'Avatar Skin',
|
'userbox.avatarSkin': 'Avatar Skin',
|
||||||
'userbox.item':'Avatar Item',
|
'userbox.avatarItem': 'Avatar Item',
|
||||||
'userbox.front':'Avatar Front',
|
'userbox.avatarFront': 'Avatar Front',
|
||||||
'userbox.back':'Avatar Back',
|
'userbox.avatarBack': 'Avatar Back',
|
||||||
'userbox.preview.avatar':'Avatar Preview',
|
'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.nameplate':'Nameplate Preview',
|
'userbox.preview.url': 'Image URL',
|
||||||
'userbox.preview.ui':'Interface Preview',
|
'userbox.error.nodata': 'Chuni data not found',
|
||||||
'userbox.error.noprofile':'No profile was found for this game',
|
|
||||||
'userbox.error.nodata':'No data was found for this game',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EN_REF = { ...EN_REF_USER, ...EN_REF_Welcome, ...EN_REF_GENERAL,
|
export const EN_REF = { ...EN_REF_USER, ...EN_REF_Welcome, ...EN_REF_GENERAL,
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
EN_REF_LEADERBOARD,
|
EN_REF_LEADERBOARD,
|
||||||
EN_REF_SETTINGS,
|
EN_REF_SETTINGS,
|
||||||
EN_REF_USER,
|
EN_REF_USER,
|
||||||
|
EN_REF_USERBOX,
|
||||||
type EN_REF_Welcome
|
type EN_REF_Welcome
|
||||||
} from "./en_ref";
|
} from "./en_ref";
|
||||||
|
|
||||||
|
@ -147,8 +148,10 @@ const zhSettings: typeof EN_REF_SETTINGS = {
|
||||||
'settings.fields.waccaInfiniteWp.desc': '将 WP 设置为 999999',
|
'settings.fields.waccaInfiniteWp.desc': '将 WP 设置为 999999',
|
||||||
'settings.fields.waccaAlwaysVip.name': 'Wacca: 永久会员',
|
'settings.fields.waccaAlwaysVip.name': 'Wacca: 永久会员',
|
||||||
'settings.fields.waccaAlwaysVip.desc': '将 VIP 到期时间设置为 2077-01-01',
|
'settings.fields.waccaAlwaysVip.desc': '将 VIP 到期时间设置为 2077-01-01',
|
||||||
'settings.fields.chusanTeamName.name': '中二队名',
|
'settings.fields.chusanTeamName.name': '中二: 队伍名称',
|
||||||
'settings.fields.chusanTeamName.desc': '自定义显示在个人资料顶部的文本。',
|
'settings.fields.chusanTeamName.desc': '自定义显示在个人资料顶部的文本。',
|
||||||
|
'settings.fields.chusanInfinitePenguins.name': '中二: 无限企鹅',
|
||||||
|
'settings.fields.chusanInfinitePenguins.desc': '将角色界限突破的企鹅雕像数量设置为 999。',
|
||||||
'settings.fields.rounding.name': '分数舍入',
|
'settings.fields.rounding.name': '分数舍入',
|
||||||
'settings.fields.rounding.desc': '把分数四舍五入到一位小数',
|
'settings.fields.rounding.desc': '把分数四舍五入到一位小数',
|
||||||
'settings.fields.optOutOfLeaderboard.name': '不参与排行榜',
|
'settings.fields.optOutOfLeaderboard.name': '不参与排行榜',
|
||||||
|
@ -167,5 +170,26 @@ const zhSettings: typeof EN_REF_SETTINGS = {
|
||||||
'settings.export': '导出玩家数据',
|
'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,
|
export const ZH = { ...zhUser, ...zhWelcome, ...zhGeneral,
|
||||||
...zhLeaderboard, ...zhHome, ...zhSettings }
|
...zhLeaderboard, ...zhHome, ...zhSettings, ...zhUserbox }
|
||||||
|
|
|
@ -8,7 +8,7 @@ import type {
|
||||||
TrendEntry,
|
TrendEntry,
|
||||||
AquaNetUser, GameOption,
|
AquaNetUser, GameOption,
|
||||||
UserBox,
|
UserBox,
|
||||||
UserBoxItemKind
|
UserItem
|
||||||
} from './generalTypes'
|
} from './generalTypes'
|
||||||
import type { GameName } from './scoring'
|
import type { GameName } from './scoring'
|
||||||
|
|
||||||
|
@ -262,13 +262,8 @@ export const USER = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const USERBOX = {
|
export const USERBOX = {
|
||||||
getProfile: (): Promise<UserBox> =>
|
getProfile: (): Promise<{ user: UserBox, items: UserItem[] }> =>
|
||||||
get('/api/v2/game/chu3/user-box', {}),
|
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 }) =>
|
setUserBox: (d: { field: string, value: number | string }) =>
|
||||||
post(`/api/v2/game/chu3/user-detail-set`, d),
|
post(`/api/v2/game/chu3/user-detail-set`, d),
|
||||||
}
|
}
|
||||||
|
@ -305,7 +300,9 @@ export const GAME = {
|
||||||
|
|
||||||
export const DATA = {
|
export const DATA = {
|
||||||
allMusic: (game: GameName): Promise<AllMusic> =>
|
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 = {
|
export const SETTING = {
|
||||||
|
|
|
@ -9,10 +9,10 @@
|
||||||
import { pfp } from "../../libs/ui";
|
import { pfp } from "../../libs/ui";
|
||||||
import { t, ts } from "../../libs/i18n";
|
import { t, ts } from "../../libs/i18n";
|
||||||
import { FADE_IN, FADE_OUT } from "../../libs/config";
|
import { FADE_IN, FADE_OUT } from "../../libs/config";
|
||||||
import UserBox from "../../components/UserBox.svelte";
|
import UserBox from "../../components/settings/ChuniSettings.svelte";
|
||||||
import Mai2Settings from "../../components/Mai2Settings.svelte";
|
import Mai2Settings from "../../components/settings/Mai2Settings.svelte";
|
||||||
import WaccaSettings from "../../components/WaccaSettings.svelte";
|
import WaccaSettings from "../../components/settings/WaccaSettings.svelte";
|
||||||
import GeneralGameSettings from "../../components/GeneralGameSettings.svelte";
|
import GeneralGameSettings from "../../components/settings/GeneralGameSettings.svelte";
|
||||||
|
|
||||||
USER.ensureLoggedIn()
|
USER.ensureLoggedIn()
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
## Item types
|
## Item types
|
||||||
| ItemKind | Name | Single / Multiple | Notes |
|
| ItemKind | Name | Single / Multiple | Notes |
|
||||||
|----------|-------------------|-------------------|-------------------------------|
|
|----------|------------------|-------------------|---------------------------------|
|
||||||
| 1 | Nameplate | Single | - |
|
| 1 | Nameplate | Single | - |
|
||||||
| 2 | Frame | Single | - |
|
| 2 | Frame | Single | - |
|
||||||
| 3 | Trophy | Single | - |
|
| 3 | Trophy | Single | - |
|
||||||
|
@ -13,7 +13,9 @@
|
||||||
| 8 | Map Icon | Single | - |
|
| 8 | Map Icon | Single | - |
|
||||||
| 9 | System Voice | Single | - |
|
| 9 | System Voice | Single | - |
|
||||||
| 10 | Symbol Chat | Single | - |
|
| 10 | Symbol Chat | Single | - |
|
||||||
| 11 | Avatar Accessory | Single |Part can determined by category|
|
| 11 | Avatar Accessory | Single | Part can determined by category |
|
||||||
|
|
||||||
|
Note: Chuni penguin statues are tickets (ok that sounds dumb)
|
||||||
|
|
||||||
## Avatar accessory category
|
## Avatar accessory category
|
||||||
| Category ID | Part name |
|
| Category ID | Part name |
|
||||||
|
|
|
@ -21,7 +21,6 @@ import java.security.MessageDigest
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.ZonedDateTime
|
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.locks.Lock
|
import java.util.concurrent.locks.Lock
|
||||||
|
|
|
@ -3,7 +3,10 @@ package icu.samnyan.aqua.net
|
||||||
import ext.*
|
import ext.*
|
||||||
import icu.samnyan.aqua.net.db.AquaUserServices
|
import icu.samnyan.aqua.net.db.AquaUserServices
|
||||||
import icu.samnyan.aqua.net.utils.SUCCESS
|
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.autoconfigure.condition.ConditionalOnProperty
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
|
@ -18,12 +21,13 @@ class BotProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@ConditionalOnProperty("aqua-net.frontier.enabled", havingValue = "true")
|
@ConditionalOnProperty("aqua-net.bot.enabled", havingValue = "true")
|
||||||
@API("/api/v2/bot")
|
@API("/api/v2/bot")
|
||||||
class BotController(
|
class BotController(
|
||||||
val cardService: CardService,
|
|
||||||
val us: AquaUserServices,
|
val us: AquaUserServices,
|
||||||
val props: BotProps
|
val props: BotProps,
|
||||||
|
val chu3Db: Chu3Repos,
|
||||||
|
val mai2Db: Mai2Repos,
|
||||||
) {
|
) {
|
||||||
fun Str.checkSecret() {
|
fun Str.checkSecret() {
|
||||||
if (this != props.secret) 403 - "Invalid Secret"
|
if (this != props.secret) 403 - "Invalid Secret"
|
||||||
|
@ -31,14 +35,55 @@ class BotController(
|
||||||
|
|
||||||
@PostMapping("/ranking-ban")
|
@PostMapping("/ranking-ban")
|
||||||
@Doc("Ban a user from the leaderboard", "Success status")
|
@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()
|
secret.checkSecret()
|
||||||
|
|
||||||
us.cardByName(username) { card ->
|
return us.cardByName(username) { card ->
|
||||||
card.rankingBanned = true
|
card.rankingBanned = true
|
||||||
cardService.cardRepo.save(card)
|
us.cardRepo.save(card)
|
||||||
|
|
||||||
SUCCESS
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,8 +32,11 @@ class AquaGameOptions(
|
||||||
@SettingField("wacca")
|
@SettingField("wacca")
|
||||||
var waccaAlwaysVip: Boolean = false,
|
var waccaAlwaysVip: Boolean = false,
|
||||||
|
|
||||||
@SettingField("general")
|
@SettingField("chu3")
|
||||||
var chusanTeamName: String = "",
|
var chusanTeamName: String = "",
|
||||||
|
|
||||||
|
@SettingField("chu3")
|
||||||
|
var chusanInfinitePenguins: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
interface AquaGameOptionsRepo : JpaRepository<AquaGameOptions, Long>
|
interface AquaGameOptionsRepo : JpaRepository<AquaGameOptions, Long>
|
||||||
|
|
|
@ -30,15 +30,13 @@ class Chusan(
|
||||||
"trophyId" to { u, v -> u.trophyId = v.int },
|
"trophyId" to { u, v -> u.trophyId = v.int },
|
||||||
"mapIconId" to { u, v -> u.mapIconId = v.int },
|
"mapIconId" to { u, v -> u.mapIconId = v.int },
|
||||||
"voiceId" to { u, v -> u.voiceId = 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) {
|
"avatarWear" to { u, v -> u.avatarWear = v.int },
|
||||||
1 -> u.avatarWear = data
|
"avatarHead" to { u, v -> u.avatarHead = v.int },
|
||||||
2 -> u.avatarHead = data
|
"avatarFace" to { u, v -> u.avatarFace = v.int },
|
||||||
3 -> u.avatarFace = data
|
"avatarSkin" to { u, v -> u.avatarSkin = v.int },
|
||||||
4 -> u.avatarSkin = data
|
"avatarItem" to { u, v -> u.avatarItem = v.int },
|
||||||
5 -> u.avatarItem = data
|
"avatarFront" to { u, v -> u.avatarFront = v.int },
|
||||||
6 -> u.avatarFront = data
|
"avatarBack" to { u, v -> u.avatarBack = v.int },
|
||||||
7 -> u.avatarBack = data
|
|
||||||
} } }
|
|
||||||
) }
|
) }
|
||||||
|
|
||||||
override suspend fun userSummary(@RP username: Str, @RP token: String?) = us.cardByName(username) { card ->
|
override suspend fun userSummary(@RP username: Str, @RP token: String?) = us.cardByName(username) { card ->
|
||||||
|
@ -60,11 +58,9 @@ class Chusan(
|
||||||
// UserBox related APIs
|
// UserBox related APIs
|
||||||
@API("user-box")
|
@API("user-box")
|
||||||
fun userBox(@RP token: String) = us.jwt.auth(token) {
|
fun userBox(@RP token: String) = us.jwt.auth(token) {
|
||||||
userDataRepo.findByCard(it.ghostCard) ?: (404 - "Game data not found") }
|
val u = userDataRepo.findByCard(it.ghostCard) ?: (404 - "Game data not found")
|
||||||
|
mapOf("user" to u, "items" to rp.userItem.findAllByUser(u))
|
||||||
@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) }
|
|
||||||
|
|
||||||
@API("user-box-all-items")
|
@API("user-box-all-items")
|
||||||
fun userBoxAllItems() = allItems
|
fun userBoxAllItems() = allItems
|
||||||
|
|
|
@ -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.MatchingWaitState
|
||||||
import icu.samnyan.aqua.sega.chusan.model.response.data.UserEmoney
|
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.UserCharge
|
||||||
|
import icu.samnyan.aqua.sega.chusan.model.userdata.UserItem
|
||||||
import icu.samnyan.aqua.sega.chusan.model.userdata.UserMusicDetail
|
import icu.samnyan.aqua.sega.chusan.model.userdata.UserMusicDetail
|
||||||
import icu.samnyan.aqua.sega.general.model.response.UserRecentRating
|
import icu.samnyan.aqua.sega.general.model.response.UserRecentRating
|
||||||
import java.time.format.DateTimeFormatter
|
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") {
|
"GetUserItem".pagedWithKind("userItemList") {
|
||||||
val rawIndex = data["nextIndex"]!!.long
|
val rawIndex = data["nextIndex"]!!.long
|
||||||
val kind = parsing { (rawIndex / 10000000000L).int }
|
val kind = parsing { (rawIndex / 10000000000L).int }
|
||||||
data["nextIndex"] = rawIndex % 10000000000L
|
data["nextIndex"] = rawIndex % 10000000000L
|
||||||
mapOf("itemKind" to kind) grabs {
|
mapOf("itemKind" to kind) grabs {
|
||||||
// TODO: All unlock
|
// 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 {
|
} postProcess {
|
||||||
val ni = it["nextIndex"]!!.long
|
val ni = it["nextIndex"]!!.long
|
||||||
if (ni != -1L) it["nextIndex"] = ni + (kind * 10000000000L)
|
if (ni != -1L) it["nextIndex"] = ni + (kind * 10000000000L)
|
||||||
|
|
|
@ -90,6 +90,7 @@ interface Chu3UserGeneralDataRepo : Chu3UserLinked<UserGeneralData> {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Chu3UserItemRepo : Chu3UserLinked<UserItem> {
|
interface Chu3UserItemRepo : Chu3UserLinked<UserItem> {
|
||||||
|
fun findAllByUser(user: Chu3UserData): List<UserItem>
|
||||||
fun findTopByUserAndItemIdAndItemKindOrderByIdDesc(user: Chu3UserData, itemId: Int, itemKind: Int): Optional<UserItem>
|
fun findTopByUserAndItemIdAndItemKindOrderByIdDesc(user: Chu3UserData, itemId: Int, itemKind: Int): Optional<UserItem>
|
||||||
fun findByUserAndItemIdAndItemKind(user: Chu3UserData, itemId: Int, itemKind: Int): UserItem?
|
fun findByUserAndItemIdAndItemKind(user: Chu3UserData, itemId: Int, itemKind: Int): UserItem?
|
||||||
|
|
||||||
|
|
|
@ -48,3 +48,5 @@ class Card(
|
||||||
@Suppress("unused") // Used by serialization
|
@Suppress("unused") // Used by serialization
|
||||||
val isLinked get() = aquaUser != null
|
val isLinked get() = aquaUser != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Card.sensitiveInfo() = mapOf("id" to id, "extId" to extId, "luid" to luid)
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE aqua_game_options ADD chusan_infinite_penguins BIT(1) NOT NULL DEFAULT 0;
|
Loading…
Reference in New Issue