mirror of https://github.com/hykilpikonna/AquaDX
aquabox improvements (#99)
commit
574e885da3
|
@ -31,3 +31,5 @@ dist-ssr
|
|||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
public/chu3
|
|
@ -8,7 +8,7 @@
|
|||
} 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 { DATA_HOST, FADE_IN, FADE_OUT, USERBOX_DEFAULT_URL } from "../../libs/config";
|
||||
import { fade, slide } from "svelte/transition";
|
||||
import StatusOverlays from "../StatusOverlays.svelte";
|
||||
import Icon from "@iconify/svelte";
|
||||
|
@ -97,9 +97,11 @@
|
|||
|
||||
let USERBOX_PROGRESS = 0;
|
||||
let USERBOX_SETUP_RUN = false;
|
||||
let USERBOX_SETUP_MODE = false;
|
||||
let USERBOX_SETUP_TEXT = t("userbox.new.setup");
|
||||
|
||||
let USERBOX_ENABLED = useLocalStorage("userboxNew", false);
|
||||
let USERBOX_PROFILE_ENABLED = useLocalStorage("userboxNewProfile", false);
|
||||
let USERBOX_INSTALLED = false;
|
||||
let USERBOX_SUPPORT = "webkitGetAsEntry" in DataTransferItem.prototype;
|
||||
|
||||
|
@ -117,12 +119,37 @@
|
|||
}) ?? "";
|
||||
}
|
||||
|
||||
let USERBOX_URL_STATE = useLocalStorage("userboxURL", USERBOX_DEFAULT_URL);
|
||||
function userboxHandleInput(baseURL: string, isSetByServer: boolean = false) {
|
||||
if (baseURL != "")
|
||||
try {
|
||||
// validate url
|
||||
new URL(baseURL, location.href);
|
||||
} catch(err) {
|
||||
if (isSetByServer)
|
||||
return;
|
||||
return error = t("userbox.new.error.invalidUrl")
|
||||
}
|
||||
USERBOX_URL_STATE.value = baseURL;
|
||||
USERBOX_ENABLED.value = true;
|
||||
USERBOX_PROFILE_ENABLED.value = true;
|
||||
location.reload();
|
||||
}
|
||||
|
||||
if (USERBOX_DEFAULT_URL && !USERBOX_URL_STATE.value)
|
||||
userboxHandleInput(USERBOX_DEFAULT_URL, true);
|
||||
|
||||
indexedDB.databases().then(async (dbi) => {
|
||||
let databaseExists = dbi.some(db => db.name == "userboxChusanDDS");
|
||||
if (USERBOX_URL_STATE.value && databaseExists) {
|
||||
indexedDB.deleteDatabase("userboxChusanDDS")
|
||||
}
|
||||
if (databaseExists) {
|
||||
await initializeDb();
|
||||
}
|
||||
if (databaseExists || USERBOX_URL_STATE.value) {
|
||||
DDSreader = new DDS(ddsDB);
|
||||
USERBOX_INSTALLED = databaseExists;
|
||||
USERBOX_INSTALLED = databaseExists || USERBOX_URL_STATE.value != "";
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -156,9 +183,9 @@
|
|||
</div>
|
||||
{:else}
|
||||
<div class="chuni-userbox-container">
|
||||
<ChuniUserplateComponent on:click={() => userboxSelected = "nameplateId"} chuniCharacter={userbox.characterId} chuniLevel={userbox.level} chuniRating={userbox.playerRating / 100}
|
||||
<ChuniUserplateComponent chuniIsUserbox={true} on:click={() => userboxSelected = "nameplateId"} chuniCharacter={userbox.characterId} chuniLevel={userbox.level.toString()} chuniRating={userbox.playerRating / 100}
|
||||
chuniNameplate={userbox.nameplateId} chuniName={userbox.userName} chuniTrophyName={allItems.trophy[userbox.trophyId].name}></ChuniUserplateComponent>
|
||||
<ChuniPenguinComponent classPassthrough="chuni-penguin-float" chuniWear={userbox.avatarWear} chuniHead={userbox.avatarHead} chuniBack={userbox.avatarBack}
|
||||
<ChuniPenguinComponent chuniWear={userbox.avatarWear} chuniHead={userbox.avatarHead} chuniBack={userbox.avatarBack}
|
||||
chuniFront={userbox.avatarFront} chuniFace={userbox.avatarFace} chuniItem={userbox.avatarItem}
|
||||
chuniSkin={userbox.avatarSkin}></ChuniPenguinComponent>
|
||||
</div>
|
||||
|
@ -210,39 +237,28 @@
|
|||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if HAS_USERBOX_ASSETS}
|
||||
{#if USERBOX_INSTALLED}
|
||||
<!-- god this is a mess but idgaf atp -->
|
||||
<div class="field boolean" style:margin-top="1em">
|
||||
<input type="checkbox" bind:checked={USERBOX_ENABLED.value} id="newUserbox">
|
||||
<label for="newUserbox">
|
||||
<span class="name">{t("userbox.new.activate")}</span>
|
||||
<span class="desc">{t(`userbox.new.activate_desc`)}</span>
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
{#if USERBOX_SUPPORT}
|
||||
<p>
|
||||
<button on:click={() => USERBOX_SETUP_RUN = !USERBOX_SETUP_RUN}>{t(!USERBOX_INSTALLED ? `userbox.new.activate_first` : `userbox.new.activate_update`)}</button>
|
||||
</p>
|
||||
{/if}
|
||||
{#if !USERBOX_SUPPORT || !USERBOX_INSTALLED || !USERBOX_ENABLED.value}
|
||||
<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}
|
||||
{#if USERBOX_INSTALLED}
|
||||
<!-- god this is a mess but idgaf atp -->
|
||||
<div class="field boolean" style:margin-top="1em">
|
||||
<input type="checkbox" bind:checked={USERBOX_ENABLED.value} id="newUserbox">
|
||||
<label for="newUserbox">
|
||||
<span class="name">{t("userbox.new.activate")}</span>
|
||||
<span class="desc">{t(`userbox.new.activate_desc`)}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="field boolean" style:margin-top="1em">
|
||||
<input type="checkbox" bind:checked={USERBOX_PROFILE_ENABLED.value} id="newUserboxProfile">
|
||||
<label for="newUserboxProfile">
|
||||
<span class="name">{t("userbox.new.activate_profile")}</span>
|
||||
<span class="desc">{t(`userbox.new.activate_profile_desc`)}</span>
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
{#if USERBOX_SUPPORT && !USERBOX_DEFAULT_URL}
|
||||
<p>
|
||||
<button on:click={() => USERBOX_SETUP_RUN = !USERBOX_SETUP_RUN}>{t(!USERBOX_INSTALLED ? `userbox.new.activate_first` : `userbox.new.activate_update`)}</button>
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<ChuniMatchingSettings/>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -251,20 +267,32 @@
|
|||
<div class="overlay" transition:fade>
|
||||
<div>
|
||||
<h2>{t('userbox.new.name')}</h2>
|
||||
<span>{USERBOX_SETUP_TEXT}</span>
|
||||
<span>{USERBOX_SETUP_MODE ? t('userbox.new.url_warning') : USERBOX_SETUP_TEXT}</span>
|
||||
<div class="actions">
|
||||
{#if USERBOX_PROGRESS != 0}
|
||||
<div class="progress">
|
||||
<div class="progress-bar" style="width: {USERBOX_PROGRESS}%"></div>
|
||||
</div>
|
||||
{#if USERBOX_SETUP_MODE}
|
||||
<input type="text" on:keyup={e => {if (e.key == "Enter") userboxHandleInput((e.target as HTMLInputElement).value)}} class="add-margin" placeholder="Base URL">
|
||||
{:else}
|
||||
<button class="drop-btn">
|
||||
<input type="file" on:input={userboxSafeDrop} on:click={e => e.preventDefault()}>
|
||||
{t('userbox.new.drop')}
|
||||
</button>
|
||||
<button on:click={() => USERBOX_SETUP_RUN = false}>
|
||||
{t('back')}
|
||||
</button>
|
||||
{#if USERBOX_PROGRESS != 0}
|
||||
<div class="progress">
|
||||
<div class="progress-bar" style="width: {USERBOX_PROGRESS}%"></div>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="notice add-margin">
|
||||
{t('userbox.new.setup.notice')}
|
||||
</p>
|
||||
<button class="drop-btn">
|
||||
<input type="file" on:input={userboxSafeDrop} on:click={e => e.preventDefault()}>
|
||||
{t('userbox.new.drop')}
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if USERBOX_PROGRESS == 0}
|
||||
<button on:click={() => USERBOX_SETUP_RUN = false}>
|
||||
{t('back')}
|
||||
</button>
|
||||
<button on:click={() => USERBOX_SETUP_MODE = !USERBOX_SETUP_MODE}>
|
||||
{t(USERBOX_SETUP_MODE ? 'userbox.new.switch.to_drop' : 'userbox.new.switch.to_url')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -299,13 +327,15 @@ p.notice
|
|||
border-radius: 25px
|
||||
|
||||
|
||||
.add-margin, .drop-btn
|
||||
margin-bottom: 1em
|
||||
|
||||
.drop-btn
|
||||
position: relative
|
||||
width: 100%
|
||||
aspect-ratio: 3
|
||||
background: transparent
|
||||
box-shadow: 0 0 1px 1px vars.$ov-lighter
|
||||
margin-bottom: 1em
|
||||
|
||||
> input
|
||||
position: absolute
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
export var chuniItem = 1500001;
|
||||
export var chuniFront = 1600001;
|
||||
export var chuniBack = 1700001;
|
||||
export var classPassthrough: string = ``
|
||||
export var classPassthrough: string = ``;
|
||||
</script>
|
||||
<div class="chuni-penguin {classPassthrough}">
|
||||
<div class="chuni-penguin-body">
|
||||
|
@ -28,13 +28,33 @@
|
|||
<img class="chuni-penguin-beak chuni-penguin-accessory" src={imageURL} alt="Beak">
|
||||
{/await}
|
||||
|
||||
<!-- Arms (surfboard) -->
|
||||
{#await DDSreader.getFileFromSheet("surfboard:CHU_UI_Common_Avatar_body_00.dds", 0, 0, 85, 160, 0.75) then imageURL}
|
||||
<img class="chuni-penguin-arm-left chuni-penguin-arm" src={imageURL} alt="Left Arm">
|
||||
{/await}
|
||||
{#await DDSreader.getFileFromSheet("surfboard:CHU_UI_Common_Avatar_body_00.dds", 0, 0, 85, 160, 0.75) then imageURL}
|
||||
<img class="chuni-penguin-arm-right chuni-penguin-arm" src={imageURL} alt="Right Arm">
|
||||
{/await}
|
||||
{#if chuniItem != 1500001}
|
||||
<!-- Arms (straight) -->
|
||||
{#await DDSreader.getFileFromSheet("surfboard:CHU_UI_Common_Avatar_body_00.dds", 0, 0, 85, 160, 0.75) then imageURL}
|
||||
<img class="chuni-penguin-arm-left chuni-penguin-arm" src={imageURL} alt="Left Arm">
|
||||
<div class="chuni-penguin-arm-left chuni-penguin-arm-type-1 chuni-penguin-arm">
|
||||
{#await DDSreader.getFileFromSheet(`avatarAccessory:${chuniItem.toString().padStart(8, "0")}`, 0, 0, 200, 544, 0.75) then imageURL}
|
||||
<img class="chuni-penguin-item chuni-penguin-accessory chuni-penguin-item-left" src={imageURL} alt="Item">
|
||||
{/await}
|
||||
</div>
|
||||
{/await}
|
||||
{#await DDSreader.getFileFromSheet("surfboard:CHU_UI_Common_Avatar_body_00.dds", 0, 0, 85, 160, 0.75) then imageURL}
|
||||
<img class="chuni-penguin-arm-right chuni-penguin-arm" src={imageURL} alt="Right Arm">
|
||||
<div class="chuni-penguin-arm-right chuni-penguin-arm-type-1 chuni-penguin-arm">
|
||||
{#await DDSreader.getFileFromSheet(`avatarAccessory:${chuniItem.toString().padStart(8, "0")}`, 200, 0, 200, 544, 0.75) then imageURL}
|
||||
<img class="chuni-penguin-item chuni-penguin-accessory chuni-penguin-item-right" src={imageURL} alt="Item">
|
||||
{/await}
|
||||
</div>
|
||||
{/await}
|
||||
{:else}
|
||||
<!-- Arms (bent) -->
|
||||
{#await DDSreader.getFileFromSheet("surfboard:CHU_UI_Common_Avatar_body_00.dds", 80, 0, 110, 100, 0.75) then imageURL}
|
||||
<img class="chuni-penguin-arm-left chuni-penguin-arm chuni-penguin-arm-type-2" src={imageURL} alt="Left Arm">
|
||||
{/await}
|
||||
{#await DDSreader.getFileFromSheet("surfboard:CHU_UI_Common_Avatar_body_00.dds", 80, 0, 110, 100, 0.75) then imageURL}
|
||||
<img class="chuni-penguin-arm-right chuni-penguin-arm chuni-penguin-arm-type-2" src={imageURL} alt="Right Arm">
|
||||
{/await}
|
||||
{/if}
|
||||
|
||||
<!-- Wear -->
|
||||
{#await DDSreader.getFileScaled(`avatarAccessory:${chuniWear.toString().padStart(8, "0")}`, 0.75, `avatarAccessory:01100001`) then imageURL}
|
||||
|
@ -60,11 +80,6 @@
|
|||
<img class="chuni-penguin-face-accessory chuni-penguin-accessory" src={imageURL} alt="Face (Accessory)">
|
||||
{/await}
|
||||
|
||||
<!-- Item -->
|
||||
{#await DDSreader.getFileScaled(`avatarAccessory:${chuniItem.toString().padStart(8, "0")}`, 0.75, `avatarAccessory:01500001`) then imageURL}
|
||||
<img class="chuni-penguin-item chuni-penguin-accessory" src={imageURL} alt="Item">
|
||||
{/await}
|
||||
|
||||
<!-- Front -->
|
||||
{#await DDSreader.getFileScaled(`avatarAccessory:${chuniFront.toString().padStart(8, "0")}`, 0.75) then imageURL}
|
||||
<img class="chuni-penguin-front chuni-penguin-accessory" src={imageURL} alt="Front">
|
||||
|
@ -77,8 +92,11 @@
|
|||
</div>
|
||||
<div class="chuni-penguin-feet">
|
||||
<!-- Feet -->
|
||||
{#await DDSreader.getFileFromSheet(`avatarAccessory:${chuniSkin.toString().padStart(8, "0")}`, 0, 410, 167, 80, 0.75) then imageURL}
|
||||
<img src={imageURL} alt="Feet">
|
||||
{#await DDSreader.getFileFromSheet(`avatarAccessory:${chuniSkin.toString().padStart(8, "0")}`, 0, 410, 85, 80, 0.75) then imageURL}
|
||||
<img src={imageURL} alt="Foot">
|
||||
{/await}
|
||||
{#await DDSreader.getFileFromSheet(`avatarAccessory:${chuniSkin.toString().padStart(8, "0")}`, 85, 410, 85, 80, 0.75) then imageURL}
|
||||
<img src={imageURL} alt="Foot">
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -86,11 +104,11 @@
|
|||
<style lang="sass">
|
||||
@keyframes chuniPenguinBodyBob
|
||||
0%
|
||||
transform: translate(-50%, 0%) translate(0%, -50%)
|
||||
transform: translate(-50%, 5px) translate(0%, -50%)
|
||||
50%
|
||||
transform: translate(-50%, 10px) translate(0%, -50%)
|
||||
100%
|
||||
transform: translate(-50%, 0%) translate(0%, -50%)
|
||||
100%
|
||||
transform: translate(-50%, 5px) translate(0%, -50%)
|
||||
@keyframes chuniPenguinArmLeft
|
||||
0%
|
||||
transform: translate(-50%, 0) rotate(-2deg)
|
||||
|
@ -108,11 +126,21 @@
|
|||
|
||||
img
|
||||
-webkit-user-drag: none
|
||||
user-select: none
|
||||
|
||||
.chuni-penguin
|
||||
height: 512px
|
||||
aspect-ratio: 1/2
|
||||
position: relative
|
||||
pointer-events: none
|
||||
|
||||
z-index: 1
|
||||
|
||||
&.chuni-penguin-float
|
||||
position: absolute
|
||||
top: 50%
|
||||
left: 50%
|
||||
transform: translate(-50%, -50%)
|
||||
|
||||
.chuni-penguin-body, .chuni-penguin-feet
|
||||
transform: translate(-50%, -50%)
|
||||
|
@ -122,21 +150,43 @@
|
|||
.chuni-penguin-body
|
||||
top: 50%
|
||||
z-index: 1
|
||||
animation: chuniPenguinBodyBob 2s infinite cubic-bezier(0.45, 0, 0.55, 1)
|
||||
animation: chuniPenguinBodyBob 1s infinite cubic-bezier(0.45, 0, 0.55, 1)
|
||||
.chuni-penguin-feet
|
||||
top: 82.5%
|
||||
top: 80%
|
||||
z-index: 0
|
||||
width: 175px
|
||||
display: flex
|
||||
justify-content: center
|
||||
|
||||
img
|
||||
margin-left: auto
|
||||
margin-right: auto
|
||||
|
||||
.chuni-penguin-arm
|
||||
transform-origin: 95% 10%
|
||||
transform-origin: 90% 10%
|
||||
position: absolute
|
||||
top: 40%
|
||||
.chuni-penguin-arm-left
|
||||
left: 0%
|
||||
animation: chuniPenguinArmLeft 1.5s infinite cubic-bezier(0.45, 0, 0.55, 1)
|
||||
.chuni-penguin-arm-right
|
||||
left: 70%
|
||||
animation: chuniPenguinArmRight 1.5s infinite cubic-bezier(0.45, 0, 0.55, 1)
|
||||
z-index: 0
|
||||
&.chuni-penguin-arm-type-1
|
||||
width: calc(85px * 0.75)
|
||||
height: calc(160px * 0.75)
|
||||
z-index: 2
|
||||
&.chuni-penguin-arm-type-2
|
||||
transform-origin: 40% 10%
|
||||
z-index: 2
|
||||
|
||||
&.chuni-penguin-arm-left
|
||||
left: 0%
|
||||
transform: translate(-50%, 0)
|
||||
animation: chuniPenguinArmLeft 1s infinite cubic-bezier(0.45, 0, 0.55, 1)
|
||||
&.chuni-penguin-arm-type-2
|
||||
left: 15%
|
||||
&.chuni-penguin-arm-right
|
||||
left: 72.5%
|
||||
transform: translate(-50%, 0) scaleX(-1)
|
||||
animation: chuniPenguinArmRight 1s infinite cubic-bezier(0.45, 0, 0.55, 1)
|
||||
&.chuni-penguin-arm-type-2
|
||||
left: 95%
|
||||
|
||||
.chuni-penguin-accessory
|
||||
transform: translate(-50%, -50%)
|
||||
|
@ -144,12 +194,22 @@
|
|||
top: 50%
|
||||
left: 50%
|
||||
|
||||
.chuni-penguin-item
|
||||
z-index: 1
|
||||
top: 25%
|
||||
left: 0
|
||||
|
||||
&.chuni-penguin-item-left
|
||||
transform: translate(-50%, -50%) rotate(-15deg)
|
||||
&.chuni-penguin-item-right
|
||||
transform: translate(-50%, -50%) scaleX(-1) rotate(15deg)
|
||||
|
||||
.chuni-penguin-eyes
|
||||
top: 22.5%
|
||||
.chuni-penguin-beak
|
||||
top: 29.5%
|
||||
.chuni-penguin-wear
|
||||
top: 57.5%
|
||||
top: 60%
|
||||
.chuni-penguin-head
|
||||
top: 7.5%
|
||||
z-index: 10
|
||||
|
|
|
@ -4,25 +4,30 @@
|
|||
|
||||
const DDSreader = new DDS(ddsDB);
|
||||
|
||||
export var chuniLevel: number = 1
|
||||
export var chuniLevel: string = "╳"
|
||||
export var chuniName: string = "AquaDX"
|
||||
export var chuniRating: number = 1.23
|
||||
export var chuniNameplate: number = 1
|
||||
export var chuniCharacter: number = 0
|
||||
export var chuniTrophyName: string = "NEWCOMER"
|
||||
export var chuniIsUserbox: boolean = false;
|
||||
|
||||
let ratingToString = (rating: number) => {
|
||||
return rating.toFixed(2)
|
||||
}
|
||||
</script>
|
||||
{#await DDSreader?.getFile(`nameplate:${chuniNameplate.toString().padStart(8, "0")}`) then nameplateURL}
|
||||
{#await DDSreader?.getFile(`nameplate:${chuniNameplate.toString().padStart(8, "0")}`, `nameplate:00000001`) then nameplateURL}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div on:click class="chuni-nameplate" style:background={`url(${nameplateURL})`}>
|
||||
{#await DDSreader?.getFile(`characterThumbnail:${chuniCharacter.toString().padStart(6, "0")}`) then characterThumbnailURL}
|
||||
<div on:click class="chuni-nameplate" class:chuni-nameplate-clickable={chuniIsUserbox} style:background={`url(${nameplateURL})`}>
|
||||
{#await DDSreader?.getFile(`characterThumbnail:${chuniCharacter.toString().padStart(6, "0")}`, `characterThumbnail:000000`) then characterThumbnailURL}
|
||||
<img class="chuni-character" src={characterThumbnailURL} alt="Character">
|
||||
{/await}
|
||||
{#await DDSreader?.getFileFromSheet("surfboard:CHU_UI_title_rank_00_v10.dds", 5, 5 + (75 * 2), 595, 64) then trophyURL}
|
||||
<div class="chuni-trophy">
|
||||
<div class="chuni-trophy" title={chuniTrophyName}>
|
||||
{chuniTrophyName}
|
||||
<img src={trophyURL} class="chuni-trophy-bg" alt="Trophy">
|
||||
</div>
|
||||
<img src={trophyURL} class="chuni-trophy-bg" alt="Trophy" title={chuniTrophyName}>
|
||||
{/await}
|
||||
<div class="chuni-user-info">
|
||||
<div class="chuni-user-name">
|
||||
|
@ -39,7 +44,7 @@
|
|||
<div class="chuni-user-rating">
|
||||
RATING
|
||||
<span class="chuni-user-rating-number">
|
||||
{chuniRating}
|
||||
{ratingToString(chuniRating)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -53,11 +58,13 @@
|
|||
position: relative
|
||||
font-size: 16px
|
||||
/* Overlap penguin avatar when put side to side */
|
||||
z-index: 2
|
||||
cursor: pointer
|
||||
z-index: 1
|
||||
|
||||
&.chuni-nameplate-clickable
|
||||
cursor: pointer
|
||||
|
||||
.chuni-trophy
|
||||
width: 410px
|
||||
width: 390px
|
||||
height: 45px
|
||||
background-position: center
|
||||
background-size: cover
|
||||
|
@ -74,14 +81,21 @@
|
|||
font-family: sans-serif
|
||||
font-weight: bold
|
||||
|
||||
overflow-x: hidden
|
||||
white-space: nowrap
|
||||
text-overflow: ellipsis
|
||||
|
||||
z-index: 1
|
||||
text-shadow: 0 1px white
|
||||
margin: 0 10px
|
||||
|
||||
img
|
||||
width: 100%
|
||||
height: 100%
|
||||
position: absolute
|
||||
z-index: -1
|
||||
img.chuni-trophy-bg
|
||||
width: 410px
|
||||
height: 45px
|
||||
position: absolute
|
||||
top: 40px
|
||||
right: 25px
|
||||
z-index: -1
|
||||
|
||||
.chuni-character
|
||||
position: absolute
|
||||
|
@ -115,9 +129,11 @@
|
|||
.chuni-user-name
|
||||
flex: 1 0 65%
|
||||
box-shadow: 0 1px 0 #ccc
|
||||
white-space: nowrap
|
||||
text-overflow: ellipsis
|
||||
|
||||
.chuni-user-level
|
||||
font-size: 2em
|
||||
font-size: 1.5em
|
||||
margin-left: 10px
|
||||
|
||||
.chuni-user-name-text
|
||||
|
|
|
@ -16,7 +16,10 @@ export const FADE_OUT = { duration: 200 }
|
|||
export const FADE_IN = { delay: 400 }
|
||||
export const DEFAULT_PFP = '/assets/imgs/no_profile.png'
|
||||
|
||||
// USERBOX_ASSETS
|
||||
// Documentation for Userbox mode can be found in `docs/aquabox-url-mode.md`
|
||||
// Please note that if this is set, it must be manually unset by users in Chuni Settings -> Update Userbox -> Switch to URL mode -> (empty value) -> Enter key
|
||||
export const USERBOX_DEFAULT_URL = ""
|
||||
|
||||
export const HAS_USERBOX_ASSETS = true
|
||||
|
||||
// Meow meow meow
|
||||
|
|
|
@ -182,7 +182,6 @@ export const EN_REF_USERBOX = {
|
|||
'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',
|
||||
|
||||
|
@ -196,14 +195,21 @@ export const EN_REF_USERBOX = {
|
|||
|
||||
'userbox.new.name': 'AquaBox',
|
||||
'userbox.new.setup': 'Drag and drop your Chuni game folder (Lumi or newer) into the box below to display UserBoxes with their nameplate & avatar. All files are handled in-browser.',
|
||||
'userbox.new.setup.notice': 'This tool assumes your files to be in "bin/option" and "data/A000".',
|
||||
'userbox.new.setup.processing_file': 'Processing',
|
||||
'userbox.new.setup.finalizing': 'Saving to internal storage',
|
||||
'userbox.new.drop': 'Drop game folder here',
|
||||
'userbox.new.switch.to_url': 'Switch to URL mode',
|
||||
'userbox.new.switch.to_drop': 'Switch to drop mode',
|
||||
'userbox.new.url_warning': 'Enter in the path to access Userbox assets. You are responsible for any results in this state. Please read the documentation. Don\'t expect support for this mode.',
|
||||
'userbox.new.activate_first': 'Enable AquaBox (game files required)',
|
||||
'userbox.new.activate_update': 'Update AquaBox (game files required)',
|
||||
'userbox.new.activate': 'Use AquaBox',
|
||||
'userbox.new.activate_desc': 'Enable displaying UserBoxes with their nameplate & avatar',
|
||||
'userbox.new.error.invalidFolder': 'The folder you selected is invalid. Ensure that your game\'s version is Lumi or newer and that the "A001" option pack is present.'
|
||||
'userbox.new.activate_profile': 'Use AquaBox on profiles',
|
||||
'userbox.new.activate_profile_desc': 'Enable displaying UserBoxes with their nameplate & avatar on profile pages',
|
||||
'userbox.new.error.invalidFolder': 'The folder you selected is invalid. Ensure that your game\'s version is Lumi or newer and that the "A000" option pack is present.',
|
||||
'userbox.new.error.invalidUrl': 'The URL you inputted is invalid.'
|
||||
}
|
||||
|
||||
export const EN_REF = { ...EN_REF_USER, ...EN_REF_Welcome, ...EN_REF_GENERAL,
|
||||
|
|
|
@ -266,6 +266,8 @@ export const USERBOX = {
|
|||
get('/api/v2/game/chu3/user-box', {}),
|
||||
setUserBox: (d: { field: string, value: number | string }) =>
|
||||
post(`/api/v2/game/chu3/user-detail-set`, d),
|
||||
getUserProfile: (username: string): Promise<UserBox> =>
|
||||
get(`/api/v2/game/chu3/user-detail`, {username})
|
||||
}
|
||||
|
||||
export const CARD = {
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import useLocalStorage from "../hooks/useLocalStorage.svelte";
|
||||
import { USERBOX_DEFAULT_URL } from "../config";
|
||||
|
||||
export default class DDSCache {
|
||||
constructor(db: IDBDatabase | undefined) {
|
||||
this.db = db;
|
||||
|
@ -43,7 +46,13 @@ export default class DDSCache {
|
|||
* @param path Image path
|
||||
*/
|
||||
getFromDatabase(path: string): Promise<Blob | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (this.userboxURL.value != "") {
|
||||
let targetPath = path.replaceAll(":", "/");
|
||||
let response = await fetch(`${this.userboxURL.value}/${targetPath}.chu`).then(b => b.blob()).catch(reject);
|
||||
if (response)
|
||||
return resolve(response);
|
||||
};
|
||||
if (!this.db)
|
||||
return resolve(null);
|
||||
let transaction = this.db.transaction(["dds"], "readonly");
|
||||
|
@ -61,4 +70,5 @@ export default class DDSCache {
|
|||
|
||||
private urlCache: {scale: number, path: string, url: string}[] = [];
|
||||
private db: IDBDatabase | undefined;
|
||||
userboxURL = useLocalStorage("userboxURL", USERBOX_DEFAULT_URL);
|
||||
}
|
|
@ -173,7 +173,9 @@ export async function userboxFileProcess(folder: FileSystemEntry, progressUpdate
|
|||
const dataFolder = await getDirectoryFromPath(folder, "data");
|
||||
if (dataFolder)
|
||||
await scanOptionFolder(dataFolder, progressUpdate);
|
||||
useLocalStorage("userboxURL", "").value = "";
|
||||
useLocalStorage("userboxNew", false).value = true;
|
||||
useLocalStorage("userboxNewProfile", false).value = true;
|
||||
location.reload();
|
||||
|
||||
return null
|
||||
|
|
|
@ -105,6 +105,41 @@
|
|||
d!.user.rival = isAdd
|
||||
}).catch(e => error = e.message).finally(() => isLoading = false)
|
||||
}
|
||||
|
||||
/* Aquabox */
|
||||
|
||||
import { userboxFileProcess, ddsDB, initializeDb } from "../libs/userbox/userbox"
|
||||
import ChuniPenguinComponent from "../components/settings/userbox/ChuniPenguin.svelte"
|
||||
import ChuniUserplateComponent from "../components/settings/userbox/ChuniUserplate.svelte";
|
||||
import {
|
||||
type UserBox,
|
||||
type UserItem,
|
||||
} from "../libs/generalTypes";
|
||||
import { USERBOX } from "../libs/sdk";
|
||||
|
||||
let USERBOX_ACTIVE = useLocalStorage("userboxNewProfile", false);
|
||||
let USERBOX_INSTALLED = false;
|
||||
|
||||
let userbox: UserBox;
|
||||
let allItems: Record<string, Record<string, { name: string }>> = {};
|
||||
|
||||
if (game == "chu3" && USERBOX_ACTIVE.value) {
|
||||
indexedDB.databases().then(async (dbi) => {
|
||||
let databaseExists = dbi.some(db => db.name == "userboxChusanDDS");
|
||||
if (databaseExists) {
|
||||
await initializeDb();
|
||||
const profile = await USERBOX.getUserProfile(username).catch(_ => null)
|
||||
if (!profile) return;
|
||||
userbox = profile;
|
||||
console.log(userbox);
|
||||
|
||||
allItems = await DATA.allItems('chu3').catch(_ => {
|
||||
error = t("userbox.error.nodata")
|
||||
}) as typeof allItems
|
||||
USERBOX_INSTALLED = databaseExists;
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<main id="user-home" class="content">
|
||||
|
@ -132,6 +167,18 @@
|
|||
</nav>
|
||||
</div>
|
||||
|
||||
{#if USERBOX_ACTIVE.value && USERBOX_INSTALLED && game == "chu3"}
|
||||
<div class="chuni-userbox-container">
|
||||
<ChuniUserplateComponent chuniCharacter={userbox.characterId} chuniRating={d.user.rating / 100} chuniLevel={userbox.level.toString()}
|
||||
chuniNameplate={userbox.nameplateId} chuniName={userbox.userName} chuniTrophyName={allItems.trophy[userbox.trophyId].name}></ChuniUserplateComponent>
|
||||
<div class="chuni-penguin-container">
|
||||
<ChuniPenguinComponent classPassthrough="chuni-penguin-float" chuniWear={userbox.avatarWear} chuniHead={userbox.avatarHead} chuniBack={userbox.avatarBack}
|
||||
chuniFront={userbox.avatarFront} chuniFace={userbox.avatarFace} chuniItem={userbox.avatarItem}
|
||||
chuniSkin={userbox.avatarSkin}></ChuniPenguinComponent>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<h2>{titleText} {t('UserHome.Statistics')}</h2>
|
||||
<div class="scoring-info">
|
||||
|
@ -576,4 +623,18 @@
|
|||
&:before
|
||||
content: "+"
|
||||
color: vars.$c-good
|
||||
|
||||
.chuni-userbox-container
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
|
||||
.chuni-penguin-container
|
||||
height: 256px
|
||||
aspect-ratio: 1
|
||||
position: relative
|
||||
|
||||
@media (max-width: 1000px)
|
||||
.chuni-userbox-container
|
||||
flex-wrap: wrap
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# AquaBox URL Mode Setup Guide
|
||||
|
||||
## For users
|
||||
|
||||
1. Go to your Chuni game settings
|
||||
2. Go down to "Enable AquaBox" or "Upgrade AquaBox"
|
||||
3. Click on "Switch to URL mode"
|
||||
4. Enter the base URL for your AquaBox
|
||||
|
||||
## For server owners / asset hosters
|
||||
|
||||
> :warning: Assets are already not hosted on AquaDX for legal reasons.<br>
|
||||
> Hosting SEGA's assets may put you at higher risk of DMCA.
|
||||
|
||||
1. Extract your Chunithm Luminous game files.
|
||||
|
||||
It is recommended you have the latest version of the game and all of the options your users may use.
|
||||
|
||||
The script to generate the proper paths can be found in [tools/chusan-extractor.js](tools/chusan-extractor.js). Node.js or Bun is required.<br>
|
||||
Please read the comments at the top of the script for usage instructions.
|
||||
|
||||
2. Copy the new `chu3` folder where you need it to be (read #3 if you're hosting AquaNet and want to host on the same endpoints).
|
||||
3. (Optional) Update `src/lib/config.ts`.
|
||||
```ts
|
||||
// Change this to the base url of where your assets are stored.
|
||||
// If you are hosting on AquaNet, you can put the files @ /public/chu3 & use '/chu3' for your base url.
|
||||
// This will work the same way as setting it on the UI does. TEST IT ON THE UI BEFORE YOU APPLY THIS CONFIG!!!
|
||||
export const USERBOX_DEFAULT_URL = "/chu3";
|
||||
```
|
||||
4. Enjoy!
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
|
||||
Chusan asset extractor for AquaBox URL mode.
|
||||
Place your "option" (or "bin/option") and "data" folders in the same directory as this script as they're named.
|
||||
|
||||
Data will be placed into the "chu3" folder.
|
||||
Place the contents into a public directory that can be accessed by users.
|
||||
|
||||
Know Python or another common scripting language?
|
||||
Feel free to rewrite this tool and submit it to MewoLab/AquaDX!
|
||||
Or rewrite it in JavaScript again! Anything is better than this hot pile of garbage!
|
||||
|
||||
*/
|
||||
|
||||
// Allows this to be a single-file script
|
||||
const fs = require("fs");
|
||||
|
||||
const verifyDirectoryExistant = (name) => {
|
||||
return fs.existsSync(name);
|
||||
}
|
||||
const mkdir = (name) => {
|
||||
if (!fs.existsSync(name))
|
||||
fs.mkdirSync(name);
|
||||
};
|
||||
const outputTarget = "chu3";
|
||||
|
||||
const directoryPaths = [
|
||||
{
|
||||
folder: "ddsImage",
|
||||
processName: "Characters",
|
||||
path: "characterThumbnail",
|
||||
filter: (name) => name.substring(name.length - 6, name.length) == "02.dds",
|
||||
id: (name) => `0${name.substring(17, 21)}${name.substring(23, 24)}`
|
||||
},
|
||||
{
|
||||
folder: "namePlate",
|
||||
processName: "Nameplates",
|
||||
path: "nameplate",
|
||||
filter: (name) => name.substring(0, 17) == "CHU_UI_NamePlate_",
|
||||
id: (name) => name.substring(17, 25)
|
||||
},
|
||||
{
|
||||
folder: "avatarAccessory",
|
||||
processName: "Avatar Accessory Thumbnails",
|
||||
path: "avatarAccessoryThumbnail",
|
||||
filter: (name) => name.substring(14, 18) == "Icon",
|
||||
id: (name) => name.substring(19, 27)
|
||||
},
|
||||
{
|
||||
folder: "avatarAccessory",
|
||||
processName: "Avatar Accessories",
|
||||
path: "avatarAccessory",
|
||||
filter: (name) => name.substring(14, 17) == "Tex",
|
||||
id: (name) => name.substring(18, 26)
|
||||
},
|
||||
{
|
||||
folder: "texture",
|
||||
processName: "Surfboard Textures",
|
||||
useFileName: true,
|
||||
path: "surfboard",
|
||||
filter: (name) =>
|
||||
([
|
||||
"CHU_UI_Common_Avatar_body_00.dds",
|
||||
"CHU_UI_Common_Avatar_face_00.dds",
|
||||
"CHU_UI_title_rank_00_v10.dds"
|
||||
]).includes(name),
|
||||
id: (name) => name
|
||||
}
|
||||
];
|
||||
|
||||
const processFile = (fileName, path, subFolder) => {
|
||||
let localReference = directoryPaths.find(p => p.folder == subFolder && p.filter(fileName));
|
||||
if (!localReference) return;
|
||||
files.push({
|
||||
path: `${path}/${fileName}`,
|
||||
target: `${localReference.id(fileName)}.chu`,
|
||||
targetFolder: `${localReference.path}`,
|
||||
name: fileName
|
||||
});
|
||||
}
|
||||
|
||||
let files = [];
|
||||
const processFolder = (path) => {
|
||||
for (const folder of fs.readdirSync(path)) {
|
||||
let folderData = fs.statSync(`${path}/${folder}`);
|
||||
if (!folderData.isDirectory()) continue;
|
||||
for (const subFolder of fs.readdirSync(`${path}/${folder}`)) {
|
||||
let folderData = fs.statSync(`${path}/${folder}/${subFolder}`);
|
||||
let reference = directoryPaths.find(p => p.folder == subFolder);
|
||||
if (!reference || !folderData.isDirectory()) continue;
|
||||
// what a mess
|
||||
for (const subSubFolder of fs.readdirSync(`${path}/${folder}/${subFolder}`))
|
||||
if (fs.statSync(`${path}/${folder}/${subFolder}/${subSubFolder}`).isDirectory()) {
|
||||
for (const subSubSubFile of fs.readdirSync(`${path}/${folder}/${subFolder}/${subSubFolder}`))
|
||||
processFile(subSubSubFile, `${path}/${folder}/${subFolder}/${subSubFolder}`, subFolder)
|
||||
} else
|
||||
processFile(subSubFolder, `${path}/${folder}/${subFolder}`, subFolder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!verifyDirectoryExistant("data"))
|
||||
return console.log("Data folder non-existant.")
|
||||
if (!verifyDirectoryExistant("bin"))
|
||||
if (!verifyDirectoryExistant("option"))
|
||||
return console.log("Option folder non-existant.")
|
||||
|
||||
processFolder("data");
|
||||
if (verifyDirectoryExistant("bin")) {
|
||||
processFolder("bin/option");
|
||||
} else
|
||||
processFolder("option");
|
||||
|
||||
|
||||
console.log(`Found ${files.length} files.`);
|
||||
console.log(`Copying now, please wait.`)
|
||||
|
||||
if (verifyDirectoryExistant(outputTarget))
|
||||
return console.log("Output folder exists.");
|
||||
mkdir(outputTarget);
|
||||
|
||||
files.forEach(fileData => {
|
||||
console.log(`Copying ${fileData.name}`)
|
||||
mkdir(`${outputTarget}/${fileData.targetFolder}`)
|
||||
fs.copyFileSync(fileData.path, `${outputTarget}/${fileData.targetFolder}/${fileData.target}`)
|
||||
})
|
Loading…
Reference in New Issue