Add wish banner detail

pull/1/head
Made Baruna 2021-03-18 20:41:57 +08:00
parent 4d03718046
commit 88a4b4c716
32 changed files with 1047 additions and 16 deletions

View File

@ -28,6 +28,7 @@
"@rollup/plugin-replace": "^2.3.4",
"@rollup/plugin-url": "^5.0.0",
"autoprefixer": "^10.0.1",
"chart.js": "^2.9.4",
"dayjs": "^1.9.4",
"dotenv": "^8.2.0",
"postcss": "^8.1.2",

View File

@ -9,7 +9,7 @@
}
</script>
<div class="flex items-center lg:hidden fixed w-full h-16 header bg-background z-30 shadow-md overflow-hidden">
<div class="flex items-center lg:hidden fixed w-full h-16 header bg-background z-50 shadow-md overflow-hidden">
<a href="/" class="flex-1 pl-4 md:pl-8 font-display text-3xl font-black text-white relative z-10 pt-2">
Paimon<span class="text-xl text-primary">.moe</span>
</a>

View File

@ -4,13 +4,21 @@
import Icon from '../Icon.svelte';
export let className = '';
export let styleList = '';
export let sort = false;
export let order = false;
export let align = 'left';
export let padding = 'px-4';
</script>
<th class={`text-gray-400 select-none font-display text-lg cursor-pointer px-4 text-${align}`} on:click>
<span class="relative"><slot />
<th
class={`text-gray-400 select-none font-display text-lg cursor-pointer ${padding} text-${align} ${className}`}
style={styleList}
on:click
>
<span class="relative"
><slot />
{#if sort}
<div transition:fade={{ duration: 100 }} class="absolute" style="right: -21px; top: 3px;">
<Icon className={`mb-1 duration-100 ${order ? 'transform -rotate-180' : ''}`} path={mdiChevronDown} />

153
src/data/banners.js Normal file
View File

@ -0,0 +1,153 @@
export const banners = {
beginners: [
{
name: "Beginners' Wish",
start: '2000-01-01 00:00:00',
end: '2200-01-01 00:00:00',
},
],
standard: [
{
name: 'Wanderlust Invocation',
start: '2000-01-01 00:00:00',
end: '2200-01-01 00:00:00',
},
],
characters: [
{
name: 'Ballad in Goblets',
shortName: 'Venti',
start: '2020-09-28 00:00:00',
end: '2020-10-18 18:00:00',
color: '#55E4B0',
},
{
name: 'Sparkling Steps',
shortName: 'Klee',
start: '2020-10-20 18:00:00',
end: '2020-11-10 18:00:00',
color: '#CA360E',
},
{
name: 'Farewell of Snezhnaya',
shortName: 'Tartaglia',
start: '2020-11-11 06:00:00',
end: '2020-12-01 18:00:00',
color: '#50A3C0',
},
{
name: 'Gentry of Hermitage',
shortName: 'Zhongli',
start: '2020-12-01 18:00:00',
end: '2020-12-22 15:00:00',
color: '#D1A55C',
},
{
name: 'Secretum Secretorum',
shortName: 'Albedo',
start: '2020-12-23 10:00:00',
end: '2021-01-12 16:00:00',
color: '#FCFE83',
},
{
name: 'Adrift in the Harbor',
shortName: 'Ganyu',
start: '2021-01-12 18:00:00',
end: '2021-02-02 15:00:00',
color: '#6994DF',
},
{
name: 'Invitation to Mundane Life',
shortName: 'Xiao',
start: '2021-02-03 06:00:00',
end: '2021-02-17 16:00:00',
color: '#2BE3F8',
},
{
name: 'Dance of Lanterns',
shortName: 'Keqing',
start: '2021-02-17 18:00:00',
end: '2021-03-02 16:00:00',
color: '#AB6CD7',
},
{
name: 'Moment of Bloom',
shortName: 'Hu Tao',
start: '2021-03-02 18:00:00',
end: '2021-03-16 15:00:00',
color: '#BF5042',
},
{
name: 'Ballad in Goblets',
shortName: 'Venti',
start: '2021-03-17 06:00:00',
end: '2021-04-06 16:00:00',
color: '#35C297',
},
],
weapons: [
{
name: 'Epitome Invocation',
start: '2020-09-28 00:00:00',
end: '2020-10-18 18:00:00',
shortName: 'Amos',
color: '#f54e42'
},
{
name: 'Epitome Invocation',
start: '2020-10-20 18:00:00',
end: '2020-11-10 18:00:00',
shortName: 'WGS',
color: '#f5c242'
},
{
name: 'Epitome Invocation',
start: '2020-11-11 06:00:00',
end: '2020-12-01 18:00:00',
shortName: 'Skyward',
color: '#f5ef42'
},
{
name: 'Epitome Invocation',
start: '2020-12-01 18:00:00',
end: '2020-12-22 15:00:00',
shortName: 'Vortex',
color: '#7ef542'
},
{
name: 'Epitome Invocation',
start: '2020-12-23 10:00:00',
end: '2021-01-12 16:00:00',
shortName: 'Summit',
color: '#42ecf5'
},
{
name: 'Epitome Invocation',
start: '2021-01-12 18:00:00',
end: '2021-02-02 15:00:00',
shortName: 'Amos',
color: '#424ef5'
},
{
name: 'Epitome Invocation',
start: '2021-02-03 06:00:00',
end: '2021-02-23 16:00:00',
shortName: 'Primordial',
color: '#b042f5'
},
{
name: 'Epitome Invocation',
start: '2021-02-23 18:00:00',
end: '2021-03-16 15:00:00',
shortName: 'Homa',
color: '#f542c8'
},
{
name: 'Epitome Invocation',
start: '2021-03-17 11:00:00',
end: '2021-04-06 16:00:00',
shortName: 'Elegy',
color: '#f54e42'
},
],
};

View File

@ -1071,7 +1071,7 @@ export const weaponList = {
atk: 42,
secondary: 'Elemental Mastery',
type: weapons.sword,
source: 'forgingnorthlander sword prototype x1crystal chunk x50white iron chunk x50',
source: 'forging',
ascension: [
{
items: [
@ -1130,7 +1130,7 @@ export const weaponList = {
atk: 44,
secondary: 'Physical DMG Bonus',
type: weapons.sword,
source: 'adventure rank 10 rewardforging:northlander sword prototype x1crystal chunk x50white iron chunk x50',
source: 'adventure rank 10 reward, forging',
ascension: [
{
items: [
@ -1307,7 +1307,7 @@ export const weaponList = {
atk: 42,
secondary: 'ATK',
type: weapons.bow,
source: 'forgingnorthlander bow prototype x1crystal chunk x50white iron chunk x50',
source: 'forging',
ascension: [
{
items: [
@ -1602,7 +1602,7 @@ export const weaponList = {
atk: 44,
secondary: 'Physical DMG Bonus',
type: weapons.polearm,
source: 'forgingnorthlander polearm prototype x1crystal chunk x50white iron chunk x50',
source: 'forging',
ascension: [
{
items: [
@ -2664,7 +2664,7 @@ export const weaponList = {
atk: 42,
secondary: 'Energy Recharge',
type: weapons.polearm,
source: 'forgingnorthlander polearm prototype x1crystal chunk x50white iron chunk x50',
source: 'forging',
ascension: [
{
items: [
@ -2723,7 +2723,7 @@ export const weaponList = {
atk: 42,
secondary: 'DEF',
type: weapons.claymore,
source: 'forgingnorthlander claymore prototype x1crystal chunk x50white iron chunk x50',
source: 'forging',
ascension: [
{
items: [
@ -2841,7 +2841,7 @@ export const weaponList = {
atk: 41,
secondary: 'Physical DMG Bonus',
type: weapons.bow,
source: 'forgingnorthlander bow prototype x1crystal chunk x50white iron chunk x50',
source: 'forging',
ascension: [
{
items: [
@ -3313,7 +3313,7 @@ export const weaponList = {
atk: 44,
secondary: 'Elemental Mastery',
type: weapons.catalyst,
source: 'forgingnorthlander catalyst prototype x1crystal chunk x50white iron chunk x50',
source: 'forging',
ascension: [
{
items: [
@ -3903,7 +3903,7 @@ export const weaponList = {
atk: 44,
secondary: 'ATK',
type: weapons.claymore,
source: 'forgingnorthlander claymore prototype x1crystal chunk x50white iron chunk x50',
source: 'forging',
ascension: [
{
items: [

View File

@ -127,6 +127,24 @@
"Or you can add or edit the table manually."
]
}
},
"types": {
"beginners": "Beginners' Wish",
"standard": "Standard",
"character-event": "Character Event",
"weapon-event": "Weapon Event"
},
"detail": {
"weapon": "Weapon",
"character": "Character",
"time": "Time",
"pity": "Pity",
"name": "Name",
"type": "Type",
"banner": "Banner",
"roll": "#Roll",
"totalThisBanner": "Total pull on this banner",
"worth": "Worth"
}
},
"calculator": {

View File

@ -127,6 +127,24 @@
"Atau kamu bisa menambahkan atau mengedit tabel nya secara manual."
]
}
},
"types": {
"beginners": "Beginners' Wish",
"standard": "Standard",
"character-event": "Event Karakter",
"weapon-event": "Event Senjata"
},
"detail": {
"weapon": "Senjata",
"character": "Karakter",
"time": "Waktu",
"pity": "Pity",
"name": "Nama",
"type": "Tipe",
"banner": "Banner",
"roll": "#Roll",
"totalThisBanner": "Total pull di banner ini",
"worth": "Setara dengan"
}
},
"calculator": {

654
src/routes/wish/[id].svelte Normal file
View File

@ -0,0 +1,654 @@
<script context="module">
export async function preload(page) {
const { id } = page.params;
return { id };
}
</script>
<script>
import { t } from 'svelte-i18n';
import { getContext, onMount, tick } from 'svelte';
import { mdiArrowLeft, mdiStar } from '@mdi/js';
import dayjs from 'dayjs';
import Chart from 'chart.js';
import { banners } from '../../data/banners';
import Icon from '../../components/Icon.svelte';
import TableHeader from '../../components/Table/TableHeader.svelte';
import WishDetailModal from './_detail.svelte';
import { characters } from '../../data/characters';
import { weaponList } from '../../data/weaponList';
import { getAccountPrefix } from '../../stores/account';
import { fromRemote, readSave } from '../../stores/saveManager';
Chart.defaults.global.defaultFontColor = '#cbd5e0';
Chart.defaults.global.defaultFontFamily = 'Poppins';
let numberFormat = Intl.NumberFormat();
const { open: openModal } = getContext('simple-modal');
export let id;
const bannerTypes = {
'character-event': 'characters',
'weapon-event': 'weapons',
standard: 'standard',
beginners: 'beginners',
};
const bannerType = bannerTypes[id];
let bannerChart;
let pieChart;
let loading = true;
let pullData = [];
let pulls = [];
let sorted = [];
let total = 0;
let legendary = 0;
let rare = 0;
let allLegendary = [];
let allRare = [];
let sortBy = 'time';
let sortOrder;
let currentBannerIndex = -1;
let selectedBanners;
selectedBanners = banners[bannerType].map((e) => {
const start = dayjs(e.start, 'YYYY-MM-DD HH:mm:ss');
const end = dayjs(e.end, 'YYYY-MM-DD HH:mm:ss');
const image = `/images/banners/${e.name} ${start.format('YYYY-MM-DD')}.png`;
return {
...e,
start: start.unix(),
end: end.unix(),
image,
total: 0,
legendary: [],
rare: {
character: [],
weapon: [],
},
};
});
function openDetail(banner) {
openModal(
WishDetailModal,
{
banner,
},
{
closeButton: false,
styleWindow: { background: '#25294A', width: '600px' },
},
);
}
function readLocalData() {
console.log('wish read local');
const prefix = getAccountPrefix();
const data = readSave(`${prefix}${path}`);
if (data !== null) {
const counterData = JSON.parse(data);
total = counterData.total;
legendary = counterData.legendary;
rare = counterData.rare;
pullData = counterData.pulls || [];
}
processPullData();
}
function getNextBanner(time) {
for (let i = currentBannerIndex + 1; i < selectedBanners.length; i++) {
console.log('change banner', i, dayjs.unix(time).format(), dayjs.unix(selectedBanners[i].start).format());
if (time >= selectedBanners[i].start && time < selectedBanners[i].end) {
currentBannerIndex = i;
return selectedBanners[i];
}
}
}
async function processPullData() {
const currentPulls = [];
console.log(selectedBanners);
let currentBanner = null;
let grouped = false;
let striped = false;
let startBanner = false;
for (let i = 0; i < pullData.length; i++) {
const pull = pullData[i];
const next = pullData[i + 1] || { time: dayjs().year(2000).unix() };
if (currentBanner === null || currentBanner.end < pull.time) {
currentBanner = getNextBanner(pull.time);
startBanner = true;
if (i > 0) {
currentPulls[i - 1].end = true;
}
}
const item =
pull.type === 'character'
? characters[pull.id]
: pull.type === 'weapon'
? weaponList[pull.id]
: { name: 'Unknown', rarity: 3 };
selectedBanners[currentBannerIndex].total++;
const newPull = {
...pull,
formattedTime: formatTime(pull.time),
name: item.name,
rarity: item.rarity,
banner: currentBanner,
start: startBanner,
at: selectedBanners[currentBannerIndex].total,
};
if (item.rarity === 5) {
selectedBanners[currentBannerIndex].legendary.push(newPull);
allLegendary.push(newPull);
} else if (item.rarity === 4) {
allRare.push(newPull);
if (pull.type === 'character') {
selectedBanners[currentBannerIndex].rare.character.push(newPull);
} else if (pull.type === 'weapon') {
selectedBanners[currentBannerIndex].rare.weapon.push(newPull);
}
}
if (!grouped && pull.time === next.time) {
striped = !striped;
newPull.group = 'start';
grouped = true;
} else if (grouped && pull.time !== next.time) {
newPull.group = 'end';
grouped = false;
} else if (grouped) {
newPull.group = 'group';
} else {
striped = !striped;
}
if (i === pullData.length - 1) {
newPull.end = true;
}
newPull.striped = striped;
startBanner = false;
currentPulls.push(newPull);
}
console.log(currentPulls.slice());
pulls = currentPulls;
sorted = pulls.reverse();
let labels = [];
let totalEachBanner = [];
let totalLegendaryEachBanner = [];
let totalRareEachBanner = [];
let backgrounds = [];
let borders = [];
for (let e of selectedBanners) {
const curLegendary = e.legendary.length;
const curRare = e.rare.character.length + e.rare.weapon.length;
const curLeft = e.total - curLegendary - curRare;
labels.push(e.shortName);
totalEachBanner.push(curLeft);
totalLegendaryEachBanner.push(curLegendary);
totalRareEachBanner.push(curRare);
borders.push(e.color);
const rgb = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e.color || '#ffffff');
backgrounds.push(`rgba(${parseInt(rgb[1], 16)}, ${parseInt(rgb[2], 16)}, ${parseInt(rgb[3], 16)}, 0.7)`);
}
console.log(totalEachBanner, totalLegendaryEachBanner, totalRareEachBanner);
loading = false;
await tick();
new Chart(pieChart, {
type: 'pie',
data: {
labels: ['Total Pulls', 'Total 5*', 'Total 4*'],
datasets: [
{
label: 'total',
data: [currentPulls.length, allLegendary.length, allRare.length],
backgroundColor: ['rgba(107, 161, 192, 0.7)', 'rgba(255, 177, 63, 0.7)', 'rgba(210, 143, 214, 0.7)'],
borderColor: ['#6BA1C0', '#FFB13F', '#D28FD6'],
borderWidth: 1,
},
],
},
options: {
responsive: true,
legend: {
display: false,
},
tooltips: {
mode: 'dataset',
},
},
});
new Chart(bannerChart, {
type: 'bar',
data: {
labels,
datasets: [
{
label: '5* pulls',
data: totalLegendaryEachBanner,
backgroundColor: 'rgba(255, 177, 63, 0.7)',
borderColor: '#FFB13F',
borderWidth: 1,
},
{
label: '4* pulls',
data: totalRareEachBanner,
backgroundColor: 'rgba(210, 143, 214, 0.7)',
borderColor: '#D28FD6',
borderWidth: 1,
},
{
label: 'Total pulls',
data: totalEachBanner,
backgroundColor: backgrounds,
borderColor: borders,
borderWidth: 1,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
legend: {
display: false,
},
tooltips: {
mode: 'index',
intersect: false,
callbacks: {
title: (tooltipItem) => {
return selectedBanners[tooltipItem[0].index].name;
},
label: (tooltipItem, data) => {
console.log(tooltipItem, data);
const label = data.datasets[tooltipItem.datasetIndex].label;
const value =
tooltipItem.datasetIndex === 2
? data.datasets[0].data[tooltipItem.index] +
data.datasets[1].data[tooltipItem.index] +
data.datasets[2].data[tooltipItem.index]
: tooltipItem.value;
return `${label}: ${value}`;
},
},
},
scales: {
yAxes: [{ stacked: true, gridLines: { color: '#2d3748' } }],
xAxes: [{ stacked: true }],
},
},
});
}
function sortPulls() {
if (sortBy === 'time') {
if (sortOrder) {
sorted = pulls.slice().reverse();
} else {
sorted = pulls;
}
} else if (sortBy === 'type') {
if (sortOrder) {
sorted = pulls.slice().sort((a, b) => a.type.localeCompare(b.type));
} else {
sorted = pulls.slice().sort((a, b) => b.type.localeCompare(a.type));
}
} else if (sortBy === 'rare') {
if (sortOrder) {
sorted = pulls.slice().sort((a, b) => a.rarity - b.rarity);
} else {
sorted = pulls.slice().sort((a, b) => b.rarity - a.rarity);
}
} else if (sortBy === 'pity') {
if (sortOrder) {
sorted = pulls.slice().sort((a, b) => a.pity - b.pity);
} else {
sorted = pulls.slice().sort((a, b) => b.pity - a.pity);
}
} else if (sortBy === 'name') {
if (sortOrder) {
sorted = pulls.slice().sort((a, b) => a.name.localeCompare(b.name));
} else {
sorted = pulls.slice().sort((a, b) => b.name.localeCompare(a.name));
}
}
}
onMount(() => {
readLocalData();
});
function sort(by) {
if (sortBy === by) {
sortOrder = !sortOrder;
} else {
sortBy = by;
sortOrder = false;
}
sortPulls();
}
function formatTime(time) {
return dayjs.unix(time).format('ddd YYYY-MM-DD HH:mm:ss');
}
function calculateLegendaryColor(percentage) {
const hue = percentage * 120;
return `color: hsl(${hue}, 100%, 60%);`;
}
function calculateRareColor(percentage) {
const opacity = percentage + 0.3;
return `opacity: ${opacity};`;
}
$: path = `wish-counter-${id}`;
$: if ($fromRemote) {
readLocalData();
}
</script>
<div class="pt-20 lg:ml-64 lg:pt-8">
<div class="flex items-center text-gray-400 px-4 md:px-8">
<a href="/wish" class="pr-2">
<Icon path={mdiArrowLeft} size={1.2} />
</a>
<h2 class="font-display font-bold text-2xl text-gray-400 flex-1">
Wish Counter
<span class="text-white">{$t(`wish.types.${id}`)}</span>
</h2>
</div>
{#if loading}
<div class="text-white pl-4 md:pl-8 mt-4">Loading...</div>
{:else}
<div class="flex mt-4 wrapper">
<div class="block overflow-x-auto xl:overflow-x-visible whitespace-no-wrap px">
<div class="pr-4 pl-4 md:pl-8 xl:pr-2 table">
<table
class="{sortBy === 'time'
? 'list-table'
: ''} w-full block pl-4 pr-4 py-2 md:pl-8 md:py-4 bg-item rounded-xl"
>
<tr>
<TableHeader
className="sticky top-0 bg-item z-30"
styleList="top: -1px;"
on:click={() => sort('time')}
sort={sortBy === 'time'}
order={sortOrder}
align="left"
>
{$t('wish.detail.time')}
<div class="absolute h-full w-8 bg-item" style="left: -2rem; top: -1px;" />
</TableHeader>
<TableHeader
className="sticky top-0 bg-item z-30"
styleList="top: -1px;"
on:click={() => sort('pity')}
sort={sortBy === 'pity'}
order={sortOrder}
align="center"
padding="px-2"
>
{$t('wish.detail.pity')}
</TableHeader>
<TableHeader
className="sticky top-0 bg-item z-30"
styleList="top: -1px;"
on:click={() => sort('name')}
sort={sortBy === 'name'}
order={sortOrder}
align="left"
>
{$t('wish.detail.name')}
</TableHeader>
<TableHeader
className="sticky top-0 bg-item z-30"
styleList="top: -1px;"
on:click={() => sort('rare')}
sort={sortBy === 'rare'}
order={sortOrder}
align="center"
padding="px-2"
>
<Icon path={mdiStar} className="pb-1" />
</TableHeader>
<TableHeader
className="sticky top-0 bg-item z-30"
styleList="top: -1px;"
on:click={() => sort('type')}
sort={sortBy === 'type'}
order={sortOrder}
align="left"
>
{$t('wish.detail.type')}
</TableHeader>
<TableHeader className="sticky top-0 bg-item" styleList="top: -1px;" align="center">
{$t('wish.detail.roll')}
</TableHeader>
<TableHeader
className="sticky top-0 bg-item z-30 {sortBy === 'time' ? 'xl:hidden' : ''}"
styleList="top: -1px;"
align="left"
>
{$t('wish.detail.banner')}
</TableHeader>
</tr>
{#each sorted as pull}
<tr class="rarity-{pull.rarity}{pull.striped && sortBy === 'time' ? ' striped' : ''}">
<td
class="border-t border-gray-700 px-4 text-gray-200 whitespace-no-wrap relative"
style="font-family: monospace;"
>
{pull.formattedTime}
{#if sortBy === 'time' && (sortOrder ? pull.group === 'start' : pull.group === 'end')}
<div class="group-bar"><span>x10</span></div>
{/if}
</td>
<td class="border-t border-gray-700 px-2 text-gray-200 text-center">
<span
style={pull.rarity === 5
? calculateLegendaryColor((90 - pull.pity) / 90)
: calculateRareColor((10 - pull.pity) / 10)}
>
{pull.pity}
</span>
</td>
<td class="border-t border-gray-700 pl-4 text-gray-200 flex items-center">
<img
class="w-8 h-8 mr-2"
src={pull.type === 'character'
? `/images/characters/${pull.id}.png`
: pull.type === 'weapon'
? `/images/weapons/${pull.id}.png`
: '/images/wish.png'}
alt={pull.name}
/>
<span class="pr-4">{pull.name}</span>
</td>
<td class="border-t border-gray-700 px-2 text-gray-200 text-center">{pull.rarity}</td>
<td class="border-t border-gray-700 px-4 text-gray-200">
{$t(`wish.detail.${pull.type}`)}
</td>
<td class="border-t border-gray-700 px-2 text-gray-200 text-center">
{pull.at}
</td>
{#if sortBy === 'time' && ((pull.end && !sortOrder) || (pull.start && sortOrder))}
<td class="relative hidden xl:table-cell">
<div class="border-t border-gray-700 absolute top-0 z-10" style="width: 266px;" />
</td>
<td class="sticky w-0 hidden xl:table-cell pl-2" style="top: 8px;">
<div
class="w-64 absolute top-0 pt-2 bg-item cursor-pointer"
on:click={() => openDetail(pull.banner)}
>
<img class="w-full rounded-lg" src={pull.banner.image} alt={pull.banner.name} />
<p class="bg-gray-900 rounded-lg mt-2 text-center text-gray-200">
{pull.banner.total} Pulls
<img class="h-4 inline ml-2" src="/images/primogem.png" alt="primogem" />
{numberFormat.format(pull.banner.total * 160)}
</p>
</div>
</td>
{/if}
<td
class="border-t border-gray-700 px-4 text-gray-200 top-0 text-center {sortBy === 'time'
? 'xl:hidden'
: ''}"
>
<img
on:click={() => openDetail(pull.banner)}
class="h-8 inline cursor-pointer"
src={pull.banner.image}
alt={pull.banner.name}
/>
</td>
</tr>
{/each}
</table>
</div>
</div>
<div class="chart-area flex flex-wrap">
<div class="flex">
<div
class="bg-background px-4 py-2 rounded-xl flex flex-col items-center justify-center mr-4"
style="height: 200px;"
>
<p class="text-gray-400 font-body">Total</p>
<p class="text-gray-400 font-body text-xl font-bold">{total}</p>
<p class="text-gray-400 font-body mt-2 flex items-center">5<Icon size={0.7} path={mdiStar} /> Pity</p>
<p class="text-legendary-from font-body text-xl font-bold">{legendary}</p>
<p class="text-gray-400 font-body mt-2 flex items-center">4<Icon size={0.7} path={mdiStar} /> Pity</p>
<p class="text-rare-from font-body text-xl font-bold">{rare}</p>
</div>
<div class="bg-background rounded-xl inline-block mb-4 p-2 pie-chart mr-4">
<canvas width="200" height="200" bind:this={pieChart} />
</div>
</div>
{#if id === 'character-event' || id === 'weapon-event'}
<div class="bg-background rounded-xl inline-block mb-4 p-2 banner-chart">
<canvas width="500" height="200" bind:this={bannerChart} />
</div>
{/if}
</div>
</div>
{/if}
</div>
<style>
.wrapper {
@apply flex-col-reverse;
.chart-area {
@apply px-4;
@screen md {
@apply px-8;
}
}
@media (min-width: 1920px) {
@apply flex-row;
.chart-area {
@apply px-2;
@apply flex-col;
}
}
}
.banner-chart {
max-width: 500px;
height: 200px;
width: 100%;
}
.pie-chart {
max-width: 200px;
height: 200px;
width: 100%;
}
tr.striped {
background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.4) 20%, rgba(0, 0, 0, 0) 100%);
}
tr.rarity-5 {
background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(185, 129, 46, 0.55) 20%, rgba(0, 0, 0, 0) 100%);
}
tr.rarity-4 {
background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(173, 118, 176, 0.55) 20%, rgba(0, 0, 0, 0) 100%);
}
.group-bar {
width: 1px;
height: 317px;
left: 0;
top: 17px;
@apply bg-white;
@apply absolute;
@apply select-none;
&::before,
&::after {
content: '';
width: 8px;
height: 1px;
left: 0;
top: 0;
@apply bg-white;
@apply absolute;
}
&::after {
top: initial;
bottom: 0;
}
span {
top: 155px;
left: -20px;
@apply absolute;
@apply transform;
@apply -rotate-90;
}
}
table.list-table {
@screen xl {
padding-right: 17rem;
}
}
</style>

View File

@ -3,7 +3,7 @@
import { onMount, getContext } from 'svelte';
import { slide } from 'svelte/transition';
import { mdiPencil, mdiStar, mdiChevronDown } from '@mdi/js';
import { mdiPencil, mdiStar, mdiChevronDown, mdiTableOfContents } from '@mdi/js';
import debounce from 'lodash/debounce';
const { open: openModal, close: closeModal } = getContext('simple-modal');
@ -245,13 +245,18 @@
</script>
<div class="bg-item rounded-xl p-4 inline-flex flex-col w-full" style="height: min-content;">
<div class="flex justify-between mb-2">
<h2 class="font-display font-bold text-2xl text-white">{name}</h2>
<div class="flex mb-2">
<h2 class="font-display font-bold text-2xl text-white flex-1">{name}</h2>
{#if manualInput}
<Button size="sm" on:click={toggleEdit}>
<Icon path={mdiPencil} color="white" />
</Button>
{/if}
<a href="/wish/{id}">
<Button className="ml-2" size="sm">
<Icon path={mdiTableOfContents} color="white" />
</Button>
</a>
</div>
<div class="flex flex-col w-full">
<div

View File

@ -0,0 +1,146 @@
<script>
import { t } from 'svelte-i18n';
import dayjs from 'dayjs';
import Icon from '../../components/Icon.svelte';
import { mdiStar } from '@mdi/js';
let numberFormat = Intl.NumberFormat('en', {
maximumFractionDigits: 1,
minimumFractionDigits: 0,
});
export let banner;
function calculateColor(percentage) {
const hue = percentage * 120;
return `color: hsl(${hue}, 100%, 60%);`;
}
const legendaryPity = banner.legendary.reduce((prev, next) => {
prev += next.pity;
return prev;
}, 0);
let rarePity = 0;
let rarePityCharacter = 0;
let rarePityWeapon = 0;
let rareTotal = banner.rare.character.length + banner.rare.weapon.length;
for (let item of banner.rare.character) {
rarePity += item.pity;
rarePityCharacter += item.pity;
}
for (let item of banner.rare.weapon) {
rarePity += item.pity;
rarePityWeapon += item.pity;
}
</script>
<div>
<img src={banner.image} class="w-full rounded-lg" alt={banner.name} />
<h1 class="mt-4 text-white font-display font-semibold text-xl">{banner.name}</h1>
<p class="text-gray-400 font-body flex flex-col md:flex-row">
<span class="flex">
<span>{dayjs.unix(banner.start).format('ddd, D MMM YYYY HH:mm')}</span>
<span class="mx-2">-</span>
</span>
<span>{dayjs.unix(banner.end).format('ddd, D MMM YYYY HH:mm')}</span>
</p>
<p class="text-gray-400 pr-2 mt-4">
{$t('wish.detail.totalThisBanner')}
<span class="text-gray-200 font-semibold">{banner.total}</span>
</p>
<p class="text-gray-400 pr-2">
{$t('wish.detail.worth')}
<img class="inline h-4" src="/images/primogem.png" alt="primogem" />
<span class="text-gray-200 font-semibold">{banner.total * 160}</span>
</p>
<table class="mt-4">
<tr>
<td class="text-gray-400 text-sm font-display pr-2 md:pr-4 text-left">Rarity</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(banner.legendary.length)}
</td>
<td class="text-legendary-from font-semibold pr-2 md:pr-4 text-right border-t border-gray-700">
{numberFormat.format((banner.legendary.length / banner.total) * 100)}%
</td>
<td class="text-legendary-from font-semibold text-right border-t border-gray-700">
{banner.legendary.length ? numberFormat.format(legendaryPity / banner.legendary.length) : 0}
</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(rareTotal)}
</td>
<td class="text-rare-from font-semibold pr-2 md:pr-4 text-right border-t border-gray-700">
{numberFormat.format((rareTotal / banner.total) * 100)}%
</td>
<td class="text-rare-from font-semibold text-right border-t border-gray-700">
{rareTotal > 0 ? numberFormat.format(rarePity / rareTotal) : 0}
</td>
</tr>
<tr>
<td class="text-rare-from font-semibold pl-4 md:pl-4 pr-2 md:pr-4 border-t border-gray-700 whitespace-no-wrap">
└ Character
</td>
<td class="text-rare-from font-semibold pr-2 md:pr-4 text-right border-t border-gray-700">
{numberFormat.format(banner.rare.character.length)}
</td>
<td class="text-rare-from font-semibold pr-2 md:pr-4 text-right border-t border-gray-700">
{numberFormat.format((banner.rare.character.length / banner.total) * 100)}%
</td>
<td class="text-rare-from font-semibold text-right border-t border-gray-700">
{rareTotal > 0 ? numberFormat.format(rarePityCharacter / rareTotal) : 0}
</td>
</tr>
<tr>
<td class="text-rare-from font-semibold pl-4 md:pl-4 pr-2 md:pr-4 border-t border-gray-700 whitespace-no-wrap">
└ Weapon
</td>
<td class="text-rare-from font-semibold pr-2 md:pr-4 text-right border-t border-gray-700">
{numberFormat.format(banner.rare.weapon.length)}
</td>
<td class="text-rare-from font-semibold pr-2 md:pr-4 text-right border-t border-gray-700">
{numberFormat.format((banner.rare.weapon.length / banner.total) * 100)}%
</td>
<td class="text-rare-from font-semibold text-right border-t border-gray-700">
{rareTotal > 0 ? numberFormat.format(rarePityWeapon / rareTotal) : 0}
</td>
</tr>
</table>
<div class="flex flex-wrap mt-4">
{#each banner.legendary as pull}
<span class="pity">{pull.name} <span style={calculateColor((90 - pull.pity) / 90)}>{pull.pity}</span></span>
{/each}
</div>
</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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

View File

@ -1163,6 +1163,29 @@ chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chart.js@^2.9.4:
version "2.9.4"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.4.tgz#0827f9563faffb2dc5c06562f8eb10337d5b9684"
integrity sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==
dependencies:
chartjs-color "^2.1.0"
moment "^2.10.2"
chartjs-color-string@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz#1df096621c0e70720a64f4135ea171d051402f71"
integrity sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==
dependencies:
color-name "^1.0.0"
chartjs-color@^2.1.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.4.1.tgz#6118bba202fe1ea79dd7f7c0f9da93467296c3b0"
integrity sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==
dependencies:
chartjs-color-string "^0.6.0"
color-convert "^1.9.3"
clean-css@^4.2.1:
version "4.2.3"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78"
@ -1170,7 +1193,7 @@ clean-css@^4.2.1:
dependencies:
source-map "~0.6.0"
color-convert@^1.9.0, color-convert@^1.9.1:
color-convert@^1.9.0, color-convert@^1.9.1, color-convert@^1.9.3:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
@ -1874,6 +1897,11 @@ minimist@^1.1.1, minimist@^1.2.5:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
moment@^2.10.2:
version "2.29.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
mri@^1.1.0:
version "1.1.6"
resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.6.tgz#49952e1044db21dbf90f6cd92bc9c9a777d415a6"