├── src
├── styles
│ └── _variables.scss
├── app
│ ├── footer
│ │ ├── footer.component.scss
│ │ ├── footer.component.html
│ │ └── footer.component.ts
│ ├── output
│ │ ├── output.component.scss
│ │ ├── output.component.html
│ │ └── output.component.ts
│ ├── io-row
│ │ ├── io-row.component.html
│ │ ├── io-row.component.scss
│ │ └── io-row.component.ts
│ ├── efficiency-graph
│ │ ├── efficiency-graph.component.html
│ │ └── efficiency-graph.component.ts
│ ├── equations
│ │ ├── equations.component.html
│ │ ├── equations.component.ts
│ │ └── equations.component.scss
│ ├── app.component.scss
│ ├── app.component.html
│ ├── input
│ │ ├── input.component.html
│ │ ├── input.component.scss
│ │ └── input.component.ts
│ ├── app.component.ts
│ ├── ratio.pipe.ts
│ ├── percentage.pipe.ts
│ ├── calculator
│ │ ├── calculator.component.ts
│ │ ├── calculator.component.scss
│ │ └── calculator.component.html
│ ├── calculation-row
│ │ ├── calculation-row.component.ts
│ │ └── calculation-row.component.html
│ ├── safe-html.pipe.ts
│ ├── base-graph
│ │ ├── base-graph.component.scss
│ │ ├── base-graph.component.html
│ │ └── base-graph.component.ts
│ ├── crit-info.ts
│ ├── app.module.ts
│ └── math.html
├── html.d.ts
├── main.ts
├── index.html
└── styles.scss
├── webpack.config.js
├── tsconfig.app.json
├── tsconfig.spec.json
├── .editorconfig
├── .gitignore
├── README.md
├── tsconfig.json
├── package.json
├── LICENSE
└── angular.json
/src/styles/_variables.scss:
--------------------------------------------------------------------------------
1 | $hoz-gap: 2em;
2 |
--------------------------------------------------------------------------------
/src/app/footer/footer.component.scss:
--------------------------------------------------------------------------------
1 | footer {
2 | text-align: center;
3 | }
--------------------------------------------------------------------------------
/src/app/output/output.component.scss:
--------------------------------------------------------------------------------
1 | .label-text {
2 | margin-bottom: 4px;
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/io-row/io-row.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/html.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.html" {
2 | const content: string;
3 | export default content;
4 | }
5 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | module: {
3 | rules: [
4 | {
5 | test: /\.html$/i,
6 | use: 'raw-loader'
7 | }
8 | ]
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/src/app/footer/footer.component.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/io-row/io-row.component.scss:
--------------------------------------------------------------------------------
1 | .row {
2 | text-align: left;
3 | display: flex;
4 | gap: 1em;
5 | justify-content: space-evenly;
6 | min-width: min-content;
7 | margin: 1em 0;
8 | }
9 |
--------------------------------------------------------------------------------
/src/app/efficiency-graph/efficiency-graph.component.html:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/app/equations/equations.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Equations
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
2 | import { AppModule } from './app/app.module';
3 |
4 | platformBrowserDynamic().bootstrapModule(AppModule)
5 | .catch(err => console.error(err));
6 |
--------------------------------------------------------------------------------
/src/app/output/output.component.html:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./out-tsc/app",
5 | "types": []
6 | },
7 | "files": [
8 | "src/main.ts"
9 | ],
10 | "include": [
11 | "src/**/*.d.ts"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./out-tsc/spec",
5 | "types": [
6 | "jasmine"
7 | ]
8 | },
9 | "include": [
10 | "src/**/*.spec.ts",
11 | "src/**/*.d.ts"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/io-row/io-row.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-io-row',
5 | templateUrl: './io-row.component.html',
6 | styleUrl: './io-row.component.scss'
7 | })
8 | export class IORowComponent {
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/footer/footer.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-footer',
5 | templateUrl: './footer.component.html',
6 | styleUrls: ['./footer.component.scss']
7 | })
8 | export class FooterComponent {
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/app.component.scss:
--------------------------------------------------------------------------------
1 | @use '../styles/variables' as *;
2 |
3 | header {
4 | text-align: center;
5 | }
6 |
7 | main {
8 | display: flex;
9 | flex-direction: column;
10 | max-width: max-content;
11 | min-width: min(350px, 100%);
12 | margin: auto;
13 | gap: $hoz-gap;
14 | }
15 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Critical Analysis
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = tab
6 | indent_size = 4
7 | insert_final_newline = true
8 | trim_trailing_whitespace = true
9 |
10 | [*.ts]
11 | quote_type = single
12 |
13 | [*.md]
14 | max_line_length = off
15 | trim_trailing_whitespace = false
16 |
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 | Critical Analysis
3 | CRIT Rate and Damage Efficiency Analyzer
4 | Genshin Impact • Honkai: Star Rail
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/app/input/input.component.html:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { CritInfo } from './crit-info';
3 |
4 | @Component({
5 | selector: 'app-root',
6 | templateUrl: './app.component.html',
7 | styleUrls: ['./app.component.scss']
8 | })
9 | export class AppComponent {
10 | critInfo = new CritInfo(0.05, 0.5)
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/ratio.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from '@angular/core';
2 | import { formatNumber } from '@angular/common';
3 |
4 | @Pipe({
5 | name: 'ratio'
6 | })
7 | export class RatioPipe implements PipeTransform {
8 | transform(value: number): unknown {
9 | return '1:' + formatNumber(value, 'en', '1.0-2');
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/equations/equations.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import math from './../math.html';
3 |
4 | @Component({
5 | selector: 'app-equations',
6 | templateUrl: './equations.component.html',
7 | styleUrls: ['./equations.component.scss']
8 | })
9 | export class EquationsComponent {
10 | math = math;
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/input/input.component.scss:
--------------------------------------------------------------------------------
1 | :host ::ng-deep {
2 | .mat-mdc-form-field-infix {
3 | width: 5em !important;
4 | padding-top: 8px !important;
5 | padding-bottom: 8px !important;
6 | min-height: unset !important;
7 |
8 | input {
9 | margin: 0;
10 | }
11 | }
12 | }
13 |
14 | .label-text {
15 | margin-bottom: 8px;
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/output/output.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-output',
5 | templateUrl: './output.component.html',
6 | styleUrls: ['./output.component.scss']
7 | })
8 | export class OutputComponent {
9 | @Input() name?: string;
10 | @Input() tooltip: string = '';
11 | }
12 |
13 |
--------------------------------------------------------------------------------
/src/app/percentage.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from '@angular/core';
2 | import { formatNumber } from '@angular/common';
3 |
4 | @Pipe({
5 | name: 'percentage'
6 | })
7 | export class PercentagePipe implements PipeTransform {
8 | transform(value: number): unknown {
9 | return formatNumber(value * 100, 'en', '1.0-2') + '%';
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/calculator/calculator.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 | import { CritInfo } from '../crit-info';
3 |
4 | @Component({
5 | selector: 'app-calculator',
6 | templateUrl: './calculator.component.html',
7 | styleUrls: ['./calculator.component.scss']
8 | })
9 | export class CalculatorComponent {
10 | @Input() critInfo!: CritInfo;
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/equations/equations.component.scss:
--------------------------------------------------------------------------------
1 | :host ::ng-deep {
2 | mat-expansion-panel-header {
3 | .mat-content {
4 | align-items: center;
5 | justify-content: center;
6 | }
7 | }
8 |
9 | math {
10 | * {
11 | text-align: left;
12 | }
13 | }
14 |
15 | h3 {
16 | margin: 1.5em 0 0.75em;
17 |
18 | &:first-child {
19 | margin-top: 0;
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/calculation-row/calculation-row.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-calculation-row',
5 | templateUrl: './calculation-row.component.html'
6 | })
7 | export class CalculationRowComponent {
8 | @Input() value!: number;
9 | @Input() ratio!: number;
10 | @Input() multiplier!: number;
11 | @Input() efficiency!: number;
12 | }
13 |
--------------------------------------------------------------------------------
/src/app/safe-html.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from '@angular/core';
2 | import { DomSanitizer } from '@angular/platform-browser';
3 |
4 | @Pipe({
5 | name: 'safeHtml'
6 | })
7 | export class SafeHtmlPipe implements PipeTransform {
8 | constructor(private sanitizer: DomSanitizer){}
9 |
10 | transform(value: string): unknown {
11 | return this.sanitizer.bypassSecurityTrustHtml(value);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/base-graph/base-graph.component.scss:
--------------------------------------------------------------------------------
1 | path {
2 | vector-effect: non-scaling-stroke;
3 | }
4 |
5 | .point {
6 | stroke-width: 10px;
7 | stroke-linecap: round;
8 | fill: none;
9 | }
10 |
11 | .crisp {
12 | shape-rendering: crispedges;
13 | }
14 |
15 | .text {
16 | paint-order: stroke;
17 | stroke: #424242;
18 | stroke-width: 2px;
19 |
20 | &.sideways {
21 | transform: rotate(180deg);
22 | writing-mode: vertical-lr;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/app/calculator/calculator.component.scss:
--------------------------------------------------------------------------------
1 | @use '../../styles/variables' as *;
2 |
3 | article {
4 | display: flex;
5 | gap: $hoz-gap;
6 | text-align: center;
7 | flex-direction: column;
8 | }
9 |
10 | .crit-row {
11 | font-size: 115%;
12 | }
13 |
14 | mat-card-header {
15 | justify-content: center;
16 |
17 | h2 {
18 | margin: 0;
19 | }
20 | }
21 |
22 | .graph {
23 | display: block;
24 | width: 22em;
25 | margin: 1em auto 0;
26 | }
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled output
2 | /dist
3 | /tmp
4 | /out-tsc
5 | /bazel-out
6 |
7 | # Node
8 | /node_modules
9 | npm-debug.log
10 | yarn-error.log
11 |
12 | # IDEs and editors
13 | .idea/
14 | .project
15 | .classpath
16 | .c9/
17 | *.launch
18 | .settings/
19 | *.sublime-workspace
20 |
21 | # Visual Studio Code
22 | .vscode/*
23 | !.vscode/settings.json
24 | !.vscode/tasks.json
25 | !.vscode/launch.json
26 | !.vscode/extensions.json
27 | .history/*
28 |
29 | # Miscellaneous
30 | /.angular/cache
31 | .sass-cache/
32 | /connect.lock
33 | /coverage
34 | /libpeerconnection.log
35 | testem.log
36 | /typings
37 |
38 | # System files
39 | .DS_Store
40 | Thumbs.db
41 |
--------------------------------------------------------------------------------
/src/app/calculation-row/calculation-row.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{value | percentage}}
4 |
5 |
6 | {{ratio | ratio}}
7 |
8 |
9 | {{multiplier | percentage}}
10 |
11 |
12 | {{efficiency | percentage}}
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/app/input/input.component.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter, Component, Input, Output } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-input',
5 | templateUrl: './input.component.html',
6 | styleUrls: ['./input.component.scss']
7 | })
8 | export class InputComponent {
9 | static PRECISION = 1000;
10 |
11 | @Input() name?: string;
12 | @Input() value: number = 0;
13 | @Input() min: number = -Infinity;
14 |
15 | @Output() valueChange = new EventEmitter();
16 |
17 | updateValue(e: Event) {
18 | const el = e.target as HTMLInputElement;
19 |
20 | this.valueChange.emit(Math.max(this.min, el.valueAsNumber / 100));
21 | }
22 |
23 | get displayValue() {
24 | return Math.round(this.value * 100 * InputComponent.PRECISION) / InputComponent.PRECISION;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Critical Analysis
2 |
3 | > Genshin Impact and Honkai: Star Rail CRIT rate and damage efficiency analyzer.
4 |
5 | Critical Analysis is a calculator for visualizing and optimizing the efficiency of a character's CRIT Rate and CRIT Damage in Genshin Impact or Honkai: Star Rail.
6 |
7 | ## Development server
8 |
9 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
10 |
11 | ## Code scaffolding
12 |
13 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
14 |
15 | ## Build
16 |
17 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
18 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "outDir": "./dist/out-tsc",
6 | "forceConsistentCasingInFileNames": true,
7 | "esModuleInterop": true,
8 | "strict": true,
9 | "noImplicitOverride": true,
10 | "noPropertyAccessFromIndexSignature": true,
11 | "noImplicitReturns": true,
12 | "noFallthroughCasesInSwitch": true,
13 | "sourceMap": true,
14 | "declaration": false,
15 | "experimentalDecorators": true,
16 | "moduleResolution": "node",
17 | "importHelpers": true,
18 | "target": "ES2022",
19 | "module": "ES2022",
20 | "useDefineForClassFields": false,
21 | "lib": [
22 | "ES2022",
23 | "dom"
24 | ]
25 | },
26 | "angularCompilerOptions": {
27 | "enableI18nLegacyMessageIdFormat": false,
28 | "strictInjectionParameters": true,
29 | "strictInputAccessModifiers": true,
30 | "strictTemplates": true
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/app/base-graph/base-graph.component.html:
--------------------------------------------------------------------------------
1 |
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "critical-analysis",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "ng": "ng",
6 | "start": "ng serve",
7 | "build": "ng build"
8 | },
9 | "private": true,
10 | "dependencies": {
11 | "@angular/animations": "^18.2.6",
12 | "@angular/cdk": "^18.2.6",
13 | "@angular/common": "^18.2.6",
14 | "@angular/compiler": "^18.2.6",
15 | "@angular/core": "^18.2.6",
16 | "@angular/forms": "^18.2.6",
17 | "@angular/material": "^18.2.6",
18 | "@angular/platform-browser": "^18.2.6",
19 | "@angular/platform-browser-dynamic": "^18.2.6",
20 | "@angular/router": "^18.2.6",
21 | "rxjs": "^7.8.0",
22 | "tslib": "^2.3.0",
23 | "zone.js": "~0.14.0"
24 | },
25 | "devDependencies": {
26 | "@angular-builders/custom-webpack": "^18.0.0",
27 | "@angular/build": "^18.2.6",
28 | "@angular/cli": "^18.2.6",
29 | "@angular/compiler-cli": "^18.2.6",
30 | "raw-loader": "^4.0.2",
31 | "typescript": "^5.5.4"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 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/app/crit-info.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from "@angular/core";
2 |
3 | export class CritInfo {
4 | value!: number;
5 | multiplier!: number;
6 | ratio!: number;
7 |
8 | bestRate!: number;
9 | bestDmg!: number;
10 | bestMultiplier!: number;
11 | bestRatio!: number;
12 |
13 | efficiency!: number;
14 | afterRecalculate = new EventEmitter();
15 |
16 | constructor(private _rate: number, private _dmg: number) {
17 | this.recalculate();
18 | }
19 |
20 | get rate() {
21 | return this._rate;
22 | }
23 |
24 | set rate(val: number) {
25 | this._rate = val;
26 | this.recalculate();
27 | }
28 |
29 | get dmg() {
30 | return this._dmg;
31 | }
32 |
33 | set dmg(val: number) {
34 | this._dmg = val;
35 | this.recalculate();
36 | }
37 |
38 | recalculate() {
39 | this.value = 2 * this._rate + this._dmg;
40 | this.multiplier = 1 + Math.max(0, Math.min(1, this.rate) * this.dmg);
41 | this.ratio = this.dmg / this.rate;
42 |
43 | this.bestRate = this.value < 0 ? this.value / 2 : (this.value >= 4 ? 1 : this.value / 4);
44 | this.bestDmg = this.value < 0 ? 0 : (this.value >= 4 ? this.value - 2 : this.value / 2);
45 | this.bestMultiplier = 1 + this.bestRate * this.bestDmg;
46 | this.bestRatio = this.bestDmg / this.bestRate;
47 |
48 | this.efficiency = this.multiplier / this.bestMultiplier;
49 |
50 | this.afterRecalculate.emit();
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | @use '@angular/material' as mat;
2 |
3 | $main-palette: (
4 | 50: #e6f3ff,
5 | 100: #c3dfff,
6 | 200: #9fccff,
7 | 300: #7db8ff,
8 | 400: #69a8ff,
9 | 500: #5e99ff,
10 | 600: #5b8af0,
11 | 700: #5578db,
12 | 800: #5066c8,
13 | 900: #4847a7,
14 | contrast: (
15 | 50: rgba(black, 0.87),
16 | 100: rgba(black, 0.87),
17 | 200: rgba(black, 0.87),
18 | 300: rgba(black, 0.87),
19 | 400: rgba(black, 0.87),
20 | 500: white,
21 | 600: white,
22 | 700: white,
23 | 800: white,
24 | 900: white
25 | )
26 | );
27 |
28 | @include mat.core();
29 |
30 | $my-primary: mat.m2-define-palette($main-palette, 500);
31 | $my-accent: mat.m2-define-palette(mat.$m2-teal-palette, A200, A100, A400);
32 | $my-theme: mat.m2-define-dark-theme((
33 | color: (
34 | primary: $my-primary,
35 | accent: $my-accent,
36 | ),
37 | typography: mat.m2-define-typography-config(),
38 | density: 0
39 | ));
40 |
41 | @include mat.form-field-theme($my-theme);
42 | @include mat.card-theme($my-theme);
43 | @include mat.expansion-theme($my-theme);
44 | @include mat.tooltip-theme($my-theme);
45 |
46 | :root {
47 | color-scheme: dark;
48 | }
49 |
50 | html,
51 | body {
52 | height: unset;
53 | }
54 |
55 | body {
56 | background: #222;
57 | color: #fff;
58 | font-family: sans-serif;
59 | }
60 |
61 | mat-card-content {
62 | overflow-x: auto;
63 | overflow-y: hidden;
64 | }
65 |
66 | .mat-expansion-panel {
67 | .mat-expansion-panel-content {
68 | overflow-x: auto;
69 | overflow-y: hidden;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/app/calculator/calculator.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Current
5 |
6 |
7 |
8 |
9 |
10 |
11 |
16 |
17 |
18 |
19 |
20 | Optimal
21 |
22 |
23 |
24 | {{critInfo.bestRate | percentage}}
25 | {{critInfo.bestDmg | percentage}}
26 |
27 |
32 |
33 |
34 |
35 |
36 | CRIT Multipliers when CRIT Value is {{critInfo.value * 100 | number:'1.0-2'}}%
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { BrowserModule } from '@angular/platform-browser';
3 | import { AppComponent } from './app.component';
4 | import { CalculatorComponent } from './calculator/calculator.component';
5 | import { InputComponent } from './input/input.component';
6 | import { SafeHtmlPipe } from './safe-html.pipe';
7 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
8 | import { MatExpansionModule } from '@angular/material/expansion';
9 | import { MatInputModule } from '@angular/material/input';
10 | import { MatCardModule } from '@angular/material/card';
11 | import { EquationsComponent } from './equations/equations.component';
12 | import { EfficiencyGraphComponent } from './efficiency-graph/efficiency-graph.component';
13 | import { FooterComponent } from './footer/footer.component';
14 | import { OutputComponent } from './output/output.component';
15 | import { MatTooltipModule } from '@angular/material/tooltip';
16 | import { IORowComponent } from './io-row/io-row.component';
17 | import { CalculationRowComponent } from './calculation-row/calculation-row.component';
18 | import { PercentagePipe } from './percentage.pipe';
19 | import { RatioPipe } from './ratio.pipe';
20 | import { BaseGraphComponent } from './base-graph/base-graph.component';
21 |
22 | @NgModule({
23 | declarations: [
24 | AppComponent,
25 | CalculatorComponent,
26 | InputComponent,
27 | SafeHtmlPipe,
28 | EquationsComponent,
29 | EfficiencyGraphComponent,
30 | FooterComponent,
31 | OutputComponent,
32 | IORowComponent,
33 | CalculationRowComponent,
34 | PercentagePipe,
35 | RatioPipe,
36 | BaseGraphComponent
37 | ],
38 | imports: [
39 | BrowserModule,
40 | BrowserAnimationsModule,
41 | MatExpansionModule,
42 | MatCardModule,
43 | MatInputModule,
44 | MatTooltipModule
45 | ],
46 | bootstrap: [AppComponent]
47 | })
48 | export class AppModule { }
49 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "schematics": {
6 | "@schematics/angular:component": {
7 | "skipTests": true,
8 | "standalone": false,
9 | "style": "scss"
10 | }
11 | },
12 | "projects": {
13 | "critical-analysis": {
14 | "projectType": "application",
15 | "root": "",
16 | "sourceRoot": "src",
17 | "prefix": "app",
18 | "architect": {
19 | "build": {
20 | "builder": "@angular-builders/custom-webpack:browser",
21 | "options": {
22 | "customWebpackConfig": {
23 | "path": "./webpack.config.js"
24 | },
25 | "outputPath": "dist/critical-analysis",
26 | "index": "src/index.html",
27 | "main": "src/main.ts",
28 | "polyfills": [
29 | "zone.js"
30 | ],
31 | "tsConfig": "tsconfig.app.json",
32 | "inlineStyleLanguage": "scss",
33 | "assets": [
34 | "src/favicon.ico"
35 | ],
36 | "styles": [
37 | "src/styles.scss"
38 | ],
39 | "scripts": []
40 | },
41 | "configurations": {
42 | "production": {
43 | "budgets": [
44 | {
45 | "type": "initial",
46 | "maximumWarning": "500kb",
47 | "maximumError": "1mb"
48 | },
49 | {
50 | "type": "anyComponentStyle",
51 | "maximumWarning": "2kb",
52 | "maximumError": "4kb"
53 | }
54 | ],
55 | "outputHashing": "all"
56 | },
57 | "development": {
58 | "buildOptimizer": false,
59 | "optimization": false,
60 | "vendorChunk": true,
61 | "extractLicenses": false,
62 | "sourceMap": true,
63 | "namedChunks": true
64 | }
65 | },
66 | "defaultConfiguration": "production"
67 | },
68 | "serve": {
69 | "builder": "@angular-builders/custom-webpack:dev-server",
70 | "configurations": {
71 | "production": {
72 | "buildTarget": "critical-analysis:build:production"
73 | },
74 | "development": {
75 | "buildTarget": "critical-analysis:build:development"
76 | }
77 | },
78 | "defaultConfiguration": "development"
79 | }
80 | }
81 | }
82 | },
83 | "cli": {
84 | "analytics": false
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/app/math.html:
--------------------------------------------------------------------------------
1 | CRIT Value
2 |
11 | Current Multiplier
12 |
58 | Current Efficiency
59 |
74 | Optimal CRIT Rate
75 |
130 | Optimal CRIT DMG
131 |
183 | Optimal Multiplier
184 |
269 |
--------------------------------------------------------------------------------
/src/app/efficiency-graph/efficiency-graph.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, AfterContentInit } from '@angular/core';
2 | import { CritInfo } from '../crit-info';
3 | import { GraphPoint, GraphAxis, GraphLineCommand, BaseGraphComponent, GridPoint } from '../base-graph/base-graph.component';
4 |
5 | @Component({
6 | selector: 'app-efficiency-graph',
7 | templateUrl: './efficiency-graph.component.html'
8 | })
9 | export class EfficiencyGraphComponent implements AfterContentInit {
10 | @Input() critInfo!: CritInfo;
11 |
12 | startPoint: GridPoint = [0, 0];
13 | size: GridPoint = [0, 0];
14 | points: GraphPoint[] = [];
15 | axes: GraphAxis[] = [];
16 | lineCmds: GraphLineCommand[] = [];
17 |
18 | static readonly CRIT_DMG_COLOR = '#B1CC00';
19 | static readonly CRIT_RATE_COLOR = '#F2BEFC';
20 | static readonly CURRENT_COLOR = '#F63C61';
21 | static readonly OPTIMAL_COLOR = '#FFA552';
22 |
23 | setBounds() {
24 | const lowestValue = Math.min(this.critInfo.rate * 220, 0, this.critInfo.value * -10);
25 | const highestValue = Math.max(this.critInfo.rate * -20, this.critInfo.value * 110);
26 |
27 | this.startPoint = [
28 | lowestValue,
29 | this.critInfo.bestMultiplier * -10
30 | ]
31 |
32 | this.size = [
33 | highestValue - lowestValue,
34 | this.critInfo.bestMultiplier * 130
35 | ];
36 | }
37 |
38 | /**
39 | * Get the CRIT multiplier for the graph at the given CRIT rate value.
40 | */
41 | getMultiplier(rateValue: number) {
42 | // Either CRIT rate or DMG are 0 or less
43 | if (rateValue <= 0 || this.critInfo.value * 100 <= rateValue) return 100;
44 |
45 | // CRIT rate >= 100
46 | if (rateValue >= 200) return this.critInfo.value * 100 - rateValue + 100;
47 |
48 | // [CRIT dmg] * [CRIT rate] + 1
49 | return (this.critInfo.value * 100 - rateValue) * (rateValue / 200) + 100;
50 | }
51 |
52 | drawAxes() {
53 | this.axes = [
54 | {
55 | dir: 'hoz',
56 | color: EfficiencyGraphComponent.CRIT_RATE_COLOR,
57 | offset: this.critInfo.value * 100,
58 | label: 'CRIT Rate (%)',
59 | labelOffset: 1.5,
60 | textPos: [
61 | {position: 'near', textOffset: 1, alignment: 'text-top'}
62 | ],
63 | getOverride: value => ({
64 | to: this.getMultiplier(value)
65 | }),
66 | displayFactor: 0.5
67 | },
68 | {
69 | dir: 'hoz',
70 | color: EfficiencyGraphComponent.CRIT_DMG_COLOR,
71 | offset: this.critInfo.value * 100,
72 | label: 'CRIT DMG (%)',
73 | labelOffset: -0.7,
74 | textPos: [
75 | {position: 'far', textOffset: -1, alignment: 'hanging'}
76 | ],
77 | getOverride: value => ({
78 | from: this.startPoint[1] + this.size[1],
79 | to: this.getMultiplier(value)
80 | }),
81 | inverse: true
82 | },
83 | {
84 | dir: 'vert',
85 | offset: this.critInfo.value * 100,
86 | label: 'CRIT Multiplier (%)',
87 | labelOffset: -2.55,
88 | textPos: [
89 | {position: 'near', textOffset: -0.5, alignment: 'end'},
90 | {position: 'far', textOffset: 0.5, alignment: 'start'}
91 | ]
92 | }
93 | ];
94 | }
95 |
96 | drawLines() {
97 | // Start line
98 | this.lineCmds = [
99 | {
100 | type: 'move',
101 | x: this.startPoint[0],
102 | y: 100
103 | },
104 | {
105 | type: 'line-to',
106 | x: 0,
107 | y: 100
108 | }
109 | ];
110 |
111 | // Parabola
112 | if (this.critInfo.value > 0) {
113 | const curveEndX = this.critInfo.value > 2 ? 200 : this.critInfo.value * 100;
114 |
115 | // https://math.stackexchange.com/a/1258196
116 | this.lineCmds.push({
117 | type: 'bezier',
118 | ctrlX: curveEndX / 2,
119 | ctrlY: (curveEndX / 4) * this.critInfo.value + 100,
120 | x: curveEndX,
121 | y: this.getMultiplier(curveEndX)
122 | });
123 | }
124 |
125 | // CRIT rate > 100
126 | if (this.critInfo.value > 2)
127 | this.lineCmds.push({
128 | type: 'line-to',
129 | x: this.critInfo.value * 100,
130 | y: 100
131 | });
132 |
133 | // End line
134 | this.lineCmds.push({
135 | type: 'line-to',
136 | x: this.startPoint[0] + this.size[0],
137 | y: 100
138 | });
139 | }
140 |
141 | drawPoints() {
142 | const isOptimal = Math.abs(this.critInfo.multiplier - this.critInfo.bestMultiplier) < BaseGraphComponent.EPSILON;
143 |
144 | this.points = [
145 | {
146 | x: this.critInfo.rate * 200,
147 | y: this.critInfo.multiplier * 100,
148 | label: isOptimal ? 'Current (Optimal)' : 'Current',
149 | color: EfficiencyGraphComponent.CURRENT_COLOR,
150 | labelRelX: 0.5 * (this.critInfo.rate <= this.critInfo.bestRate ? 1 : -1),
151 | labelRelY: 0.5,
152 | anchor: (this.critInfo.rate <= this.critInfo.bestRate) ? 'start' : 'end',
153 | baseline: 'hanging'
154 | }
155 | ];
156 |
157 | if (!isOptimal)
158 | this.points.push({
159 | x: this.critInfo.bestRate * 200,
160 | y: this.critInfo.bestMultiplier * 100,
161 | label: 'Optimal',
162 | color: EfficiencyGraphComponent.OPTIMAL_COLOR,
163 | labelRelX: 0.5,
164 | labelRelY: -0.5
165 | });
166 | }
167 |
168 | update() {
169 | this.setBounds();
170 | this.drawAxes();
171 | this.drawLines();
172 | this.drawPoints();
173 | }
174 |
175 | ngAfterContentInit() {
176 | this.critInfo.afterRecalculate.subscribe(this.update.bind(this));
177 | this.update();
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/src/app/base-graph/base-graph.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 |
3 | export interface GraphPoint {
4 | x: number;
5 | y: number;
6 | label: string;
7 | color: string;
8 | labelRelX: number;
9 | labelRelY: number;
10 | anchor?: string;
11 | baseline?: string;
12 | }
13 |
14 | export interface GraphAxis {
15 | dir: 'hoz' | 'vert',
16 | color?: string;
17 | offset: number;
18 | label: string;
19 | labelOffset: number;
20 | textPos: {
21 | position: 'near' | 'far';
22 | textOffset: number;
23 | alignment: string;
24 | }[];
25 | displayFactor?: number;
26 | inverse?: boolean;
27 | getOverride?: (value: number) => {from?: number, to?: number};
28 | }
29 |
30 | export type GraphLineCommand = {
31 | type: 'move' | 'line-to';
32 | x: number;
33 | y: number;
34 | } | {
35 | type: 'bezier';
36 | ctrlX: number;
37 | ctrlY: number;
38 | x: number;
39 | y: number;
40 | }
41 |
42 | export type GridPoint = [number, number];
43 |
44 | @Component({
45 | selector: 'app-base-graph',
46 | templateUrl: './base-graph.component.html',
47 | styleUrl: './base-graph.component.scss'
48 | })
49 | export class BaseGraphComponent {
50 | /**
51 | * Bottom-left corner of the graph.
52 | */
53 | @Input() startPoint: GridPoint = [0, 0];
54 | /**
55 | * Width and height of the graph.
56 | */
57 | @Input() size: GridPoint = [0, 0];
58 | @Input() points: GraphPoint[] = [];
59 | @Input() axes: GraphAxis[] = [];
60 | @Input() lineCmds: GraphLineCommand[] = [];
61 |
62 | static readonly EPSILON = 0.00001;
63 |
64 | static readonly STEPS = [
65 | [20, 5], [40, 10], [80, 20], [160, 40],
66 | [400, 100], [1000, 200], [Infinity, 500]
67 | ];
68 |
69 | static readonly LINE_COLOR = '#5E9AFF';
70 | static readonly LINE_SIZE = 3;
71 | static readonly DEFAULT_AXIS_COLOR = '#FFFFFF';
72 |
73 | static SVG_COMMANDS: Record = {
74 | 'move': 'M',
75 | 'line-to': 'L',
76 | 'bezier': 'Q'
77 | };
78 |
79 | scaleX = 0;
80 | scaleY = -1;
81 | viewBox = '0 0 0 0';
82 | fontSize = 12;
83 |
84 | svgPaths: {
85 | size: number;
86 | color: string;
87 | cmd: string;
88 | crisp?: boolean;
89 | }[] = [];
90 |
91 | svgTexts: {
92 | x: number;
93 | y: number;
94 | str: string;
95 | baseline?: string;
96 | anchor?: string;
97 | sideways?: boolean;
98 | fontScale?: number;
99 | color?: string;
100 | }[] = [];
101 |
102 | svgPoints: {
103 | x: number;
104 | y: number;
105 | color?: string;
106 | }[] = [];
107 |
108 | private calculateTextPos(
109 | hoz: boolean,
110 | val: 'center' | number,
111 | textDimension: GraphAxis['textPos'][0]
112 | ) {
113 | if (val === 'center') {
114 | const posIndex = hoz ? 0 : 1;
115 | val = this.startPoint[posIndex] + this.size[posIndex] / 2;
116 | }
117 |
118 | const otherPos = hoz ? 1 : 0;
119 | const otherAxis = textDimension.position === 'near'
120 | ? this.startPoint[otherPos]
121 | : this.startPoint[otherPos] + this.size[otherPos];
122 |
123 | return {
124 | x: hoz
125 | ? val * this.scaleX
126 | : otherAxis * this.scaleX + textDimension.textOffset * this.fontSize,
127 | y: hoz
128 | ? otherAxis * this.scaleY + textDimension.textOffset * this.fontSize
129 | : val * this.scaleY,
130 | };
131 | }
132 |
133 | drawAxis({
134 | dir, color, offset, label, labelOffset, textPos, getOverride, displayFactor, inverse
135 | }: GraphAxis) {
136 | displayFactor ??= 1;
137 | color ??= BaseGraphComponent.DEFAULT_AXIS_COLOR;
138 |
139 | const hoz = dir === 'hoz';
140 | const posIndex = hoz ? 0 : 1;
141 |
142 | let start = this.startPoint[posIndex];
143 | let end = start + this.size[posIndex];
144 |
145 | if (inverse) {
146 | [end, start] = [start, end];
147 | }
148 |
149 | const shouldContinue: (a: number, b: number) => boolean = inverse
150 | ? (a, b) => a >= b
151 | : (a, b) => a <= b;
152 |
153 | const range = Math.abs(start - end);
154 | const step = BaseGraphComponent.STEPS.find(([maxRange]) => range < maxRange)![1];
155 | const increment = (step / 5) * (inverse ? -1 : 1);
156 |
157 | start += ((inverse ? offset : 0) - start) % increment;
158 |
159 | this.svgTexts.push({
160 | ...this.calculateTextPos(hoz, 'center', {
161 | ...textPos[0],
162 | textOffset: textPos[0].textOffset + labelOffset
163 | }),
164 | str: label,
165 | baseline: 'baseline',
166 | anchor: 'middle',
167 | sideways: !hoz,
168 | fontScale: 1.15
169 | });
170 |
171 | for (let value = start; shouldContinue(value, end); value += increment) {
172 | const adjustedValue = inverse
173 | ? Math.round((offset - value) * 1000) / 1000
174 | : value;
175 |
176 | const secondaryOrAxis = adjustedValue % step === 0;
177 |
178 | if (secondaryOrAxis)
179 | textPos.forEach(textDimension => this.svgTexts.push({
180 | ...this.calculateTextPos(hoz, value, textDimension),
181 | str: (adjustedValue * displayFactor).toString(),
182 | anchor: hoz ? 'middle' : textDimension.alignment,
183 | baseline: hoz ? textDimension.alignment : 'middle',
184 | color
185 | }));
186 |
187 | const override = getOverride?.(value) ?? {};
188 |
189 | const cmd = hoz
190 | ? `M ${value * this.scaleX} ${(override.from ?? this.startPoint[1]) * this.scaleY} L ${value * this.scaleX} ${(override.to ?? (this.startPoint[1] + this.size[1])) * this.scaleY}`
191 | : `M ${(override.from ?? this.startPoint[0]) * this.scaleX} ${value * this.scaleY} L ${(override.to ?? (this.startPoint[0] + this.size[0])) * this.scaleX} ${value * this.scaleY}`
192 |
193 | if (adjustedValue === 0) {
194 | this.svgPaths.push({size: 2, color, crisp: true, cmd});
195 | continue;
196 | }
197 |
198 | if (secondaryOrAxis) {
199 | this.svgPaths.push({size: 1, color, crisp: true, cmd});
200 | continue;
201 | }
202 |
203 | this.svgPaths.push({size: 1, color: color + '55', crisp: true, cmd});
204 | }
205 | }
206 |
207 | drawPoint({
208 | x, y, label, color, labelRelX, labelRelY, anchor, baseline
209 | }: GraphPoint) {
210 | this.svgPoints.push({x: x * this.scaleX, y: y * this.scaleY, color});
211 |
212 | this.svgTexts.push({
213 | str: label,
214 | fontScale: 1.25,
215 | x: x * this.scaleX + labelRelX * this.fontSize,
216 | y: y * this.scaleY + labelRelY * this.fontSize,
217 | color,
218 | anchor,
219 | baseline
220 | });
221 | }
222 |
223 | drawLine() {
224 | const cmd = this.lineCmds.map(cmd => {
225 | const svgCmd = BaseGraphComponent.SVG_COMMANDS[cmd.type];
226 |
227 | if (cmd.type === 'bezier') {
228 | return `${svgCmd} ${cmd.ctrlX * this.scaleX} ${cmd.ctrlY * this.scaleY} ${cmd.x * this.scaleX} ${cmd.y * this.scaleY}`;
229 | }
230 |
231 | return `${svgCmd} ${cmd.x * this.scaleX} ${cmd.y * this.scaleY}`;
232 | }).join(' ');
233 |
234 | this.svgPaths.push({
235 | size: BaseGraphComponent.LINE_SIZE,
236 | color: BaseGraphComponent.LINE_COLOR,
237 | cmd
238 | });
239 | }
240 |
241 | ngOnChanges() {
242 | this.scaleX = this.size[1] / this.size[0];
243 | this.fontSize = this.size[1] / 22;
244 |
245 | this.viewBox = [
246 | this.startPoint[0] * this.scaleX - this.fontSize * 3.65,
247 | (this.startPoint[1] + this.size[1]) * this.scaleY - this.fontSize * 2.8,
248 | this.size[0] * this.scaleX + this.fontSize * 7.3,
249 | -this.size[1] * this.scaleY + this.fontSize * 5.6
250 | ].join(' ');
251 |
252 | this.svgPaths = [];
253 | this.svgPoints = [];
254 | this.svgTexts = [];
255 |
256 | this.axes.forEach(axis => this.drawAxis(axis));
257 | this.points.forEach(point => this.drawPoint(point));
258 | this.drawLine();
259 | }
260 | }
261 |
--------------------------------------------------------------------------------