├── .npmrc ├── src ├── routes │ ├── +layout.ts │ ├── +layout.svelte │ └── +page.svelte ├── lib │ ├── id.ts │ ├── components │ │ ├── label-content-grid.svelte │ │ ├── row.svelte │ │ ├── icon.svelte │ │ ├── v-sep.svelte │ │ ├── bottom.svelte │ │ ├── add-icon.svelte │ │ ├── title.svelte │ │ ├── info-icon.svelte │ │ ├── number-display.svelte │ │ ├── close-icon.svelte │ │ ├── resin.svelte │ │ ├── cell.svelte │ │ ├── fodder-input.svelte │ │ ├── mat-input.svelte │ │ ├── level-exp-table.svelte │ │ ├── resin-breakdown.svelte │ │ ├── required-cells.svelte │ │ └── exp-input.svelte │ ├── exp-calcs.ts │ └── data.ts ├── app.html ├── app.d.ts └── app.pcss ├── static ├── resin.webp ├── essence.png └── unction.png ├── .prettierignore ├── vite.config.ts ├── .gitignore ├── README.md ├── postcss.config.cjs ├── .eslintignore ├── .prettierrc ├── tailwind.config.cjs ├── tsconfig.json ├── .eslintrc.cjs ├── svelte.config.js └── package.json /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | export const prerender = true; 2 | -------------------------------------------------------------------------------- /static/resin.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrandonXLF/artifact-exp-calc/main/static/resin.webp -------------------------------------------------------------------------------- /src/lib/id.ts: -------------------------------------------------------------------------------- 1 | let count = 0; 2 | 3 | export function makeId() { 4 | return 'id-' + ++count; 5 | } 6 | -------------------------------------------------------------------------------- /static/essence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrandonXLF/artifact-exp-calc/main/static/essence.png -------------------------------------------------------------------------------- /static/unction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrandonXLF/artifact-exp-calc/main/static/unction.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore files for PNPM, NPM and YARN 2 | pnpm-lock.yaml 3 | package-lock.json 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /src/lib/components/label-content-grid.svelte: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | 7 |
8 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()] 6 | }); 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | build.sh 12 | .htaccess -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Genshin Artifact EXP Calculator 2 | 3 | Calculate the level fodder artifacts and artifact enhancement materials will bring an artifact to and how much artifact EXP / resin is needed to reach a specific level. 4 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const tailwindcss = require('tailwindcss'); 2 | const autoprefixer = require('autoprefixer'); 3 | 4 | const config = { 5 | plugins: [tailwindcss(), autoprefixer] 6 | }; 7 | 8 | module.exports = config; 9 | -------------------------------------------------------------------------------- /src/lib/components/row.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | 7 |
8 | -------------------------------------------------------------------------------- /src/lib/components/icon.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /src/lib/components/v-sep.svelte: -------------------------------------------------------------------------------- 1 |
2 |

3 |
4 |
5 |
6 |
7 | -------------------------------------------------------------------------------- /src/lib/components/bottom.svelte: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/lib/components/add-icon.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/components/title.svelte: -------------------------------------------------------------------------------- 1 |
2 |

Genshin Artifact EXP Calculator

3 |

4 | Calculate the level fodder artifacts and artifact enhancement materials will bring an artifact 5 | to and how much artifact EXP / resin is needed to reach a specific level. 6 |

