Add TCG deck builder

pull/1/head
Made Baruna 2022-12-17 00:57:47 +08:00
parent de5d432871
commit ab8b58be65
364 changed files with 56091 additions and 27 deletions

View File

@ -27,8 +27,8 @@
"svelte": "^3.17.3",
"svelte-i18n": "^3.4.0",
"svelte-preprocess": "^4.10.7",
"svelte-simple-modal": "^0.6.1",
"tailwindcss": "^3.1.6",
"svelte-simple-modal": "^1.4.5",
"tailwindcss": "^3.2.4",
"vite": "^3.0.2"
}
}

View File

@ -18,8 +18,8 @@ specifiers:
svelte: ^3.17.3
svelte-i18n: ^3.4.0
svelte-preprocess: ^4.10.7
svelte-simple-modal: ^0.6.1
tailwindcss: ^3.1.6
svelte-simple-modal: ^1.4.5
tailwindcss: ^3.2.4
vite: ^3.0.2
devDependencies:
@ -40,8 +40,8 @@ devDependencies:
svelte: 3.49.0
svelte-i18n: 3.4.0_svelte@3.49.0
svelte-preprocess: 4.10.7_nxvsp6sjiltnatqa6jdm4mr6zu
svelte-simple-modal: 0.6.1_svelte@3.49.0
tailwindcss: 3.1.6_postcss@8.4.14
svelte-simple-modal: 1.4.5_svelte@3.49.0
tailwindcss: 3.2.4_postcss@8.4.14
vite: 3.0.2
packages:
@ -785,8 +785,8 @@ packages:
'@fast-csv/parse': 4.3.6
dev: true
/fast-glob/3.2.11:
resolution: {integrity: sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==}
/fast-glob/3.2.12:
resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==}
engines: {node: '>=8.6.0'}
dependencies:
'@nodelib/fs.stat': 2.0.5
@ -1228,6 +1228,16 @@ packages:
postcss-selector-parser: 6.0.10
dev: true
/postcss-nested/6.0.0_postcss@8.4.14:
resolution: {integrity: sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==}
engines: {node: '>=12.0'}
peerDependencies:
postcss: ^8.2.14
dependencies:
postcss: 8.4.14
postcss-selector-parser: 6.0.10
dev: true
/postcss-selector-parser/6.0.10:
resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==}
engines: {node: '>=4'}
@ -1513,10 +1523,10 @@ packages:
svelte: 3.49.0
dev: true
/svelte-simple-modal/0.6.1_svelte@3.49.0:
resolution: {integrity: sha512-GJGYj+jymzuar105fwkZ73dtcSFCordpbHqt53iE1N1GdqhvEmSs24idRzyIcO7TrTD/V/287X1icFXp88RQHQ==}
/svelte-simple-modal/1.4.5_svelte@3.49.0:
resolution: {integrity: sha512-KJMQaU6GD/WnIfURIKwS4R4aB6s2DDG6NiYRJkUFRVbKPRvqcohb7Z5KFLyA5UsqdPvkEdFZ0UxqjNw7Lfqgtg==}
peerDependencies:
svelte: ^3.18.2
svelte: ^3.31.2
dependencies:
svelte: 3.49.0
dev: true
@ -1526,8 +1536,8 @@ packages:
engines: {node: '>= 8'}
dev: true
/tailwindcss/3.1.6_postcss@8.4.14:
resolution: {integrity: sha512-7skAOY56erZAFQssT1xkpk+kWt2NrO45kORlxFPXUt3CiGsVPhH1smuH5XoDH6sGPXLyBv+zgCKA2HWBsgCytg==}
/tailwindcss/3.2.4_postcss@8.4.14:
resolution: {integrity: sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==}
engines: {node: '>=12.13.0'}
hasBin: true
peerDependencies:
@ -1539,10 +1549,11 @@ packages:
detective: 5.2.1
didyoumean: 1.2.2
dlv: 1.1.3
fast-glob: 3.2.11
fast-glob: 3.2.12
glob-parent: 6.0.2
is-glob: 4.0.3
lilconfig: 2.0.6
micromatch: 4.0.5
normalize-path: 3.0.0
object-hash: 3.0.0
picocolors: 1.0.0
@ -1550,7 +1561,7 @@ packages:
postcss-import: 14.1.0_postcss@8.4.14
postcss-js: 4.0.0_postcss@8.4.14
postcss-load-config: 3.1.4_postcss@8.4.14
postcss-nested: 5.0.6_postcss@8.4.14
postcss-nested: 6.0.0_postcss@8.4.14
postcss-selector-parser: 6.0.10
postcss-value-parser: 4.2.0
quick-lru: 5.1.1

View File

@ -132,6 +132,13 @@
label={$t('sidebar.timeline')}
href="/timeline"
/>
<SidebarItem
on:clicked={close}
active={segment === 'tcg'}
image="/images/tcg.png"
label={$t('sidebar.tcg')}
href="/tcg"
/>
<SidebarItem
on:clicked={close}
active={segment === 'settings'}

3593
src/data/tcg/de.json Normal file

File diff suppressed because it is too large Load Diff

3593
src/data/tcg/en.json Normal file

File diff suppressed because it is too large Load Diff

3587
src/data/tcg/es.json Normal file

File diff suppressed because it is too large Load Diff

3587
src/data/tcg/fr.json Normal file

File diff suppressed because it is too large Load Diff

3593
src/data/tcg/id.json Normal file

File diff suppressed because it is too large Load Diff

3593
src/data/tcg/it.json Normal file

File diff suppressed because it is too large Load Diff

3593
src/data/tcg/ja.json Normal file

File diff suppressed because it is too large Load Diff

3593
src/data/tcg/ko.json Normal file

File diff suppressed because it is too large Load Diff

3593
src/data/tcg/pt.json Normal file

File diff suppressed because it is too large Load Diff

3593
src/data/tcg/ru.json Normal file

File diff suppressed because it is too large Load Diff

42
src/data/tcg/tags/de.json Normal file
View File

@ -0,0 +1,42 @@
{
"unique": "Einzigartig",
"slowly": "Kampfaktion",
"attack": "Aktion unmöglich",
"freezing": "Immunität gegen Gefroren",
"control": "Immunität gegen Kontrolle",
"mondstadt": "Mondstadt",
"liyue": "Liyue",
"inazuma": "Inazuma",
"sumeru": "Sumeru",
"fontaine": "Fontaine",
"natlan": "Natlan",
"snezhnaya": "Snezhnaya",
"khaenriah": "Khaenriah",
"fatui": "Fatui",
"hilichurl": "Hilichurl",
"monster": "Monster",
"kairagi": "Kairagi",
"none": "Kein Elementartyp",
"catalyst": "Katalysator",
"bow": "Bogen",
"claymore": "Zweihänder",
"pole": "Stangenwaffe",
"sword": "Einhänder",
"cryo": "Kryo",
"hydro": "Hydro",
"pyro": "Pyro",
"electro": "Elektro",
"anemo": "Anemo",
"geo": "Geo",
"dendro": "Dendro",
"weapon": "Waffe",
"artifact": "Artefakt",
"talent": "Talent",
"sheild": "Schild",
"place": "Spielfeld",
"ally": "Gefährte",
"item": "Objekte",
"resonance": "Elementarer Einklang",
"food": "Gericht",
"produce": "Dendro-Objekt"
}

42
src/data/tcg/tags/en.json Normal file
View File

@ -0,0 +1,42 @@
{
"unique": "Only One",
"slowly": "Combat Action",
"attack": "Unable to Act",
"freezing": "Immune to Frozen",
"control": "Immune to Disables",
"mondstadt": "Mondstadt",
"liyue": "Liyue",
"inazuma": "Inazuma",
"sumeru": "Sumeru",
"fontaine": "Fontaine",
"natlan": "Natlan",
"snezhnaya": "Snezhnaya",
"khaenriah": "Khaenri'ah",
"fatui": "Fatui",
"hilichurl": "Hilichurl",
"monster": "Monster",
"kairagi": "Kairagi",
"none": "No Elemental Type",
"catalyst": "Catalyst",
"bow": "Bow",
"claymore": "Claymore",
"pole": "Polearm",
"sword": "Sword",
"cryo": "Cryo",
"hydro": "Hydro",
"pyro": "Pyro",
"electro": "Electro",
"anemo": "Anemo",
"geo": "Geo",
"dendro": "Dendro",
"weapon": "Weapon",
"artifact": "Artifact",
"talent": "Talent",
"sheild": "Shield",
"place": "Location",
"ally": "Companion",
"item": "Item",
"resonance": "Elemental Resonance",
"food": "Food",
"produce": "Dendro Construct"
}

42
src/data/tcg/tags/es.json Normal file
View File

