Add character detail

pull/1/head
Made Baruna 2021-04-18 23:43:26 +08:00
parent 8b9d67d894
commit 086ed376eb
457 changed files with 46529 additions and 203 deletions

View File

@ -24,7 +24,7 @@
"@mdi/js": "^5.7.55",
"@rollup/plugin-babel": "^5.0.0",
"@rollup/plugin-commonjs": "^16.0.0",
"@rollup/plugin-dynamic-import-vars": "^1.1.0",
"@rollup/plugin-dynamic-import-vars": "^1.1.1",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^10.0.0",
"@rollup/plugin-replace": "^2.3.4",

View File

@ -7,6 +7,7 @@ import svelte from 'rollup-plugin-svelte';
import babel from '@rollup/plugin-babel';
import { terser } from 'rollup-plugin-terser';
import json from '@rollup/plugin-json';
import dynamicImportVars from '@rollup/plugin-dynamic-import-vars';
import config from 'sapper/config/rollup.js';
import { config as envConfig } from 'dotenv';
@ -59,6 +60,12 @@ export default {
}),
commonjs(),
json(),
dynamicImportVars({
include: [
'**/*.svelte',
'**/*.json',
],
}),
legacy &&
babel({
@ -121,6 +128,12 @@ export default {
}),
commonjs(),
json(),
dynamicImportVars({
include: [
'**/*.svelte',
'**/*.json',
],
}),
],
external: Object.keys(pkg.dependencies).concat(require('module').builtinModules),
@ -134,12 +147,7 @@ export default {
...config.serviceworker.output(),
file: config.serviceworker.output().file.replace('service-worker', 'firebase-messaging-sw'),
},
plugins: [
replace(envData),
resolve(),
commonjs(),
!dev && terser(),
],
plugins: [replace(envData), resolve(), commonjs(), !dev && terser()],
preserveEntrySignatures: false,
onwarn,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -71,29 +71,29 @@ export const eventsData = [
},
],
[
{
name: 'Invitation of Windblume - 1.4 Event',
pos: '0% 20%',
image: 'update14.png',
start: '2021-03-19 10:00:00',
end: '2021-04-05 04:00:00',
color: '#79D2EB',
zoom: '120%',
url: 'https://genshin.mihoyo.com/en/news/detail/9407',
showOnHome: true,
},
{
name: 'Wishful Drops - Oceanid Event',
pos: '0% 20%',
image: 'wishful_drops.jpg',
start: '2021-04-09 10:00:00',
end: '2021-04-16 04:00:00',
color: '#579DE5',
zoom: '170%',
url: 'https://www.hoyolab.com/genshin/article/286280',
showOnHome: true,
},
],
{
name: 'Invitation of Windblume - 1.4 Event',
pos: '0% 20%',
image: 'update14.png',
start: '2021-03-19 10:00:00',
end: '2021-04-05 04:00:00',
color: '#79D2EB',
zoom: '120%',
url: 'https://genshin.mihoyo.com/en/news/detail/9407',
showOnHome: true,
},
{
name: 'Wishful Drops - Oceanid Event',
pos: '0% 20%',
image: 'wishful_drops.jpg',
start: '2021-04-09 10:00:00',
end: '2021-04-16 04:00:00',
color: '#579DE5',
zoom: '170%',
url: 'https://www.hoyolab.com/genshin/article/286280',
showOnHome: true,
},
],
[
{
name: 'Act I',

View File

@ -0,0 +1,38 @@
import { getAccountPrefix } from "../stores/account";
import { readSave, updateSave } from "../stores/saveManager";
const bannerCategories = ['beginners', 'standard', 'character-event', 'weapon-event'];
function readLocalData(path) {
const prefix = getAccountPrefix();
const data = readSave(`${prefix}${path}`);
if (data !== null) {
const counterData = JSON.parse(data);
const pullData = counterData.pulls || [];
return pullData;
}
return null;
}
export function processCharacters() {
const characters = {};
for (const id of bannerCategories) {
const data = readLocalData(`wish-counter-${id}`);
if (data === null) continue;
for (const item of data) {
if (item.type === 'character') {
if (characters[item.id] === undefined) {
characters[item.id] = 0;
}
characters[item.id]++;
}
}
}
const prefix = getAccountPrefix();
updateSave(`${prefix}characters`, JSON.stringify(characters));
}

View File

@ -68,9 +68,30 @@
"element": "Element",
"rarity": "Rarity",
"weapon": "Weapon",
"talents": "Talents",
"passiveTalents": "Passive Talents",
"constellations": "Constellations",
"asc": "ASC",
"lvl": "LVL",
"hp": "HP",
"atk": "ATK",
"def": "DEF"
"def": "DEF",
"hpPercent": "HP%",
"atkPercent": "ATK%",
"defPercent": "DEF%",
"critRate": "CRIT Rate",
"critDamage": "CRIT DMG",
"em": "Elemental Mastery",
"er": "Energy Recharge",
"healingBonus": "Healing Bonus",
"pyroDamageBonus": "Pyro DMG Bonus",
"hydroDamageBonus": "Hydro DMG Bonus",
"dendroDamageBonus": "Dendro DMG Bonus",
"electroDamageBonus": "Electro DMG Bonus",
"cryoDamageBonus": "Cryo DMG Bonus",
"anemoDamageBonus": "Anemo DMG Bonus",
"physicalDamageBonus": "Physical DMG Bonus",
"geoDamageBonus": "Geo DMG Bonus"
},
"wish": {
"title": "Wish Counter",

View File

@ -1,165 +0,0 @@
<script>
import { t } from 'svelte-i18n'
import { mdiStar } from '@mdi/js';
import Icon from '../components/Icon.svelte';
import TableHeader from '../components/Table/TableHeader.svelte';
import { characters } from '../data/characters';
let sortBy = '';
let sortOrder = false;
$: chars = Object.entries(characters).sort((a, b) => {
switch (sortBy) {
case 'name':
if (sortOrder) {
return a[1].name.localeCompare(b[1].name);
} else {
return b[1].name.localeCompare(a[1].name);
}
case 'element':
if (sortOrder) {
return a[1].element.name.localeCompare(b[1].element.name);
} else {
return b[1].element.name.localeCompare(a[1].element.name);
}
case 'rarity':
if (sortOrder) {
return a[1].rarity - b[1].rarity;
} else {
return b[1].rarity - a[1].rarity;
}
case 'weapon':
if (sortOrder) {
return a[1].weapon.name.localeCompare(b[1].weapon.name);
} else {
return b[1].weapon.name.localeCompare(a[1].weapon.name);
}
case 'hp':
if (sortOrder) {
return a[1].stats.hp - b[1].stats.hp;
} else {
return b[1].stats.hp - a[1].stats.hp;
}
case 'atk':
if (sortOrder) {
return a[1].stats.atk - b[1].stats.atk;
} else {
return b[1].stats.atk - a[1].stats.atk;
}
case 'def':
if (sortOrder) {
return a[1].stats.def - b[1].stats.def;
} else {
return b[1].stats.def - a[1].stats.def;
}
}
});
function sort(by) {
if (sortBy === by) {
sortOrder = !sortOrder;
} else {
sortBy = by;
}
}
</script>
<svelte:head>
<title>Paimon.moe</title>
<meta
name="description"
content="Your best Genshin Impact companion! Help you plan what to farm with ascension calculator and database. Also track your progress with todo and wish counter."
/>
<meta
property="og:description"
content="Your best Genshin Impact companion! Help you plan what to farm with ascension calculator and database. Also track your progress with todo and wish counter."
/>
</svelte:head>
<div class="lg:ml-64 pt-20 lg:pt-8">
<h1 class="font-display px-4 md:px-8 font-black text-5xl text-white">{$t('characters.title')}</h1>
<p class="text-gray-400 px-4 md:px-8 font-medium pb-4" style="margin-top: -1rem;">
{$t('characters.subtitle')}
</p>
<div class="block overflow-x-auto whitespace-no-wrap pb-8">
<div class="px-4 md:px-8 table">
<table class="w-full block p-4 bg-item rounded-xl">
<thead>
<th style="min-width: 4rem;" />
<TableHeader on:click={() => sort('name')} sort={sortBy === 'name'} order={sortOrder}>{$t('characters.name')}</TableHeader>
<TableHeader on:click={() => sort('element')} sort={sortBy === 'element'} order={sortOrder} align="center">
{$t('characters.element')}
</TableHeader>
<TableHeader on:click={() => sort('rarity')} sort={sortBy === 'rarity'} order={sortOrder} align="center">
{$t('characters.rarity')}
</TableHeader>
<TableHeader on:click={() => sort('weapon')} sort={sortBy === 'weapon'} order={sortOrder} align="center">
{$t('characters.weapon')}
</TableHeader>
<TableHeader on:click={() => sort('hp')} sort={sortBy === 'hp'} order={sortOrder} align="center">
{$t('characters.hp')}
</TableHeader>
<TableHeader on:click={() => sort('atk')} sort={sortBy === 'atk'} order={sortOrder} align="center">
{$t('characters.atk')}
</TableHeader>
<TableHeader on:click={() => sort('def')} sort={sortBy === 'def'} order={sortOrder} align="center">
{$t('characters.def')}
</TableHeader>
</thead>
<tbody>
{#each chars as [id, char] (id)}
<tr class={`rounded cursor-pointer ${char.rarity === 4 ? 'rare' : 'legendary'}`}>
<td class="rarity w-16 sticky" style="padding: 0; left: 0px;">
<img class="w-12 h-12 rounded-full" src={`/images/characters/${id}.png`} alt={char.name} />
</td>
<td>{char.name}</td>
<td class="text-center">
<img class="w-8 h-8 inline" src={`/images/elements/${char.element.id}.png`} alt={char.element.name} />
</td>
<td class="text-center">
<Icon color={char.rarity === 5 ? '#B9812E' : '#AD76B0'} path={mdiStar} />
</td>
<td class="text-center">
<img class="w-8 h-8 inline" src={`/images/weapons/${char.weapon.id}.png`} alt={char.weapon.name} />
</td>
<td class="text-center">{char.stats.hp}</td>
<td class="text-center">{char.stats.atk}</td>
<td class="text-center">{char.stats.def}</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</div>
<style>
tr.rare:hover {
background: linear-gradient(
90deg,
rgba(0, 0, 0, 0) 0%,
rgba(173, 118, 176, 0.85) 10%,
rgba(102, 86, 128, 0.85) 80%,
rgba(0, 0, 0, 0) 100%
);
}
tr.legendary:hover {
background: linear-gradient(
90deg,
rgba(0, 0, 0, 0) 0%,
rgba(185, 129, 46, 0.85) 10%,
rgba(132, 99, 50, 0.85) 80%,
rgba(0, 0, 0, 0) 100%
);
}
td {
@apply text-white;
@apply px-2;
padding-top: 0.85rem;
padding-bottom: 0.85rem;
}
</style>

View File

@ -0,0 +1,303 @@
<script context="module">
export async function preload(page) {
const { id } = page.params;
const data = await import(`../../data/characterData/${id}.json`);
return { id, data };
}
</script>
<script>
export let id;
export let data;
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import { mdiCircle, mdiContentSave, mdiMinus, mdiPencil, mdiPlus, mdiStar } from '@mdi/js';
import Icon from '../../components/Icon.svelte';
import Button from '../../components/Button.svelte';
import { getAccountPrefix } from '../../stores/account';
import { readSave } from '../../stores/saveManager';
import { characters } from '../../data/characters';
import { itemGroup } from '../../data/itemGroup';
import SkillCard from './_skillCard.svelte';
import PassiveSkillCard from './_passiveSkillCard.svelte';
let constellationDiv;
const numberFormat = Intl.NumberFormat('en', {
maximumFractionDigits: 2,
minimumFractionDigits: 0,
});
const character = characters[id];
const bookId = character.material.book[0].id;
const book = itemGroup[bookId];
const materials = character.ascension[1].items;
let constellationCount = -1;
let manualCount = 0;
let editConstallation = false;
const showedIndex = [1, 20, 21, 41, 42, 52, 53, 63, 64, 74, 75, 85, 86, 96];
const level = [1, 20, 20, 40, 40, 50, 50, 60, 60, 70, 70, 80, 80, 90];
const ascen = [0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6];
function getConstellationCount() {
const prefix = getAccountPrefix();
const data = readSave(`${prefix}characters`);
if (data !== null) {
const constellation = JSON.parse(data);
if (constellation[id]) {
constellationCount = constellationCount[id].default + constellationCount[id].wish - 1;
manualCount = constellationCount[id].manual;
} else {
constellationCount = 0;
}
}
}
function editConstellationCount(val) {
manualCount = Math.max(0, manualCount + val);
}
function saveConstellationCount() {
editConstallation = false;
}
function scrollToView(view) {
view.scrollIntoView({ behavior: 'smooth' });
}
onMount(() => {
getConstellationCount();
});
$: constellationCountTotal = constellationCount + manualCount;
</script>
<svelte:head>
<title>Paimon.moe</title>
<meta
name="description"
content="Genshin Impact {character.name} build, guide, constellation, and skill information"
/>
<meta
property="og:description"
content="Genshin Impact {character.name} build, guide, constellation, and skill information"
/>
</svelte:head>
<div class="lg:ml-64 pt-20 lg:pt-8">
<div class="flex flex-col xl:flex-row items-start">
<img
class="character-image object-cover md:pl-8 self-center xl:self-auto"
src="/images/characters/full/{id}.png"
alt={character.name}
/>
<div class="flex flex-col items-start mt-4 xl:mt-0 side-detail pt-4 xl:pt-0">
<div class="flex items-center px-4 md:px-8">
<h1 class="font-display font-black text-4xl md:text-5xl text-white mr-4 z-0">{character.name}</h1>
<img
class="h-10 mr-4 z-10 object-contain"
src="/images/elements/{character.element.id}.png"
alt={character.element.name}
/>
<div class="flex gap-1 {editConstallation ? 'flex-col' : ''} md:flex-row items-center">
{#if constellationCountTotal > -1}
<p class="text-3xl text-gray-200 bg-black bg-opacity-50 rounded-xl px-2 font-semibold">
C{constellationCountTotal}
</p>
{/if}
{#if editConstallation}
<div class="flex flex-wrap gap-1">
<Button size="sm" on:click={() => editConstellationCount(1)}>
<Icon path={mdiPlus} />
</Button>
<Button size="sm" on:click={() => editConstellationCount(-1)}>
<Icon path={mdiMinus} />
</Button>
<Button size="sm" on:click={saveConstellationCount}>
<Icon path={mdiContentSave} />
</Button>
</div>
{:else}
<div
class="ml-2 rounded-xl hover:bg-black hover:bg-opacity-25 cursor-pointer p-2"
on:click={() => {
editConstallation = true;
}}
>
<Icon path={mdiPencil} className="text-gray-400" />
</div>
{/if}
</div>
</div>
<div
class="{character.rarity === 5
? 'text-legendary-from'
: 'text-rare-from'} px-4 md:px-8 text-2xl flex items-center z-0 -mt-2 md:-mt-4"
>
<Icon path={mdiStar} />
<Icon path={mdiStar} />
<Icon path={mdiStar} />
<Icon path={mdiStar} />
{#if character.rarity === 5}
<Icon path={mdiStar} />
{/if}
<Icon path={mdiCircle} size={0.4} className="mx-2 mt-1" color="white" />
<p class="text-base text-white font-semibold mt-1">{character.weapon.name}</p>
</div>
<p class="text-gray-200 px-4 md:px-8">{data.description}</p>
<div class="flex flex-col md:flex-row mt-4 gap-4 px-4 md:px-8">
<div class="text-gray-200 rounded-xl border border-gray-200 border-opacity-25 p-4">
<p>Talent Book</p>
<div class="flex items-center mt-2">
<div class="mr-2 h-12 w-12 bg-background rounded-xl p-1">
<img src="/images/items/{bookId}.png" alt={book.name} class="h-full max-w-full object-contain" />
</div>
<p class="mb-1 font-semibold">{book.name}</p>
</div>
</div>
<div class="text-gray-200 rounded-xl border border-gray-200 border-opacity-25 p-4">
<p>Ascension Materials</p>
<div class="flex items-center mt-2">
{#each materials as material}
{#if material.item.id !== 'none'}
<div class="mr-2 h-12 w-12 bg-background rounded-xl p-1">
<img
src="/images/items/{material.item.id}.png"
alt={material.item.name}
title={material.item.name}
class="h-full max-w-full object-contain mx-auto"
/>
</div>
{/if}
{/each}
</div>
</div>
</div>
<div class="md:px-4 mt-4 block overflow-x-auto whitespace-no-wrap w-screen md:w-auto">
<div class="px-4" style="width: min-content;">
<div class="table max-w-full rounded-xl border border-gray-200 border-opacity-25">
<table class="text-gray-200 w-full">
<tr>
<td class="text-center whitespace-no-wrap border-gray-700 font-semibold px-2">
{$t('characters.asc')}
</td>
<td class="text-center whitespace-no-wrap border-gray-700 font-semibold px-2">
{$t('characters.lvl')}
</td>
<td class="text-center whitespace-no-wrap border-gray-700 font-semibold px-2">
{$t('characters.hp')}
</td>
<td class="text-center whitespace-no-wrap border-gray-700 font-semibold px-2">
{$t('characters.atk')}
</td>
<td class="text-center whitespace-no-wrap border-gray-700 font-semibold px-2">
{$t('characters.def')}
</td>
<td class="text-center whitespace-no-wrap border-gray-700 font-semibold px-2"
>{$t('characters.critRate')}
</td>
<td class="text-center whitespace-no-wrap border-gray-700 font-semibold px-2"
>{$t('characters.critDamage')}
</td>
{#if data.statGrow !== 'critRate' && data.statGrow !== 'critDamage'}
<td class="text-center whitespace-no-wrap border-gray-700 font-semibold px-2">
{$t(`characters.${data.statGrow}`)}
</td>
{/if}
</tr>
{#each showedIndex as index, i}
<tr>
{#if i % 2 === 0}
<td rowspan={2} class="text-center border-t border-gray-700 px-2">{ascen[i]}</td>
{/if}
<td class="text-center border-t border-gray-700 px-2">{level[i]}</td>
<td class="text-center border-t border-gray-700 px-2">{Math.round(data.hp[index])}</td>
<td class="text-center border-t border-gray-700 px-2">{Math.round(data.atk[index])}</td>
<td class="text-center border-t border-gray-700 px-2">{Math.round(data.def[index])}</td>
{#if data.statGrow === 'critRate'}
<td class="text-center border-t border-gray-700 px-2">
{numberFormat.format(data.critRate[index] * 100)}%
</td>
{:else}
<td class="text-center border-t border-gray-700 px-2">5%</td>
{/if}
{#if data.statGrow === 'critDamage'}
<td class="text-center border-t border-gray-700 px-2">
{numberFormat.format(data.critDamage[index] * 100)}%
</td>
{:else}
<td class="text-center border-t border-gray-700 px-2">50%</td>
{/if}
{#if data.statGrow !== 'critRate' && data.statGrow !== 'critDamage'}
<td class="text-center border-t border-gray-700 px-2">
{numberFormat.format(data[data.statGrow][index] * 100)}%
</td>
{/if}
</tr>
{/each}
</table>
</div>
</div>
</div>
<Button className="mt-4 mx-4 md:mx-8" on:click={() => scrollToView(constellationDiv)}>
{$t('characters.constellations')}
</Button>
</div>
</div>
<div class="flex flex-col mt-4 text-white px-4 md:px-8">
<p class="font-black font-display text-2xl mt-4">{$t('characters.talents')}</p>
<SkillCard {id} image="talent_1" data={data.attack} withQuote={false} />
<SkillCard {id} image="talent_2" data={data.elementalSkill} withQuote={true} />
<SkillCard {id} image="talent_3" data={data.burst} withQuote={true} />
</div>
<div class="flex flex-col text-white px-4 md:px-8">
<p class="font-black font-display text-2xl mt-4">{$t('characters.passiveTalents')}</p>
{#each data.passives as passive, i}
<PassiveSkillCard {id} image="talent_{i + 4}" data={passive} />
{/each}
</div>
<div class="flex flex-col text-white px-4 md:px-8" id="constellations" bind:this={constellationDiv}>
<a href="/characters/{id}/#constellations" class="font-black font-display text-2xl mt-4"
>{$t('characters.constellations')}</a
>
{#each data.constellations as constellation, i}
<PassiveSkillCard
{id}
fade={constellationCountTotal > -1 && constellationCountTotal < i + 1}
image={`constellation_${i + 1}`}
data={constellation}
/>
{/each}
</div>
</div>
<style>
.character-image {
height: calc(100vh - 4rem);
max-height: 700px;
max-width: 100%;
}
.side-detail {
margin-top: -40vh;
background: linear-gradient(180deg, rgba(37, 41, 74, 0) 0%, rgba(37, 41, 74, 0.75) 10%);
}
@screen xl {
.character-image {
max-width: 550px;
}
.side-detail {
margin-top: 0;
}
}
td:not(:last-child) {
@apply border-r;
}
</style>

View File

@ -0,0 +1,19 @@
<script>
export let id;
export let image;
export let data;
export let fade = false;
const description = data.description.replace(/\\n/g, '<br/>').replace(/·/g, '- ');
const name = data.name;
</script>
<div class="py-4 rounded-xl bg-item flex flex-col mb-4" style="{fade ? 'filter: grayscale(30%);' : ''}">
<div class="flex mb-2 items-start px-4">
<img src="/images/skills/{id}/{image}.png" alt={name} class="w-16 h-16 mr-4" />
<div>
<p class="font-black font-display text-xl">{name}</p>
<p class="skill-description">{@html description}</p>
</div>
</div>
</div>

View File

@ -0,0 +1,122 @@
<script>
import { t } from 'svelte-i18n';
export let id;
export let image;
export let data;
export let withQuote;
let iter = [...new Array(13)];
const lastIndex = withQuote ? data.description.indexOf('<i>') : data.description.length;
const description = data.description.substring(0, lastIndex).replace(/\\n/g, '<br/>').replace(/·/g, '- ');
const quote = data.description
.substring(lastIndex, data.description.length)
.replace('<i>', '')
.replace('</i>', '')
.replace(/\\n/g, '<br/>')
.replace(/·/g, '- ');
const name = data.name;
const numberFormat1Digit = Intl.NumberFormat('en', {
maximumFractionDigits: 1,
minimumFractionDigits: 0,
});
const numberFormat2Digit = Intl.NumberFormat('en', {
maximumFractionDigits: 2,
minimumFractionDigits: 0,
});
function formatter(type, number) {
switch (type) {
case 'i':
return Math.round(number);
case 'p':
return `${Math.round(number * 100)}%`;
case '1p':
return `${numberFormat1Digit.format(number * 100)}%`;
case '2p':
return `${numberFormat2Digit.format(number * 100)}%`;
case '1f':
return `${numberFormat1Digit.format(number)}`;
case '2f':
return `${numberFormat2Digit.format(number)}`;
}
}
function format(str, args) {
// let formatted = str.replace(/(\/{)|(\+{)|\s[^+]+/g, (token) => ` ${token} `);
let formatted = str.replace(/[\+\/]{/g, (token) => ` ${token}`);
formatted = formatted.replace(/{[0-9]:\w+}/g, (text) => {
const splitted = text.substring(1, text.length - 1).split(':');
return formatter(splitted[1], args[splitted[0]]);
});
return formatted;
}
</script>
<div class="py-4 rounded-xl bg-item flex flex-col mb-4">
<div class="flex mb-2 items-center px-4">
<img src="/images/skills/{id}/{image}.png" alt={name} class="w-16 h-16 mr-4" />
<div>
<p class="font-black font-display text-xl">{name}</p>
</div>
</div>
<p class="skill-description px-4">{@html description}</p>
{#if withQuote}
<p class="text-sm text-gray-400 italic mt-2 px-4">{@html quote}</p>
{/if}
<div class="mt-4 block overflow-x-auto">
<div class="px-4" style="width: fit-content;">
<div class="table max-w-full rounded-xl border border-gray-200 border-opacity-25">
<table class="text-gray-200 text-sm">
<tr>
<td class="border-gray-700 px-2">{$t('characters.lvl')}</td>
{#each iter as _, i}
<td class="text-center border-gray-700 px-2">{i + 1}</td>
{/each}
</tr>
{#each data.skillLabels as label, i}
<tr>
<td class="border-t border-gray-700 px-2" style="min-width: 150px;">{label}</td>
{#each data.skillStats[i].slice(0, 13) as stat}
<td class="text-center border-t border-gray-700 px-2">
{@html format(data.skillStatsLabels[i], stat)}
</td>
{/each}
</tr>
{/each}
</table>
</div>
</div>
</div>
</div>
<style>
td:not(:last-child) {
@apply border-r;
}
:global(span.color) {
@apply text-primary font-semibold;
}
:global(p.skill-description > br) {
line-height: 1px;
}
@screen lg {
::-webkit-scrollbar {
height: 8px;
}
::-webkit-scrollbar-track {
@apply bg-transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.35);
@apply rounded-xl;
}
}
</style>

View File

@ -0,0 +1,252 @@
<script>
import { t } from 'svelte-i18n';
import { onMount } from 'svelte';
import { mdiStar, mdiViewGrid, mdiViewList } from '@mdi/js';
import Icon from '../../components/Icon.svelte';
import TableHeader from '../../components/Table/TableHeader.svelte';
import { characters } from '../../data/characters';
import { getAccountPrefix } from '../../stores/account';
import { readSave } from '../../stores/saveManager';
let sortBy = '';
let sortOrder = false;
let type = 'grid';
let showConstellation = false;
let constellation = {};
$: chars = Object.entries(characters).sort((a, b) => {
switch (sortBy) {
case 'name':
if (sortOrder) {
return a[1].name.localeCompare(b[1].name);
} else {
return b[1].name.localeCompare(a[1].name);
}
case 'element':
if (sortOrder) {
return a[1].element.name.localeCompare(b[1].element.name);
} else {
return b[1].element.name.localeCompare(a[1].element.name);
}
case 'rarity':
if (sortOrder) {
return a[1].rarity - b[1].rarity;
} else {
return b[1].rarity - a[1].rarity;
}
case 'weapon':
if (sortOrder) {
return a[1].weapon.name.localeCompare(b[1].weapon.name);
} else {
return b[1].weapon.name.localeCompare(a[1].weapon.name);
}
case 'hp':
if (sortOrder) {
return a[1].stats.hp - b[1].stats.hp;
} else {
return b[1].stats.hp - a[1].stats.hp;
}
case 'atk':
if (sortOrder) {
return a[1].stats.atk - b[1].stats.atk;
} else {
return b[1].stats.atk - a[1].stats.atk;
}
case 'def':
if (sortOrder) {
return a[1].stats.def - b[1].stats.def;
} else {
return b[1].stats.def - a[1].stats.def;
}
}
});
function sort(by) {
if (sortBy === by) {
sortOrder = !sortOrder;
} else {
sortBy = by;
}
}
function getConstellation() {
const prefix = getAccountPrefix();
const data = readSave(`${prefix}characters`);
if (data !== null) {
constellation = JSON.parse(data);
showConstellation = true;
}
}
onMount(() => {
getConstellation();
});
</script>
<svelte:head>
<title>Paimon.moe</title>
<meta
name="description"
content="Your best Genshin Impact companion! Help you plan what to farm with ascension calculator and database. Also track your progress with todo and wish counter."
/>
<meta
property="og:description"
content="Your best Genshin Impact companion! Help you plan what to farm with ascension calculator and database. Also track your progress with todo and wish counter."
/>
</svelte:head>
<div class="lg:ml-64 pt-20 lg:pt-8">
<div class="flex items-center px-4 md:px-8">
<h1 class="font-display font-black text-4xl md:text-5xl text-white mr-4">{$t('characters.title')}</h1>
<div class="flex text-white" style="height: fit-content;">
<button
class="{type === 'grid' ? 'bg-background' : 'bg-item'} p-2 rounded-l-xl cursor-pointer hover:bg-opacity-75"
on:click={() => {
type = 'grid';
}}
>
<Icon path={mdiViewGrid} />
</button>
<button
class="{type === 'table'
? 'bg-background'
: 'bg-item'} bg-background p-2 rounded-r-xl cursor-pointer hover:bg-opacity-75"
on:click={() => {
type = 'table';
}}
>
<Icon path={mdiViewList} />
</button>
</div>
</div>
{#if type === 'grid'}
<div class="px-4 md:pl-6 md:pr-4 flex flex-wrap max-w-screen-xl mt-2">
{#each chars as [id, char] (id)}
<a
href="/characters/{id}"
class="m-2 cell relative cursor-pointer transition duration-100 hover:opacity-100 hover:shadow-xl rounded-xl {!showConstellation ||
constellation[id]
? ''
: 'opacity-50'}"
>
<div
class="w-full rounded-t-xl bg-opacity-50 overflow-hidden {char.rarity === 5
? 'bg-legendary-from'
: 'bg-rare-from'}"
>
<img class="w-full h-full" src={`/images/characters/${id}.png`} alt={char.name} />
</div>
<div
class="absolute top-0 right-0 bg-black bg-opacity-75 rounded-full flex items-center shadow-md"
style="padding: 4px; margin: -10px;"
>
{#if constellation[id]}
<span class="mx-1 text-white text-xs font-semibold">
C{Math.min(constellation[id] - 1, 6)}
</span>
{/if}
<img class="w-4 h-4" src={`/images/elements/${char.element.id}.png`} alt={char.element.name} />
</div>
<div class="w-full bg-item rounded-b-xl overflow-hidden">
<p class="text-white text-sm p-1 text-center whitespace-no-wrap">{char.name}</p>
</div>
</a>
{/each}
</div>
{:else}
<p class="text-gray-400 px-4 md:px-8 font-medium pb-2 mt-4">
{$t('characters.subtitle')}
</p>
<div class="block overflow-x-auto whitespace-no-wrap pb-8">
<div class="px-4 md:px-8 table">
<table class="w-full block p-4 bg-item rounded-xl">
<thead>
<th style="min-width: 4rem;" />
<TableHeader on:click={() => sort('name')} sort={sortBy === 'name'} order={sortOrder}
>{$t('characters.name')}</TableHeader
>
<TableHeader on:click={() => sort('element')} sort={sortBy === 'element'} order={sortOrder} align="center">
{$t('characters.element')}
</TableHeader>
<TableHeader on:click={() => sort('rarity')} sort={sortBy === 'rarity'} order={sortOrder} align="center">
{$t('characters.rarity')}
</TableHeader>
<TableHeader on:click={() => sort('weapon')} sort={sortBy === 'weapon'} order={sortOrder} align="center">
{$t('characters.weapon')}
</TableHeader>
<TableHeader on:click={() => sort('hp')} sort={sortBy === 'hp'} order={sortOrder} align="center">
{$t('characters.hp')}
</TableHeader>
<TableHeader on:click={() => sort('atk')} sort={sortBy === 'atk'} order={sortOrder} align="center">
{$t('characters.atk')}
</TableHeader>
<TableHeader on:click={() => sort('def')} sort={sortBy === 'def'} order={sortOrder} align="center">
{$t('characters.def')}
</TableHeader>
</thead>
<tbody>
{#each chars as [id, char] (id)}
<tr class={`rounded cursor-pointer ${char.rarity === 4 ? 'rare' : 'legendary'}`}>
<td class="rarity w-16 sticky" style="padding: 0; left: 0px;">
<img class="w-12 h-12 rounded-full" src={`/images/characters/${id}.png`} alt={char.name} />
</td>
<td>{char.name}</td>
<td class="text-center">
<img class="w-8 h-8 inline" src={`/images/elements/${char.element.id}.png`} alt={char.element.name} />
</td>
<td class="text-center">
<Icon color={char.rarity === 5 ? '#B9812E' : '#AD76B0'} path={mdiStar} />
</td>
<td class="text-center">
<img class="w-8 h-8 inline" src={`/images/weapons/${char.weapon.id}.png`} alt={char.weapon.name} />
</td>
<td class="text-center">{char.stats.hp}</td>
<td class="text-center">{char.stats.atk}</td>
<td class="text-center">{char.stats.def}</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{/if}
</div>
<style>
.cell {
width: calc(33.33333% - 1rem);
@screen md {
@apply w-24;
}
}
tr.rare:hover {
background: linear-gradient(
90deg,
rgba(0, 0, 0, 0) 0%,
rgba(173, 118, 176, 0.85) 10%,
rgba(102, 86, 128, 0.85) 80%,
rgba(0, 0, 0, 0) 100%
);
}
tr.legendary:hover {
background: linear-gradient(
90deg,
rgba(0, 0, 0, 0) 0%,
rgba(185, 129, 46, 0.85) 10%,
rgba(132, 99, 50, 0.85) 80%,
rgba(0, 0, 0, 0) 100%
);
}
td {
@apply text-white;
@apply px-2;
padding-top: 0.85rem;
padding-bottom: 0.85rem;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Some files were not shown because too many files have changed in this diff Show More