7 |
8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], 7 | "overrides": [ 8 | { 9 | "files": "*.svelte", 10 | "options": { 11 | "parser": "svelte" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/components/info-icon.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/components/number-display.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | {formattedNumber}{#if unit}{' '}{unit}{/if} 14 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Genshin Artifact EXP Calculator 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/components/close-icon.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/lib/components/resin.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | 12 | type Rarity = 1 | 2 | 3 | 4 | 5; 13 | type DomainLevel = 1 | 2 | 3 | 4; 14 | type DomainRarity = 3 | 4 | 5; 15 | 16 | type ResinInfo = { 17 | domainLevel: DomainLevel; 18 | use: Record; 19 | }; 20 | 21 | type Fodder = [number, Rarity, number]; 22 | } 23 | 24 | export {}; 25 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config}*/ 2 | const config = { 3 | content: ['./src/**/*.{html,js,svelte,ts}'], 4 | 5 | theme: { 6 | extend: { 7 | borderColor: { 8 | DEFAULT: '#aaa', 9 | light: '#777', 10 | 'extra-light': '#444' 11 | }, 12 | colors: { 13 | main: '#222', 14 | secondary: '#333', 15 | input: '#282828' 16 | }, 17 | dropShadow: { 18 | popover: '0 3px 6px #222' 19 | }, 20 | gridTemplateColumns: { 21 | 'label-content': 'max-content 1fr' 22 | } 23 | } 24 | }, 25 | 26 | plugins: [] 27 | }; 28 | 29 | module.exports = config; 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 15 | // 16 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 17 | // from the referenced tsconfig.json - TypeScript does not merge them in 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/components/cell.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | {#if title} 11 |

{title}

12 | {/if} 13 |
22 | 23 |
24 |
25 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type { import("eslint").Linter.Config } */ 2 | module.exports = { 3 | root: true, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:svelte/recommended', 8 | 'prettier' 9 | ], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['@typescript-eslint'], 12 | parserOptions: { 13 | sourceType: 'module', 14 | ecmaVersion: 2020, 15 | extraFileExtensions: ['.svelte'] 16 | }, 17 | env: { 18 | browser: true, 19 | es2017: true, 20 | node: true 21 | }, 22 | overrides: [ 23 | { 24 | files: ['*.svelte'], 25 | parser: 'svelte-eslint-parser', 26 | parserOptions: { 27 | parser: '@typescript-eslint/parser' 28 | } 29 | } 30 | ] 31 | }; 32 | -------------------------------------------------------------------------------- /src/app.pcss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | @layer base { 4 | html { 5 | line-height: 1; 6 | } 7 | 8 | body { 9 | @apply bg-main; 10 | @apply text-white; 11 | @apply m-4; 12 | @apply [color-scheme:dark]; 13 | @apply [height:unset]; 14 | } 15 | 16 | main { 17 | line-height: 1.5; 18 | } 19 | 20 | a { 21 | @apply text-blue-300; 22 | } 23 | 24 | a:focus, 25 | a:hover { 26 | @apply underline; 27 | } 28 | 29 | summary { 30 | @apply cursor-pointer; 31 | @apply select-none; 32 | } 33 | 34 | select, 35 | input[type='number'] { 36 | @apply px-1; 37 | @apply py-0.5; 38 | @apply border; 39 | @apply border-light; 40 | @apply rounded-md; 41 | @apply bg-input; 42 | } 43 | 44 | button { 45 | @apply border-none; 46 | @apply hover:bg-secondary; 47 | @apply focus:bg-secondary; 48 | } 49 | 50 | h3 { 51 | @apply mb-2; 52 | } 53 | } 54 | 55 | @tailwind components; 56 | @tailwind utilities; 57 | -------------------------------------------------------------------------------- /src/lib/components/fodder-input.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 | {#each fodderList as [exp, rarity, times], i} 16 | 17 | deleteFodder(i)} 24 | /> 25 | 26 | {/each} 27 |
28 | 35 |
36 |
37 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-static'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: [vitePreprocess({})], 9 | 10 | kit: { 11 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 12 | // If your environment is not supported or you settled on a specific environment, switch out the adapter. 13 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 14 | adapter: adapter({ 15 | // default options are shown. On some platforms 16 | // these options are set automatically — see below 17 | pages: 'build', 18 | assets: 'build', 19 | fallback: undefined, 20 | precompress: false, 21 | strict: true 22 | }) 23 | } 24 | }; 25 | 26 | export default config; 27 | -------------------------------------------------------------------------------- /src/lib/components/mat-input.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 24 | 25 | 28 | 29 |
=
30 | 31 |
32 | -------------------------------------------------------------------------------- /src/lib/components/level-exp-table.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {#each EXP_AMOUNTS[rarity] as x, i} 18 | 19 | 22 | 25 | {#if i === 0} 26 | 27 | {/if} 28 | 29 | 30 | {#if i < LEVEL_MAXES[rarity]} 31 | 34 | {:else} 35 | 36 | {/if} 37 | 38 | {/each} 39 | 40 |
LevelTotal EXPEXP to Next
20 | {i} 21 | 23 | 24 |
32 | + 33 |
41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "artifact-exp-calc", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite dev", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 11 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 12 | "lint": "prettier --check . && eslint .", 13 | "format": "prettier --write ." 14 | }, 15 | "devDependencies": { 16 | "@sveltejs/adapter-static": "^3.0.1", 17 | "@sveltejs/kit": "^2.0.0", 18 | "@sveltejs/vite-plugin-svelte": "^3.0.0", 19 | "@types/eslint": "^8.56.0", 20 | "@typescript-eslint/eslint-plugin": "^7.0.0", 21 | "@typescript-eslint/parser": "^7.0.0", 22 | "autoprefixer": "^10.4.16", 23 | "eslint": "^8.56.0", 24 | "eslint-config-prettier": "^9.1.0", 25 | "eslint-plugin-svelte": "^2.35.1", 26 | "postcss": "^8.4.32", 27 | "postcss-load-config": "^5.0.2", 28 | "prettier": "^3.1.1", 29 | "prettier-plugin-svelte": "^3.1.2", 30 | "prettier-plugin-tailwindcss": "^0.5.9", 31 | "svelte": "^4.2.7", 32 | "svelte-check": "^3.6.0", 33 | "svelte-popperjs": "^1.3.2", 34 | "tailwindcss": "^3.3.6", 35 | "tslib": "^2.4.1", 36 | "typescript": "^5.0.0", 37 | "vite": "^5.0.3" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/exp-calcs.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ARTIFACTS_PER_RUN, 3 | DOMAIN_RARITIES, 4 | EXP_AMOUNTS, 5 | EXP_BASES, 6 | FODDER_EXP_VALUE, 7 | LEVEL_MAXES 8 | } from './data'; 9 | 10 | export function calcFodderExp(exp: number, rarity: Rarity) { 11 | return EXP_BASES[rarity] + exp * FODDER_EXP_VALUE; 12 | } 13 | 14 | export function calculateTotalExp(current: number, mat: number, fodderList: Fodder[]) { 15 | return ( 16 | current + 17 | mat + 18 | fodderList.reduce((prev, [exp, rarity, times]) => prev + calcFodderExp(exp, rarity) * times, 0) 19 | ); 20 | } 21 | 22 | export function calcRunsRequired(exp: number, { domainLevel, use }: ResinInfo) { 23 | let expPerRun = 0; 24 | 25 | for (const rarity of DOMAIN_RARITIES) { 26 | if (!use[rarity]) continue; 27 | expPerRun += EXP_BASES[rarity] * ARTIFACTS_PER_RUN[domainLevel][rarity]; 28 | } 29 | 30 | return exp / expPerRun; 31 | } 32 | 33 | export function calcLevelAndRemainder(exp: number, rarity: Rarity): [number, number] { 34 | let start = 0; 35 | let end: number = EXP_AMOUNTS[rarity].length; 36 | 37 | while (end - start > 1) { 38 | const currentIndex = Math.floor((start + end) / 2); 39 | 40 | if (EXP_AMOUNTS[rarity][currentIndex] < exp) { 41 | start = currentIndex; 42 | } else if (EXP_AMOUNTS[rarity][currentIndex] > exp) { 43 | end = currentIndex; 44 | } else { 45 | return [currentIndex, 0]; 46 | } 47 | } 48 | 49 | return [start, exp - EXP_AMOUNTS[rarity][start]]; 50 | } 51 | 52 | export function calcMaxExp(rarity: Rarity) { 53 | return EXP_AMOUNTS[rarity][LEVEL_MAXES[rarity]]!; 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/components/resin-breakdown.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 |
21 |
22 | 31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {#each DOMAIN_RARITIES as rarity} 43 | 44 | 45 | 46 | 51 | 56 | 57 | {/each} 58 | 59 |
Averagex1 Only
{rarity}-star 47 | {#if resinInfo.use[rarity]} 48 | 49 | {:else}-{/if} 50 | 52 | {#if resinInfo.use[rarity]} 53 | 54 | {:else}-{/if} 55 |
60 |
61 | -------------------------------------------------------------------------------- /src/lib/data.ts: -------------------------------------------------------------------------------- 1 | export const FODDER_EXP_VALUE = 0.8; 2 | export const AVERAGE_BONUS = 1.13; 3 | 4 | type LevelValues = T extends { length: L } 5 | ? [number, ...T] 6 | : LevelValues; 7 | 8 | type RarityLevelValues = { 9 | [K in Rarity]: LevelValues<(typeof LEVEL_MAXES)[K]>; 10 | }; 11 | 12 | export const EXP_AMOUNTS: RarityLevelValues = { 13 | 5: [ 14 | 0, 3000, 6725, 11150, 16300, 22200, 28875, 36375, 44725, 53950, 64075, 75125, 87150, 100175, 15 | 115325, 132925, 153300, 176800, 203850, 234900, 270475 16 | ], 17 | 4: [ 18 | 0, 2400, 5375, 8925, 13050, 17775, 23125, 29125, 35800, 43175, 51275, 60125, 69750, 80175, 19 | 92300, 106375, 122675 20 | ], 21 | 3: [0, 1800, 4025, 6675, 9775, 13325, 17325, 21825, 26825, 32350, 38425, 45050, 52275], 22 | 2: [0, 1200, 2700, 4475, 6525], 23 | 1: [0, 600, 1350, 2225, 3250] 24 | }; 25 | 26 | export const EXP_MAXES: RarityLevelValues = { 27 | 5: [ 28 | 3000, 3725, 4425, 5150, 5900, 6675, 7500, 8350, 9225, 10125, 11050, 12025, 13025, 15150, 17600, 29 | 20375, 23500, 27050, 31050, 35575, 0 30 | ], 31 | 4: [ 32 | 2400, 2975, 3550, 4125, 4725, 5350, 6000, 6675, 7375, 8100, 8850, 9625, 10425, 12125, 14075, 33 | 16300, 0 34 | ], 35 | 3: [1800, 2225, 2650, 3100, 3550, 4000, 4500, 5000, 5525, 6075, 6625, 7225, 0], 36 | 2: [1200, 1500, 1775, 2050, 0], 37 | 1: [600, 750, 875, 1025, 0] 38 | }; 39 | 40 | export const EXP_BASES: Record = { 41 | 1: 420, 42 | 2: 840, 43 | 3: 1260, 44 | 4: 2520, 45 | 5: 3780 46 | }; 47 | 48 | export const LEVEL_MAXES = { 49 | 5: 20, 50 | 4: 16, 51 | 3: 12, 52 | 2: 4, 53 | 1: 4 54 | } as const satisfies Record; 55 | 56 | export const ARTIFACTS_PER_RUN: Record> = { 57 | 1: { 58 | 3: 6.39, 59 | 4: 0.71, 60 | 5: 0 61 | }, 62 | 2: { 63 | 3: 5.68, 64 | 4: 1.42, 65 | 5: 0 66 | }, 67 | 3: { 68 | 3: 4.97, 69 | 4: 1.775, 70 | 5: 0.355 71 | }, 72 | 4: { 73 | 3: 3.55, 74 | 4: 2.485, 75 | 5: 1.064 76 | } 77 | }; 78 | 79 | export const RESIN_PER_RUN = 20; 80 | export const DOMAIN_RARITIES = [3, 4, 5] as DomainRarity[]; 81 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 |
27 | 28 | 29 | </Row> 30 | <Row noSep> 31 | <Cell title="Current"> 32 | <ExpInput bind:exp={currentExp} bind:rarity /> 33 | </Cell> 34 | <VSep /> 35 | <Cell title="Materials"> 36 | <MatInput bind:exp={matExp} /> 37 | </Cell> 38 | <VSep /> 39 | <Cell title="Fodder / Salvage" fullHeight noStyle> 40 | <FodderInput bind:fodderList /> 41 | </Cell> 42 | </Row> 43 | <Row noSep> 44 | <details> 45 | <summary>Show EXP at each level</summary> 46 | <div class="mt-4"> 47 | <Cell title="EXP at Each Level"> 48 | <LevelExpTable {rarity} /> 49 | </Cell> 50 | </div> 51 | </details> 52 | </Row> 53 | <Row> 54 | <Cell title="Final"> 55 | <ExpInput bind:exp={final} {rarity} noRaritySet readOnly /> 56 | </Cell> 57 | </Row> 58 | <Row> 59 | <Cell title="Target"> 60 | <ExpInput bind:exp={targetExp} {rarity} noRaritySet noRemainder /> 61 | </Cell> 62 | </Row> 63 | <Row> 64 | <RequiredCells {waste} {baseRequired} /> 65 | </Row> 66 | <Row noSep> 67 | <Bottom /> 68 | </Row> 69 | </div> 70 | -------------------------------------------------------------------------------- /src/lib/components/required-cells.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import Resin from '$lib/components/resin.svelte'; 3 | import NumberDisplay from '$lib/components/number-display.svelte'; 4 | import { AVERAGE_BONUS } from '$lib/data'; 5 | import ResinBreakdown from '$lib/components/resin-breakdown.svelte'; 6 | import { calcRunsRequired } from '$lib/exp-calcs'; 7 | import Cell from '$lib/components/cell.svelte'; 8 | import InfoIcon from './info-icon.svelte'; 9 | import { createPopperActions } from 'svelte-popperjs'; 10 | import { makeId } from '$lib/id'; 11 | import LabelContentGrid from './label-content-grid.svelte'; 12 | 13 | let requiredWC: number; 14 | export { requiredWC as baseRequired }; 15 | export let waste: boolean; 16 | 17 | const [popperRef, popperContent] = createPopperActions(); 18 | const popperId = makeId(); 19 | 20 | let resinInfo = { 21 | domainLevel: 4, 22 | use: { 3: true, 4: true, 5: false } 23 | } satisfies ResinInfo; 24 | let showResinInfo = false; 25 | let mouseEnterTick = false; 26 | 27 | $: requiredAvg = requiredWC / AVERAGE_BONUS; 28 | $: requiredRunsAvg = calcRunsRequired(requiredAvg, resinInfo); 29 | $: requiredRunsWC = calcRunsRequired(requiredWC, resinInfo); 30 | </script> 31 | 32 | <Cell title={waste ? 'Excess' : 'Required'} titleClass={waste ? 'text-red-400' : ''}> 33 | <LabelContentGrid> 34 | <span title="Including 2x and 5x enhancements">Average</span> 35 | <span> 36 | <NumberDisplay number={requiredAvg} unit="exp" /> 37 | ≈ 38 | <Resin requiredRuns={requiredRunsAvg} /> 39 | </span> 40 | <span>x1 Only</span> 41 | <span> 42 | <NumberDisplay number={requiredWC} unit="exp" /> 43 | ≈ 44 | <Resin requiredRuns={requiredRunsWC} /> 45 | </span> 46 | </LabelContentGrid> 47 | </Cell> 48 | 49 | <div class="flex flex-col items-center"> 50 | <h3 class="text-xl font-bold">​</h3> 51 | <!-- svelte-ignore a11y-no-static-element-interactions --> 52 | <div class="flex flex-1 items-center" on:mouseleave={() => (showResinInfo = false)}> 53 | <button 54 | use:popperRef 55 | aria-label="Toggle resin breakdown" 56 | aria-expanded={showResinInfo ? 'true' : 'false'} 57 | aria-controls={showResinInfo ? popperId : undefined} 58 | class="rounded-lg border border-solid border-light bg-secondary p-2" 59 | on:click={() => { 60 | if (!mouseEnterTick) { 61 | showResinInfo = !showResinInfo; 62 | } 63 | }} 64 | on:mouseenter={() => { 65 | showResinInfo = true; 66 | 67 | mouseEnterTick = true; 68 | setTimeout(() => (mouseEnterTick = false)); 69 | }} 70 | > 71 | <InfoIcon /> 72 | </button> 73 | {#if showResinInfo} 74 | <div 75 | id={popperId} 76 | use:popperContent={{ 77 | placement: 'top-start', 78 | strategy: 'fixed', 79 | modifiers: [{ name: 'offset', options: { offset: [0, 10] } }] 80 | }} 81 | > 82 | <Cell 83 | title="Resin Breakdown" 84 | titleClass="drop-shadow-popover" 85 | cellClass="drop-shadow-popover" 86 | > 87 | <ResinBreakdown bind:resinInfo {requiredRunsAvg} {requiredRunsWC} /> 88 | </Cell> 89 | </div> 90 | {/if} 91 | </div> 92 | </div> 93 | -------------------------------------------------------------------------------- /src/lib/components/exp-input.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import { calcFodderExp, calcLevelAndRemainder, calcMaxExp } from '$lib/exp-calcs'; 3 | import { EXP_AMOUNTS, EXP_MAXES, LEVEL_MAXES } from '$lib/data'; 4 | import { createEventDispatcher } from 'svelte'; 5 | import NumberDisplay from '$lib/components/number-display.svelte'; 6 | import { makeId } from '$lib/id'; 7 | import CloseIcon from './close-icon.svelte'; 8 | import LabelContentGrid from './label-content-grid.svelte'; 9 | 10 | export let exp = 0; 11 | export let times = 1; 12 | export let rarity: Rarity = 5; 13 | export let noRemainder = false; 14 | export let noRaritySet = false; 15 | export let readOnly = false; 16 | export let showTimes = false; 17 | export let isFodder = false; 18 | 19 | let expVersion = 1; 20 | let levelInput: HTMLInputElement | undefined; 21 | let remainderInput: HTMLInputElement | undefined; 22 | 23 | const dispatch = createEventDispatcher(); 24 | const exportId = makeId(); 25 | const levelId = makeId(); 26 | const expId = makeId(); 27 | 28 | function setExp(newLevel: number, newRemainder: number, capRemainder = false) { 29 | const maxLevel = LEVEL_MAXES[rarity]; 30 | if (newLevel > maxLevel) newLevel = maxLevel; 31 | 32 | if (capRemainder) { 33 | let maxExtra = EXP_MAXES[rarity][newLevel]; 34 | if (maxExtra > 0) maxExtra--; 35 | 36 | newRemainder = Math.min(newRemainder, maxExtra); 37 | } 38 | 39 | const inputExp = EXP_AMOUNTS[rarity][newLevel] + newRemainder; 40 | exp = Math.min(inputExp, calcMaxExp(rarity)); 41 | 42 | expVersion++; 43 | } 44 | 45 | function setRarity(newRarity: Rarity) { 46 | rarity = newRarity; 47 | setExp(level, remainder, true); // Important: Increments expVersion 48 | } 49 | 50 | function updateInputs() { 51 | if (levelInput) levelInput.valueAsNumber = level; 52 | if (remainderInput) remainderInput.valueAsNumber = remainder; 53 | } 54 | 55 | $: [level, remainder] = calcLevelAndRemainder(exp, rarity); 56 | $: expVersion, updateInputs(); 57 | </script> 58 | 59 | <LabelContentGrid> 60 | {#if !noRaritySet} 61 | <label for={exportId} class="text-right">Rarity</label> 62 | <select id={exportId} value={rarity} on:change={(e) => setRarity(+e.target.value)}> 63 | <option value={1}>1</option> 64 | <option value={2}>2</option> 65 | <option value={3}>3</option> 66 | <option value={4}>4</option> 67 | <option value={5}>5</option> 68 | </select> 69 | <hr class="col-span-2 border-t" /> 70 | {/if} 71 | <label for={levelId} class="text-right">Level</label> 72 | {#if readOnly} 73 | <NumberDisplay number={level} /> 74 | {:else} 75 | <input 76 | id={levelId} 77 | type="number" 78 | value={level} 79 | min="0" 80 | max={LEVEL_MAXES[rarity]} 81 | readonly={readOnly} 82 | bind:this={levelInput} 83 | on:change={(e) => setExp(e.currentTarget.valueAsNumber, remainder, true)} 84 | /> 85 | {/if} 86 | {#if !noRemainder} 87 | <label for={expId} class="text-right">EXP</label> 88 | {#if readOnly} 89 | <span class="whitespace-nowrap"> 90 | <NumberDisplay number={remainder} /> 91 | / 92 | <NumberDisplay number={EXP_MAXES[rarity][level]} /> 93 | </span> 94 | {:else} 95 | <input 96 | id={expId} 97 | type="number" 98 | value={remainder} 99 | min="0" 100 | max={EXP_MAXES[rarity][level]} 101 | readonly={readOnly} 102 | bind:this={remainderInput} 103 | on:change={(e) => setExp(level, e.currentTarget.valueAsNumber)} 104 | /> 105 | {/if} 106 | {/if} 107 | <div class="text-right">=</div> 108 | <NumberDisplay number={exp} unit="exp" /> 109 | {#if !noRaritySet && (isFodder || showTimes)} 110 | <hr class="col-span-2 border-t" /> 111 | {/if} 112 | {#if isFodder} 113 | <div class="text-right">Value</div> 114 | <NumberDisplay number={calcFodderExp(exp, rarity)} unit="exp" /> 115 | {/if} 116 | {#if showTimes} 117 | <div class="text-right text-lg">×</div> 118 | <div class="flex gap-2"> 119 | <input bind:value={times} type="number" min="0" class="w-12" /> 120 | <button title="Remove" class="text-red-300" on:click={() => dispatch('delete')}> 121 | <CloseIcon /> 122 | </button> 123 | </div> 124 | {/if} 125 | </LabelContentGrid> 126 | --------------------------------------------------------------------------------