@ -0,0 +1,42 @@
{
"unique": "Único",
"slowly": "Acción de combate",
"attack": "Acción imposible",
"freezing": "Inmune a congelado",
"control": "Inmune a pérdida de control",
"mondstadt": "Mondstadt",
"liyue": "Liyue",
"inazuma": "Inazuma",
"sumeru": "Sumeru",
"fontaine": "Fontaine",
"natlan": "Natlan",
"snezhnaya": "Snezhnaya",
"khaenriah": "Khaenri'ah",
"fatui": "Fatui",
"hilichurl": "Hilichurl",
"monster": "Monstruo",
"kairagi": "Samurái Kairagi",
"none": "Sin tipo elemental",
"catalyst": "Catalizador",
"bow": "Arco",
"claymore": "Mandoble",
"pole": "Lanza",
"sword": "Espada ligera",
"cryo": "Cryo",
"hydro": "Hydro",
"pyro": "Pyro",
"electro": "Electro",
"anemo": "Anemo",
"geo": "Geo",
"dendro": "Dendro",
"weapon": "Arma",
"artifact": "Artefacto",
"talent": "Talento",
"sheild": "Escudo",
"place": "Ubicación",
"ally": "Acompañante",
"item": "Objeto",
"resonance": "Consonancia elemental",
"food": "Comida",
"produce": "Creación Dendro"
}

42
src/data/tcg/tags/fr.json Normal file
View File

@ -0,0 +1,42 @@
{
"unique": "Unique",
"slowly": "Action de combat",
"attack": "Aucune action possible",
"freezing": "Anti-gel",
"control": "Anti-contrôle",
"mondstadt": "Mondstadt",
"liyue": "Liyue",
"inazuma": "Inazuma",
"sumeru": "Sumeru",
"fontaine": "Fontaine",
"natlan": "Natlan",
"snezhnaya": "Snezhnaya",
"khaenriah": "Khaenri'ah",
"fatui": "Fatui",
"hilichurl": "Brutocollinus",
"monster": "Monstre",
"kairagi": "Oni des mers",
"none": "Sans élément",
"catalyst": "Catalyseur",
"bow": "Arc",
"claymore": "Épée à deux mains",
"pole": "Arme d'hast",
"sword": "Épée à une main",
"cryo": "Cryo",
"hydro": "Hydro",
"pyro": "Pyro",
"electro": "Électro",
"anemo": "Anémo",
"geo": "Géo",
"dendro": "Dendro",
"weapon": "Arme",
"artifact": "Artéfact",
"talent": "Aptitude",
"sheild": "Bouclier",
"place": "Terrain",
"ally": "Compagnon",
"item": "Objet",
"resonance": "Résonance élémentaire",
"food": "Plat",
"produce": "Construction Dendro"
}

42
src/data/tcg/tags/id.json Normal file
View File

@ -0,0 +1,42 @@
{
"unique": "Satu-satunya",
"slowly": "Aksi Tempur",
"attack": "Tidak dapat beraksi",
"freezing": "Kebal terhadap Frozen",
"control": "Kebal terhadap kendali",
"mondstadt": "Mondstadt",
"liyue": "Liyue",
"inazuma": "Inazuma",
"sumeru": "Sumeru",
"fontaine": "Fontaine",
"natlan": "Natlan",
"snezhnaya": "Snezhnaya",
"khaenriah": "Khaenri'ah",
"fatui": "Fatui",
"hilichurl": "Hilichurl",
"monster": "Monster",
"kairagi": "Kairagi",
"none": "Tidak ada tipe elemen",
"catalyst": "Catalyst",
"bow": "Bow",
"claymore": "Claymore",
"pole": "Polearm",
"sword": "Sword",
"cryo": "Cryo",
"hydro": "Hydro",
"pyro": "Pyro",
"electro": "Electro",
"anemo": "Anemo",
"geo": "Geo",
"dendro": "Dendro",
"weapon": "Senjata",
"artifact": "Artefak",
"talent": "Talenta",
"sheild": "Perisai",
"place": "Lokasi",
"ally": "Teman",
"item": "Item",
"resonance": "Resonansi Elemental",
"food": "Masakan",
"produce": "Dendro Construct"
}

42
src/data/tcg/tags/it.json Normal file
View File

@ -0,0 +1,42 @@
{
"unique": "Unico",
"slowly": "Azione di combattimento",
"attack": "Impossibile agire",
"freezing": "Immunità a Congelamento",
"control": "Immunità a Controllo",
"mondstadt": "Mondstadt",
"liyue": "Liyue",
"inazuma": "Inazuma",
"sumeru": "Sumeru",
"fontaine": "Fontaine",
"natlan": "Natlan",
"snezhnaya": "Snezhnaya",
"khaenriah": "Khaenri'ah",
"fatui": "I Fatui",
"hilichurl": "Hilichurl",
"monster": "Mostro",
"kairagi": "Kairagi",
"none": "Nessun tipo elementale",
"catalyst": "Catalizzatore",
"bow": "Arco",
"claymore": "Claymore",
"pole": "Alabarda",
"sword": "Spada",
"cryo": "Cryo",
"hydro": "Hydro",
"pyro": "Pyro",
"electro": "Electro",
"anemo": "Anemo",
"geo": "Geo",
"dendro": "Dendro",
"weapon": "Arma",
"artifact": "Manufatto",
"talent": "Talento",
"sheild": "Scudo",
"place": "Posizione",
"ally": "Compagno",
"item": "Oggetto",
"resonance": "Risonanza elementale",
"food": "Cibo",
"produce": "Costrutto di Dendro"
}

42
src/data/tcg/tags/ja.json Normal file
View File

@ -0,0 +1,42 @@
{
"unique": "唯一",
"slowly": "戦闘アクション",
"attack": "行動不能",
"freezing": "凍結無効",
"control": "行動妨害無効",
"mondstadt": "モンド",
"liyue": "璃月",
"inazuma": "稲妻",
"sumeru": "スメール",
"fontaine": "フォンテーヌ",
"natlan": "ナタ",
"snezhnaya": "スネージナヤ",
"khaenriah": "カーンルイア",
"fatui": "ファデュイ",
"hilichurl": "ヒルチャール",
"monster": "魔物",
"kairagi": "海乱鬼",
"none": "元素タイプなし",
"catalyst": "法器",
"bow": "弓",
"claymore": "両手剣",
"pole": "長柄武器",
"sword": "片手剣",
"cryo": "氷元素",
"hydro": "水元素",
"pyro": "炎元素",
"electro": "雷元素",
"anemo": "風元素",
"geo": "岩元素",
"dendro": "草元素",
"weapon": "武器",
"artifact": "聖遺物",
"talent": "天賦",
"sheild": "シールド",
"place": "フィールド",
"ally": "仲間",
"item": "アイテム",
"resonance": "元素共鳴",
"food": "料理",
"produce": "草元素の産物"
}

42
src/data/tcg/tags/ko.json Normal file
View File

@ -0,0 +1,42 @@
{
"unique": "유일",
"slowly": "전투 행동",
"attack": "행동 불가",
"freezing": "빙결 면역",
"control": "제어 면역",
"mondstadt": "몬드",
"liyue": "리월",
"inazuma": "이나즈마",
"sumeru": "수메르",
"fontaine": "폰타인",
"natlan": "나타",
"snezhnaya": "스네즈나야",
"khaenriah": "켄리아",
"fatui": "우인단",
"hilichurl": "츄츄족",
"monster": "마물",
"kairagi": "해란귀",
"none": "원소 타입 없음",
"catalyst": "법구",
"bow": "활",
"claymore": "양손검",
"pole": "장병기",
"sword": "한손검",
"cryo": "얼음 원소",
"hydro": "물 원소",
"pyro": "불 원소",
"electro": "번개 원소",
"anemo": "바람 원소",
"geo": "바위 원소",
"dendro": "풀 원소",
"weapon": "무기",
"artifact": "성유물",
"talent": "특성",
"sheild": "보호막",
"place": "필드",
"ally": "동료",
"item": "아이템",
"resonance": "원소 공명",
"food": "요리",
"produce": "풀 원소 산물"
}

42
src/data/tcg/tags/pt.json Normal file
View File

@ -0,0 +1,42 @@
{
"unique": "Único",
"slowly": "Ação de Combate",
"attack": "Não pode fazer ações",
"freezing": "Imunidade a Congelado",
"control": "Imunidade a Controle",
"mondstadt": "Mondstadt",
"liyue": "Liyue",
"inazuma": "Inazuma",
"sumeru": "Sumeru",
"fontaine": "Fontaine",
"natlan": "Natlan",
"snezhnaya": "Snezhnaya",
"khaenriah": "Khaenri'ah",
"fatui": "Fatui",
"hilichurl": "Hilichurl",
"monster": "Monstro",
"kairagi": "Kairagi",
"none": "Sem Tipo Elemental",
"catalyst": "Catalisador",
"bow": "Arco",
"claymore": "Espadão",
"pole": "Lança",
"sword": "Espada",
"cryo": "Cryo",
"hydro": "Hydro",
"pyro": "Pyro",
"electro": "Electro",
"anemo": "Anemo",
"geo": "Geo",
"dendro": "Dendro",
"weapon": "Arma",
"artifact": "Artefato",
"talent": "Talento",
"sheild": "Escudo",
"place": "Campo",
"ally": "Companheiro",
"item": "Ítem",
"resonance": "Ressonância Elemental",
"food": "Prato",
"produce": "Produto Dendro"
}

42
src/data/tcg/tags/ru.json Normal file
View File

