;
25 | }
--------------------------------------------------------------------------------
/src/components/AttacksExpr.tsx:
--------------------------------------------------------------------------------
1 | import DamageData from "../types/DamageData";
2 | import Attack from "../utils/Attack";
3 |
4 | const colors = ['#6dd0fc', '#fc6d85', '#befc6d'];
5 |
6 | export default function AttacksExpr(props: Readonly<{
7 | attacks: Attack[];
8 | prop: keyof DamageData;
9 | }>) {
10 | return props.attacks.map((attack, i) => {
11 | let level = 0;
12 |
13 | return [
14 | i > 0 ? ' + ' : '',
15 | attack.damage.getWithDefault(props.prop).fullRawExpr
16 | .replace(/\*/g, '×')
17 | .split(/([()])/g)
18 | .map((m, j) => {
19 | if (m !== '(' && m !== ')')
20 | return {m};
21 |
22 | if (m === ')') level--;
23 | const out = {m};
24 | if (m === '(') level++;
25 |
26 | return out;
27 | })
28 | ];
29 | }).flat();
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/LabelRow.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Column from "../utils/Column";
3 | import FormInput from "./FormInput";
4 | import RowLabel from "./RowLabel";
5 | import { ColumnStateAction } from "../types/ColumnState";
6 | import { topDescs } from "../utils/topDescs";
7 |
8 | export default function LabelRow(props: Readonly<{
9 | columns: Column[];
10 | dispatch: React.Dispatch;
11 | }>) {
12 | return <>
13 |
14 | {props.columns.map(column => props.dispatch({
18 | type: 'modifyAttack',
19 | colId: column.id,
20 | atkId: column.first.id,
21 | modifier: attack => {
22 | attack.label = value;
23 | }
24 | })}
25 | />)}
26 | >;
27 | }
--------------------------------------------------------------------------------
/src/less/HelpPage.less:
--------------------------------------------------------------------------------
1 | .help-page {
2 | line-height: 1.5em;
3 | max-width: 800px;
4 | text-align: start;
5 | margin-bottom: 2em;
6 | font-size: 1rem;
7 |
8 | nav {
9 | margin-top: 1em;
10 |
11 | ol {
12 | display: inline-flex;
13 | list-style: none;
14 | gap: 0.5em;
15 | margin: 0;
16 | padding: 0;
17 | padding-left: 0.5em;
18 | flex-wrap: wrap;
19 | }
20 | }
21 |
22 | h2 {
23 | margin-top: 0;
24 | padding-top: 4px;
25 | }
26 |
27 | & > h3 {
28 | margin: 2em 0 1em;
29 | padding-bottom: 0.5em;
30 | border-bottom: 1px solid white;
31 | }
32 |
33 | dl {
34 | display: grid;
35 | grid-template-columns: minmax(min-content, 20%) auto;
36 | grid-column-gap: 1em;
37 | grid-row-gap: 0.5em;
38 | margin-bottom: 1em;
39 |
40 | dt,
41 | dd {
42 | margin: 0;
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/SVGButton.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import '../less/SVGButton.less';
3 |
4 | type SVGButtonProps = {
5 | svg?: React.ReactNode;
6 | label: string;
7 | onClick?: () => any;
8 | hideLabel?: boolean;
9 | mini?: boolean;
10 | disabled?: boolean;
11 | title?: string;
12 | style?: React.CSSProperties;
13 | };
14 |
15 | export default React.forwardRef((props, ref) =>
16 |
30 | );
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Numeric Genshin Damage Calculator
2 |
3 | This React app allows you to easily calculate in-game character damage for Genshin Impact based off of numeric stat values.
4 |
5 | The live version of the app is located at .
6 |
7 | ## Running
8 |
9 | Install dependencies using `npm install`. Launch the app in development mode using `npm run start` and produce a build of the app using `npm run build`.
10 |
11 | ## Environment Variables
12 |
13 | Environment variables are defined in `.env` and can be overridden locally in the `.env.local` file see [Vite's documentation](https://vite.dev/guide/env-and-mode#env-files) for more info.
14 |
15 | * `VITE_ENKA_PROXY` - The prefix to use for requests to Enka.Network API to work around CORS. The proxy should send requests to `https://enka.network/api/[REQUEST_PATH]` By default this is .
16 |
--------------------------------------------------------------------------------
/src/components/DamageOutput.tsx:
--------------------------------------------------------------------------------
1 | import CalculationPopup from "./CalculationPopup";
2 | import DifferenceOutput from "./DifferenceOutput";
3 | import '../less/DamageOutput.less';
4 | import displayDamage from "../utils/displayDamage";
5 | import Column from "../utils/Column";
6 | import DisplayedProp from "../types/DisplayedProp";
7 | import DamageData from "../types/DamageData";
8 |
9 | export default function DamageOutput(props: Readonly<{
10 | column: Column;
11 | displayedProp: DisplayedProp;
12 | value: number;
13 | error?: boolean;
14 | initial?: number;
15 | }>) {
16 | return
17 |
18 | {' '}
19 |
20 | {' '}
21 |
22 |
23 | }
--------------------------------------------------------------------------------
/src/types/Stat.ts:
--------------------------------------------------------------------------------
1 | import attributes from "../utils/attributes";
2 | import elements from "../utils/elements";
3 | import DamageGroup from "./DamageGroups";
4 | import DisplayedProp from "./DisplayedProp";
5 | import StatData from "./StatData";
6 | import { StatSection } from "./StatSectionDefinition";
7 |
8 | type MapInfo =
9 | | {
10 | map: 'char';
11 | mapNumber: number;
12 | }
13 | | {
14 | map: 'fight';
15 | mapNumber: number | Record;
16 | };
17 |
18 | type Stat = DisplayedProp & (MapInfo | {}) & {
19 | attr?: typeof attributes[number];
20 | default: number;
21 | type: StatType;
22 | section: StatSection;
23 | groups?: DamageGroup;
24 | /**
25 | * True if the stat is a series of multipliers that are multiplied with in-game attributes (ATK etc.)
26 | */
27 | usesAttrs?: boolean;
28 | icon: React.ReactNode;
29 | };
30 |
31 | export const enum StatType {
32 | Number,
33 | Percent,
34 | Seconds
35 | }
36 |
37 | export default Stat;
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Brandon Fowler
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/components/AttackList.tsx:
--------------------------------------------------------------------------------
1 | import SVGButton from "./SVGButton";
2 | import DeleteSVG from "../svgs/DeleteSVG";
3 | import '../less/AttackList.less';
4 | import Attack from "../utils/Attack";
5 |
6 | export default function AttackList(props: Readonly<{
7 | attacks: Attack[];
8 | activeIndex: number;
9 | setActive: (id: number, index: number) => void;
10 | deleteAttack?: (id: number) => void;
11 | }>) {
12 | return <>
13 | {props.attacks.map((attack, i) =>
17 | props.setActive(attack.id, i)}
21 | />
22 | {props.deleteAttack && props.attacks.length > 1 && }
24 | label="Delete Attack"
25 | mini
26 | hideLabel
27 | onClick={() => props.deleteAttack!(attack.id)}
28 | />}
29 | )}
30 | >;
31 | }
--------------------------------------------------------------------------------
/src/components/TopButtonRow.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SVGButton from "./SVGButton";
3 | import AddSVG from "../svgs/AddSVG";
4 | import LoadSavedPopup from "./LoadSavedPopup";
5 | import '../less/TopButtonRow.less';
6 | import ImportPopup from "./ImportPopup";
7 | import ExportPopup from "./ExportPopup";
8 | import ColumnState, { ColumnStateAction } from '../types/ColumnState';
9 | import HelpSVG from '../svgs/HelpSVG';
10 | import { Link } from 'react-router';
11 |
12 | export default function TopButtonRow(props: Readonly<{
13 | state: ColumnState;
14 | dispatch: React.Dispatch;
15 | }>) {
16 | return
19 | The reaction type of the attack. Different reactions have different properties and multipliers.
20 |
21 |
22 | {spreadingReactions.map((reaction, i, arr) =>
23 |
24 | {i == arr.length - 1 ? ' and ' : i > 0 && ', '}
25 | {reaction.name}
26 |
27 | )}{' '}
28 | can trigger a further reaction when spread to another enemy. An additional input will be shown to calculate the additional damage from this secondary reaction.
29 |