Add character detail
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -93,7 +93,7 @@ export const eventsData = [
|
|||
url: 'https://www.hoyolab.com/genshin/article/286280',
|
||||
showOnHome: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'Act I',
|
||||
|
|
|
@ -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));
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
After Width: | Height: | Size: 146 KiB |
After Width: | Height: | Size: 160 KiB |
After Width: | Height: | Size: 150 KiB |
After Width: | Height: | Size: 174 KiB |
After Width: | Height: | Size: 147 KiB |
After Width: | Height: | Size: 131 KiB |
After Width: | Height: | Size: 137 KiB |
After Width: | Height: | Size: 162 KiB |
After Width: | Height: | Size: 149 KiB |
After Width: | Height: | Size: 152 KiB |
After Width: | Height: | Size: 200 KiB |
After Width: | Height: | Size: 150 KiB |
After Width: | Height: | Size: 184 KiB |
After Width: | Height: | Size: 183 KiB |
After Width: | Height: | Size: 156 KiB |
After Width: | Height: | Size: 140 KiB |
After Width: | Height: | Size: 147 KiB |
After Width: | Height: | Size: 211 KiB |
After Width: | Height: | Size: 138 KiB |
After Width: | Height: | Size: 189 KiB |
After Width: | Height: | Size: 235 KiB |
After Width: | Height: | Size: 167 KiB |
After Width: | Height: | Size: 161 KiB |
After Width: | Height: | Size: 186 KiB |
After Width: | Height: | Size: 192 KiB |
After Width: | Height: | Size: 168 KiB |
After Width: | Height: | Size: 167 KiB |
After Width: | Height: | Size: 138 KiB |
After Width: | Height: | Size: 239 KiB |
After Width: | Height: | Size: 142 KiB |
After Width: | Height: | Size: 265 KiB |
After Width: | Height: | Size: 191 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 2.6 KiB |