@ -0,0 +1,42 @@
{
"unique": "Уникальный",
"slowly": "Боевое действие",
"attack": "Невозможно действовать",
"freezing": "Иммунитет к заморозке",
"control": "Иммунитет к контролю",
"mondstadt": "Мондштадт",
"liyue": "Ли Юэ",
"inazuma": "Инадзума",
"sumeru": "Сумеру",
"fontaine": "Фонтейн",
"natlan": "Натлан",
"snezhnaya": "Снежная",
"khaenriah": "Каэнри'ах",
"fatui": "Фатуи",
"hilichurl": "Хиличурл",
"monster": "Монстр",
"kairagi": "Кайраги",
"none": "Без элемента",
"catalyst": "Катализатор",
"bow": "Стрелковое",
"claymore": "Двуручное",
"pole": "Древковое",
"sword": "Одноручное",
"cryo": "Крио",
"hydro": "Гидро",
"pyro": "Пиро",
"electro": "Электро",
"anemo": "Анемо",
"geo": "Гео",
"dendro": "Дендро",
"weapon": "Оружие",
"artifact": "Артефакт",
"talent": "Талант",
"sheild": "Щит",
"place": "Место",
"ally": "Друзья",
"item": "Предмет",
"resonance": "Элементальный резонанс",
"food": "Блюдо",
"produce": "Дендро конструкция"
}

42
src/data/tcg/tags/th.json Normal file
View File

@ -0,0 +1,42 @@
{
"unique": "หนึ่งเดียว",
"slowly": "ดำเนินการต่อสู้",
"attack": "ไม่สามารถดำเนินการได้",
"freezing": "ต้านทานการแช่แข็ง",
"control": "ต้านทานการถูกควบคุม",
"mondstadt": "Mondstadt",
"liyue": "Liyue",
"inazuma": "Inazuma",
"sumeru": "Sumeru",
"fontaine": "Fontaine",
"natlan": "Natlan",
"snezhnaya": "Snezhnaya",
"khaenriah": "Khaenri'ah",
"fatui": "Fatui",
"hilichurl": "Hilichurl",
"monster": "มอนสเตอร์",
"kairagi": "Kairagi",
"none": "ไม่มีธาตุ",
"catalyst": "สื่อเวท",
"bow": "ธนู",
"claymore": "ดาบใหญ่",
"pole": "หอก",
"sword": "ดาบ",
"cryo": "ธาตุน้ำแข็ง",
"hydro": "ธาตุน้ำ",
"pyro": "ธาตุไฟ",
"electro": "ธาตุไฟฟ้า",
"anemo": "ธาตุลม",
"geo": "ธาตุหิน",
"dendro": "ธาตุไม้",
"weapon": "อาวุธ",
"artifact": "อาร์ติแฟกต์",
"talent": "พรสวรรค์",
"sheild": "โล่ป้องกัน",
"place": "สถานที่",
"ally": "เพื่อน",
"item": "ไอเทม",
"resonance": "การสั่นพ้องของธาตุ",
"food": "อาหาร",
"produce": "วัตถุธาตุไม้"
}

42
src/data/tcg/tags/tr.json Normal file
View File

@ -0,0 +1,42 @@
{
"unique": "Tek",
"slowly": "Savaş Hamlesi",
"attack": "Hamle Yapılamıyor",
"freezing": "Donma Bağışıklığı",
"control": "Kontrol Bağışıklığı",
"mondstadt": "Mondstadt",
"liyue": "Liyue",
"inazuma": "Inazuma",
"sumeru": "Sumeru",
"fontaine": "Fontaine",
"natlan": "Natlan",
"snezhnaya": "Snezhnaya",
"khaenriah": "Khaenri'ah",
"fatui": "Fatui",
"hilichurl": "Dağ Yabanisi",
"monster": "Canavar",
"kairagi": "Kairagi",
"none": "Element Türü Yok",
"catalyst": "Katalizör",
"bow": "Yay",
"claymore": "Çift Elli Kılıç",
"pole": "Mızrak",
"sword": "Kılıç",
"cryo": "Buz",
"hydro": "Su",
"pyro": "Ateş",
"electro": "Elektrik",
"anemo": "Rüzgar",
"geo": "Toprak",
"dendro": "Doğa",
"weapon": "Silah",
"artifact": "Yadigar",
"talent": "Yetenek",
"sheild": "Kalkan",
"place": "Konum",
"ally": "Ortak",
"item": "Eşya",
"resonance": "Element Rezonansı",
"food": "Yiyecek",
"produce": "Doğa Yapısı"
}

42
src/data/tcg/tags/tw.json Normal file
View File

@ -0,0 +1,42 @@
{
"unique": "唯一",
"slowly": "戰鬥行動",
"attack": "無法行動",
"freezing": "免疫凍結",
"control": "免疫控制",
"mondstadt": "蒙德",
"liyue": "璃月",
"inazuma": "稻妻",
"sumeru": "須彌",
"fontaine": "楓丹",
"natlan": "納塔",
"snezhnaya": "至冬",
"khaenriah": "坎瑞亞",
"fatui": "愚人眾",
"hilichurl": "丘丘人",
"monster": "魔物",
"kairagi": "海亂鬼",
"none": "無元素類型",
"catalyst": "法器",
"bow": "弓",
"claymore": "雙手劍",
"pole": "長柄武器",
"sword": "單手劍",
"cryo": "冰元素",
"hydro": "水元素",
"pyro": "火元素",
"electro": "雷元素",
"anemo": "風元素",
"geo": "岩元素",
"dendro": "草元素",
"weapon": "武器",
"artifact": "聖遺物",
"talent": "天賦",
"sheild": "護盾",
"place": "場地",
"ally": "夥伴",
"item": "道具",
"resonance": "元素共鳴",
"food": "料理",
"produce": "草元素產物"
}

42
src/data/tcg/tags/vi.json Normal file
View File

@ -0,0 +1,42 @@
{
"unique": "Duy Nhất",
"slowly": "Hành Động Chiến Đấu",
"attack": "Không thể hành động",
"freezing": "Miễn Dịch Đóng Băng",
"control": "Miễn Dịch Khống Chế",
"mondstadt": "Mondstadt",
"liyue": "Liyue",
"inazuma": "Inazuma",
"sumeru": "Sumeru",
"fontaine": "Fontaine",
"natlan": "Natlan",
"snezhnaya": "Snezhnaya",
"khaenriah": "Khaenri'ah",
"fatui": "Fatui",
"hilichurl": "Hilichurl",
"monster": "Ma Vật",
"kairagi": "Kairagi",
"none": "Không Nguyên Tố",
"catalyst": "Pháp Khí",
"bow": "Cung",
"claymore": "Trọng Kiếm",
"pole": "Vũ Khí Cán Dài",
"sword": "Kiếm Đơn",
"cryo": "Nguyên Tố Băng",
"hydro": "Nguyên Tố Thủy",
"pyro": "Nguyên Tố Hỏa",
"electro": "Nguyên Tố Lôi",
"anemo": "Nguyên Tố Phong",
"geo": "Nguyên Tố Nham",
"dendro": "Nguyên Tố Thảo",
"weapon": "Vũ Khí",
"artifact": "Thánh Di Vật",
"talent": "Thiên Phú",
"sheild": "Khiên",
"place": "Địa Danh",
"ally": "Đồng Đội",
"item": "Đạo Cụ",
"resonance": "Cộng Hưởng Nguyên Tố",
"food": "Thức Ăn",
"produce": "Tạo Vật Thảo"
}

42
src/data/tcg/tags/zh.json Normal file
View File

@ -0,0 +1,42 @@
{
"unique": "唯一",
"slowly": "战斗行动",
"attack": "无法行动",
"freezing": "免疫冻结",
"control": "免疫控制",
"mondstadt": "蒙德",
"liyue": "璃月",
"inazuma": "稻妻",
"sumeru": "须弥",
"fontaine": "枫丹",
"natlan": "纳塔",
"snezhnaya": "至冬",
"khaenriah": "坎瑞亚",
"fatui": "愚人众",
"hilichurl": "丘丘人",
"monster": "魔物",
"kairagi": "海乱鬼",
"none": "无元素类型",
"catalyst": "法器",
"bow": "弓",
"claymore": "双手剑",
"pole": "长柄武器",
"sword": "单手剑",
"cryo": "冰元素",
"hydro": "水元素",
"pyro": "火元素",
"electro": "雷元素",
"anemo": "风元素",
"geo": "岩元素",
"dendro": "草元素",
"weapon": "武器",
"artifact": "圣遗物",
"talent": "天赋",
"sheild": "护盾",
"place": "场地",
"ally": "伙伴",
"item": "道具",
"resonance": "元素共鸣",
"food": "料理",
"produce": "草元素产物"
}

3593
src/data/tcg/th.json Normal file

File diff suppressed because it is too large Load Diff

3593
src/data/tcg/tr.json Normal file

File diff suppressed because it is too large Load Diff

3593
src/data/tcg/tw.json Normal file

File diff suppressed because it is too large Load Diff

3593
src/data/tcg/vi.json Normal file

File diff suppressed because it is too large Load Diff

