Add wish auto import

pull/1/head
I Made Setia Baruna 2021-03-08 21:49:22 +08:00
parent 439ca8fa71
commit 5e96df8aa9
8 changed files with 1029 additions and 26 deletions

View File

@ -0,0 +1,39 @@
<script>
export let className = '';
export let placeholder = '';
export let step = undefined;
export let min = Math.min();
export let max = Math.max();
export let value = '';
const handleInput = (event) => {
value = event.target.value;
};
</script>
<div
class={`container overflow-hidden flex flex-1 relative items-center bg-background rounded-2xl focus-within:border-primary border-2 border-transparent ease-in duration-100 ${className}`}
>
<textarea
{placeholder}
{value}
{min}
{max}
{step}
on:change
on:input={handleInput}
class={`w-full min-h-full pr-4 text-white placeholder-gray-500 leading-none bg-transparent border-none focus:outline-none`}
/>
</div>
<style>
.container {
height: fit-content;
}
textarea {
min-height: 100px;
@apply p-4;
}
</style>

View File

@ -9,7 +9,7 @@
}
</script>
<div class="fixed left-0 right-0 bottom-0 text-center px-4 z-50 md:left-auto md:max-w-screen-sm">
<div class="fixed left-0 right-0 bottom-0 text-center px-4 md:left-auto md:max-w-screen-sm" style="z-index:99999;">
{#each $toasts as toast (toast._id)}
<div
class={`rounded-xl px-4 py-2 mb-4 w-full bg-black bg-opacity-75 ${types[toast.type]}`}

View File

@ -0,0 +1,615 @@
<script>
import { mdiClose, mdiDownload, mdiHelpCircle, mdiLoading } from '@mdi/js';
import { onMount, tick } from 'svelte';
import dayjs from 'dayjs';
import { pushToast } from '../stores/toast';
import Button from './Button.svelte';
import Icon from './Icon.svelte';
import Input from './Input.svelte';
import Textarea from './Textarea.svelte';
import { weaponList } from '../data/weaponList';
import { characters } from '../data/characters';
import { readSave, updateSave } from '../stores/saveManager';
export let closeModal;
const fetchController = new AbortController();
const fetchSignal = fetchController.signal;
let numberFormat = Intl.NumberFormat();
let showFaq = false;
let selectedType = 'pc';
let generatedTextInput = '';
let genshinLink = '';
let types = {
100: {
name: "Beginners' Wish",
id: 'beginners',
},
200: {
name: 'Standard',
id: 'standard',
},
301: {
name: 'Character Event',
id: 'character-event',
},
302: {
name: 'Weapon Event',
id: 'weapon-event',
},
};
let wishes = {};
let url;
let processingLog = false;
let fetchingWishes = false;
let finishedProcessingLog = false;
let calculatingPity = false;
let cancelled = false;
let region = '';
let currentBanner = '';
let currentPage = 1;
function cancel() {
fetchController.abort();
cancelled = true;
setTimeout(() => {
closeModal();
}, 2000);
}
function getDeviceType() {
switch (selectedType) {
case 'pc':
return 'pc';
case 'android':
case 'ios':
return 'mobile';
}
}
async function startImport() {
if (selectedType === 'pclocal') {
importFromGeneratedText();
} else {
processLogs();
}
}
async function processLogs() {
processingLog = true;
try {
url = new URL(genshinLink);
} catch (err) {
pushToast('Invalid link, please check it again', 'error');
}
try {
for (const [wishNumber, type] of Object.entries(types)) {
await getLog(wishNumber, type);
if (cancelled) return;
await sleep(2000);
}
finishedProcessingLog = true;
} catch (err) {
wishes = {};
processingLog = false;
fetchingWishes = false;
finishedProcessingLog = false;
calculatingPity = false;
region = '';
currentBanner = '';
currentPage = 1;
}
}
async function getLog(wishNumber, type) {
fetchingWishes = true;
console.log(wishNumber, type);
url.searchParams.set('auth_appid', 'webview_gacha');
url.searchParams.set('init_type', '301');
url.searchParams.set('gacha_id', 'd610857102f9256ba143ccf2e03b964c76a6ed');
url.searchParams.set('lang', 'en');
url.searchParams.set('device_type', getDeviceType());
if (region !== '') url.searchParams.set('region', region);
url.searchParams.set('gacha_type', wishNumber);
url.searchParams.set('size', 20);
url.searchParams.append('lang', 'en-us');
url.hash = '';
url.host = 'hk4e-api-os.mihoyo.com';
url.pathname = 'event/gacha_info/api/getGachaLog';
currentBanner = type.name;
const weapons = Object.values(weaponList);
const chars = Object.values(characters);
let page = 1;
let result = [];
do {
if (cancelled) return;
url.searchParams.set('page', page);
currentPage = page;
try {
const res = await fetchRetry(
__paimon.env.CORS_HOST,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: url.toString(),
}),
signal: fetchSignal,
},
2,
);
if (cancelled) return;
if (!res.ok) {
processingLog = false;
pushToast('Error code returned from MiHoYo API, please try again later!', 'error');
throw 'error code';
}
const dat = await res.json();
if (dat.retcode !== 0) {
processingLog = false;
if (dat.message === 'authkey timeout') {
pushToast('Link expired, please try again!', 'error');
throw 'error code';
}
pushToast('Error code returned from MiHoYo API, please try again later!', 'error');
throw 'error code';
}
region = dat.data.region;
result = dat.data.list;
} catch (err) {
processingLog = false;
pushToast('Connection timeout, please wait a moment and try again later', 'error');
throw 'network error';
}
try {
for (let row of result) {
const code = row.gacha_type;
const time = dayjs(row.time);
const name = row.name;
const type = row.item_type.replace(/ /g, '');
let id;
if (type === 'Weapon') {
id = weapons.find((e) => e.name === name).id;
} else if (type === 'Character') {
id = chars.find((e) => e.name === name).id;
}
if (wishes[code] === undefined) {
wishes[code] = [];
}
wishes[code] = [
...wishes[code],
{
type: type.toLowerCase(),
id,
time: time.unix(),
pity: 0,
},
];
}
page = page + 1;
await sleep(1000);
console.log(wishes);
} catch (err) {
processingLog = false;
pushToast('Invalid data returned from API, try again later!', 'error');
throw 'invalid data';
}
} while (result.length > 0);
}
async function fetchRetry(url, options, n) {
let error;
for (let i = 0; i < n; i++) {
if (cancelled) return;
try {
return await fetch(url, options);
} catch (err) {
error = err;
}
}
throw error;
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function toggleFaqs(show) {
showFaq = show;
}
function changeSelectedType(type) {
selectedType = type;
}
function detectPlatform() {
const userAgent = navigator.userAgent || navigator.vendor;
if (/android/i.test(userAgent)) {
selectedType = 'android';
}
if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) {
selectedType = 'ios';
}
}
function importFromGeneratedText() {
if (!generatedTextInput.startsWith('paimonmoeimporterv1###')) {
pushToast('Invalid data, please use the latest importer app', 'error');
return;
}
processingLog = true;
const rows = generatedTextInput.substring(22).split(';');
const weapons = Object.values(weaponList);
const chars = Object.values(characters);
try {
for (let row of rows) {
if (row === '') continue;
const cell = row.split(',');
const code = Number(cell[0]);
const time = dayjs(cell[1]);
const name = cell[2];
const type = cell[3].replace(/ /g, '');
let id;
if (type === 'Weapon') {
id = weapons.find((e) => e.name === name).id;
} else if (type === 'Character') {
id = chars.find((e) => e.name === name).id;
}
if (wishes[code] === undefined) {
wishes[code] = [];
}
wishes[code] = [
...wishes[code],
{
type: type.toLowerCase(),
id,
time: time.unix(),
pity: 0,
},
];
}
} catch (err) {
processingLog = false;
pushToast('Invalid data, please use the latest importer app', 'error');
return;
}
finishedProcessingLog = true;
console.log(wishes);
}
function saveData() {
calculatingPity = true;
for (let [code, type] of Object.entries(types)) {
processWishes(code, type);
}
calculatingPity = false;
pushToast('Import success 😀!');
closeModal();
}
function processWishes(code, type) {
if (wishes[code] === undefined) return;
const path = `wish-counter-${type.id}`;
const localData = readSave(path);
let localWishes = [];
if (localData !== null) {
const counterData = JSON.parse(localData);
localWishes = counterData.pulls || [];
}
const importedWishes = wishes[code].slice().reverse();
const oldestWish = importedWishes[0];
localWishes = localWishes
.slice()
.filter((e) => e.time < oldestWish.time)
.sort((a, b) => a.time - b.time);
const combined = [...localWishes, ...importedWishes];
let rare = 0;
let legendary = 0;
for (let i = 0; i < combined.length; i++) {
if (combined[i].pity !== 0) continue;
rare++;
legendary++;
let rarity;
if (combined[i].type === 'character') {
rarity = characters[combined[i].id].rarity;
} else if (combined[i].type === 'weapon') {
rarity = weaponList[combined[i].id].rarity;
}
if (rarity === 5) {
combined[i].pity = legendary;
legendary = 0;
rare = 0;
} else if (rarity === 4) {
combined[i].pity = rare;
rare = 0;
} else {
combined[i].pity = 1;
}
}
const data = JSON.stringify({
total: combined.length,
legendary,
rare,
pulls: combined,
});
updateSave(path, data);
}
onMount(() => {
detectPlatform();
});
</script>
{#if processingLog}
<h1 class="font-display text-white text-xl mb-2">Import Wish History</h1>
<div class="bg-background rounded-xl px-4 py-2 text-white mt-2">
{#if finishedProcessingLog}
<table class="min-w-full md:min-w-0">
{#each Object.entries(types) as [code, type]}
{#if wishes[code] !== undefined}
<tr>
<td class="border-b border-gray-700 py-1">
<span class="text-white mr-2 whitespace-no-wrap">{type.name} Banner</span>
</td>
<td class="border-b border-gray-700 py-1">
<span class="text-white mr-2 whitespace-no-wrap">
<Icon size={0.5} path={mdiClose} />
{numberFormat.format(wishes[code].length)}
</span>
</td>
</tr>
{/if}
{/each}
</table>
<p class="mt-4">Imported wishes will be appended or replaced accordingly to existing data</p>
<p>If you don't have any data saved before, first wish will be counted as pity 1</p>
<p class="font-semibold">Save the data?</p>
{:else if calculatingPity}
<Icon path={mdiLoading} spin color="white" />
Re-calculating pity...
{:else if fetchingWishes}
<div class="flex">
<Icon path={mdiLoading} spin color="white" />
<div class="ml-2">
<p>{`Processing ${currentBanner} Banner`}</p>
<p>{`Page ${currentPage}`}</p>
</div>
</div>
<table class="min-w-full md:min-w-0 mt-2">
{#each Object.entries(types) as [code, type]}
{#if wishes[code] !== undefined}
<tr>
<td class="border-b border-gray-700 py-1">
<span class="text-white mr-2 whitespace-no-wrap">{type.name} Banner</span>
</td>
<td class="border-b border-gray-700 py-1">
<span class="text-white mr-2 whitespace-no-wrap">
<Icon size={0.5} path={mdiClose} />
{numberFormat.format(wishes[code].length)}
</span>
</td>
</tr>
{/if}
{/each}
</table>
{:else}
<Icon path={mdiLoading} spin color="white" />
Parsing...
{/if}
</div>
<div class="flex justify-end mt-4">
{#if finishedProcessingLog && !calculatingPity}
<Button on:click={saveData} color="green" className="mr-4">Save</Button>
{/if}
<Button on:click={cancel} disabled={calculatingPity || cancelled}>{cancelled ? 'Cancelling...' : 'Cancel'}</Button>
</div>
{:else}
<div>
{#if showFaq}
<h1 class="font-display text-white text-xl mb-2">Import Wish History FAQS</h1>
<div class="font-body">
<p class="text-white font-semibold">How does it work?</p>
<p class="text-gray-400">
Genshin Impact wish history is basically a web page, so you can access it by opening the web page url. A
temporary key will be generated after you open the wish history page or the feedback page, and the importer
will automatically use the MiHoYo API to fetch your wish history.
</p>
<p class="text-white font-semibold mt-4">Is it safe? Will I get banned?</p>
<p class="text-gray-400">
Paimon.moe use the same request that Genshin Impact use to get the wish history, and Paimon.moe has no way
whatsoever to modify any game files or memory, and it should be safe. But use it at your own risk (well I use
it on my main account). You still can input your data manually 😀.
</p>
<p class="text-white font-semibold mt-4">
Hey I checked the request and stuff, but why it request to your domain instead of MiHoYo API?
</p>
<p class="text-gray-400">
Paimon.moe cannot request directly to MiHoYo API because of
<a
class="text-primary hover:underline"
href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS"
target="__blank"
>
CORS</a
>, so the request redirected to a simple cors proxy to make it work. You can see the code
<a
class="text-primary hover:underline"
href="https://gist.github.com/MadeBaruna/64785ae992c924e0cbfe575e404b7155"
target="__blank">here</a
>
</p>
<p class="text-white font-semibold mt-4">Do you store my temporary key?</p>
<p class="text-gray-400">
Paimon.moe never store your key, and use HTTPS to pass your url to a cors proxy to make the CORS works.
<!-- If you don't want any passing around your url, you can use the small importer app to process the wish
history on your local PC (PC Local option) -->
</p>
</div>
{:else}
<div class="flex items-center">
<h1 class="font-display text-white text-xl mb-2 mr-2">Import Wish History</h1>
<Button size="sm" on:click={() => toggleFaqs(true)}>
<Icon path={mdiHelpCircle} color="white" />
FAQS
</Button>
</div>
<div class="flex mt-4 flex-wrap">
<button on:click={() => changeSelectedType('pc')} class={`pill ${selectedType === 'pc' ? 'active' : ''}`}>
PC
</button>
<!-- <button
on:click={() => changeSelectedType('pclocal')}
class={`pill ${selectedType === 'pclocal' ? 'active' : ''}`}
>
PC Local
</button> -->
<button
on:click={() => changeSelectedType('android')}
class={`pill ${selectedType === 'android' ? 'active' : ''}`}
>
Android
</button>
<button on:click={() => changeSelectedType('ios')} class={`pill ${selectedType === 'ios' ? 'active' : ''}`}>
iOS
</button>
</div>
{#if selectedType === 'pc'}
<div class="bg-background rounded-xl px-4 py-2 text-white mb-4 mt-2">
<ol class="list-decimal ml-4">
<li class="my-2">Open Paimon menu [ESC]</li>
<li class="my-2">Click Feedback</li>
<li class="my-2">Wait for it to load and a browser page should open</li>
<li class="my-2">Copy & paste the link to the textbox below</li>
</ol>
</div>
<Input bind:value={genshinLink} placeholder="Paste link here... https://webstatic..." />
{:else if selectedType === 'pclocal'}
<div class="bg-background rounded-xl px-4 py-2 text-white mb-4 mt-2">
<ol class="list-decimal ml-4">
<li class="mt-2 mb-0">
Downlod the importer app <Button size="sm" on:click={() => toggleFaqs(true)}>
<Icon path={mdiDownload} color="white" />
Download
</Button>
</li>
<li class="my-2">Open the wish history on your Genshin impact in this PC</li>
<li class="my-2">Press IMPORT</li>
<li class="my-2">Copy & paste the generated text to the textbox below</li>
</ol>
</div>
<Textarea bind:value={generatedTextInput} placeholder="Paste the generated text here..." />
{:else if selectedType === 'android'}
<div class="bg-background rounded-xl px-4 py-2 text-white mb-4 mt-2">
<ol class="list-decimal ml-4">
<li class="my-2">Open Paimon menu</li>
<li class="my-2">Press Feedback</li>
<li class="my-2">Wait for it to load and a feedback page should open</li>
<li class="my-2">Turn off your wifi and data connection</li>
<li class="my-2">Press refresh on top right corner</li>
<li class="my-2">The page should error and show you a link with black font, copy that link</li>
<li class="my-2">Turn on your wifi or data connection</li>
<li class="my-2">Paste the link to the textbox below</li>
</ol>
</div>
<Input bind:value={genshinLink} placeholder="Paste link here... https://webstatic..." />
{:else if selectedType === 'ios'}
<div class="bg-background rounded-xl px-4 py-2 text-white mb-4 mt-2">
Sorry I don't know yet how to access the link from iOS...<br />If you have the link you can still paste it
below.
</div>
<Input bind:value={genshinLink} placeholder="Paste link here... https://webstatic..." />
{/if}
{/if}
<div class="flex justify-end mt-4">
{#if !showFaq}
<Button on:click={startImport} color="green" className="mr-4">Import</Button>
{/if}
<Button on:click={showFaq ? () => toggleFaqs(false) : () => closeModal()}>Close</Button>
</div>
</div>
{/if}
<style>
.pill {
@apply rounded-2xl;
@apply border-2;
@apply border-white;
@apply border-opacity-25;
@apply text-white;
@apply px-4;
@apply py-1;
@apply mr-2;
@apply mb-2;
@apply outline-none;
@apply transition;
@apply duration-100;
&:hover {
@apply border-primary;
}
&.active {
@apply bg-primary;
@apply border-primary;
@apply text-background;
}
}
</style>

View File

@ -32,11 +32,21 @@
let legendaryEdit = 0;
let rareEdit = 0;
let showRarity = [true, true, false];
$: path = `wish-counter-${id}`;
$: if ($fromRemote) {
readLocalData();
}
$: sortedPull = pulls.sort((a, b) => b.time - a.time);
$: sortedPull = pulls
.filter((e) => {
if (e.type === 'character') {
return showRarity[5 - characters[e.id].rarity];
} else if (e.type === 'weapon') {
return showRarity[5 - weaponList[e.id].rarity];
}
})
.sort((a, b) => b.time - a.time);
onMount(() => {
readLocalData();
@ -46,6 +56,10 @@
isDetailOpen = !isDetailOpen;
}
function toggleShowRarity(index) {
showRarity[index] = !showRarity[index];
}
function openAddModal(pity) {
openModal(
AddModal,
@ -124,7 +138,7 @@
isEdit = false;
}
function readLocalData() {
export function readLocalData() {
console.log('wish read local');
const data = readSave(path);
if (data !== null) {
@ -152,21 +166,42 @@
total += val;
rare += val;
if (rare >= 10) {
openAddModal(rare);
rare = 0;
} else if (rare < 0) {
rare = 9;
}
legendary += val;
let filler = val;
if (legendary >= legendaryPity) {
openAddModal(legendary);
openAddModal(Math.min(rare, legendaryPity));
legendary = 0;
rare = 0;
filler--;
} else if (legendary < 0) {
legendary = 89;
} else {
rare += val;
if (rare >= 10) {
openAddModal(Math.min(rare, 10));
rare = 0;
filler--;
} else if (rare < 0) {
rare = 9;
}
}
if (filler > 0) {
pulls = [
...pulls,
...[...new Array(filler)].map((e) => ({
type: 'unknown_3_star',
id: 'unknown_3_star',
time: dayjs().unix(),
pity: 1,
})),
];
}
if (val < 0) {
const cloned = [...pulls];
cloned.pop();
pulls = cloned;
}
saveData();
@ -274,6 +309,17 @@
</div>
<Button size="sm" className="w-16" on:click={() => openAddModal(0)}>Add</Button>
</div>
<div class="flex">
<button on:click={() => toggleShowRarity(0)} class={`pill legendary ${showRarity[0] ? 'active' : ''}`}>
5 <Icon path={mdiStar} size={0.75} className="mb-1" />
</button>
<button on:click={() => toggleShowRarity(1)} class={`pill rare ${showRarity[1] ? 'active' : ''}`}>
4 <Icon path={mdiStar} size={0.75} className="mb-1" />
</button>
<button on:click={() => toggleShowRarity(2)} class={`pill normal ${showRarity[2] ? 'active' : ''}`}>
3 <Icon path={mdiStar} size={0.75} className="mb-1" />
</button>
</div>
<table class="w-full">
<tr>
<th class="border-b border-gray-700 text-gray-400 font-display text-left pl-2">Name</th>
@ -285,17 +331,27 @@
{#if pull.type === 'character'}
<td
class={`border-b border-gray-700 py-1 pl-2 font-semibold ${
characters[pull.id].rarity === 5 ? 'text-legendary-from' : 'text-rare-from'
characters[pull.id].rarity === 5
? 'text-legendary-from'
: characters[pull.id].rarity === 4
? 'text-rare-from'
: 'text-primary'
}`}>{characters[pull.id].name}</td
>
{:else}
{:else if pull.type === 'weapon'}
<td
class={`border-b border-gray-700 py-1 pl-2 font-semibold ${
weaponList[pull.id].rarity === 5 ? 'text-legendary-from' : 'text-rare-from'
weaponList[pull.id].rarity === 5
? 'text-legendary-from'
: weaponList[pull.id].rarity === 4
? 'text-rare-from'
: 'text-primary'
}`}>{weaponList[pull.id].name}</td
>
{/if}
<td class="border-b border-gray-700 text-sm py-1 px-2 whitespace-no-wrap" style="font-family: monospace;">{dayjs.unix(pull.time).format('YYYY-MM-DD HH:mm:ss')}</td>
<td class="border-b border-gray-700 text-sm py-1 px-2 whitespace-no-wrap" style="font-family: monospace;"
>{dayjs.unix(pull.time).format('YYYY-MM-DD HH:mm:ss')}</td
>
<td class="text-right border-b border-gray-700 py-1">{pull.pity}</td>
</tr>
{/each}
@ -303,3 +359,40 @@
</div>
{/if}
</div>
<style>
.pill {
@apply rounded-2xl;
@apply border-2;
@apply border-white;
@apply border-opacity-25;
@apply text-white;
@apply px-4;
@apply py-1;
@apply mr-2;
@apply mb-2;
@apply outline-none;
@apply transition;
@apply duration-100;
&:hover {
@apply border-primary;
}
&.active {
@apply bg-primary;
@apply border-primary;
@apply text-background;
&.legendary {
@apply bg-legendary-from;
@apply border-legendary-from;
}
&.rare {
@apply bg-rare-from;
@apply border-rare-from;
}
}
}
</style>

View File

@ -0,0 +1,120 @@
<script>
import { mdiStar } from '@mdi/js';
import { onMount } from 'svelte';
import Icon from '../../components/Icon.svelte';
import { characters } from '../../data/characters';
import { weaponList } from '../../data/weaponList';
import { readSave, updateTime, fromRemote } from '../../stores/saveManager';
import SummaryItem from './_summaryItem.svelte';
const types = [
{
name: 'Character Event',
id: 'character-event',
},
{
name: 'Weapon Event',
id: 'weapon-event',
},
{
name: 'Standard',
id: 'standard',
},
{
name: "Beginners' Wish",
id: 'beginners',
},
];
let loading = true;
const avg = {};
$: if ($fromRemote) {
readLocalData();
}
$: if ($updateTime) {
readLocalData();
}
onMount(() => {
readLocalData();
});
export function readLocalData() {
console.log('wish summary read local');
for (let type of types) {
const path = `wish-counter-${type.id}`;
const data = readSave(path);
if (data !== null) {
const counterData = JSON.parse(data);
const pulls = counterData.pulls || [];
const total = counterData.total;
let legendary = 0;
let legendaryPity = 0;
let legendaryPulls = [];
let rare = 0;
let rarePity = 0;
for (let pull of pulls) {
let rarity;
let itemName;
if (pull.type === 'character') {
rarity = characters[pull.id].rarity;
itemName = characters[pull.id].name;
} else if (pull.type === 'weapon') {
rarity = weaponList[pull.id].rarity;
itemName = weaponList[pull.id].name;
}
if (rarity === 5) {
legendary++;
legendaryPity += pull.pity;
legendaryPulls.push({ name: itemName, pity: pull.pity });
} else if (rarity === 4) {
rare++;
rarePity += pull.pity;
}
}
avg[type.id] = {
rare: {
total: rare,
percentage: total > 0 ? rare / total : 0,
pity: rare > 0 ? rarePity / rare : 0,
},
legendary: {
total: legendary,
percentage: total > 0 ? legendary / total : 0,
pity: legendary > 0 ? legendaryPity / legendary : 0,
pulls: legendaryPulls,
},
};
}
}
console.log(avg);
loading = false;
}
</script>
{#if !loading}
<div class="flex flex-col">
{#if avg[types[0].id]}
<SummaryItem avg={avg[types[0].id]} type={types[0]} withBottomSpace />
{/if}
{#if avg[types[1].id]}
<SummaryItem avg={avg[types[1].id]} type={types[1]} />
{/if}
</div>
<div class="flex flex-col">
{#if avg[types[2].id]}
<SummaryItem avg={avg[types[2].id]} type={types[2]} withBottomSpace />
{/if}
{#if avg[types[3].id]}
<SummaryItem avg={avg[types[3].id]} type={types[3]} />
{/if}
</div>
{/if}

View File

@ -0,0 +1,90 @@
<script>
import { mdiStar } from '@mdi/js';
import Icon from '../../components/Icon.svelte';
export let withBottomSpace;
export let avg;
export let type;
let numberFormat = Intl.NumberFormat('en', {
maximumFractionDigits: 1,
minimumFractionDigits: 0,
});
function calculateColor(percentage) {
const a = [255, 177, 63];
const b = [255, 77, 77];
const av = percentage;
const bv = 1 - percentage;
const color = [a[0] * av + b[0] * bv, a[1] * av + b[1] * bv, a[2] * av + b[2] * bv];
return `color: rgb(${color[0]},${color[1]},${color[2]});`;
}
</script>
<div class={`bg-item rounded-xl p-4 flex flex-col w-full ${withBottomSpace ? 'mb-4' : ''}`} style="height: min-content;">
<table>
<tr>
<td class="text-white text-md font-semibold pr-2 md:pr-4 flex-1 w-full">{type.name}</td>
<td class="text-gray-400 text-sm font-display pr-2 md:pr-4 text-right">Total</td>
<td class="text-gray-400 text-sm font-display pr-2 md:pr-4 text-right">Percent</td>
<td class="text-gray-400 text-sm font-display text-right whitespace-no-wrap">Pity AVG</td>
</tr>
<tr>
<td class="text-legendary-from font-semibold pr-2 md:pr-4 border-t border-gray-700">
5 <Icon path={mdiStar} color="#FFB13F" size="0.6" />
</td>
<td class="text-legendary-from font-semibold pr-2 md:pr-4 text-right border-t border-gray-700">
{numberFormat.format(avg.legendary.total)}
</td>
<td class="text-legendary-from font-semibold pr-2 md:pr-4 text-right border-t border-gray-700">
{numberFormat.format(avg.legendary.percentage * 100)}%
</td>
<td class="text-legendary-from font-semibold text-right border-t border-gray-700">
{numberFormat.format(avg.legendary.pity)}
</td>
</tr>
<tr>
<td class="text-rare-from font-semibold pr-2 md:pr-4 border-t border-gray-700">
4 <Icon path={mdiStar} color="#AD76B0" size="0.6" />
</td>
<td class="text-rare-from font-semibold pr-2 md:pr-4 text-right border-t border-gray-700">
{numberFormat.format(avg.rare.total)}
</td>
<td class="text-rare-from font-semibold pr-2 md:pr-4 text-right border-t border-gray-700">
{numberFormat.format(avg.rare.percentage * 100)}%
</td>
<td class="text-rare-from font-semibold text-right border-t border-gray-700">
{numberFormat.format(avg.rare.pity)}
</td>
</tr>
</table>
{#if avg.legendary.pulls.length > 0}
<div class="flex flex-wrap mt-2">
{#each avg.legendary.pulls as pull}
<span class="pity">{pull.name} <span style={calculateColor((90 - pull.pity) / 90)}>{pull.pity}</span></span>
{/each}
</div>
{/if}
</div>
<style>
span.pity {
@apply rounded-xl;
@apply text-gray-400;
@apply border;
@apply border-legendary-from;
@apply whitespace-no-wrap;
@apply px-2;
@apply mb-1;
@apply mr-1;
& > span {
@apply font-semibold;
@apply pl-1;
}
}
</style>

View File

@ -1,14 +1,22 @@
<script>
import { mdiHelpCircle } from '@mdi/js';
import { mdiDatabaseImport, mdiHelpCircle } from '@mdi/js';
import { getContext } from 'svelte';
import Button from '../../components/Button.svelte';
import Icon from '../../components/Icon.svelte';
import HowToModal from '../../components/WishCounterHowToModal.svelte';
import ImportModal from '../../components/WishImportModal.svelte';
import Summary from './_summary.svelte';
import Counter from './_counter.svelte';
const { open: openModal } = getContext('simple-modal');
const { open: openModal, close: closeModal } = getContext('simple-modal');
let counter1;
let counter2;
let counter3;
let counter4;
let summary;
function openHowTo() {
openModal(
@ -20,30 +28,68 @@
},
);
}
function openImport() {
openModal(
ImportModal,
{
closeModal: closeImportModal,
},
{
closeButton: false,
closeOnOuterClick: false,
styleWindow: { background: '#25294A', width: '800px' },
},
);
}
function closeImportModal() {
closeModal();
counter1.readLocalData();
counter2.readLocalData();
counter3.readLocalData();
counter4.readLocalData();
}
</script>
<svelte:head>
<title>Wish Counter - Paimon.moe</title>
<meta
name="description"
content="Genshin Impact Wish Counter to track your pity counter and track when you get the character or weapon"
content="Genshin Impact Wish Counter to track your pity counter and track when you get the character or weapon. You can also auto import the logs from your PC."
/>
<meta
property="og:description"
content="Genshin Impact Wish Counter to track your pity counter and track when you get the character or weapon"
content="Genshin Impact Wish Counter to track your pity counter and track when you get the character or weapon. You can also auto import the logs from your PC."
/>
</svelte:head>
<div class="pt-20 lg:ml-64 lg:pt-8 px-4 md:px-8">
<div class="flex flex-col md:flex-row mb-4 items-center">
<h1 class="font-display font-black text-5xl text-white text-center md:text-left md:mr-4">Wish Counter</h1>
<Button on:click={openHowTo}>
<Button on:click={openHowTo} className="hidden md:block">
<Icon size={0.8} path={mdiHelpCircle} />
How To Use
</Button>
<Button className="ml-2 hidden md:block" on:click={openImport}>
<Icon size={0.8} path={mdiDatabaseImport} />
Auto Import
</Button>
<div class="md:hidden flex flex-wrap justify-center">
<Button className="m-1" on:click={openHowTo}>
<Icon size={0.8} path={mdiHelpCircle} />
How To Use
</Button>
<Button className="m-1" on:click={openImport}>
<Icon size={0.8} path={mdiDatabaseImport} />
Auto Import
</Button>
</div>
</div>
<div class="grid gap-4 grid-cols-1 md:grid-cols-2 xl:grid-cols-3 max-w-screen-xl">
<Counter id="character-event" name="Character Event" />
<Counter id="weapon-event" name="Weapon Event" legendaryPity={80} />
<Counter id="standard" name="Standard" />
<Counter bind:this={counter1} id="character-event" name="Character Event" />
<Counter bind:this={counter2} id="weapon-event" name="Weapon Event" legendaryPity={80} />
<Counter bind:this={counter3} id="standard" name="Standard" />
<Counter bind:this={counter4} id="beginners" name="Beginners' Wish" />
<Summary bind:this={summary} />
</div>
</div>

View File

@ -25,7 +25,7 @@ module.exports = {
to: '#665680',
},
legendary: {
from: '#B9812E',
from: '#FFB13F',
to: '#846332',
},
},