3593
src/data/tcg/zh.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,7 @@
"radiantSpincrystal": "Radiant Spincrystal",
"calendar": "Calendar",
"banners": "Character Reruns",
"tcg": "TCG",
"settings": "Settings",
"donate": "Donate"
},
@ -142,7 +143,10 @@
"manualButton": "Enable Manual Input",
"errorBanner": "Banner time mismatch! Please adjust your server on the settings page. Still not working? Please leave a message on Discord 😅",
"globalWishTally": "Global Wish Stats",
"pityTooltip": ["Shows your current {rarity} pity", "{count} pulls to guaranteed {rarity}"],
"pityTooltip": [
"Shows your current {rarity} pity",
"{count} pulls to guaranteed {rarity}"
],
"import": {
"title": "Import Wish History",
"faqsButton": "FAQ - READ FIRST",
@ -178,7 +182,11 @@
"server": "Select your server:",
"wishTallyCheck": "Submit pity for global wish stats",
"wishTally": "We are doing a global wish stats! You can submit your wish stats to participate. All pity data will be aggregated to know what is the average pity of paimon.moe users.",
"wishTallyCollected": ["What will be collected:", "and", "pity from your wish history"],
"wishTallyCollected": [
"What will be collected:",
"and",
"pity from your wish history"
],
"forceUpdateCheck": "Force update wish history (enable only if your wish history is not updating)",
"header": [
"Import and backup your Genshin Impact wish history to keep it for more than 6 months. It also automatically tracks your pity and statistics about your wishes!",
@ -388,7 +396,11 @@
"exportFinish": "Export success, please wait until your browser downloads the file!",
"wishTallyTitle": "Submit Wish Stats",
"wishTally": "We are doing a global wish stats! You can submit your wish stats to participate. All pity data will be aggregated to know what is the average pity of paimon.moe users.",
"wishTallyCollected": ["What will be collected:", "and", "pity from your wish history"],
"wishTallyCollected": [
"What will be collected:",
"and",
"pity from your wish history"
],
"wishTallySubmit": "Submit Wish Stats",
"wishTallyThankyou": "Thank you for participating!",
"manualTitle": "Manual Input Settings",
@ -400,13 +412,22 @@
"subtitle": "After a 1x Wish:",
"pressWhenYouGet": "Press {button} when you get {rarity}★",
"p1": "It will automatically add the lifetime pulls, 5★, and 4★ pity",
"p2": ["When the", "pity reaches 10, it will automatically be reset to 0"],
"p3": ["When the", "pity reaches 90, it will automatically be reset to 0"],
"p2": [
"When the",
"pity reaches 10, it will automatically be reset to 0"
],
"p3": [
"When the",
"pity reaches 90, it will automatically be reset to 0"
],
"p4": [
"After a 10x Wish, press",
"but keep in mind that the pity counter might not be accurate, because there is no way to tell when the drop occured (maybe you got it on the 1st or even the 10th pull). To ensure that the counter is still accurate, you need to check the history table and add it one-by-one like you do 1x Wishes."
],
"p5": ["You can also press the", "button to edit the values manually!"],
"p5": [
"You can also press the",
"button to edit the values manually!"
],
"p6": "Press the arrow on the bottom to see your pulls' details. A popup will show up when you get a 5★ or 4★. You can also add or edit the table manually."
}
},
@ -546,7 +567,11 @@
"calculateTalent": "Calculate Talent Material?",
"inputTalentLevel": "Input the 1st, 2nd & 3rd current talent level",
"inputTalentNotice": "If it has a different color, subtract it by 3",
"inputTalent": ["1st talent lvl", "2nd talent lvl", "3rd talent lvl"],
"inputTalent": [
"1st talent lvl",
"2nd talent lvl",
"3rd talent lvl"
],
"talentToLevel": "to level",
"calculate": "Calculate",
"unknownInformation": "There are some unknown information",
@ -555,7 +580,11 @@
"expWasted": "EXP Wasted",
"addToTodo": "Add to Todo List",
"addedToTodo": "Added to Todo List",
"talent": ["Attack", "Skill", "Burst"]
"talent": [
"Attack",
"Skill",
"Burst"
]
},
"expTable": {
"level": "Level",
@ -647,7 +676,10 @@
"todo": {
"title": "Todo List",
"summary": "Summary",
"empty": ["Nothing to do yet 😀", "Add some from the Items page or the Calculator!"],
"empty": [
"Nothing to do yet 😀",
"Add some from the Items page or the Calculator!"
],
"farmableToday": "Farmable Today",
"resin": "Resin needed",
"based": "Based on AR:{ar} and WL:{wl}",
@ -966,7 +998,10 @@
},
"common": {
"dataSynced": "Data has been synced!",
"driveError": "Drive sync not available right now 😔"
"driveError": "Drive sync not available right now 😔",
"open": "Open",
"delete": "Delete",
"deleteConfirm": "Delete?"
},
"update": {
"newUpdate": "Paimon.moe has a new update!",
@ -984,5 +1019,31 @@
"sortByRerun": "Sorted by oldest re-run",
"bannerTitle": "Character Release Timeline",
"bannerSubtitle": "See when the character is released and their last re-run"
},
"tcg": {
"title": "Genius Invokation TCG",
"requirementResonance": "You need at least 2 {element} characters in your deck to add this card!",
"requirementTalent": "You need {character} in your deck to add this card!",
"addedToDeck": "Added to deck!",
"deletedFromDeck": "Deleted from deck!",
"removedFromDeck": "Removed one from deck!",
"alreadyMaxCharacters": "You already have 3 character cards in the deck!",
"alreadyMaxActions": "You already have 30 action cards in the deck!",
"requirementInDeck": "You need to remove {card} card to remove {character} from your deck!",
"addToDeck": "Add to Deck",
"removeFromDeck": "{type} from Deck",
"delete": "Delete",
"remove": "Remove",
"compare": "Compare",
"noCardOnDeck": "There is no card yet in this deck",
"loadDefaultDeck": "Load Starter Deck",
"saveDeck": "Save Deck",
"hideDeck": "Hide Deck",
"showDeck": "Show Deck",
"addDeck": "Add Deck",
"selectDeck": "Select Deck",
"loadingLink": "Generating link...",
"loadingLinkError": "Error generating link 😥",
"shareDeck": "Share Deck \"{name}\""
}
}
}

View File

@ -53,7 +53,7 @@
</script>
<Header />
<Modal>
<Modal styleWindowWrap={{ margin: '1rem' }}>
<Sidebar {segment} />
{#if $showSidebar}
<Sidebar {segment} mobile />

View File

@ -0,0 +1,18 @@
<script context="module">
export function load({ params }) {
const { id } = params;
return {
status: 301,
redirect: `/tcg/${id}`,
};
}
</script>
<svelte:head>
<title>Genius Invokation TCG - Paimon.moe</title>
<meta name="description" content="Genshin Impact Genius Invokation TCG Deck Builder" />
<meta
property="og:description"
content="Genshin Impact Genius Invokation TCG Deck Builder, see card information, build and share your deck!"
/>
</svelte:head>

View File

@ -0,0 +1,76 @@
<script context="module">
export async function load({ params }) {
const { id } = params;
return { props: { id } };
}
</script>
<script>
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import Index from './index.svelte';
export let id;
let loading = true;
let error = false;
let deck = null;
async function getDeck() {
try {
const url = new URL(`${import.meta.env.VITE_API_HOST}/deck/${id}`);
const res = await fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
if (res.status !== 200) {
error = true;
loading = false;
return;
}
const data = await res.json();
const deckData = JSON.parse(data.deck);
deck = {
name: data.name,
...deckData,
};
loading = false;
} catch (err) {
error = true;
}
}
onMount(() => {
if (id === '@') {
if (deck === null) {
goto('/tcg');
}
return;
}
getDeck();
});
</script>
<svelte:head>
<title>Genius Invokation TCG - Paimon.moe</title>
<meta name="description" content="Genshin Impact Genius Invokation TCG Deck Builder" />
<meta
property="og:description"
content="Genshin Impact Genius Invokation TCG Deck Builder, see card information, build and share your deck!"
/>
</svelte:head>
{#if loading}
<div class="lg:ml-64 pt-20 lg:pt-8 px-4 lg:px-8 max-w-full">
<h1 class="text-white text-3xl">Loading Deck...</h1>
</div>
{:else if error}
<div class="lg:ml-64 pt-20 lg:pt-8 px-4 lg:px-8 max-w-full">
<h1 class="text-white text-3xl">Deck not found 😪</h1>
</div>
{:else}
<Index sharedDeck={deck} sharedId={id} />
{/if}

328
src/routes/tcg/_card.svelte Normal file
View File

@ -0,0 +1,328 @@
<script>
import { mdiCompare, mdiMinus, mdiPlus } from '@mdi/js';
import { onMount, tick, createEventDispatcher, getContext } from 'svelte';
import Detail from './_detail.svelte';
import CardButton from './_cardButton.svelte';
import DetailModal from './_detailModal.svelte';
import { t } from 'svelte-i18n';
export let card;
export let compare;
export let count = 0;
export let index = 0;
export let showDetail = false;
export let smallScreen = false;
export let draggable = false;
const dispatch = createEventDispatcher();
const { open, close } = getContext('simple-modal');
const sizes = {
base: {
card: 'w-32',
hp: 'w-8',
hpVal: 'text-2xl',
element: 'w-10',
elementVal: 'text-2xl',
energyStar: 'w-6',
energyStarMargin: '-mb-1',
energyStarPos: 'top-2 -right-3',
},
small: {
card: 'w-24',
hp: 'w-6',
hpVal: 'text-xl',
element: 'w-8',
elementVal: 'text-xl',
energyStar: 'w-6',
energyStarMargin: '-mb-1',
energyStarPos: 'top-2 -right-3',
},
large: {
card: 'w-64',
hp: 'w-10',
hpVal: 'text-3xl',
element: 'w-12',
elementVal: 'text-3xl',
energyStar: 'w-8',
energyStarMargin: '-mb-1',
energyStarPos: 'top-5 -right-4',
},
};
export let size = 'base';
let loaded = false;
let nameLabel;
let smallName;
let isDragging = false;
let isDragHovered = false;
let isHovered = false;
let x, y, width, height;
let lastMoveEvent;
let cardContainer;
let detailContainer;
async function adjustNameSize() {
if (nameLabel === undefined) return;
smallName = false;
await tick();
const height = nameLabel.clientHeight;
smallName = height > 40;
}
async function mouseOver(event) {
if (!showDetail) return;
let bounds = cardContainer.getBoundingClientRect();
x = event.clientX - bounds.left + 10;
y = event.clientY - bounds.top + 20;
isHovered = true;
await tick();
const detailRect = detailContainer.getBoundingClientRect();
width = detailRect.width;
height = detailRect.height;
mouseMove(event);
}
function mouseMove(event) {
lastMoveEvent = event;
let bounds = cardContainer.getBoundingClientRect();
let curX = event.clientX - bounds.left + 20;
let curY = event.clientY - bounds.top + 5;
const detailRectRightX = event.clientX + width + 40;
const detailRectBottomY = event.clientY + height + 20;
if (detailRectRightX >= window.innerWidth) {
curX -= width + 30;
}
if (detailRectBottomY >= window.innerHeight) {
curY = window.innerHeight - height - bounds.top - 5;
}
x = curX;
y = curY;
}
function mouseLeave() {
lastMoveEvent = undefined;
isHovered = false;
}
async function refreshPos() {
if (!loaded || lastMoveEvent === undefined) return;
await mouseOver(lastMoveEvent);
mouseMove(lastMoveEvent);
}
async function setCompare() {
dispatch('compare');
}
async function addToDeck() {
dispatch('addToDeck');
}
async function removeFromDeck() {
dispatch('removeFromDeck');
}
function showDetailModal() {
open(
DetailModal,
{
card,
compare,
showCompare,
count,
addToDeck,
removeFromDeck,
close,
},
{
closeButton: false,
styleWindow: {
background: '#25294A',
width: 'fit-content',
'max-width': showCompare ? '1280px' : '860px',
},
styleContent: { padding: 0 },
},
);
}
function handleDragStart(e) {
isHovered = false;
isDragging = true;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', index);
}
function handleDragEnd() {
isDragging = false;
}
function handleDragEnter() {
isDragHovered = true;
}
function handleDragOver(e) {
e.preventDefault();
}
function handleDragLeave() {
isDragHovered = false;
}
function handleDrop(e) {
e.stopPropagation();
isDragHovered = false;
const sourceIndex = Number(e.dataTransfer.getData('text/plain'));
dispatch('swapOrder', { from: index, to: sourceIndex });
}
onMount(() => {
adjustNameSize();
loaded = true;
if (draggable) {
cardContainer.addEventListener('dragstart', handleDragStart);
cardContainer.addEventListener('dragend', handleDragEnd);
cardContainer.addEventListener('dragenter', handleDragEnter);
cardContainer.addEventListener('dragleave', handleDragLeave);
cardContainer.addEventListener('dragover', handleDragOver);
cardContainer.addEventListener('drop', handleDrop);
}
});
$: card.name, adjustNameSize();
$: showCompare = compare !== undefined;
$: compare, refreshPos();
</script>
<div
class="group relative {draggable ? 'cursor-move' : 'cursor-pointer'} {sizes[size].card}"
on:mouseover={mouseOver}
on:mouseleave={mouseLeave}
on:mousemove={mouseMove}
on:focus={mouseOver}
bind:this={cardContainer}
>
<div class="relative">
<img
src="/images/tcg/{card.id}.png"
alt={card.name}
class="w-full rounded-xl duration-100 {isDragHovered ? 'ring-4 ring-primary rounded-xl' : ''} {isDragging
? 'opacity-40'
: ''}"
loading="lazy"
width="420"
height="720"
on:click={showDetailModal}
/>
{#if count}
<div class="absolute bottom-0 right-0">
<div class="relative -mb-[1px] -mr-[1px]">
<img src="/images/tcg/icons/counter.png" alt="counter" width="68" height="68" class="w-8 h-8" />
<div class="w-full h-full flex items-center justify-center top-0 left-0 absolute">
<p
class="font-bold text-white pt-[2px] pr-[1px] {sizes[size].elementVal}"
style="-webkit-text-stroke: 1.2px black;"
>
{count}
</p>
</div>
</div>
</div>
{/if}
{#if size !== 'large'}
<div class="group-hover:hidden group-hover:md:flex absolute hidden bottom-1 left-1 flex-col gap-1">
{#if count > 0}
<CardButton
icon={mdiMinus}
label={count > 1 ? $t('tcg.remove') : $t('tcg.delete')}
on:click={removeFromDeck}
/>
{#if card.type !== 'character'}
<CardButton icon={mdiPlus} label={$t('tcg.addToDeck')} on:click={addToDeck} />
{/if}
{:else}
<CardButton icon={mdiPlus} label={$t('tcg.addToDeck')} on:click={addToDeck} />
{/if}
<CardButton icon={mdiCompare} label={$t('tcg.compare')} on:click={setCompare} />
</div>
{/if}
{#if isHovered}
<div
class="absolute z-50 flex {showCompare ? 'w-[860px]' : 'w-[600px]'}"
style="top: {y}px; left: {x}px;"
bind:this={detailContainer}
>
<Detail {card} {smallScreen} />
{#if showCompare}
<div class="w-2" />
<Detail card={compare} {smallScreen} />
{/if}
</div>
{/if}
</div>
{#if size !== 'large'}
<p
bind:this={nameLabel}
class="text-white text-center leading-none pt-1 min-h-[32px] {smallName ? 'text-xs' : 'text-sm'}"
>
{card.name}
</p>
{/if}
{#if card.type === 'character'}
<div class="absolute top-0 left-0 -mx-1 -my-2 z-10">
<img src="/images/tcg/icons/hp.png" alt="HP" class={sizes[size].hp} width="140" height="172" />
<div class="w-full h-full flex items-center justify-center top-0 left-0 absolute">
<p class="font-bold text-white pt-1 {sizes[size].hpVal}" style="-webkit-text-stroke: 1.2px black;">
{card.hp}
</p>
</div>
</div>
<div class="absolute {sizes[size].energyStarPos}">
{#each Array(card.energy) as _}
<img
src="/images/tcg/icons/energy_card.png"
alt="Energy"
class="{sizes[size].energyStar} {sizes[size].energyStarMargin}"
width="100"
height="100"
/>
{/each}
</div>
{:else}
<div class="absolute top-0 left-0 -mx-2 -my-2 flex">
{#each card.skills.cost.length > 0 ? card.skills.cost : [{}] as cost}
<div class="relative">
<img
src="/images/tcg/icons/{cost.type || 'same'}.png"
alt={cost.type}
class={sizes[size].element}
width="140"
height="172"
/>
<div class="w-full h-full flex items-center justify-center top-0 left-0 absolute">
<p
class="font-bold text-white pt-[2px] pr-[1px] {sizes[size].elementVal}"
style="-webkit-text-stroke: 1.2px black;"
>
{cost.count || 0}
</p>
</div>
</div>
{/each}
</div>
{/if}
</div>

View File

@ -0,0 +1,17 @@
<script>
import Icon from '../../components/Icon.svelte';
export let icon;
export let label;
</script>
<button
on:click
class="group/button w-8 hover:w-fit h-8 text-sm bg-black bg-opacity-90 rounded-full
text-white text-center hover:ring-2 ring-primary duration-100 flex items-center justify-center z-30"
>
<div class="min-w-[2rem] w-8">
<Icon path={icon} />
</div>
<p class="hidden group-hover/button:block pr-2 text-xs leading-none">{label}</p>
</button>

218
src/routes/tcg/_deck.svelte Normal file
View File

@ -0,0 +1,218 @@
<script>
import { mdiContentSave, mdiDownload, mdiPencil, mdiShareVariant } from '@mdi/js';
import { createEventDispatcher, getContext, onMount } from 'svelte';
import { t } from 'svelte-i18n';
import { slide } from 'svelte/transition';
import Button from '../../components/Button.svelte';
import Icon from '../../components/Icon.svelte';
import Input from '../../components/Input.svelte';
import Card from './_card.svelte';
import DeckModal from './_deckModal.svelte';
import ShareModal from './_shareModal.svelte';
const dispatch = createEventDispatcher();
const { open, close } = getContext('simple-modal');
export let cards, decks, deck, size, characterCount, actionCount, compare, showDetail;
export let sharedDeck, sharedId;
export let setCompare, addToDeck, removeFromDeck, selectDeck, loadDeck, swapCharacterCardPos;
let editName = false;
let name = '';
function showDeckSelectionModal() {
open(
DeckModal,
{
decks,
close,
selectDeck: changeDeck,
save: () => dispatch('save'),
},
{
closeButton: false,
styleWindow: { background: '#25294A', width: '600px' },
},
);
}
function share() {
open(
ShareModal,
{
deck,
setShareId,
id: sharedId,
},
{
closeButton: false,
styleWindow: { background: '#25294A', width: '400px' },
},
);
}
function changeDeck(index) {
selectDeck(index);
setShareId(null);
}
function setShareId(id) {
console.log('set', id);
sharedId = id;
}
function saveSharedDeck() {
dispatch('saveShared');
}
function toggleEditName() {
name = deck.name;
editName = true;
}
function saveName() {
deck.name = name;
editName = false;
dispatch('save');
}
function swapOrder(event) {
swapCharacterCardPos(event.detail.from, event.detail.to);
}
function loadDefaultDeck() {
loadDeck(
{ diluc: 1, kaeya: 1, sucrose: 1 },
{
magic_guide: 1,
raven_bow: 1,
white_iron_greatsword: 1,
white_tassel: 1,
travelers_handy_sword: 1,
broken_rimes_echo: 1,
'wine-stained_tricorne': 1,
witchs_scorching_hat: 1,
thunder_summoners_crown: 1,
viridescent_venerers_diadem: 1,
mask_of_solitude_basalt: 1,
laurel_coronet: 1,
changing_shifts: 1,
strategize: 1,
'i_havent_lost_yet!': 1,
'leave_it_to_me!': 1,
when_the_crane_returned: 1,
starsigns: 1,
calxs_arts: 1,
master_of_weaponry: 1,
blessing_of_the_divine_relics_installation: 1,
quick_knit: 1,
send_off: 1,
guardians_oath: 1,
sweet_madame: 1,
minty_meat_rolls: 1,
dawn_winery: 1,
favonius_cathedral: 1,
paimon: 1,
'the_bestest_travel_companion!': 1,
},
);
}
</script>
<div class="relative bg-black bg-opacity-50 px-4 pt-4 pb-2 rounded-xl mb-4" transition:slide={{ duration: 200 }}>
<div class="absolute top-4 right-4 flex flex-row-reverse md:flex-col gap-4 xl:gap-0">
<button
class="rounded-lg pl-2 pr-4 py-1 w-fit ring-2 ring-gray-700 hover:ring-primary duration-100"
on:click={() => dispatch('toggleDeck')}
>
<div class="flex gap-2">
<div class="flex items-center">
<img src="/images/tcg/icons/card_character.png" alt="character card" class="w-8" />
<p class="text-white text-xl">{characterCount}</p>
</div>
<div class="flex items-center">
<img src="/images/tcg/icons/card.png" alt="character card" class="w-8" />
<p class="text-white text-xl">{actionCount}</p>
</div>
</div>
<p class="pl-2 text-white text-xs text-center">{$t('tcg.hideDeck')}</p>
</button>
<button
class="rounded-lg md:mt-4 px-2 py-1 ring-2 ring-gray-700 hover:ring-primary duration-100 text-white"
on:click={showDeckSelectionModal}
>
{$t('tcg.selectDeck')}
</button>
</div>
<div class="pt-24 md:pt-0 pb-4 flex items-center w-full">
{#if editName}
<div class="flex w-full max-w-screen-sm">
<div class="w-full max-w-full">
<Input className="w-full" bind:value={name} />
</div>
<button class="text-white ml-4 rounded-xl hover:ring-2 ring-primary p-1 duration-100" on:click={saveName}>
<Icon path={mdiContentSave} />
</button>
</div>
{:else}
<div class="flex xl:pr-96 w-full items-center">
<h1 class="text-white font-bold text-lg xl:text-3xl max-w-full break-words">
{deck.name}
</h1>
{#if sharedDeck === null}
<button
class="text-white ml-4 rounded-xl hover:ring-2 ring-primary p-1 duration-100"
on:click={toggleEditName}
>
<Icon path={mdiPencil} />
</button>
<button class="text-white ml-4 rounded-xl hover:ring-2 ring-primary p-1 duration-100" on:click={share}>
<Icon path={mdiShareVariant} />
</button>
{:else}
<button class="text-white ml-4 rounded-xl hover:ring-2 ring-primary p-1 duration-100" on:click={share}>
<Icon path={mdiShareVariant} />
</button>
<Button className="ml-4" on:click={saveSharedDeck}>{$t('tcg.saveDeck')}</Button>
{/if}
</div>
{/if}
</div>
{#if characterCount === 0 && actionCount === 0}
<div>
<p class="text-white text-lg pb-2">{$t('tcg.noCardOnDeck')}</p>
<Button on:click={loadDefaultDeck}>{$t('tcg.loadDefaultDeck')}</Button>
</div>
{/if}
<div class="flex flex-wrap gap-x-4 gap-y-3 pb-4">
{#each Object.keys(deck.characters) as id, index}
<Card
card={cards[id]}
{size}
{compare}
{showDetail}
{index}
draggable
count={1}
on:compare={() => setCompare(cards[id])}
on:addToDeck={() => addToDeck('characters', cards[id])}
on:removeFromDeck={() => removeFromDeck('characters', cards[id])}
on:swapOrder={swapOrder}
/>
{/each}
</div>
<div class="flex flex-wrap gap-x-4 gap-y-3">
{#each Object.entries(deck.actions) as [id, count]}
<Card
card={cards[id]}
{size}
{compare}
{count}
{showDetail}
on:compare={() => setCompare(cards[id])}
on:addToDeck={() => addToDeck('actions', cards[id])}
on:removeFromDeck={() => removeFromDeck('actions', cards[id])}
/>
{/each}
</div>
</div>

View File

@ -0,0 +1,71 @@
<script>
import { t } from 'svelte-i18n';
import Button from '../../components/Button.svelte';
export let decks;
export let selectDeck, close, save;
const confirmDelete = [];
function addDeck() {
decks.push({
name: `Deck #${decks.length + 1}`,
characters: {},
actions: {},
});
decks = decks;
save();
}
function deleteDeck(index) {
if (confirmDelete[index] === true) {
deleteDeckConfirm(index);
return;
}
confirmDelete[index] = true;
setTimeout(() => {
confirmDelete[index] = false;
}, 5000);
}
function deleteDeckConfirm(index) {
decks.splice(index, 1);
decks = decks;
selectDeck(0);
save();
}
function changeDeck(index) {
selectDeck(index);
close();
}
</script>
<div>
<div class="pb-4">
<Button on:click={addDeck}>{$t('tcg.addDeck')}</Button>
</div>
<table class="border-separate border-spacing-0">
{#each decks as deck, i}
<tr class="group">
<td class="group-last:border-b-0 border-b border-gray-700 w-full px-2 text-white align-middle">
<div class="flex items-center gap-2">
{#each Object.keys(deck.characters) as char}
<img src="/images/tcg/avatar/{char}.png" alt={char} class="w-10" />
{/each}
<p class="pt-1 pl-2">{deck.name}</p>
</div>
</td>
<td class="group-last:border-b-0 border-b border-gray-700 py-2 pr-2 whitespace-nowrap text-right">
{#if decks.length > 1}
<Button size="sm" color="red" on:click={() => deleteDeck(i)}>
{$t(confirmDelete[i] ? 'common.deleteConfirm' : 'common.delete')}
</Button>
{/if}
<Button size="sm" on:click={() => changeDeck(i)}>{$t('common.open')}</Button>
</td>
</tr>
{/each}
</table>
</div>

View File

@ -0,0 +1,153 @@
<script context="module">
import tagsJson from '../../data/tcg/tags/en.json';
</script>
<script>
export let card;
export let smallScreen = false;
export let withBackground = true;
const sizes = {
base: {
text: 'text-sm',
title: 'text-base',
gap: 'gap-y-2',
tag: 'w-8',
image: 'w-12',
element: 'w-10',
padding: 'p-2',
},
small: {
text: 'text-xs',
title: 'text-xs',
gap: 'gap-y-1',
tag: 'w-6',
image: 'w-6',
element: 'w-8',
padding: 'p-1',
},
};
let tags = tagsJson;
$: size = sizes[smallScreen ? 'small' : 'base'];
</script>
{#if card.type === 'character'}
<div
class="flex flex-col bg-opacity-95 rounded-xl text-white w-full h-fit
origin-top-left {withBackground ? 'bg-background' : ''} {size.gap} {size.padding}"
>
<div class="flex gap-2 items-center">
<img
src="/images/tcg/icons/elements/{card.element}.png"
alt={card.element}
class={size.tag}
width="128"
height="128"
/>
<p class="{size.title} font-bold flex-1">{card.name}</p>
<img
src="/images/tcg/icons/weapons/{card.weapon}.png"
alt={card.weapon}
class={size.tag}
width="64"
height="64"
/>
</div>
<div class="flex flex-col text-sm {size.gap}">
{#each card.skills as skill, i}
<div class="bg-black bg-opacity-60 p-2 rounded-xl">
<div class="flex items-center pb-2">
<img class={size.image} src="/images/tcg/skills/{card.id}/skill_{i + 1}.png" alt={skill.name} />
<div class="pr-3 flex-1">
<p class="pl-2 font-bold leading-tight {size.title}">{skill.name}</p>
<p class="pl-2 {size.title}" style="color: #ffd780ff;">{skill.type}</p>
</div>
<div class="flex">
{#each skill.cost as cost}
<div class="relative">
<img
src="/images/tcg/icons/{cost.type}.png"
alt={cost.type}
class={size.element}
width="140"
height="172"
/>
<div class="w-full h-full flex items-center justify-center top-0 left-0 absolute">
<p
class="font-bold text-2xl text-white pt-[2px] pr-[1px]"
style="-webkit-text-stroke: 1.2px black;"
>
{cost.count}
</p>
</div>
</div>
{/each}
</div>
</div>
<p class={size.text}>{@html skill.desc.replace(/\\n/g, '<br/>')}</p>
{#if skill.sub}
<p class="{size.title} font-bold pt-4">{skill.sub.name}</p>
<p class={size.text}>
{@html skill.sub.desc.replace(/\\n/g, '<br/>')}
</p>
{/if}
</div>
{/each}
</div>
</div>
{:else}
<div
class="flex flex-col gap-2 bg-opacity-95 p-2 rounded-xl text-white w-full h-fit
{withBackground ? 'bg-background' : ''}"
>
<div class="flex gap-2 items-center">
<p class="{size.title} font-bold flex-1">{card.name}</p>
</div>
<div class="bg-black bg-opacity-60 p-2 rounded-xl">
<div class="flex items-center pb-2">
<div class="pr-3 flex-1 flex gap-2 items-center">
<img
src="/images/tcg/icons/types/{card.type}.png"
alt={card.type}
class={size.image}
width="64"
height="64"
/>
<p class="{size.title} font-bold leading-tight">{tags[card.type]}</p>
</div>
<div class="flex">
{#if card.skills.cost.length === 0}
<div class="relative">
<img src="/images/tcg/icons/same.png" alt="same" class={size.image} width="140" height="172" />
<div class="w-full h-full flex items-center justify-center top-0 left-0 absolute">
<p class="font-bold text-2xl text-white pt-[2px] pr-[1px]" style="-webkit-text-stroke: 1.2px black;">
0
</p>
</div>
</div>
{/if}
{#each card.skills.cost as cost}
<div class="relative">
<img
src="/images/tcg/icons/{cost.type}.png"
alt={cost.type}
class={size.element}
width="140"
height="172"
/>
<div class="w-full h-full flex items-center justify-center top-0 left-0 absolute">
<p class="font-bold text-2xl text-white pt-[2px] pr-[1px]" style="-webkit-text-stroke: 1.2px black;">
{cost.count}
</p>
</div>
</div>
{/each}
</div>
</div>
<p class={size.text}>{@html card.skills.desc.replace(/\\n/g, '<br/>')}</p>
</div>
</div>
{/if}

View File

@ -0,0 +1,43 @@
<script>
import { t } from 'svelte-i18n';
import Button from '../../components/Button.svelte';
import Card from './_card.svelte';
import Detail from './_detail.svelte';
export let card;
export let showCompare;
export let compare;
export let count;
export let addToDeck;
export let removeFromDeck;
export let close;
function add() {
addToDeck();
close();
}
function remove() {
removeFromDeck();
close();
}
</script>
<div class="flex flex-col-reverse items-center md:items-start md:flex-row">
<div class="p-2 md:p-4 flex flex-col-reverse md:flex-col gap-4 md:gap-2">
<Card {card} {count} size="large" />
{#if (card.type === 'character' && count === undefined) || card.type !== 'character'}
<Button on:click={add}>{$t('tcg.addToDeck')}</Button>
{/if}
{#if count > 0}
<Button on:click={remove}>
{$t('tcg.removeFromDeck', { values: { type: count === 1 ? $t('tcg.delete') : $t('tcg.remove') } })}
</Button>
{/if}
</div>
<Detail {card} withBackground={false} />
{#if showCompare}
<div class="w-2" />
<Detail card={compare} withBackground={false} />
{/if}
</div>

View File

@ -0,0 +1,80 @@
<script>
import { mdiCheck, mdiContentCopy } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import Button from '../../components/Button.svelte';
import Icon from '../../components/Icon.svelte';
export let deck;
let loading = true;
let error = false;
let copied = false;
export let id = null;
export let setShareId;
async function submitDeck() {
try {
const url = new URL(`${import.meta.env.VITE_API_HOST}/deck`);
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(deck),
});
if (res.status !== 200) {
error = true;
loading = false;
return;
}
const data = await res.json();
id = data.id;
setShareId(id);
loading = false;
} catch (err) {
error = true;
}
}
function copyLink() {
try {
navigator.clipboard.writeText(generatedLink);
copied = true;
setTimeout(() => {
copied = false;
}, 2000);
} catch (err) {}
}
onMount(() => {
if (id === null) {
submitDeck();
} else {
loading = false;
}
});
$: generatedLink = `paimon.moe/deck/${id}`;
</script>
{#if loading}
<p class="text-white">{$t('tcg.loadingLink')}</p>
{:else if error}
<p class="text-white">{$t('tcg.loadingLinkError')}</p>
{:else}
<div class="flex flex-col">
<p class="text-gray-400 pb-4">{$t('tcg.shareDeck', { values: { name: deck.name } })}</p>
<div class="flex gap-4">
<div class="bg-background rounded-xl px-4 flex items-center flex-1">
<p class="text-white">{generatedLink}</p>
</div>
<Button on:click={copyLink}>
<Icon path={copied ? mdiCheck : mdiContentCopy} />
</Button>
</div>
</div>
{/if}

468
src/routes/tcg/index.svelte Normal file
View File

@ -0,0 +1,468 @@
<script context="module">
import dataJson from '../../data/tcg/en.json';
import tagsJson from '../../data/tcg/tags/en.json';
</script>
<script>
import { onMount } from 'svelte';
import { fly } from 'svelte/transition';
import { goto } from '$app/navigation';
import { mdiDockWindow, mdiImageSizeSelectLarge, mdiImageSizeSelectSmall } from '@mdi/js';
import { locale, t } from 'svelte-i18n';
import Icon from '../../components/Icon.svelte';
import Card from './_card.svelte';
import Deck from './_deck.svelte';
import { pushToast } from '../../stores/toast';
import debounce from 'lodash.debounce';
import { getAccountPrefix } from '../../stores/account';
import { readSave, updateSave } from '../../stores/saveManager';
let data = dataJson;
let tags = tagsJson;
export let sharedDeck = null;
export let sharedId = null;
let cards = {};
let characters = [];
let _actions = [];
let actions = [];
/**
* @typedef Card
* @type {Object}
* @property {string} id
* @property {string} type
* @property {string} name
* @property {string} element
* @property {string} [requirement]
*/
/**
* @typedef Deck
* @type {Object}
* @property {Object.<string, number>} characters
* @property {Object.<string, number>} actions
*/
/** @type Deck[] */
let decks = [
{
name: 'Deck #1',
characters: {},
actions: {},
},
];
let activeDeck = 0;
let compare;
let display = 'character';
let size = 'base';
let showDetail = true;
let showDeck = false;
let smallScreen = false;
let filter = {
ally: true,
artifact: true,
food: true,
item: true,
none: true,
place: true,
resonance: true,
talent: true,
weapon: true,
};
function process() {
cards = {};
characters = [];
_actions = [];
actions = [];
for (const card of data) {
cards[card.id] = card;
if (card.type === 'character') {
characters.push(card);
} else {
_actions.push(card);
}
}
characters.sort((a, b) => a.name.localeCompare(b.name));
_actions.sort((a, b) => a.type.localeCompare(b.type) || a.name.localeCompare(b.name));
actions = _actions;
}
function setCompare(card) {
compare = card;
}
function removeCompare() {
compare = undefined;
}
function changeDisplay(type) {
display = type;
}
function changeSize(type) {
size = type;
}
function toggleDetail() {
showDetail = !showDetail;
}
function toggleFilter(type) {
const filterCount = Object.keys(filter).length;
const trueCount = Object.values(filter).reduce((prev, cur) => prev + (cur ? 1 : 0), 0);
if (trueCount === filterCount) {
Object.keys(filter).forEach((e) => {
filter[e] = false;
});
} else if (trueCount === 1 && filter[type]) {
Object.keys(filter).forEach((e) => {
if (e === type) {
filter[type] = false;
return;
}
filter[e] = true;
});
}
filter[type] = !filter[type];
actions = _actions.filter((card) => filter[card.type]);
}
function toggleShowDeck() {
showDeck = !showDeck;
if (showDeck) {
window.scrollTo(0, 0);
}
}
function selectDeck(index) {
sharedDeck = null;
activeDeck = index;
saveData();
}
function loadDeck(characters, actions) {
deck.characters = characters;
deck.actions = actions;
saveData();
}
function saveSharedDeck() {
decks.push(sharedDeck);
decks = decks;
activeDeck = decks.length - 1;
sharedDeck = null;
sharedId = null;
saveData();
goto('/tcg/@');
}
/**
* @param {string} type
* @param {Card} card
*/
function addToDeck(type, card) {
if (type === 'characters' && characterCount === 3) {
pushToast($t('tcg.alreadyMaxCharacters'));
return;
} else if (type === 'actions' && actionCount === 30) {
pushToast($t('tcg.alreadyMaxActions'));
return;
}
if (card.type === 'resonance') {
const elementCount = {};
for (const id of Object.keys(deck.characters)) {
const charCard = cards[id];
if (elementCount[charCard.element] === undefined) elementCount[charCard.element] = 0;
elementCount[charCard.element]++;
}
if ((elementCount[card.requirement] || 0) < 2) {
pushToast($t('tcg.requirementResonance', { values: { element: tags[card.requirement] } }));
return;
}
}
if (card.type === 'talent') {
if (deck.characters[card.requirement] === undefined) {
pushToast($t('tcg.requirementTalent', { values: { character: cards[card.requirement].name } }));
return;
}
}
pushToast($t('tcg.addedToDeck'));
if (deck[type][card.id] === undefined) {
deck[type][card.id] = 0;
}
deck[type][card.id] += 1;
saveData();
}
/**
* @param {string} type
* @param {Card} card
*/
function removeFromDeck(type, card) {
let deleted = false;
if (type === 'characters') {
const elementCount = {};
for (const id of Object.keys(deck.characters)) {
if (id === card.id) continue;
const charCard = cards[id];
if (elementCount[charCard.element] === undefined) elementCount[charCard.element] = 0;
elementCount[charCard.element]++;
}
for (const id of Object.keys(deck.actions)) {
const c = cards[id];
if (c.requirement === card.id || (c.requirement === card.element && elementCount[card.element] < 2)) {
pushToast($t('tcg.requirementInDeck', { values: { character: card.name, card: c.name } }));
return;
}
}
}
deck[type][card.id]--;
if (deck[type][card.id] === 0) {
deleted = true;
delete deck[type][card.id];
}
deck[type] = deck[type];
if (deleted) {
pushToast($t('tcg.deletedFromDeck'));
} else {
pushToast($t('tcg.removedFromDeck'));
}
saveData();
}
function swapCharacterCardPos(from, to) {
const keys = Object.keys(deck.characters);
[keys[from], keys[to]] = [keys[to], keys[from]];
deck.characters = keys.reduce((prev, cur) => {
prev[cur] = 1;
return prev;
}, {});
saveData();
}
async function readLocalData() {
const prefix = getAccountPrefix();
const tcgData = await readSave(`${prefix}tcg-decks`);
const tcgSelectedDeck = await readSave(`${prefix}tcg-activedeck`);
if (tcgData !== null) {
decks = tcgData;
activeDeck = tcgSelectedDeck;
}
}
const saveData = debounce(async () => {
const prefix = getAccountPrefix();
await updateSave(`${prefix}tcg-decks`, decks);
await updateSave(`${prefix}tcg-activedeck`, activeDeck);
}, 2000);
async function changeLocale(locale) {
const dataJson = await import(`../../data/tcg/${locale}.json`);
const tagsDataJson = await import(`../../data/tcg/tags/${locale}.json`);
data = dataJson.default;
tags = tagsDataJson.default;
process();
}
process();
onMount(async () => {
await readLocalData();
smallScreen = window.innerHeight < 900;
showDetail = window.innerWidth > 600;
if (smallScreen) {
size = 'small';
}
if (sharedDeck !== null) {
showDeck = true;
}
locale.subscribe((val) => {
changeLocale(val);
});
});
/** @type Deck */
$: deck = sharedDeck === null ? decks[activeDeck] : sharedDeck;
$: characterCount = Object.keys(deck.characters).length;
$: actionCount = Object.values(deck.actions).reduce((prev, cur) => prev + cur, 0);
</script>
<svelte:head>
<title>Genius Invokation TCG - Paimon.moe</title>
<meta name="description" content="Genshin Impact Genius Invokation TCG Deck Builder" />
<meta
property="og:description"
content="Genshin Impact Genius Invokation TCG Deck Builder, see card information, build and share your deck!"
/>
</svelte:head>
<div class="lg:ml-64 pt-20 lg:pt-8 px-4 lg:px-8 max-w-full">
{#if showDeck}
<Deck
{cards}
{decks}
{deck}
{size}
{characterCount}
{actionCount}
{compare}
{setCompare}
{showDetail}
{addToDeck}
{removeFromDeck}
{selectDeck}
{loadDeck}
{sharedDeck}
{sharedId}
{swapCharacterCardPos}
on:toggleDeck={toggleShowDeck}
on:save={() => saveData()}
on:saveShared={saveSharedDeck}
/>
{/if}
<div class="flex gap-2 justify-center md:justify-start">
<div class="flex">
<button on:click={() => changeDisplay('character')} class="pill {display === 'character' ? 'active' : ''}">
Character Card
</button>
<button on:click={() => changeDisplay('action')} class="pill {display === 'action' ? 'active' : ''}">
Action Card
</button>
</div>
<div class="flex">
<button on:click={() => changeSize('base')} class="pill {size === 'base' ? 'active' : ''}">
<Icon path={mdiImageSizeSelectLarge} />
</button>
<button on:click={() => changeSize('small')} class="pill {size === 'small' ? 'active' : ''}">
<Icon path={mdiImageSizeSelectSmall} />
</button>
</div>
<button on:click={() => toggleDetail()} class="hidden md:block pill solo {showDetail ? 'active' : ''}">
<Icon path={mdiDockWindow} />
</button>
</div>
{#if display === 'action'}
<div class="pt-2 flex flex-wrap gap-2 justify-center md:justify-start">
{#each Object.entries(filter) as [key, val]}
<button on:click={() => toggleFilter(key)} class="pill solo {val ? 'active' : ''}">
{tags[key]}
</button>
{/each}
</div>
{/if}
{#if display === 'character'}
<div class="flex flex-wrap justify-center md:justify-start gap-x-4 gap-y-3 pt-8">
{#each characters as card}
<Card
{card}
{compare}
{size}
{showDetail}
{smallScreen}
count={deck.characters[card.id]}
on:compare={() => setCompare(card)}
on:addToDeck={() => addToDeck('characters', card)}
on:removeFromDeck={() => removeFromDeck('characters', card)}
/>
{/each}
</div>
{:else}
<div class="flex flex-wrap justify-center md:justify-start gap-x-4 gap-y-3 pt-8">
{#each actions as card}
<Card
{card}
{compare}
{size}
{showDetail}
{smallScreen}
count={deck.actions[card.id]}
on:compare={() => setCompare(card)}
on:addToDeck={() => addToDeck('actions', card)}
on:removeFromDeck={() => removeFromDeck('actions', card)}
/>
{/each}
</div>
{/if}
<div class="fixed bottom-2 md:bottom-auto md:top-0 right-0 z-50 flex justify-end">
{#if !showDeck}
<button
class="bg-black bg-opacity-80 rounded-lg pl-2 pr-4 py-1 mr-2 mt-2 w-fit hover:ring-2 ring-primary duration-100"
transition:fly={{ duration: 100, y: -100 }}
on:click={toggleShowDeck}
>
<div class="flex gap-2">
<div class="flex items-center">
<img src="/images/tcg/icons/card_character.png" alt="character card" class="w-8" />
<p class="text-white text-xl">{characterCount}</p>
</div>
<div class="flex items-center">
<img src="/images/tcg/icons/card.png" alt="character card" class="w-8" />
<p class="text-white text-xl">{actionCount}</p>
</div>
</div>
<p class="pl-2 text-white text-xs text-center">{$t('tcg.showDeck')}</p>
</button>
{/if}
</div>
{#if compare !== undefined}
<button
class="fixed bottom-2 right-2 bg-black bg-opacity-80 text-white rounded-lg px-3 py-2 mt-2 mr-2 hover:ring-2 ring-primary duration-100"
on:click={removeCompare}
transition:fly={{ duration: 100, y: 100 }}
>
<p>Remove Compare</p>
</button>
{/if}
</div>
<style lang="postcss">
.pill {
@apply border-2 border-white border-opacity-25;
@apply text-white text-sm 2xl:text-base;
@apply px-2 2xl:px-4 py-1 whitespace-nowrap;
@apply outline-none;
@apply transition duration-100;
@apply first:rounded-l-xl first:border-r-0;
@apply last:rounded-r-xl last:border-l-0;
&.solo {
@apply rounded-xl border-2;
}
&:hover {
@apply border-primary;
}
&.active {
@apply bg-primary;
@apply border-primary;
@apply text-background;
}
}
</style>

BIN
static/images/tcg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

BIN
static/images/tcg/cyno.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

BIN
static/images/tcg/diluc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

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