mirror of
https://github.com/mhwikicn/mhdos-armor-set-searcher
synced 2025-12-06 04:59:04 +08:00
init from mhtri-armor-set-searcher
This commit is contained in:
commit
21086eb0d9
3
.eslintignore
Normal file
3
.eslintignore
Normal file
@ -0,0 +1,3 @@
|
||||
/dist
|
||||
/node_modules
|
||||
/.cache
|
||||
22
.eslintrc.json
Normal file
22
.eslintrc.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"standard"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"no-undef": ["off"],
|
||||
"indent": ["error", 2],
|
||||
"comma-dangle": ["error", "always-multiline"]
|
||||
}
|
||||
}
|
||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
/dist
|
||||
/node_modules
|
||||
/.cache
|
||||
/venv
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Tim
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
54
README.md
Normal file
54
README.md
Normal file
@ -0,0 +1,54 @@
|
||||
# MH3 Armor Set Searcher
|
||||
|
||||
## About
|
||||
|
||||
This is an armor set searcher for the game Monster Hunter Tri, as seen for many of the newer (and even older) Monster Hunter games. Oddly enough, there wasn't one for this game when almost every other entry in the series has one, so now here it is 13 years after the fact. Put in your charms, select the skills you want to have and hopefully find your sets. The website is hosted [right here](https://timh96.github.io/mhtri-armor-set-searcher/) on GitHub via gh-pages.
|
||||
|
||||
The development of this was driven by the revival of the online features and return to Loc Lac by the community developed and hosted servers. Here you can find the [MH3SP repository](https://github.com/sepalani/MH3SP) and the related [community Discord server](https://discord.com/invite/4sBmXC55V6).
|
||||
|
||||
## Development
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Node v14 or higher to run and develop the actual app
|
||||
- Python v3.9 or higher to run the data scripts
|
||||
|
||||
### Init
|
||||
|
||||
Clone the repo and install its dependencies.
|
||||
|
||||
```shell
|
||||
$ git clone https://github.com/TimH96/mhtri-armor-set-searcher
|
||||
$ cd mhtri-armor-set-searcher
|
||||
$ npm i
|
||||
```
|
||||
|
||||
### Run development server
|
||||
|
||||
```shell
|
||||
$ npm run start
|
||||
```
|
||||
|
||||
### Build and deploy
|
||||
|
||||
```shell
|
||||
$ npm run build
|
||||
$ npm run deploy
|
||||
```
|
||||
|
||||
### Run data scripts
|
||||
|
||||
```shell
|
||||
$ cd scripts
|
||||
$ python get-skill-categories.py
|
||||
$ python get-skills.py
|
||||
$ python get-equipment-and-decos.py
|
||||
```
|
||||
|
||||
## Known issues
|
||||
|
||||
This is a list of currently known bugs or other issues with the searcher. I believe these are low priority and I don't consider them worth the effort fixing. Please do not report them.
|
||||
|
||||
+ The searcher will sometimes recommend too many decorations, "overkilling" on a skill when it doesn't need to. The resulting set is still valid and fullfills all requirements, and this only happens with unoptimized sets with plenty of slots to spare. This is the case because of how the decoration search logic works, and fixing it would require reimagining the current approach entirely.
|
||||
+ The searcher struggles with sets that include 2 or more skills which commonly cancel each other out, that is to say that decorations/armor pieces that have skill A often have negative points in skill B. The searcher is still complete in finding these sets, however it might take quite some time, especially when using the "More Skills" feature. The app should not crash and you can simply wait for the search to complete. This happens because some of the heuristics that are used to prune search paths don't properly work when considering negative skills, and fixing it would require reimagining said heuristics entirely.
|
||||
+ The frontend code is a mess. I started writing simple script-style JavaScript like we are in 2010. Started out very good, but as scope and my laziness grew it became quite ugly. This should not impede on function however. Maybe one day I will rewrite it in some very baseline framework, or in webcomponents.
|
||||
9864
package-lock.json
generated
Normal file
9864
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
package.json
Normal file
41
package.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "mhdos-armor-set-searcher",
|
||||
"version": "0.0.1",
|
||||
"description": "web-based armor set searcher for Monster Hunter Dos",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/TimH96/mhdos-armor-set-searcher.git"
|
||||
},
|
||||
"files": [
|
||||
"data/",
|
||||
"src/"
|
||||
],
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext .ts",
|
||||
"lint:fix": "eslint . --fix --ext .ts",
|
||||
"start": "parcel serve src/app/pages/index.html",
|
||||
"build": "cross-env NODE_ENV=production parcel build src/app/pages/index.html --public-url .",
|
||||
"deploy": "gh-pages -d dist"
|
||||
},
|
||||
"staticFiles": {
|
||||
"staticPath": "data",
|
||||
"watcherGlob": "**"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.21.0",
|
||||
"@typescript-eslint/parser": "^5.21.0",
|
||||
"cross-env": "^5.2.0",
|
||||
"eslint": "^8.14.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-n": "^15.2.0",
|
||||
"eslint-plugin-promise": "^6.0.0",
|
||||
"gh-pages": "^1.2.0",
|
||||
"parcel-bundler": "^1.9.7",
|
||||
"parcel-plugin-static-files-copy": "^2.6.0",
|
||||
"typescript": "^2.9.2"
|
||||
},
|
||||
"author": "TimH96",
|
||||
"license": "MIT"
|
||||
}
|
||||
11
src/app/models/GlobalSettings.ts
Normal file
11
src/app/models/GlobalSettings.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import ArmorType from '../../data-provider/models/equipment/ArmorType'
|
||||
import Rarity from '../../data-provider/models/equipment/Rarity'
|
||||
import Slots from '../../data-provider/models/equipment/Slots'
|
||||
|
||||
export default interface GlobalSettings {
|
||||
armorType: ArmorType
|
||||
weaponSlots: Slots
|
||||
armorRarity: Rarity
|
||||
decoRarity: Rarity
|
||||
limit: number
|
||||
}
|
||||
205
src/app/pages/index.html
Normal file
205
src/app/pages/index.html
Normal file
@ -0,0 +1,205 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<title>MH3 Armor Set Searcher</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Armor Set Searcher for Monster Hunter Tri"
|
||||
/>
|
||||
<meta name="author" content="TimH96" />
|
||||
|
||||
<meta property="og:title" content="MH3 Armor Set Searcher" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:url"
|
||||
content="https://timh96.github.io/mhtri-armor-set-searcher/"
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Armor Set Searcher for Monster Hunter Tri"
|
||||
/>
|
||||
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="header">MH Tri Armor Set Searcher</div>
|
||||
|
||||
<div id="open-source" class="banner">
|
||||
<p>
|
||||
This is an open-source project hosted on gh-pages,
|
||||
<a href="https://github.com/TimH96/mhtri-armor-set-searcher"
|
||||
>here is the repository</a
|
||||
>. If you have a feature request or find any bugs, please create an
|
||||
issue on GitHub. You are also welcome to integrate them yourself via a
|
||||
Pull Request.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="main-body">
|
||||
<!-- navbar -->
|
||||
<div id="navbar-container">
|
||||
<ul class="navbar">
|
||||
<li class="navbar-option" data-selection="0">Search</li>
|
||||
<li class="navbar-option" data-selection="1">Charms</li>
|
||||
<li class="navbar-option" data-selection="2">Equipment</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- search -->
|
||||
<div class="panel" id="search-container" data-panel-number="0">
|
||||
<!-- global settings -->
|
||||
<div id="search-global-settings">
|
||||
<ul>
|
||||
<li>
|
||||
<select name="armor-type" id="armor-type">
|
||||
<option value="1">Blademaster</option>
|
||||
<option value="2">Gunner</option>
|
||||
</select>
|
||||
</li>
|
||||
<li>
|
||||
<select name="weapon-slots" id="weapon-slots">
|
||||
<option value="0">0 Weapon Slots</option>
|
||||
<option value="1">1 Weapon Slots</option>
|
||||
<option value="2">2 Weapon Slots</option>
|
||||
<option value="3">3 Weapon Slots</option>
|
||||
</select>
|
||||
</li>
|
||||
<li>
|
||||
<select name="armor-rarity" id="armor-rarity">
|
||||
<optgroup label="High Rank">
|
||||
<option value="7">Armor RARE7</option>
|
||||
<option value="6">Armor RARE6</option>
|
||||
<option value="5">Armor RARE5</option>
|
||||
<option value="4">Armor RARE4</option>
|
||||
</optgroup>
|
||||
<optgroup label="Low Rank">
|
||||
<option value="3">Armor RARE3</option>
|
||||
<option value="2">Armor RARE2</option>
|
||||
<option value="1">Armor RARE1</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</li>
|
||||
<li>
|
||||
<select name="deco-rarity" id="deco-rarity">
|
||||
<optgroup label="High Rank">
|
||||
<option value="7">Deco RARE7</option>
|
||||
<option value="6">Deco RARE6</option>
|
||||
</optgroup>
|
||||
<optgroup label="Low Rank">
|
||||
<option value="5">Deco RARE5</option>
|
||||
<option value="4">Deco RARE4</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</li>
|
||||
<li>
|
||||
<input
|
||||
type="number"
|
||||
name="search-limit"
|
||||
id="search-limit"
|
||||
min="1"
|
||||
max="1000"
|
||||
value="100"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- skill picker -->
|
||||
<div id="search-skill-picker"></div>
|
||||
|
||||
<!-- controls -->
|
||||
<div id="search-controls">
|
||||
<ul>
|
||||
<li><button id="search-btn">Search</button></li>
|
||||
<li><button id="more-btn">More Skills</button></li>
|
||||
<li><button id="reset-btn">Reset</button></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- results -->
|
||||
<div id="search-results"></div>
|
||||
</div>
|
||||
|
||||
<!-- charms -->
|
||||
<div class="panel" id="charms-container" data-panel-number="1">
|
||||
<!-- charm picker -->
|
||||
<div id="charm-picker">
|
||||
<ul>
|
||||
<li>Add Charm</li>
|
||||
<li>
|
||||
<select
|
||||
class="charm-skill-pick"
|
||||
name="charm-skill-1-name"
|
||||
id="charm-skill-1-name"
|
||||
></select>
|
||||
</li>
|
||||
<li>
|
||||
<select
|
||||
class="charm-points-pick"
|
||||
name="charm-skill-1-points"
|
||||
id="charm-skill-1-points"
|
||||
></select>
|
||||
</li>
|
||||
<li>
|
||||
<select
|
||||
class="charm-skill-pick"
|
||||
name="charm-skill-2-name"
|
||||
id="charm-skill-2-name"
|
||||
></select>
|
||||
</li>
|
||||
<li>
|
||||
<select
|
||||
class="charm-points-pick"
|
||||
name="charm-skill-2-points"
|
||||
id="charm-skill-2-points"
|
||||
></select>
|
||||
</li>
|
||||
<li>
|
||||
<select name="charm-slots" id="charm-slots">
|
||||
<option value="0">0 Slots</option>
|
||||
<option value="1">1 Slots</option>
|
||||
<option value="2">2 Slots</option>
|
||||
<option value="3">3 Slots</option>
|
||||
</select>
|
||||
</li>
|
||||
<li><button id="charm-add">Add</button></li>
|
||||
<li><button id="charm-import">Import</button></li>
|
||||
<li><button id="charm-export">Export</button></li>
|
||||
</ul>
|
||||
<a href="" id="charm-download" style="display: none"></a>
|
||||
<input
|
||||
type="file"
|
||||
id="charm-upload"
|
||||
accept=".csv"
|
||||
style="display: none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- charm table -->
|
||||
<div id="charm-table-container">
|
||||
<table id="charm-table">
|
||||
<tr>
|
||||
<th>Skill</th>
|
||||
<th>Points</th>
|
||||
<th>Skill</th>
|
||||
<th>Points</th>
|
||||
<th>Slots</th>
|
||||
<th>Delete</th>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- eq settings -->
|
||||
<div class="panel" id="eq-settings-container" data-panel-number="2">
|
||||
<div id="eq-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./index.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
39
src/app/pages/index.ts
Normal file
39
src/app/pages/index.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { getArms, getChest, getDecorations, getHead, getLegs, getSkillActivationMap, getSkillCategories, getSkillNameMap, getWaist } from '../../data-provider/data-provider.module'
|
||||
import StaticSkillData from '../../data-provider/models/skills/StaticSkillData'
|
||||
import { renderCharmPicker } from '../ui/charms.component'
|
||||
import { renderEqSettings } from '../ui/eq-settings.component'
|
||||
import { initiateNavbar } from '../ui/navbar.component'
|
||||
import { renderSkillPicker } from '../ui/picker.component'
|
||||
import { attachControlListeners } from '../ui/search-controls.component'
|
||||
|
||||
const main = async () => {
|
||||
// initiate static components
|
||||
initiateNavbar()
|
||||
|
||||
// load remaining data
|
||||
const armor = [
|
||||
await getHead(),
|
||||
await getChest(),
|
||||
await getArms(),
|
||||
await getWaist(),
|
||||
await getLegs(),
|
||||
]
|
||||
const decorations = await getDecorations()
|
||||
|
||||
// load skill data and render skill picker and charms with it
|
||||
const skillData: StaticSkillData = {
|
||||
skillName: await getSkillNameMap(),
|
||||
skillActivation: await getSkillActivationMap(),
|
||||
skillCategories: await getSkillCategories(),
|
||||
}
|
||||
|
||||
// render ui
|
||||
renderSkillPicker(skillData.skillActivation, skillData.skillCategories)
|
||||
renderCharmPicker(skillData.skillName, skillData.skillActivation, skillData.skillCategories)
|
||||
renderEqSettings(armor)
|
||||
|
||||
// initialize search controls
|
||||
attachControlListeners({ armor, decorations }, skillData)
|
||||
}
|
||||
|
||||
main()
|
||||
380
src/app/pages/styles.css
Normal file
380
src/app/pages/styles.css
Normal file
@ -0,0 +1,380 @@
|
||||
:root {
|
||||
--color-text: #444;
|
||||
--color-background: rgb(255, 253, 253);
|
||||
|
||||
--color-border: rgb(185, 185, 185);
|
||||
--color-highlight: rgb(243, 243, 243);
|
||||
|
||||
--color-highlighted-background: rgb(194, 218, 255);
|
||||
--color-header-background: rgb(37, 37, 37);
|
||||
|
||||
--color-tab-hover: rgb(175, 175, 175);
|
||||
|
||||
--color-negative-skill: rgb(206, 8, 8);
|
||||
|
||||
--color-more-skills: rgb(73, 121, 255);
|
||||
}
|
||||
|
||||
/* global */
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
body {
|
||||
min-width: 600px;
|
||||
background-color: var(--color-background);
|
||||
overflow-x: hidden;
|
||||
line-height: 1.4;
|
||||
color: var(--color-text);
|
||||
margin: 0 0 8em 0;
|
||||
font: 12px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
"Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
|
||||
ul > li {
|
||||
display: inline-block;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* main sections */
|
||||
|
||||
#header {
|
||||
color: var(--color-highlight);
|
||||
background-color: var(--color-header-background);
|
||||
font-size: 1.6em;
|
||||
text-align: center;
|
||||
|
||||
width: 100%;
|
||||
padding: 0.6em 1em;
|
||||
margin: -0.1em 0em 3em 0em;
|
||||
}
|
||||
|
||||
#open-source {
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
margin-bottom: 3em;
|
||||
|
||||
font-size: 1.2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#open-source p {
|
||||
padding: 0em 6em;
|
||||
}
|
||||
|
||||
#main-body {
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
/* global classes */
|
||||
|
||||
.banner {
|
||||
background-color: var(--color-highlight);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: var(--color-border);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.def {
|
||||
color: rgb(0, 0, 0);
|
||||
}
|
||||
|
||||
.fir {
|
||||
color: rgb(235, 69, 69);
|
||||
}
|
||||
|
||||
.wat {
|
||||
color: rgb(54, 91, 146);
|
||||
}
|
||||
|
||||
.ice {
|
||||
color: rgb(50, 204, 224);
|
||||
}
|
||||
|
||||
.thn {
|
||||
color: rgb(207, 204, 9);
|
||||
}
|
||||
|
||||
.drg {
|
||||
color: rgb(122, 49, 170);
|
||||
}
|
||||
|
||||
.neg-skill {
|
||||
color: var(--color-negative-skill);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.highlighted {
|
||||
background-color: var(--color-highlighted-background);
|
||||
}
|
||||
|
||||
/* navbar */
|
||||
|
||||
#navbar-container {
|
||||
font-size: 1.6em;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
margin-bottom: 1.6em;
|
||||
}
|
||||
|
||||
.navbar-option {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
width: 6em;
|
||||
padding: 0.4em 2em;
|
||||
text-align: center;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.navbar-selected {
|
||||
border-style: solid;
|
||||
border-width: 1px 1px 2px 1px;
|
||||
border-color: var(--color-border) var(--color-border) var(--color-background);
|
||||
border-radius: 0.4em 0.4em 0 0;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.navbar-hover {
|
||||
background-color: var(--color-tab-hover);
|
||||
border-radius: 0.4em 0.4em 0 0;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
/* skill picker and search controls */
|
||||
|
||||
#search-global-settings select {
|
||||
font-size: 1.4em;
|
||||
margin-right: 0.6em;
|
||||
}
|
||||
|
||||
#search-global-settings input {
|
||||
font-size: 1.4em;
|
||||
margin-right: 0.6em;
|
||||
}
|
||||
|
||||
#search-controls button {
|
||||
font-size: 1.3em;
|
||||
min-width: 8em;
|
||||
margin: 0.6em 0.6em 0 0;
|
||||
}
|
||||
|
||||
.search-picker-category-title {
|
||||
padding: 0.4em 1em;
|
||||
margin: 1em 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-picker-activation {
|
||||
margin: 0.4em 0.7em;
|
||||
width: 10em;
|
||||
display: inline-flex;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.search-picker-activation-name {
|
||||
padding: 0 0.4em;
|
||||
}
|
||||
|
||||
/* charms */
|
||||
|
||||
#charm-picker {
|
||||
padding-bottom: 0.4em;
|
||||
border-width: 0 0 1px 0;
|
||||
border-style: solid;
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
#charm-import {
|
||||
margin-left: 2em;
|
||||
}
|
||||
|
||||
.charm-skill-pick {
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
.charm-points-pick {
|
||||
width: 3em;
|
||||
}
|
||||
|
||||
#charm-table {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
#charm-table tr,
|
||||
td,
|
||||
th {
|
||||
width: 6em;
|
||||
text-align: center;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
#charm-table tr {
|
||||
border-width: 1px 0;
|
||||
border-color: var(--color-border);
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
#charm-table th {
|
||||
background-color: var(--color-highlight);
|
||||
}
|
||||
|
||||
.charm-delete {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* eq-settings */
|
||||
|
||||
#eq-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.eq-column {
|
||||
border: var(--color-border) solid 1px;
|
||||
border-radius: 6px;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.eq-column-item {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.eq-column-header {
|
||||
background-color: var(--color-highlight);
|
||||
}
|
||||
|
||||
/* results */
|
||||
|
||||
#search-results {
|
||||
padding-top: 2em;
|
||||
}
|
||||
|
||||
.results-title {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.results-banner {
|
||||
margin-top: 0.2em;
|
||||
font-size: 1.2em;
|
||||
padding: 0.4em 1em;
|
||||
}
|
||||
|
||||
.results-table {
|
||||
margin-top: 1em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.results-table th {
|
||||
font-size: 1.4em;
|
||||
text-align: center;
|
||||
background-color: var(--color-highlight);
|
||||
|
||||
border-width: 1px 0;
|
||||
border-color: var(--color-border);
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.result-set {
|
||||
border-width: 1px 0;
|
||||
border-color: var(--color-border);
|
||||
border-style: solid;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.result-set-row p {
|
||||
display: inline-block;
|
||||
margin: 0.3em 1em 0.3em 0;
|
||||
}
|
||||
|
||||
.result-set-row2 td {
|
||||
text-align: left;
|
||||
padding-left: 3em;
|
||||
}
|
||||
|
||||
.result-set-unrelated {
|
||||
margin-left: 3em;
|
||||
}
|
||||
|
||||
.result-set-unrelated-skill {
|
||||
margin-left: 2em;
|
||||
}
|
||||
|
||||
.result-set-details {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.result-set-details-container {
|
||||
cursor: default;
|
||||
margin: 1em auto;
|
||||
display: grid;
|
||||
width: 90%;
|
||||
gap: 1em;
|
||||
grid-template-columns: 1fr 3fr;
|
||||
}
|
||||
|
||||
.result-set-skill-table th {
|
||||
font-size: 1em;
|
||||
border-width: 1px 0;
|
||||
border-color: var(--color-border);
|
||||
border-style: solid;
|
||||
background-color: var(--color-highlight);
|
||||
}
|
||||
|
||||
.result-set-skill-table td {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.result-set-skill-table {
|
||||
table-layout: unset;
|
||||
}
|
||||
|
||||
.results-more-skills-act {
|
||||
font-size: 1.4em;
|
||||
color: var(--color-more-skills);
|
||||
}
|
||||
|
||||
.results-more-skills-act-content {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.result-set-piece-table {
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.result-set-piece-table th {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.result-set-piece-table td {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.result-set-piece-table tr {
|
||||
border-width: 1px 0;
|
||||
border-color: var(--color-border);
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.pin-highlighted {
|
||||
background-color: var(--color-highlighted-background);
|
||||
}
|
||||
|
||||
.excl-highlighted {
|
||||
background-color: var(--color-negative-skill);
|
||||
}
|
||||
237
src/app/ui/charms.component.ts
Normal file
237
src/app/ui/charms.component.ts
Normal file
@ -0,0 +1,237 @@
|
||||
import SkillActivationMap from '../../data-provider/models/skills/SkillActivationMap'
|
||||
import SkillNameMap from '../../data-provider/models/skills/SkillNameMap'
|
||||
import Charm from '../../data-provider/models/equipment/Charm'
|
||||
import Skill from '../../data-provider/models/skills/Skill'
|
||||
import UserCharmList from '../../data-provider/models/user/UserCharmList'
|
||||
import { htmlToElement } from '../../helper/html.helper'
|
||||
import Slots from '../../data-provider/models/equipment/Slots'
|
||||
import EquipmentCategory from '../../data-provider/models/equipment/EquipmentCategory'
|
||||
import GameID from '../../data-provider/models/GameId'
|
||||
import { range } from '../../helper/range.helper'
|
||||
import EquipmentSkills from '../../data-provider/models/equipment/EquipmentSkills'
|
||||
|
||||
const saveToStorage = (skillNames: SkillNameMap) => {
|
||||
window.localStorage.setItem('charms', UserCharmList.Instance.serialize(skillNames))
|
||||
}
|
||||
|
||||
const getFromStorage = () => {
|
||||
return window.localStorage.getItem('charms')
|
||||
}
|
||||
|
||||
const validSkill = (id: GameID, points: Skill) => {
|
||||
return points !== 0 && id !== -1
|
||||
}
|
||||
|
||||
const removeTableElement = (index: number) => {
|
||||
const ele = document.getElementsByClassName(`charm-${index}`)[0]
|
||||
ele.remove()
|
||||
}
|
||||
|
||||
const populateCharmsFromCSV = (csv: string, skillNames: SkillNameMap) => {
|
||||
UserCharmList.Instance.deserialize(csv, skillNames)
|
||||
UserCharmList.Instance.get().forEach((charm, i) => {
|
||||
addTableElement(charm, i, skillNames)
|
||||
})
|
||||
}
|
||||
|
||||
const purgeTable = () => {
|
||||
const entries = document.getElementsByClassName('charm-table-ele')
|
||||
for (const entry of Array.from(entries)) {
|
||||
entry.remove()
|
||||
}
|
||||
}
|
||||
|
||||
const addTableElement = (charm: Charm, index: number, skillNames: SkillNameMap) => {
|
||||
const ele = htmlToElement(`<tr class="charm-table-ele charm-${index}" data-index="${index}"></tr>`)
|
||||
|
||||
// get real table elements
|
||||
for (const skill of Array.from(charm.skills.keys())) {
|
||||
ele.appendChild(htmlToElement(`<td>${skillNames.get(skill)}</td>`))
|
||||
ele.appendChild(htmlToElement(`<td>${charm.skills.get(skill)}</td>`))
|
||||
}
|
||||
|
||||
// get placeholder table elements
|
||||
const amountOfSkills = Array.from(charm.skills.keys()).length
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
for (const _ in range(amountOfSkills, 2)) {
|
||||
ele.appendChild(htmlToElement('<td></td>'))
|
||||
ele.appendChild(htmlToElement('<td></td>'))
|
||||
}
|
||||
|
||||
// get slots and delete
|
||||
ele.appendChild(htmlToElement(`<td>${charm.slots}</td>`))
|
||||
const d = htmlToElement('<td class="charm-delete">X</td>')
|
||||
d.addEventListener('click', () => removeCharm(index, skillNames))
|
||||
ele.appendChild(d)
|
||||
|
||||
// add final element
|
||||
const tbody = document.getElementById('charm-table')!.children[0]
|
||||
tbody.appendChild(ele)
|
||||
}
|
||||
|
||||
const addCharm = (charm: Charm, skillNames: SkillNameMap) => {
|
||||
const i = UserCharmList.Instance.add(charm)
|
||||
addTableElement(charm, i - 1, skillNames)
|
||||
saveToStorage(skillNames)
|
||||
}
|
||||
|
||||
const removeCharm = (index: number, skillNames: SkillNameMap) => {
|
||||
UserCharmList.Instance.remove(index)
|
||||
removeTableElement(index)
|
||||
saveToStorage(skillNames)
|
||||
}
|
||||
|
||||
const onExportClick = (skillNames: SkillNameMap) => {
|
||||
const str = UserCharmList.Instance.serialize(skillNames)
|
||||
const blob = new Blob([str], { type: 'text/plain' })
|
||||
const a = document.getElementById('charm-download') as HTMLAnchorElement
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
|
||||
a.href = url
|
||||
a.download = 'charms.csv'
|
||||
a.click()
|
||||
}
|
||||
|
||||
const onImportClick = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
const inp = document.getElementById('charm-upload') as HTMLInputElement
|
||||
inp.click()
|
||||
}
|
||||
|
||||
const onFileUploaded = (skillNames: SkillNameMap) => {
|
||||
const inp = document.getElementById('charm-upload') as HTMLInputElement
|
||||
|
||||
if (!inp.files) {
|
||||
return
|
||||
}
|
||||
|
||||
const file = inp.files[0]
|
||||
file.text().then((text) => {
|
||||
try {
|
||||
UserCharmList.Instance.deserialize(text, skillNames)
|
||||
saveToStorage(skillNames)
|
||||
purgeTable()
|
||||
UserCharmList.Instance.get().forEach((charm, i) => {
|
||||
addTableElement(charm, i, skillNames)
|
||||
})
|
||||
} catch {
|
||||
alert('Could not process file')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const onAddClick = (skillNames: SkillNameMap) => {
|
||||
// parse data
|
||||
const slots = parseInt((document.getElementById('charm-slots') as HTMLSelectElement).value)
|
||||
const skills = [1, 2].map((x) => {
|
||||
return {
|
||||
id: parseInt((document.getElementById(`charm-skill-${x}-name`) as HTMLSelectElement).value),
|
||||
points: parseInt((document.getElementById(`charm-skill-${x}-points`) as HTMLSelectElement).value),
|
||||
}
|
||||
})
|
||||
|
||||
// return if charm invalid
|
||||
if (slots === 0 && !skills.some(s => validSkill(s.id, s.points))) {
|
||||
return
|
||||
}
|
||||
|
||||
// map to model
|
||||
const skillsMap = new EquipmentSkills(skills
|
||||
.filter(s => validSkill(s.id, s.points))
|
||||
.map(s => [s.id, s.points]))
|
||||
const charm: Charm = {
|
||||
name: UserCharmList.getCharmName(skillsMap, slots as Slots, skillNames),
|
||||
slots: slots as Slots,
|
||||
category: EquipmentCategory.CHARM,
|
||||
rarity: 0,
|
||||
skills: skillsMap,
|
||||
}
|
||||
|
||||
// add
|
||||
addCharm(charm, skillNames)
|
||||
}
|
||||
|
||||
const attachControlListeners = (skillNames: SkillNameMap) => {
|
||||
document.getElementById('charm-add')!.addEventListener('click', () => onAddClick(skillNames))
|
||||
document.getElementById('charm-export')!.addEventListener('click', () => onExportClick(skillNames))
|
||||
document.getElementById('charm-import')!.addEventListener('click', (e) => onImportClick(e))
|
||||
document.getElementById('charm-upload')!.addEventListener('change', () => onFileUploaded(skillNames))
|
||||
}
|
||||
|
||||
const populatePointsPickers = () => {
|
||||
const pickers = document.getElementsByClassName('charm-points-pick')
|
||||
for (const picker of Array.from(pickers)) {
|
||||
for (const amount of range(-10, 11).reverse()) {
|
||||
picker.appendChild(htmlToElement(`
|
||||
<option ${amount === 0 ? 'selected="selected"' : ''} value="${amount}">${amount}</option>
|
||||
`))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const populateSkillsPickers = (
|
||||
skillNames: SkillNameMap,
|
||||
skillActivation: SkillActivationMap,
|
||||
skillCategories: string[],
|
||||
) => {
|
||||
const pickers = document.getElementsByClassName('charm-skill-pick')
|
||||
for (const picker of Array.from(pickers)) {
|
||||
// make optgroup for each category
|
||||
const optGroups = skillCategories.map((category, i) => {
|
||||
return htmlToElement(`
|
||||
<optgroup label="${category}" data-category="${i}"></optgroup>
|
||||
`)
|
||||
})
|
||||
|
||||
// append skill options to optgroup
|
||||
skillActivation.forEach((activationList) => {
|
||||
// continue if skill cant be activated -- Torso Up
|
||||
if (activationList.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const dummyActivation = activationList[0]
|
||||
const category = dummyActivation.category
|
||||
const skill = dummyActivation.requiredSkill
|
||||
const name = skillNames.get(skill)
|
||||
|
||||
const ele = htmlToElement(`
|
||||
<option value="${skill}" data-skill="${skill}">${name}</option>
|
||||
`)
|
||||
optGroups[category].appendChild(ele)
|
||||
})
|
||||
|
||||
// add default
|
||||
optGroups.unshift(htmlToElement(`
|
||||
<option value="-1" data-skill="-1">None</option>
|
||||
`))
|
||||
|
||||
// add elements and select default
|
||||
picker.append(...optGroups)
|
||||
picker.getElementsByTagName('option')[0].selected = true
|
||||
}
|
||||
}
|
||||
|
||||
const populateCharmPicker = (
|
||||
skillNames: SkillNameMap,
|
||||
skillActivation: SkillActivationMap,
|
||||
skillCategories: string[],
|
||||
) => {
|
||||
populatePointsPickers()
|
||||
populateSkillsPickers(skillNames, skillActivation, skillCategories)
|
||||
}
|
||||
|
||||
export const renderCharmPicker = (
|
||||
skillNames: SkillNameMap,
|
||||
skillActivation: SkillActivationMap,
|
||||
skillCategories: string[],
|
||||
) => {
|
||||
populateCharmPicker(skillNames, skillActivation, skillCategories)
|
||||
attachControlListeners(skillNames)
|
||||
|
||||
const savedCharms = getFromStorage()
|
||||
if (savedCharms) {
|
||||
populateCharmsFromCSV(savedCharms, skillNames)
|
||||
}
|
||||
}
|
||||
155
src/app/ui/eq-settings.component.ts
Normal file
155
src/app/ui/eq-settings.component.ts
Normal file
@ -0,0 +1,155 @@
|
||||
import EquipmentCategory from '../../data-provider/models/equipment/EquipmentCategory'
|
||||
import UserEquipmentSettings from '../../data-provider/models/user/UserEquipmentSettings'
|
||||
import { htmlToElement } from '../../helper/html.helper'
|
||||
import EquipmentMin from '../../data-provider/models/equipment/EquipmentMin'
|
||||
|
||||
const saveToStorage = () => {
|
||||
window.localStorage.setItem(
|
||||
'eq-settings',
|
||||
UserEquipmentSettings.Instance.serialize(),
|
||||
)
|
||||
}
|
||||
|
||||
const getFromStorage = () => {
|
||||
return window.localStorage.getItem('eq-settings')
|
||||
}
|
||||
|
||||
const getExclusionElement = (x: EquipmentMin) => {
|
||||
const root = document.createElement('div')
|
||||
root.style.textAlign = 'left'
|
||||
root.setAttribute('data-name', x.name)
|
||||
root.classList.add('eq-exclusion-ele')
|
||||
|
||||
const content = htmlToElement(`<span>${x.name}</span>`)
|
||||
const remove = htmlToElement('<span>X</span>') as HTMLSpanElement
|
||||
remove.addEventListener('click', () => removeExlusion(x))
|
||||
remove.style.marginRight = '1em'
|
||||
remove.style.marginLeft = '1em'
|
||||
remove.style.cursor = 'pointer'
|
||||
|
||||
root.appendChild(remove)
|
||||
root.appendChild(content)
|
||||
return root
|
||||
}
|
||||
|
||||
const getPinPicker = (cat: EquipmentCategory, eq: EquipmentMin[]) => {
|
||||
const root = document.createElement('div')
|
||||
root.style.textAlign = 'left'
|
||||
|
||||
const content = document.createElement('select')
|
||||
content.setAttribute('id', `eq-${cat}-pin-picker`)
|
||||
content.style.width = '72%'
|
||||
for (const x of [{ name: 'None', category: cat }].concat(...eq)) {
|
||||
content.appendChild(
|
||||
htmlToElement(`<option value="${x.name}">${x.name}</option>`),
|
||||
)
|
||||
}
|
||||
content.addEventListener('change', () => {
|
||||
addPin({ name: content.value, category: cat })
|
||||
})
|
||||
const remove = htmlToElement('<span>X</span>') as HTMLSpanElement
|
||||
remove.addEventListener('click', () => removePin(eq[0].category))
|
||||
remove.style.marginRight = '1em'
|
||||
remove.style.marginLeft = '1em'
|
||||
remove.style.cursor = 'pointer'
|
||||
|
||||
root.appendChild(remove)
|
||||
root.appendChild(content)
|
||||
return root
|
||||
}
|
||||
|
||||
const renderColumns = (armor: EquipmentMin[][]) => {
|
||||
const parent = document.getElementById('eq-container')
|
||||
for (const item of [
|
||||
[EquipmentCategory.HEAD, 'Head', armor[0]],
|
||||
[EquipmentCategory.CHEST, 'Chest', armor[1]],
|
||||
[EquipmentCategory.ARMS, 'Arms', armor[2]],
|
||||
[EquipmentCategory.WAIST, 'Waist', armor[3]],
|
||||
[EquipmentCategory.LEGS, 'Legs', armor[4]],
|
||||
]) {
|
||||
const cat = item[0] as EquipmentCategory
|
||||
const name = item[1] as string
|
||||
const eq = item[2] as EquipmentMin[]
|
||||
|
||||
const root = htmlToElement(`<div class="eq-column" data-eq-column-type="${cat}"></div>`)
|
||||
|
||||
// pins
|
||||
const pinHeader = htmlToElement(`<div class="eq-column-item eq-column-header">${name} Pinned</div>`)
|
||||
const pinContent = htmlToElement('<div class="eq-column-item eq-column-content eq-column-pin"></div>')
|
||||
const pinElement = getPinPicker(cat, eq)
|
||||
pinContent.appendChild(pinElement)
|
||||
|
||||
// exclusions
|
||||
const exclusionHeader = htmlToElement(`<div class="eq-column-item eq-column-header">${name} Excluded</div>`)
|
||||
const exclusionContent = htmlToElement(`<div id="eq-${cat}-exclusion" class="eq-column-item eq-column-content eq-column-exclusion"></div>`)
|
||||
|
||||
root.appendChild(pinHeader)
|
||||
root.appendChild(pinContent)
|
||||
root.appendChild(exclusionHeader)
|
||||
root.appendChild(exclusionContent)
|
||||
parent!.appendChild(root)
|
||||
}
|
||||
}
|
||||
|
||||
const _addExclusion = (x: EquipmentMin) => {
|
||||
const parent = document.getElementById(`eq-${x.category}-exclusion`)
|
||||
parent!.appendChild(getExclusionElement(x))
|
||||
}
|
||||
|
||||
export const removeExlusion = (x: EquipmentMin) => {
|
||||
const ele = Array.from(document.getElementsByClassName('eq-exclusion-ele')).find((a) => {
|
||||
const b = a as HTMLElement
|
||||
return b.getAttribute('data-name') === x.name
|
||||
}) as HTMLElement
|
||||
if (!ele) return
|
||||
|
||||
ele.remove()
|
||||
UserEquipmentSettings.Instance.removeExclusion(x)
|
||||
saveToStorage()
|
||||
}
|
||||
|
||||
export const removePin = (cat: EquipmentCategory) => {
|
||||
const ele = document.getElementById(`eq-${cat}-pin-picker`) as HTMLSelectElement
|
||||
UserEquipmentSettings.Instance.removePin(cat)
|
||||
ele.selectedIndex = 0
|
||||
saveToStorage()
|
||||
}
|
||||
|
||||
export const addExclusion = (x: EquipmentMin) => {
|
||||
if (UserEquipmentSettings.Instance.hasExclusion(x)) return
|
||||
UserEquipmentSettings.Instance.addExclusion(x)
|
||||
_addExclusion(x)
|
||||
saveToStorage()
|
||||
}
|
||||
|
||||
export const addPin = (x: EquipmentMin) => {
|
||||
if (x.name === 'None') {
|
||||
UserEquipmentSettings.Instance.removePin(x.category)
|
||||
saveToStorage()
|
||||
return
|
||||
}
|
||||
|
||||
UserEquipmentSettings.Instance.addPin(x)
|
||||
saveToStorage()
|
||||
const select = document.getElementById(
|
||||
`eq-${x.category}-pin-picker`,
|
||||
) as HTMLSelectElement
|
||||
select.value = x.name
|
||||
}
|
||||
|
||||
export const renderEqSettings = (armor: EquipmentMin[][]) => {
|
||||
renderColumns(armor)
|
||||
|
||||
const raw = getFromStorage()
|
||||
if (raw) UserEquipmentSettings.Instance.deserialize(raw)
|
||||
|
||||
for (const exclusionList of UserEquipmentSettings.Instance.exclusions) {
|
||||
for (const x of exclusionList) {
|
||||
_addExclusion(x)
|
||||
}
|
||||
}
|
||||
UserEquipmentSettings.Instance.pins.forEach((x, i) => {
|
||||
if (x) addPin(x)
|
||||
else removePin(i)
|
||||
})
|
||||
}
|
||||
19
src/app/ui/global-settings.component.ts
Normal file
19
src/app/ui/global-settings.component.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import Rarity from '../../data-provider/models/equipment/Rarity'
|
||||
import Slots from '../../data-provider/models/equipment/Slots'
|
||||
import GlobalSettings from '../models/GlobalSettings'
|
||||
|
||||
export const getGlobalSettings = (): GlobalSettings => {
|
||||
const armorSelect = document.getElementById('armor-type') as HTMLSelectElement
|
||||
const weaponSlots = document.getElementById('weapon-slots') as HTMLSelectElement
|
||||
const armorRarity = document.getElementById('armor-rarity') as HTMLSelectElement
|
||||
const decoRarity = document.getElementById('deco-rarity') as HTMLSelectElement
|
||||
const limit = document.getElementById('search-limit') as HTMLInputElement
|
||||
|
||||
return {
|
||||
armorType: parseInt(armorSelect.value),
|
||||
weaponSlots: parseInt(weaponSlots.value) as Slots,
|
||||
armorRarity: parseInt(armorRarity.value) as Rarity,
|
||||
decoRarity: parseInt(decoRarity.value) as Rarity,
|
||||
limit: parseInt(limit.value),
|
||||
}
|
||||
}
|
||||
43
src/app/ui/navbar.component.ts
Normal file
43
src/app/ui/navbar.component.ts
Normal file
@ -0,0 +1,43 @@
|
||||
const onMouseEnter = (ele: Element) => {
|
||||
if (!ele.classList.contains('navbar-selected')) {
|
||||
ele.classList.add('navbar-hover')
|
||||
}
|
||||
}
|
||||
|
||||
const onMouseLeave = (ele: Element) => {
|
||||
ele.classList.remove('navbar-hover')
|
||||
}
|
||||
|
||||
const onClick = (parent: Element, ele: Element) => {
|
||||
for (const li of Array.from(parent.children)) {
|
||||
li.classList.remove('navbar-selected')
|
||||
li.classList.remove('navbar-hover')
|
||||
}
|
||||
ele.classList.add('navbar-selected')
|
||||
|
||||
const selection = ele.getAttribute('data-selection')
|
||||
const panels = document.getElementsByClassName('panel')
|
||||
|
||||
for (const panel of Array.from(panels)) {
|
||||
const panelNumber = panel.getAttribute('data-panel-number')
|
||||
if (selection === panelNumber) {
|
||||
panel.classList.remove('hidden')
|
||||
} else {
|
||||
panel.classList.add('hidden')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** initiate navbar state and attaches handlers */
|
||||
export const initiateNavbar = () => {
|
||||
const ul = document.getElementById('navbar-container')!
|
||||
.children[0] as HTMLElement
|
||||
|
||||
for (const li of Array.from(ul.children)) {
|
||||
li.addEventListener('mouseenter', () => onMouseEnter(li))
|
||||
li.addEventListener('mouseleave', () => onMouseLeave(li))
|
||||
li.addEventListener('click', () => onClick(ul, li))
|
||||
}
|
||||
|
||||
onClick(ul, ul.children[0])
|
||||
}
|
||||
112
src/app/ui/picker.component.ts
Normal file
112
src/app/ui/picker.component.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import SkillActivationMap from '../../data-provider/models/skills/SkillActivationMap'
|
||||
import SkillActivation from '../../data-provider/models/skills/SkillActivation'
|
||||
import { htmlToElement } from '../../helper/html.helper'
|
||||
|
||||
const getActivationElements = () => {
|
||||
return Array.from(document.getElementsByClassName('search-picker-activation'))
|
||||
}
|
||||
|
||||
/** uncheck all selected skill activations */
|
||||
const resetSkillActivations = () => {
|
||||
const activations = getActivationElements()
|
||||
|
||||
activations.forEach((element) => {
|
||||
const checkbox = element.children[0] as HTMLInputElement
|
||||
const text = element.children[1] as HTMLElement
|
||||
|
||||
checkbox.checked = false
|
||||
text.classList.remove('highlighted')
|
||||
})
|
||||
}
|
||||
|
||||
/** get list of currently selected skill activations */
|
||||
const getSkillActivations = (): SkillActivation[] => {
|
||||
const activations = getActivationElements()
|
||||
|
||||
return activations
|
||||
// get only checked skills
|
||||
.filter((element) => {
|
||||
const checkbox = element.children[0] as HTMLInputElement
|
||||
return checkbox.checked
|
||||
})
|
||||
// map to proper data model
|
||||
.map((element) => {
|
||||
const name = element.textContent!.trim()
|
||||
const id = parseInt(element.getAttribute('data-id')!)
|
||||
const requiredSkill = parseInt(element.getAttribute('data-skill')!)
|
||||
const requiredPoints = parseInt(element.getAttribute('data-points')!)
|
||||
const category = parseInt(element.parentElement!.getAttribute('data-category')!)
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
requiredPoints,
|
||||
requiredSkill,
|
||||
isPositive: requiredPoints > 0,
|
||||
category,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const renderCategories = (skillCategories: string[]) => {
|
||||
for (const index in skillCategories) {
|
||||
const categoryName = skillCategories[index]
|
||||
const node = htmlToElement(`
|
||||
<div class="search-picker-category" id="search-picker-category-${index}" data-category="${index}">
|
||||
<div class="search-picker-category-title banner">${categoryName}</div>
|
||||
</div>
|
||||
`)
|
||||
document.getElementById('search-skill-picker')!.appendChild(node)
|
||||
}
|
||||
}
|
||||
|
||||
const renderActivations = (skillActivation: SkillActivationMap) => {
|
||||
skillActivation.forEach((activationList) => {
|
||||
activationList
|
||||
.filter(activation => activation.isPositive)
|
||||
.reverse()
|
||||
.forEach((activation) => {
|
||||
const node = htmlToElement(`
|
||||
<div class="search-picker-activation" data-skill="${activation.requiredSkill}" data-points="${activation.requiredPoints}" data-id="${activation.id}">
|
||||
<input style="float:left;" type="checkbox">
|
||||
<div class="search-picker-activation-name">${activation.name}</div>
|
||||
</div>
|
||||
`)
|
||||
document.getElementById(`search-picker-category-${activation.category}`)!.appendChild(node)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const attachClickListener = () => {
|
||||
const elements = Array.from(document.getElementsByClassName('search-picker-activation'))
|
||||
for (const item of elements) {
|
||||
item.addEventListener('click', (event) => {
|
||||
// tick checkbox
|
||||
const target = event.target as Element
|
||||
const input: HTMLInputElement = item.children[0] as HTMLInputElement
|
||||
if (target.tagName !== 'INPUT') {
|
||||
input.checked = !input.checked
|
||||
}
|
||||
|
||||
// add highlight class
|
||||
const text = item.children[1]
|
||||
input.checked ? text.classList.add('highlighted') : text.classList.remove('highlighted')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** render all components of skillpicker and attach handlers */
|
||||
const renderSkillPicker = (
|
||||
skillActivation: SkillActivationMap,
|
||||
skillCategories: string[],
|
||||
) => {
|
||||
renderCategories(skillCategories)
|
||||
renderActivations(skillActivation)
|
||||
attachClickListener()
|
||||
}
|
||||
|
||||
export {
|
||||
renderSkillPicker,
|
||||
getSkillActivations,
|
||||
resetSkillActivations,
|
||||
}
|
||||
149
src/app/ui/search-controls.component.ts
Normal file
149
src/app/ui/search-controls.component.ts
Normal file
@ -0,0 +1,149 @@
|
||||
import UserCharmList from '../../data-provider/models/user/UserCharmList'
|
||||
import ArmorSet from '../../searcher/models/ArmorSet'
|
||||
import SearchConstraints from '../../searcher/models/SearchConstraints'
|
||||
import StaticEquipmentData from '../../data-provider/models/equipment/StaticEquipmentData'
|
||||
import StaticSkillData from '../../data-provider/models/skills/StaticSkillData'
|
||||
import { search } from '../../searcher/searcher.module'
|
||||
import { getGlobalSettings } from './global-settings.component'
|
||||
import { getSkillActivations, resetSkillActivations } from './picker.component'
|
||||
import { moreSkillsIterator, renderMoreSkills, renderResults } from './search-results.component'
|
||||
import SkillActivation from '../../data-provider/models/skills/SkillActivation'
|
||||
import UserEquipmentSettings from '../../data-provider/models/user/UserEquipmentSettings'
|
||||
import EquipmentMin from '../../data-provider/models/equipment/EquipmentMin'
|
||||
|
||||
const pinsOrExclusionsActive = (pins: (EquipmentMin | undefined)[], exclusions: EquipmentMin[][]): boolean => {
|
||||
return pins.some(p => p !== undefined) || exclusions.some(eL => eL.length > 0)
|
||||
}
|
||||
|
||||
const arrangeSearchData = () => {
|
||||
// build params
|
||||
const globalSettings = getGlobalSettings()
|
||||
const skillActivations = getSkillActivations()
|
||||
|
||||
// return if no skill selected
|
||||
if (skillActivations.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// sanitize activation input to only include highest version of each skill
|
||||
const sanitizedSkillActivations = skillActivations
|
||||
.filter((thisAct, i) => {
|
||||
return skillActivations.every((compareAct, j) => {
|
||||
if (i === j) return true
|
||||
if (thisAct.requiredSkill !== compareAct.requiredSkill) return true
|
||||
|
||||
return thisAct.requiredPoints >= compareAct.requiredPoints
|
||||
})
|
||||
})
|
||||
|
||||
// create search params
|
||||
const searchParams: SearchConstraints = {
|
||||
weaponSlots: globalSettings.weaponSlots,
|
||||
armorType: globalSettings.armorType,
|
||||
armorRarity: globalSettings.armorRarity,
|
||||
decoRarity: globalSettings.decoRarity,
|
||||
limit: Math.min(Math.max(globalSettings.limit, 1), 1000),
|
||||
skillActivations: sanitizedSkillActivations,
|
||||
pins: UserEquipmentSettings.Instance.pins,
|
||||
exclusions: UserEquipmentSettings.Instance.exclusions,
|
||||
}
|
||||
|
||||
return searchParams
|
||||
}
|
||||
|
||||
const searchLogic = (equData: StaticEquipmentData, skillData: StaticSkillData) => {
|
||||
const searchParams = arrangeSearchData()
|
||||
|
||||
if (!searchParams) {
|
||||
alert('Please select at least one skill')
|
||||
return
|
||||
}
|
||||
|
||||
// search for sets
|
||||
const result = search(
|
||||
equData.armor,
|
||||
equData.decorations,
|
||||
UserCharmList.Instance.get(),
|
||||
searchParams,
|
||||
skillData,
|
||||
)
|
||||
|
||||
// render results
|
||||
renderResults(result, skillData, searchParams, pinsOrExclusionsActive(searchParams.pins, searchParams.exclusions))
|
||||
}
|
||||
|
||||
const moreSkillsLogic = async (equData: StaticEquipmentData, skillData: StaticSkillData) => {
|
||||
const searchParams = arrangeSearchData()
|
||||
|
||||
if (!searchParams) {
|
||||
alert('Please select at least one skill')
|
||||
return
|
||||
}
|
||||
|
||||
const charms = UserCharmList.Instance.get()
|
||||
|
||||
const aquirableSkills: SkillActivation[] = []
|
||||
|
||||
const outputIterator = moreSkillsIterator(skillData.skillActivation)
|
||||
|
||||
for (const actMap of skillData.skillActivation) {
|
||||
const sActs = actMap[1]
|
||||
|
||||
const processedActs = sActs
|
||||
.filter(act => act.requiredPoints >= 0)
|
||||
.filter(act => !searchParams.skillActivations.map(x => x.id).includes(act.id))
|
||||
.filter(act => !searchParams.skillActivations.find(x => act.requiredSkill === x.requiredSkill && act.requiredPoints < x.requiredPoints))
|
||||
.sort((a, b) => a.requiredPoints - b.requiredPoints)
|
||||
|
||||
let breakFlag = false
|
||||
for (const act of processedActs) {
|
||||
outputIterator.next()
|
||||
if (breakFlag) continue
|
||||
|
||||
const newParams: SearchConstraints = {
|
||||
...searchParams,
|
||||
limit: 1,
|
||||
skillActivations: searchParams.skillActivations.concat(act),
|
||||
}
|
||||
|
||||
const r = await new Promise<ArmorSet[]>((resolve, _reject) => {
|
||||
setTimeout(() => {
|
||||
const innerR = search(
|
||||
equData.armor,
|
||||
equData.decorations,
|
||||
charms,
|
||||
newParams,
|
||||
skillData,
|
||||
)
|
||||
resolve(innerR)
|
||||
})
|
||||
})
|
||||
|
||||
if (r.length === 0) breakFlag = true
|
||||
else aquirableSkills.push(act)
|
||||
}
|
||||
}
|
||||
|
||||
renderMoreSkills(aquirableSkills, pinsOrExclusionsActive(searchParams.pins, searchParams.exclusions))
|
||||
}
|
||||
|
||||
const resetLogic = () => {
|
||||
resetSkillActivations()
|
||||
}
|
||||
|
||||
/** attach handlers for control buttons */
|
||||
export const attachControlListeners = (equData: StaticEquipmentData, skillData: StaticSkillData) => {
|
||||
const searchBtn = document.getElementById('search-btn') as HTMLButtonElement
|
||||
const moreSkillsBtn = document.getElementById('more-btn') as HTMLButtonElement
|
||||
const resetBtn = document.getElementById('reset-btn') as HTMLButtonElement
|
||||
|
||||
searchBtn.addEventListener('click', () => {
|
||||
searchLogic(equData, skillData)
|
||||
})
|
||||
moreSkillsBtn.addEventListener('click', () => {
|
||||
moreSkillsLogic(equData, skillData)
|
||||
})
|
||||
resetBtn.addEventListener('click', () => {
|
||||
resetLogic()
|
||||
})
|
||||
}
|
||||
288
src/app/ui/search-results.component.ts
Normal file
288
src/app/ui/search-results.component.ts
Normal file
@ -0,0 +1,288 @@
|
||||
import ArmorSet from '../../searcher/models/ArmorSet'
|
||||
import SearchConstraints from '../../searcher/models/SearchConstraints'
|
||||
import StaticSkillData from '../../data-provider/models/skills/StaticSkillData'
|
||||
import UserEquipmentSettings from '../../data-provider/models/user/UserEquipmentSettings'
|
||||
import { htmlToElement } from '../../helper/html.helper'
|
||||
import SkillActivation from '../../data-provider/models/skills/SkillActivation'
|
||||
import SkillActivationMap from '../../data-provider/models/skills/SkillActivationMap'
|
||||
import { addExclusion, addPin, removeExlusion, removePin } from './eq-settings.component'
|
||||
|
||||
export function * moreSkillsIterator (skillActivations: SkillActivationMap) {
|
||||
const rContainer = clearAndGetResultsContainer()
|
||||
const countDiv = document.createElement('div')
|
||||
rContainer.appendChild(countDiv)
|
||||
|
||||
const totalActCount = Array.from(skillActivations.values())
|
||||
.reduce((sum, c) => sum + c.length, 0)
|
||||
|
||||
for (let i = 0; i < totalActCount; i++) {
|
||||
countDiv.innerHTML = `Checked ${i} possible skills ...`
|
||||
yield i
|
||||
}
|
||||
}
|
||||
|
||||
const onSetClick = (tbNode: Node, viewGetter: () => Node) => {
|
||||
const children = tbNode.childNodes
|
||||
const finalNode = children[children.length - 1] as HTMLTableRowElement
|
||||
|
||||
// toggle if details have already been rendered
|
||||
if (finalNode.classList.contains('result-set-details')) {
|
||||
finalNode.classList.toggle('hidden')
|
||||
return
|
||||
}
|
||||
|
||||
// render and append them otherwise
|
||||
tbNode.appendChild(viewGetter())
|
||||
}
|
||||
|
||||
const PINS_OR_EXCL_ACTIVE_BANNER = htmlToElement(`
|
||||
<div class="results-banner banner">
|
||||
You have some pins or exclusions active, which may be limiting results. You may find some results by removing those pins or exclusions.
|
||||
<div>
|
||||
`)
|
||||
|
||||
const getExpandedView = (set: ArmorSet, skillData: StaticSkillData, searchParams: SearchConstraints) => {
|
||||
// build header
|
||||
const header = htmlToElement(`
|
||||
<tr>
|
||||
<th>Skill</th>
|
||||
<th style="width: 6%">Weapon</th>
|
||||
<th style="width: 6%">Head</th>
|
||||
<th style="width: 6%">Chest</th>
|
||||
<th style="width: 6%">Arms</th>
|
||||
<th style="width: 6%">Waist</th>
|
||||
<th style="width: 6%">Legs</th>
|
||||
<th style="width: 6%">Charm</th>
|
||||
<th style="width: 6%">Deco</th>
|
||||
<th style="width: 6%">Total</th>
|
||||
<th>Active</th>
|
||||
</tr>
|
||||
`)
|
||||
|
||||
// build skills rows
|
||||
const skillRows = Array.from(set.evaluation!.skills.entries())
|
||||
.sort(([_a, a], [_b, b]) => b - a)
|
||||
.map(([sId, sVal]) => {
|
||||
const r = document.createElement('tr')
|
||||
|
||||
const computedDecoValue = set.decos
|
||||
.map(d => d.skills.get(sId)!)
|
||||
.reduce((sum, c) => sum + c, 0)
|
||||
|
||||
r.appendChild(htmlToElement(`<td>${skillData.skillName.get(sId) ? skillData.skillName.get(sId)! : ''}</td>`))
|
||||
r.appendChild(htmlToElement('<td></td>')) // weapon
|
||||
for (const p of set.getPieces()) {
|
||||
r.append(htmlToElement(`<td>${p.skills.get(sId) ? p.skills.get(sId)! : ''}</td>`))
|
||||
}
|
||||
r.append(htmlToElement(`<td>${set.charm.skills.get(sId) ? set.charm.skills.get(sId)! : ''}</td>`))
|
||||
r.append(htmlToElement(`<td>${computedDecoValue || ''}</td>`))
|
||||
r.append(htmlToElement(`<td>${sVal}</td>`))
|
||||
const possibleAct = set.evaluation!.activations.find(a => a.requiredSkill === sId)
|
||||
if (possibleAct) r.append(htmlToElement(`<td ${!possibleAct.isPositive ? 'class="neg-skill"' : ''}}">${possibleAct.name}</td>`))
|
||||
return r
|
||||
})
|
||||
|
||||
// build slot list
|
||||
const slotRow = document.createElement('tr')
|
||||
slotRow.appendChild(htmlToElement('<td>Slots</td>'))
|
||||
const rawSlowList = [searchParams.weaponSlots, ...set.getPieces().map(x => x.slots), set.charm.slots]
|
||||
rawSlowList.forEach(s => slotRow.appendChild(htmlToElement(`<td>${s}</td>`)))
|
||||
|
||||
// append elements to table
|
||||
const skillTable = htmlToElement('<table class="result-set-skill-table"></table>')
|
||||
skillTable.appendChild(header)
|
||||
skillRows.forEach(x => skillTable.appendChild(x))
|
||||
skillTable.appendChild(slotRow)
|
||||
|
||||
// build deco list
|
||||
const decoNameMap: Map<string, number> = new Map()
|
||||
for (const deco of set.decos) {
|
||||
const name = deco.name
|
||||
decoNameMap.set(name, 1 + (decoNameMap.get(name) || 0))
|
||||
}
|
||||
const decoNameList = Array.from(decoNameMap.entries())
|
||||
.map(([name, amount]) => `${amount} x ${name}`)
|
||||
const decoNameString = decoNameList.join(', ')
|
||||
const decoNameContainer = htmlToElement(`
|
||||
<div><span>${decoNameString}</span></div>
|
||||
`)
|
||||
|
||||
// build piece table
|
||||
const pieceTable = htmlToElement('<table class="result-set-piece-table"></table>')
|
||||
const pieceTableHeader = htmlToElement('<tr><th>Def</th><th>Piece</th><th>Pin</th><th>Excl</th></tr>')
|
||||
pieceTable.appendChild(pieceTableHeader)
|
||||
for (const piece of set.getPieces()) {
|
||||
const pieceTableEle = document.createElement('tr')
|
||||
const pieceTableDef = htmlToElement(`<td style="width: 20%;">${piece.defense.max}</td>`)
|
||||
const pieceTableName = htmlToElement(`<td style="width: 50%;">${piece.name}</td>`)
|
||||
|
||||
const pieceTablePin = (piece.isGeneric
|
||||
? htmlToElement('<td style="user-select: none; width: 15%;"></td>')
|
||||
: htmlToElement('<td style="user-select: none; width: 15%; cursor: pointer;">✓</td>')) as HTMLElement
|
||||
const pieceTableExcl = htmlToElement('<td style="user-select: none; width: 15%; cursor: pointer;">X</td>') as HTMLElement
|
||||
if (UserEquipmentSettings.Instance.hasPin(piece)) pieceTablePin.classList.add('pin-highlighted')
|
||||
if (UserEquipmentSettings.Instance.hasExclusion(piece)) pieceTableExcl.classList.add('excl-highlighted')
|
||||
|
||||
pieceTablePin.addEventListener('click', () => {
|
||||
if (piece.isGeneric) return
|
||||
|
||||
if (UserEquipmentSettings.Instance.hasPin(piece)) {
|
||||
removePin(piece.category)
|
||||
pieceTablePin.classList.remove('pin-highlighted')
|
||||
} else {
|
||||
addPin(piece)
|
||||
pieceTablePin.classList.add('pin-highlighted')
|
||||
}
|
||||
})
|
||||
pieceTableExcl.addEventListener('click', () => {
|
||||
if (UserEquipmentSettings.Instance.hasExclusion(piece)) {
|
||||
removeExlusion(piece)
|
||||
pieceTableExcl.classList.remove('excl-highlighted')
|
||||
} else {
|
||||
addExclusion(piece)
|
||||
pieceTableExcl.classList.add('excl-highlighted')
|
||||
}
|
||||
})
|
||||
|
||||
pieceTableEle.appendChild(pieceTableDef)
|
||||
pieceTableEle.appendChild(pieceTableName)
|
||||
pieceTableEle.appendChild(pieceTablePin)
|
||||
pieceTableEle.appendChild(pieceTableExcl)
|
||||
pieceTable.appendChild(pieceTableEle)
|
||||
}
|
||||
|
||||
// return final div
|
||||
const tr = htmlToElement('<tr class="result-set-details"></tr>')
|
||||
const td = htmlToElement('<td colspan="6""></td>')
|
||||
const d = htmlToElement('<div class="result-set-details-container"></div>')
|
||||
td.appendChild(d)
|
||||
tr.appendChild(td)
|
||||
d.appendChild(pieceTable)
|
||||
d.appendChild(skillTable)
|
||||
d.appendChild(document.createElement('div')) // dummy for easy grid
|
||||
d.appendChild(decoNameContainer)
|
||||
return tr
|
||||
}
|
||||
|
||||
const getSetElement = (set: ArmorSet, skillData: StaticSkillData, searchParams: SearchConstraints) => {
|
||||
// get bonus and negative skills
|
||||
const requiredActivations = searchParams.skillActivations
|
||||
const unrelatedActivations = set.evaluation!.activations.filter((act) => {
|
||||
return !act.isPositive || // negative skill
|
||||
!requiredActivations.find(req => req.requiredSkill === act.requiredSkill) || // skill is not in required
|
||||
requiredActivations.find(req => req.requiredSkill === act.requiredSkill && act.requiredPoints > req.requiredPoints) // skill is upgrade of smth required
|
||||
})
|
||||
const unrelatedHtmlStrings = unrelatedActivations
|
||||
.sort((a, b) => b.requiredPoints - a.requiredPoints)
|
||||
.map((x) => {
|
||||
return `<span class="result-set-unrelated-skill ${!x.isPositive ? 'neg-skill' : ''}">${x.name}</span>`
|
||||
})
|
||||
|
||||
// get basic display components
|
||||
const tb = htmlToElement('<tbody class="result-set"></tbody>')
|
||||
const row1 = htmlToElement(`
|
||||
<tr class="result-set-row result-set-row1">
|
||||
<td>${set.head.name}</td>
|
||||
<td>${set.chest.name}</td>
|
||||
<td>${set.arms.name}</td>
|
||||
<td>${set.waist.name}</td>
|
||||
<td>${set.legs.name}</td>
|
||||
<td>${set.charm.name}</td>
|
||||
</tr>`)
|
||||
const row2 = htmlToElement(`
|
||||
<tr class="result-set-row result-set-row2">
|
||||
<td colspan="6">
|
||||
<p><span class="def">DEF</span> <span>${set.evaluation.defense.max}</span></p>
|
||||
<p><span class="fir">FIR</span> <span>${set.evaluation.resistance[0]}</span></p>
|
||||
<p><span class="wat">WAT</span> <span>${set.evaluation.resistance[1]}</span></p>
|
||||
<p><span class="ice">ICE</span> <span>${set.evaluation.resistance[2]}</span></p>
|
||||
<p><span class="thn">THN</span> <span>${set.evaluation.resistance[3]}</span></p>
|
||||
<p><span class="drg">DRG</span> <span>${set.evaluation.resistance[4]}</span></p>
|
||||
<span class="result-set-unrelated">${unrelatedHtmlStrings.join('')}</span>
|
||||
</td>
|
||||
</tr>`)
|
||||
|
||||
// append basic display components
|
||||
const getter = () => { return getExpandedView(set, skillData, searchParams) }
|
||||
for (const row of [row1, row2]) {
|
||||
tb.appendChild(row)
|
||||
row.addEventListener('click', () => onSetClick(tb, getter))
|
||||
}
|
||||
|
||||
return tb
|
||||
}
|
||||
|
||||
const onMoreSkillsActClick = (d: HTMLDivElement) => {
|
||||
const id = parseInt(d.getAttribute('data-id')!)
|
||||
|
||||
for (const ele of Array.from(document.getElementsByClassName('search-picker-activation'))) {
|
||||
const thisId = parseInt(ele.getAttribute('data-id')!)
|
||||
if (id === thisId) {
|
||||
(ele as HTMLDivElement).click()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const clearAndGetResultsContainer = () => {
|
||||
const resultContainer = document.getElementById('search-results')!
|
||||
for (const c of Array.from(resultContainer.children)) c.remove()
|
||||
return resultContainer
|
||||
}
|
||||
|
||||
export const renderMoreSkills = (activations: SkillActivation[], pinsOrExclActive: boolean) => {
|
||||
const resultContainer = clearAndGetResultsContainer()
|
||||
|
||||
if (activations.length === 0) {
|
||||
resultContainer.appendChild(htmlToElement(`
|
||||
<div class="results-banner banner">
|
||||
Can't fit more skills
|
||||
<div>
|
||||
`))
|
||||
|
||||
if (pinsOrExclActive) resultContainer.appendChild(PINS_OR_EXCL_ACTIVE_BANNER)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for (const act of activations) {
|
||||
const d = htmlToElement(`<div class="results-more-skills-act" data-id="${act.id}"></div>`) as HTMLDivElement
|
||||
d.appendChild(htmlToElement(`<span class="results-more-skills-act-content">${act.name}</span>`))
|
||||
d.addEventListener('click', () => { onMoreSkillsActClick(d) })
|
||||
resultContainer.appendChild(d)
|
||||
}
|
||||
}
|
||||
|
||||
export const renderResults = (sets: ArmorSet[], skillData: StaticSkillData, searchParams: SearchConstraints, pinsOrExclActive: boolean) => {
|
||||
const resultContainer = clearAndGetResultsContainer()
|
||||
|
||||
// add search param element
|
||||
resultContainer.appendChild(htmlToElement(`
|
||||
<div class="results-title">Results for ${searchParams.skillActivations.map(x => x.name).join(', ')} (${sets.length} matches)</div>
|
||||
`))
|
||||
|
||||
// return if no results
|
||||
if (sets.length === 0) {
|
||||
resultContainer.appendChild(htmlToElement(`
|
||||
<div class="results-banner banner">
|
||||
No matching armor sets
|
||||
<div>
|
||||
`))
|
||||
|
||||
if (pinsOrExclActive) resultContainer.appendChild(PINS_OR_EXCL_ACTIVE_BANNER)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// build table and table header
|
||||
const table = htmlToElement('<table class="results-table" id="results-table"></table>')
|
||||
const header = htmlToElement('<tr><th>Head</th><th>Torso</th><th>Arms</th><th>Waist</th><th>Legs</th><th>Charm</th></tr>')
|
||||
resultContainer.appendChild(table)
|
||||
table.appendChild(header)
|
||||
|
||||
// build and append html elements for each armor set
|
||||
sets
|
||||
.sort((a, b) => b.evaluation.defense.max - a.evaluation.defense.max)
|
||||
.map(set => getSetElement(set, skillData, searchParams))
|
||||
.forEach(ele => table.appendChild(ele))
|
||||
}
|
||||
146
src/data-filter/data-filter.module.ts
Normal file
146
src/data-filter/data-filter.module.ts
Normal file
@ -0,0 +1,146 @@
|
||||
import { DUMMY_PIECE, MAX_RARITY, TORSO_UP_ID } from '../data-provider/data-provider.module'
|
||||
import ArmorPiece from '../data-provider/models/equipment/ArmorPiece'
|
||||
import ArmorType from '../data-provider/models/equipment/ArmorType'
|
||||
import Charm from '../data-provider/models/equipment/Charm'
|
||||
import EquipmentCategory from '../data-provider/models/equipment/EquipmentCategory'
|
||||
import EquipmentMin from '../data-provider/models/equipment/EquipmentMin'
|
||||
import EquipmentSkills from '../data-provider/models/equipment/EquipmentSkills'
|
||||
import Rarity from '../data-provider/models/equipment/Rarity'
|
||||
import SkilledItem from '../data-provider/models/equipment/SkilledItem'
|
||||
import Slots from '../data-provider/models/equipment/Slots'
|
||||
import SkillActivation from '../data-provider/models/skills/SkillActivation'
|
||||
|
||||
const filterType = (piece: ArmorPiece, type: ArmorType) => {
|
||||
return piece.type === ArmorType.ALL || piece.type === type
|
||||
}
|
||||
|
||||
const filterExclusions = (piece: ArmorPiece, exclusionNames: string[]) => {
|
||||
return !exclusionNames.includes(piece.name)
|
||||
}
|
||||
|
||||
const filterRarity = (item: SkilledItem, rarity: Rarity) => {
|
||||
return item.rarity <= rarity
|
||||
}
|
||||
|
||||
const filterHasSkill = (item: SkilledItem, desiredSkills: SkillActivation[]) => {
|
||||
return desiredSkills.some((act) => {
|
||||
const s = item.skills.get(act.requiredSkill)
|
||||
return s && s > 0
|
||||
})
|
||||
}
|
||||
|
||||
const applyRarityFilter = (items: SkilledItem[], rarity: Rarity) => {
|
||||
if (rarity === MAX_RARITY) return items
|
||||
return items.filter(x => filterRarity(x, rarity))
|
||||
}
|
||||
|
||||
const applyCharmFilter = (charms: Charm[], skills: SkillActivation[]) => {
|
||||
// find generic slot charms
|
||||
const genericSlotCharms: Charm[] = []
|
||||
for (const slots of [3, 2, 1]) {
|
||||
const x = charms.find(c => c.slots === slots)
|
||||
if (x) {
|
||||
const newC: Charm = {
|
||||
name: `${slots} Slot Charm`,
|
||||
slots: slots as Slots,
|
||||
category: EquipmentCategory.CHARM,
|
||||
rarity: 0,
|
||||
skills: new EquipmentSkills(),
|
||||
}
|
||||
genericSlotCharms.push(newC)
|
||||
}
|
||||
}
|
||||
|
||||
// build list of charms with wanted skills or with slots
|
||||
const result = charms
|
||||
.filter(x => filterHasSkill(x, skills))
|
||||
.concat(...genericSlotCharms)
|
||||
|
||||
// return list with dummy charm if there are no pieces
|
||||
if (result.length === 0) {
|
||||
return [{
|
||||
...DUMMY_PIECE,
|
||||
category: EquipmentCategory.CHARM,
|
||||
}]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const applyArmorFilter = (
|
||||
pieces: ArmorPiece[],
|
||||
rarity: Rarity,
|
||||
type: ArmorType,
|
||||
category: EquipmentCategory,
|
||||
pin: EquipmentMin | undefined,
|
||||
exclusions: EquipmentMin[],
|
||||
skills: SkillActivation[],
|
||||
) => {
|
||||
if (pin) return [pieces.find(x => x.name === pin.name)!]
|
||||
|
||||
const excludedNames = exclusions.map(e => e.name)
|
||||
|
||||
const rarityFiltered = applyRarityFilter(pieces, rarity) as ArmorPiece[]
|
||||
const typeFiltered = rarityFiltered.filter(p => filterType(p, type))
|
||||
const exclusionFiltered = typeFiltered.filter(p => filterExclusions(p, excludedNames))
|
||||
const sorted = exclusionFiltered.sort((a, b) => b.defense.max - a.defense.max)
|
||||
|
||||
// find generic slot pieces with highest defense
|
||||
const genericSlotPieces: ArmorPiece[] = []
|
||||
for (const slots of [3, 2, 1]) {
|
||||
const x = sorted.find(p => p.slots === slots)
|
||||
if (x) {
|
||||
const p: ArmorPiece = {
|
||||
type: x.type,
|
||||
defense: x.defense,
|
||||
resistance: x.resistance,
|
||||
name: `${slots} Slot Piece`,
|
||||
slots: slots as Slots,
|
||||
category: x.category,
|
||||
rarity: x.rarity,
|
||||
skills: new EquipmentSkills(),
|
||||
isGeneric: true,
|
||||
}
|
||||
if (filterExclusions(p, excludedNames)) genericSlotPieces.push(p)
|
||||
}
|
||||
}
|
||||
|
||||
// find piece with torso up with highest defense
|
||||
const torsoUpPieces: ArmorPiece[] = [sorted.find(p => p.skills.has(TORSO_UP_ID))]
|
||||
.filter(x => x !== undefined)
|
||||
.map(x => {
|
||||
const renamed: ArmorPiece = {
|
||||
...x!,
|
||||
name: 'Torso Up Piece',
|
||||
isGeneric: true,
|
||||
}
|
||||
return renamed
|
||||
})
|
||||
.filter(x => filterExclusions(x, excludedNames)) as ArmorPiece[]
|
||||
|
||||
// build list of pieces with wanted skills, with slots, or with torso up
|
||||
const result = sorted
|
||||
.filter(x => filterHasSkill(x, skills))
|
||||
.concat(...genericSlotPieces)
|
||||
.concat(...torsoUpPieces)
|
||||
|
||||
// return list with dummy element if there are no pieces
|
||||
if (result.length === 0) {
|
||||
return [{
|
||||
...DUMMY_PIECE,
|
||||
type,
|
||||
category,
|
||||
}]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export {
|
||||
filterType,
|
||||
filterRarity,
|
||||
filterHasSkill,
|
||||
applyRarityFilter,
|
||||
applyCharmFilter,
|
||||
applyArmorFilter,
|
||||
}
|
||||
124
src/data-provider/data-provider.module.ts
Normal file
124
src/data-provider/data-provider.module.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import ArmorPiece from './models/equipment/ArmorPiece'
|
||||
import Decoration from './models/equipment/Decoration'
|
||||
import EquipmentSkills from './models/equipment/EquipmentSkills'
|
||||
import GameID from './models/GameId'
|
||||
import SkillActivation from './models/skills/SkillActivation'
|
||||
import SkillActivationMap from './models/skills/SkillActivationMap'
|
||||
import Skill from './models/skills/Skill'
|
||||
import SkillNameMap from './models/skills/SkillNameMap'
|
||||
|
||||
const MAX_RARITY = 7
|
||||
|
||||
const TORSO_UP_ID = 83
|
||||
|
||||
const DUMMY_PIECE: ArmorPiece = {
|
||||
name: 'None',
|
||||
type: -1,
|
||||
defense: { base: 0, max: 0, maxLr: 0 },
|
||||
resistance: [0, 0, 0, 0, 0],
|
||||
category: -1,
|
||||
slots: 0,
|
||||
rarity: 0,
|
||||
skills: new EquipmentSkills(),
|
||||
isGeneric: true,
|
||||
}
|
||||
|
||||
/** fetch from data directory */
|
||||
const getRawData = async (url: string) => {
|
||||
return (await fetch(url)).json()
|
||||
}
|
||||
|
||||
/** fetch and parse generic equipment data */
|
||||
const getDataWithTransformedSkillMap = async (url: string): Promise<{skills: EquipmentSkills}[]> => {
|
||||
const raw = await getRawData(url)
|
||||
return raw.map((rawX: any) => {
|
||||
const skillMap: EquipmentSkills = new EquipmentSkills()
|
||||
for (const x in rawX.skills) {
|
||||
const skill: Skill = rawX.skills[x]
|
||||
skillMap.set(parseInt(x), skill)
|
||||
}
|
||||
return {
|
||||
...rawX,
|
||||
skills: skillMap,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** get a list of all head armor pieces */
|
||||
const getHead = async (): Promise<ArmorPiece[]> => {
|
||||
return getDataWithTransformedSkillMap('./head.json') as unknown as ArmorPiece[]
|
||||
}
|
||||
|
||||
/** get a list of all chest armor pieces */
|
||||
const getChest = async (): Promise<ArmorPiece[]> => {
|
||||
return getDataWithTransformedSkillMap('./chest.json') as unknown as ArmorPiece[]
|
||||
}
|
||||
|
||||
/** get a list of all arms armor pieces */
|
||||
const getArms = async (): Promise<ArmorPiece[]> => {
|
||||
return getDataWithTransformedSkillMap('./arms.json') as unknown as ArmorPiece[]
|
||||
}
|
||||
|
||||
/** get a list of all waist armor pieces */
|
||||
const getWaist = async (): Promise<ArmorPiece[]> => {
|
||||
return getDataWithTransformedSkillMap('./waist.json') as unknown as ArmorPiece[]
|
||||
}
|
||||
|
||||
/** get a list of all legs armor pieces */
|
||||
const getLegs = async (): Promise<ArmorPiece[]> => {
|
||||
return getDataWithTransformedSkillMap('./legs.json') as unknown as ArmorPiece[]
|
||||
}
|
||||
|
||||
/** get a list of all decorations */
|
||||
const getDecorations = async (): Promise<Decoration[]> => {
|
||||
return getDataWithTransformedSkillMap('./decorations.json') as unknown as Decoration[]
|
||||
}
|
||||
|
||||
/** get a mapping of internal id to name for all skills */
|
||||
const getSkillNameMap = async (): Promise<SkillNameMap> => {
|
||||
const raw = await getRawData('./skill-names.json')
|
||||
const map: Map<GameID, string> = new Map()
|
||||
for (const id in raw) {
|
||||
map.set(parseInt(id), raw[id])
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
/** get a list of skill category names, as used in the UI */
|
||||
const getSkillCategories = async (): Promise<string[]> => {
|
||||
return getRawData('./skill-categories.json')
|
||||
}
|
||||
|
||||
/** get a mapping of internal id of skill to all activations (positive and negative) of that skill */
|
||||
const getSkillActivationMap = async (): Promise<SkillActivationMap> => {
|
||||
const raw = await getRawData('./skills.json')
|
||||
const map: Map<GameID, SkillActivation[]> = new Map()
|
||||
for (const id in raw) {
|
||||
const parsedId = parseInt(id)
|
||||
map.set(
|
||||
parsedId,
|
||||
raw[id].map((activation: any) => {
|
||||
return {
|
||||
...activation,
|
||||
requiredSkill: parsedId,
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
export {
|
||||
MAX_RARITY,
|
||||
TORSO_UP_ID,
|
||||
DUMMY_PIECE,
|
||||
getHead,
|
||||
getChest,
|
||||
getArms,
|
||||
getWaist,
|
||||
getLegs,
|
||||
getDecorations,
|
||||
getSkillNameMap,
|
||||
getSkillCategories,
|
||||
getSkillActivationMap,
|
||||
}
|
||||
3
src/data-provider/models/GameID.ts
Normal file
3
src/data-provider/models/GameID.ts
Normal file
@ -0,0 +1,3 @@
|
||||
type GameID = number;
|
||||
|
||||
export default GameID
|
||||
10
src/data-provider/models/equipment/ArmorPiece.ts
Normal file
10
src/data-provider/models/equipment/ArmorPiece.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import Defense from './Defense'
|
||||
import ArmorType from './ArmorType'
|
||||
import Resistance from './Resistance'
|
||||
import SkilledEquipment from './SkilledEquipment'
|
||||
|
||||
export default interface ArmorPiece extends SkilledEquipment {
|
||||
type: ArmorType;
|
||||
defense: Defense;
|
||||
resistance: Resistance;
|
||||
}
|
||||
8
src/data-provider/models/equipment/ArmorType.ts
Normal file
8
src/data-provider/models/equipment/ArmorType.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
enum ArmorType {
|
||||
ALL = 0,
|
||||
BLADEMASTER = 1,
|
||||
GUNNER = 2,
|
||||
}
|
||||
|
||||
export default ArmorType
|
||||
4
src/data-provider/models/equipment/Charm.ts
Normal file
4
src/data-provider/models/equipment/Charm.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import SkilledEquipment from './SkilledEquipment'
|
||||
|
||||
export default interface Charm extends SkilledEquipment {
|
||||
}
|
||||
10
src/data-provider/models/equipment/Decoration.ts
Normal file
10
src/data-provider/models/equipment/Decoration.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import Rarity from './Rarity'
|
||||
import SkilledItem from './SkilledItem'
|
||||
import Slots from './Slots'
|
||||
|
||||
export default interface Decoration extends SkilledItem {
|
||||
name: string;
|
||||
requiredSlots: Slots;
|
||||
rarity: Rarity;
|
||||
affectedByTorsoUp?: boolean;
|
||||
}
|
||||
5
src/data-provider/models/equipment/Defense.ts
Normal file
5
src/data-provider/models/equipment/Defense.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default interface Defense {
|
||||
base: number,
|
||||
max: number,
|
||||
maxLr?: number, // only for low rank pieces
|
||||
}
|
||||
10
src/data-provider/models/equipment/Elements.ts
Normal file
10
src/data-provider/models/equipment/Elements.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
enum Elements {
|
||||
FIRE = 0,
|
||||
WATER = 1,
|
||||
ICE = 2,
|
||||
THUNDER = 3,
|
||||
DRAGON = 4,
|
||||
}
|
||||
|
||||
export default Elements
|
||||
11
src/data-provider/models/equipment/Equipment.ts
Normal file
11
src/data-provider/models/equipment/Equipment.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import EquipmentCategory from './EquipmentCategory'
|
||||
import EquipmentMin from './EquipmentMin'
|
||||
import Rarity from './Rarity'
|
||||
import Slots from './Slots'
|
||||
|
||||
export default interface Equipment extends EquipmentMin {
|
||||
name: string;
|
||||
slots: Slots;
|
||||
category: EquipmentCategory;
|
||||
rarity: Rarity;
|
||||
}
|
||||
12
src/data-provider/models/equipment/EquipmentCategory.ts
Normal file
12
src/data-provider/models/equipment/EquipmentCategory.ts
Normal file
@ -0,0 +1,12 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
enum EquipmentCategory {
|
||||
HEAD = 0,
|
||||
CHEST = 1,
|
||||
ARMS = 2,
|
||||
WAIST = 3,
|
||||
LEGS = 4,
|
||||
CHARM = 5,
|
||||
WEAPON = 6,
|
||||
}
|
||||
|
||||
export default EquipmentCategory
|
||||
7
src/data-provider/models/equipment/EquipmentMin.ts
Normal file
7
src/data-provider/models/equipment/EquipmentMin.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import EquipmentCategory from './EquipmentCategory'
|
||||
|
||||
export default interface EquipmentMin {
|
||||
name: string;
|
||||
category: EquipmentCategory;
|
||||
isGeneric?: boolean;
|
||||
}
|
||||
34
src/data-provider/models/equipment/EquipmentSkills.ts
Normal file
34
src/data-provider/models/equipment/EquipmentSkills.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import GameID from '../GameId'
|
||||
import Skill from '../skills/Skill'
|
||||
|
||||
export default class EquipmentSkills extends Map<GameID, Skill> {
|
||||
get (key: GameID): Skill {
|
||||
return super.get(key) || 0
|
||||
}
|
||||
|
||||
add (key: GameID, val: Skill) {
|
||||
this.set(key, val + this.get(key))
|
||||
}
|
||||
|
||||
addSkills (m: EquipmentSkills) {
|
||||
for (const [k, v] of m) {
|
||||
this.add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
substract (key: GameID, val: Skill) {
|
||||
this.set(key, val + this.get(key))
|
||||
}
|
||||
|
||||
substractSkills (m: EquipmentSkills) {
|
||||
for (const [k, v] of m) {
|
||||
this.substract(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
multiply (factor: number) {
|
||||
for (const [k, v] of this) {
|
||||
this.set(k, v * factor)
|
||||
}
|
||||
}
|
||||
}
|
||||
3
src/data-provider/models/equipment/Rarity.ts
Normal file
3
src/data-provider/models/equipment/Rarity.ts
Normal file
@ -0,0 +1,3 @@
|
||||
type Rarity = 0|1|2|3|4|5|6|7;
|
||||
|
||||
export default Rarity
|
||||
3
src/data-provider/models/equipment/Resistance.ts
Normal file
3
src/data-provider/models/equipment/Resistance.ts
Normal file
@ -0,0 +1,3 @@
|
||||
type Resistance = number[];
|
||||
|
||||
export default Resistance
|
||||
5
src/data-provider/models/equipment/SkilledEquipment.ts
Normal file
5
src/data-provider/models/equipment/SkilledEquipment.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import Equipment from './Equipment'
|
||||
import SkilledItem from './SkilledItem'
|
||||
|
||||
export default interface SkilledEquipment extends Equipment, SkilledItem {
|
||||
}
|
||||
7
src/data-provider/models/equipment/SkilledItem.ts
Normal file
7
src/data-provider/models/equipment/SkilledItem.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import EquipmentSkills from './EquipmentSkills'
|
||||
import Rarity from './Rarity'
|
||||
|
||||
export default interface SkilledItem {
|
||||
rarity: Rarity,
|
||||
skills: EquipmentSkills,
|
||||
}
|
||||
3
src/data-provider/models/equipment/Slots.ts
Normal file
3
src/data-provider/models/equipment/Slots.ts
Normal file
@ -0,0 +1,3 @@
|
||||
type Slots = 0|1|2|3;
|
||||
|
||||
export default Slots
|
||||
@ -0,0 +1,7 @@
|
||||
import ArmorPiece from './ArmorPiece'
|
||||
import Decoration from './Decoration'
|
||||
|
||||
export default interface StaticEquipmentData {
|
||||
armor: ArmorPiece[][];
|
||||
decorations: Decoration[];
|
||||
}
|
||||
3
src/data-provider/models/skills/Skill.ts
Normal file
3
src/data-provider/models/skills/Skill.ts
Normal file
@ -0,0 +1,3 @@
|
||||
type Skill = number;
|
||||
|
||||
export default Skill
|
||||
10
src/data-provider/models/skills/SkillActivation.ts
Normal file
10
src/data-provider/models/skills/SkillActivation.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import GameID from '../GameId'
|
||||
|
||||
export default interface SkillActivation {
|
||||
id: GameID,
|
||||
name: string,
|
||||
requiredPoints: number,
|
||||
requiredSkill: GameID,
|
||||
isPositive: boolean,
|
||||
category: GameID,
|
||||
}
|
||||
6
src/data-provider/models/skills/SkillActivationMap.ts
Normal file
6
src/data-provider/models/skills/SkillActivationMap.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import GameID from '../GameId'
|
||||
import SkillActivation from './SkillActivation'
|
||||
|
||||
type SkillActivationMap = Map<GameID, SkillActivation[]>;
|
||||
|
||||
export default SkillActivationMap
|
||||
5
src/data-provider/models/skills/SkillNameMap.ts
Normal file
5
src/data-provider/models/skills/SkillNameMap.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import GameID from '../GameId'
|
||||
|
||||
type SkillNameMap = Map<GameID, string>
|
||||
|
||||
export default SkillNameMap
|
||||
8
src/data-provider/models/skills/StaticSkillData.ts
Normal file
8
src/data-provider/models/skills/StaticSkillData.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import SkillActivationMap from './SkillActivationMap'
|
||||
import SkillNameMap from './SkillNameMap'
|
||||
|
||||
export default interface StaticSkillData {
|
||||
skillName: SkillNameMap,
|
||||
skillActivation: SkillActivationMap,
|
||||
skillCategories: string[],
|
||||
}
|
||||
122
src/data-provider/models/user/UserCharmList.ts
Normal file
122
src/data-provider/models/user/UserCharmList.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import { range } from '../../../helper/range.helper'
|
||||
import SkillNameMap from '../skills/SkillNameMap'
|
||||
import Charm from '../equipment/Charm'
|
||||
import EquipmentCategory from '../equipment/EquipmentCategory'
|
||||
import EquipmentSkills from '../equipment/EquipmentSkills'
|
||||
import Slots from '../equipment/Slots'
|
||||
|
||||
export default class UserCharmList {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
private static _instance: UserCharmList
|
||||
|
||||
private list: Charm[]
|
||||
|
||||
private constructor () {
|
||||
this.list = []
|
||||
}
|
||||
|
||||
public static get Instance () {
|
||||
return this._instance || (this._instance = new this())
|
||||
}
|
||||
|
||||
public static getCharmName (
|
||||
skills: EquipmentSkills,
|
||||
slots: Slots,
|
||||
skillNames: SkillNameMap,
|
||||
): string {
|
||||
const skillStrings = Array.from(skills.entries()).map(
|
||||
(s) => `${skillNames.get(s[0])}:${s[1]}`,
|
||||
)
|
||||
const slotString = slots !== 0 ? `${slots} Slots` : ''
|
||||
|
||||
return [...skillStrings, slotString].join(' ').trim()
|
||||
}
|
||||
|
||||
/** get the list of charms */
|
||||
get () {
|
||||
return this.list
|
||||
}
|
||||
|
||||
/** adds a given charm to list */
|
||||
add (charm: Charm): number {
|
||||
return this.list.push(charm)
|
||||
}
|
||||
|
||||
/** removes charm at specified index from list */
|
||||
remove (index: number) {
|
||||
this.list = this.list.filter((_, i) => i !== index)
|
||||
}
|
||||
|
||||
/** serializes charm list as csv */
|
||||
serialize (skillNames: SkillNameMap): string {
|
||||
return this.list
|
||||
.map((charm) => {
|
||||
const s = []
|
||||
|
||||
const skillArray = Array.from(charm.skills.entries())
|
||||
skillArray.forEach(([sId, sVal]) => {
|
||||
s.push(`${skillNames.get(sId)},${sVal},`)
|
||||
})
|
||||
|
||||
const amountOfSkills = skillArray.length
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
for (const _ in range(amountOfSkills, 2)) {
|
||||
s.push(',,')
|
||||
}
|
||||
|
||||
s.push(`${charm.slots}`)
|
||||
|
||||
return s.join('')
|
||||
})
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
/** populate charm list from csv */
|
||||
deserialize (csv: string, skillNames: SkillNameMap): Charm[] {
|
||||
const newList = []
|
||||
|
||||
for (const charm of csv.split('\n')) {
|
||||
const spl = charm.split(',')
|
||||
|
||||
const slots = parseInt(spl[4])
|
||||
const skills = [
|
||||
[0, 1],
|
||||
[2, 3],
|
||||
]
|
||||
.filter(([_, j]) => !isNaN(parseInt(spl[j])))
|
||||
.map(([i, j]) => {
|
||||
const name = spl[i]
|
||||
|
||||
const id = Array.from(skillNames.entries()).find(([_, n]) => {
|
||||
return n === name
|
||||
})![0]
|
||||
|
||||
// build skill model
|
||||
const skill = {
|
||||
name,
|
||||
points: parseInt(spl[j]),
|
||||
id,
|
||||
}
|
||||
return skill
|
||||
})
|
||||
|
||||
const skillMap: EquipmentSkills = new EquipmentSkills(
|
||||
skills.map((skill) => {
|
||||
return [skill.id, skill.points]
|
||||
}),
|
||||
)
|
||||
const newCharm: Charm = {
|
||||
name: UserCharmList.getCharmName(skillMap, slots as Slots, skillNames),
|
||||
category: EquipmentCategory.CHARM,
|
||||
slots: slots as Slots,
|
||||
rarity: 0,
|
||||
skills: skillMap,
|
||||
}
|
||||
|
||||
newList.push(newCharm)
|
||||
}
|
||||
|
||||
this.list = newList
|
||||
return newList
|
||||
}
|
||||
}
|
||||
90
src/data-provider/models/user/UserEquipmentSettings.ts
Normal file
90
src/data-provider/models/user/UserEquipmentSettings.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import EquipmentCategory from '../equipment/EquipmentCategory'
|
||||
import EquipmentMin from '../equipment/EquipmentMin'
|
||||
|
||||
export default class UserEquipmentSettings {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
private static _instance: UserEquipmentSettings
|
||||
|
||||
pins: (EquipmentMin | undefined)[]
|
||||
|
||||
exclusions: EquipmentMin[][]
|
||||
|
||||
isActive: boolean
|
||||
|
||||
private constructor () {
|
||||
this.pins = []
|
||||
this.exclusions = []
|
||||
|
||||
const supportedCategoires = [
|
||||
EquipmentCategory.HEAD,
|
||||
EquipmentCategory.CHEST,
|
||||
EquipmentCategory.ARMS,
|
||||
EquipmentCategory.WAIST,
|
||||
EquipmentCategory.LEGS,
|
||||
]
|
||||
|
||||
supportedCategoires.forEach((_) => {
|
||||
this.pins.push(undefined)
|
||||
this.exclusions.push([])
|
||||
})
|
||||
|
||||
this.isActive = false
|
||||
}
|
||||
|
||||
public static get Instance () {
|
||||
return this._instance || (this._instance = new this())
|
||||
}
|
||||
|
||||
/** pins given equipment to corresponding category */
|
||||
addPin (x: EquipmentMin): void {
|
||||
this.pins[x.category] = x
|
||||
}
|
||||
|
||||
/** removes pin of category */
|
||||
removePin (cat: EquipmentCategory): void {
|
||||
this.pins[cat] = undefined
|
||||
}
|
||||
|
||||
/** adds given equipment to exclusion list of corresponding category */
|
||||
addExclusion (x: EquipmentMin): void {
|
||||
this.exclusions[x.category].push(x)
|
||||
}
|
||||
|
||||
/** removes equipment from exclusion list */
|
||||
removeExclusion (x: EquipmentMin): void {
|
||||
const arr = this.exclusions[x.category]
|
||||
const index = arr.findIndex((y) => y.name === x.name)
|
||||
this.exclusions[x.category].splice(index, 1)
|
||||
}
|
||||
|
||||
/** returns true if pin is same as given element */
|
||||
hasPin (x: EquipmentMin | undefined): boolean {
|
||||
if (!x) return false
|
||||
if (x.isGeneric) return false
|
||||
|
||||
const pin = this.pins[x.category]
|
||||
if (!pin) return false
|
||||
return pin.name === x.name
|
||||
}
|
||||
|
||||
/** returns true if piece is already excluded */
|
||||
hasExclusion (x: EquipmentMin): boolean {
|
||||
return !!this.exclusions[x.category].find(y => y.name === x.name)
|
||||
}
|
||||
|
||||
/** serializes settings as json */
|
||||
serialize (): string {
|
||||
return JSON.stringify({ pins: this.pins, exclusions: this.exclusions })
|
||||
}
|
||||
|
||||
/** populate settings from json */
|
||||
deserialize (raw: string): void {
|
||||
const parsed = JSON.parse(raw) as {
|
||||
pins: (EquipmentMin | undefined)[];
|
||||
exclusions: EquipmentMin[][];
|
||||
|
||||
}
|
||||
this.pins = parsed.pins
|
||||
this.exclusions = parsed.exclusions
|
||||
}
|
||||
}
|
||||
6
src/helper/html.helper.ts
Normal file
6
src/helper/html.helper.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export const htmlToElement = (html: string): Node => {
|
||||
const template = document.createElement('template')
|
||||
html = html.trim()
|
||||
template.innerHTML = html
|
||||
return template.content.firstChild as Node
|
||||
}
|
||||
1
src/helper/range.helper.ts
Normal file
1
src/helper/range.helper.ts
Normal file
@ -0,0 +1 @@
|
||||
export const range = (start: number, end: number) => Array.from({ length: (end - start) }, (_, k) => k + start)
|
||||
65
src/scorer/models/ArmorEvaluation.ts
Normal file
65
src/scorer/models/ArmorEvaluation.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { TORSO_UP_ID } from '../../data-provider/data-provider.module'
|
||||
import EquipmentCategory from '../../data-provider/models/equipment/EquipmentCategory'
|
||||
import EquipmentSkills from '../../data-provider/models/equipment/EquipmentSkills'
|
||||
import ScoredSkilledEquipment from './ScoredSkilledEquipment'
|
||||
|
||||
export default class ArmorEvaluation {
|
||||
equipment: ScoredSkilledEquipment[]
|
||||
skills: EquipmentSkills = new EquipmentSkills()
|
||||
score: number = 0
|
||||
totalSlots: number = 0
|
||||
torsoUp: number = 0
|
||||
|
||||
constructor (
|
||||
equipment: ScoredSkilledEquipment[],
|
||||
skills?: EquipmentSkills,
|
||||
score?: number,
|
||||
totalSlots?: number,
|
||||
torsoUp?: number,
|
||||
) {
|
||||
this.equipment = equipment
|
||||
if (skills) this.skills = skills
|
||||
if (score) this.score = score
|
||||
if (totalSlots) this.totalSlots = totalSlots
|
||||
if (torsoUp) this.torsoUp = torsoUp
|
||||
}
|
||||
|
||||
getSlots () {
|
||||
return this.equipment
|
||||
.map(x => x.slots)
|
||||
.filter(x => x > 0)
|
||||
}
|
||||
|
||||
getSlotsExceptChest () {
|
||||
return this.equipment
|
||||
.filter(x => x.category !== EquipmentCategory.CHEST)
|
||||
.map(x => x.slots)
|
||||
.filter(x => x > 0)
|
||||
}
|
||||
|
||||
copy () {
|
||||
return new ArmorEvaluation(
|
||||
this.equipment.map(x => x),
|
||||
new EquipmentSkills(this.skills),
|
||||
this.score,
|
||||
this.totalSlots,
|
||||
this.torsoUp,
|
||||
)
|
||||
}
|
||||
|
||||
addPiece (piece: ScoredSkilledEquipment) {
|
||||
if (piece.skills.has(TORSO_UP_ID)) this.torsoUp++
|
||||
else {
|
||||
if (piece.category === EquipmentCategory.CHEST && this.torsoUp > 0) {
|
||||
for (const [k, v] of piece.skills) {
|
||||
this.skills.add(k, v * (this.torsoUp + 1))
|
||||
}
|
||||
} else {
|
||||
this.skills.addSkills(piece.skills)
|
||||
}
|
||||
}
|
||||
this.equipment[piece.category] = piece
|
||||
this.score = this.score + piece.score
|
||||
this.totalSlots = this.totalSlots + piece.slots
|
||||
}
|
||||
}
|
||||
67
src/scorer/models/DecoEvaluation.ts
Normal file
67
src/scorer/models/DecoEvaluation.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import Decoration from '../../data-provider/models/equipment/Decoration'
|
||||
import EquipmentSkills from '../../data-provider/models/equipment/EquipmentSkills'
|
||||
import Slots from '../../data-provider/models/equipment/Slots'
|
||||
import DecoMinSlotMap from './DecoMinSlotMap'
|
||||
import DecoPermutation from './DecoPermutation'
|
||||
|
||||
export default class DecoEvaluation {
|
||||
decoMinSlotMap: DecoMinSlotMap
|
||||
unusedSlotsSum: number
|
||||
missingSkills: EquipmentSkills
|
||||
decos: Decoration[] = []
|
||||
requiredSlots: number = 0
|
||||
|
||||
constructor (
|
||||
decoMinSlotMap: DecoMinSlotMap,
|
||||
unusedSlotsSum: number,
|
||||
missingSkills: EquipmentSkills,
|
||||
decos?: Decoration[],
|
||||
requiredSlots?: number,
|
||||
) {
|
||||
this.decoMinSlotMap = decoMinSlotMap
|
||||
this.unusedSlotsSum = unusedSlotsSum
|
||||
this.missingSkills = missingSkills
|
||||
if (decos) this.decos = decos
|
||||
this.requiredSlots = requiredSlots || this.calculateRequiredSlots()
|
||||
}
|
||||
|
||||
copy () {
|
||||
return new DecoEvaluation(
|
||||
this.decoMinSlotMap,
|
||||
this.unusedSlotsSum,
|
||||
new EquipmentSkills(this.missingSkills),
|
||||
this.decos.map(x => x),
|
||||
this.requiredSlots,
|
||||
)
|
||||
}
|
||||
|
||||
calculateRequiredSlots (): number {
|
||||
let newRequiredSlots: number = 0
|
||||
for (const w of this.missingSkills) {
|
||||
const sId = w[0]
|
||||
const sVal = w[1]
|
||||
newRequiredSlots += this.decoMinSlotMap.getMinRequiredSlotsForSkill(sId, sVal)
|
||||
}
|
||||
this.requiredSlots = newRequiredSlots
|
||||
return newRequiredSlots
|
||||
}
|
||||
|
||||
addPerm (perm: DecoPermutation, slotLevel: Slots) {
|
||||
this.unusedSlotsSum -= slotLevel
|
||||
this.decos.push(...perm.decos)
|
||||
|
||||
// use custom loop instead of EquipmentSkills.substractSkills and DecoEvaluation.calculateRequiredSlots
|
||||
// to save on processing because this method is called a lot
|
||||
let newRequiredSlots: number = 0
|
||||
for (const w of this.missingSkills) {
|
||||
const sId = w[0]
|
||||
const sVal = w[1]
|
||||
|
||||
const newVal = sVal - perm.skills.get(sId)
|
||||
this.missingSkills.set(sId, newVal)
|
||||
newRequiredSlots += this.decoMinSlotMap.getMinRequiredSlotsForSkill(sId, newVal)
|
||||
}
|
||||
|
||||
this.requiredSlots = newRequiredSlots
|
||||
}
|
||||
}
|
||||
74
src/scorer/models/DecoMinSlotMap.ts
Normal file
74
src/scorer/models/DecoMinSlotMap.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import Decoration from '../../data-provider/models/equipment/Decoration'
|
||||
import EquipmentSkills from '../../data-provider/models/equipment/EquipmentSkills'
|
||||
import GameID from '../../data-provider/models/GameId'
|
||||
|
||||
function * decoVariationMinSlotsGenerator (
|
||||
decosOfSkill: Decoration[],
|
||||
skillId: GameID,
|
||||
requiredPoints: number,
|
||||
requiredSlots: number,
|
||||
existingPoints: number,
|
||||
): Generator<number, void, undefined> {
|
||||
for (const deco of decosOfSkill) {
|
||||
const newExistingPoints = existingPoints + deco.skills.get(skillId)!
|
||||
const newRequiredSlots = requiredSlots + deco.requiredSlots
|
||||
|
||||
if (newExistingPoints >= requiredPoints) {
|
||||
yield newRequiredSlots
|
||||
} else {
|
||||
yield * decoVariationMinSlotsGenerator(
|
||||
decosOfSkill,
|
||||
skillId,
|
||||
requiredPoints,
|
||||
newRequiredSlots,
|
||||
newExistingPoints,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** calculates and saves how many slots are required to get x points of a certain skill */
|
||||
export default class DecoMinSlotMap {
|
||||
private static readonly DUMMY_SCORE = 1000
|
||||
|
||||
private decorationsOfSkillMap: Map<GameID, Decoration[]> = new Map()
|
||||
private calculations: Map<GameID, Map<number, number>> = new Map()
|
||||
|
||||
constructor (allDecos: Decoration[], wantedSkills: EquipmentSkills) {
|
||||
for (const w of wantedSkills) {
|
||||
const sId = w[0]
|
||||
|
||||
// set decorations of skill
|
||||
const decosOfSkill = allDecos
|
||||
.filter(x => x.skills.get(sId) > 0)
|
||||
.sort((a, b) => b.skills.get(sId) - a.skills.get(sId))
|
||||
this.decorationsOfSkillMap.set(sId, decosOfSkill)
|
||||
|
||||
// init calculation map of that skill
|
||||
this.calculations.set(sId, new Map())
|
||||
}
|
||||
}
|
||||
|
||||
private calculateMinRequiredSlots (skillId: GameID, skillPoints: number): number {
|
||||
const decosOfSkill = this.decorationsOfSkillMap.get(skillId)!
|
||||
if (decosOfSkill.length === 0) return DecoMinSlotMap.DUMMY_SCORE
|
||||
|
||||
let minRequiredSlots = DecoMinSlotMap.DUMMY_SCORE
|
||||
for (const reqSlots of decoVariationMinSlotsGenerator(decosOfSkill, skillId, skillPoints, 0, 0)) {
|
||||
if (reqSlots < minRequiredSlots) minRequiredSlots = reqSlots
|
||||
}
|
||||
|
||||
return minRequiredSlots
|
||||
}
|
||||
|
||||
getMinRequiredSlotsForSkill (skillId: GameID, skillPoints: number): number {
|
||||
const m = this.calculations.get(skillId)!
|
||||
|
||||
if (skillPoints <= 0) return 0
|
||||
if (m.has(skillPoints)) return m.get(skillPoints)!
|
||||
|
||||
const newCalc = this.calculateMinRequiredSlots(skillId, skillPoints)
|
||||
m.set(skillPoints, newCalc)
|
||||
return newCalc
|
||||
}
|
||||
}
|
||||
8
src/scorer/models/DecoPermutation.ts
Normal file
8
src/scorer/models/DecoPermutation.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import Decoration from '../../data-provider/models/equipment/Decoration'
|
||||
import EquipmentSkills from '../../data-provider/models/equipment/EquipmentSkills'
|
||||
|
||||
export default interface DecoPermutation {
|
||||
skills: EquipmentSkills,
|
||||
decos: Decoration[],
|
||||
score: number,
|
||||
}
|
||||
5
src/scorer/models/ScoredSkilledEquipment.ts
Normal file
5
src/scorer/models/ScoredSkilledEquipment.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import SkilledEquipment from '../../data-provider/models/equipment/SkilledEquipment'
|
||||
|
||||
export default interface ScoredSkilledEquipment extends SkilledEquipment {
|
||||
score: number,
|
||||
}
|
||||
118
src/scorer/scorer.module.ts
Normal file
118
src/scorer/scorer.module.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import { TORSO_UP_ID } from '../data-provider/data-provider.module'
|
||||
import Decoration from '../data-provider/models/equipment/Decoration'
|
||||
import EquipmentSkills from '../data-provider/models/equipment/EquipmentSkills'
|
||||
import Slots from '../data-provider/models/equipment/Slots'
|
||||
import DecoPermutation from './models/DecoPermutation'
|
||||
import ScoredSkilledEquipment from './models/ScoredSkilledEquipment'
|
||||
|
||||
/** get score of a skill map relative to wanted skills */
|
||||
const getScoreFromSkillMap = (m: EquipmentSkills, w: EquipmentSkills): number => {
|
||||
let score = 0
|
||||
for (const [sId] of w) {
|
||||
score += m.get(sId) || 0
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
const scoreTorsoUpPieces = (piece: ScoredSkilledEquipment, maxTorsoScore: number) => {
|
||||
if (piece.skills.has(TORSO_UP_ID)) {
|
||||
const newPiece: ScoredSkilledEquipment = {
|
||||
...piece,
|
||||
score: maxTorsoScore,
|
||||
}
|
||||
|
||||
return newPiece
|
||||
}
|
||||
|
||||
return piece
|
||||
}
|
||||
|
||||
/** apply score to a list of decos */
|
||||
const evaluateListOfDecos = (decos: Decoration[], wantedSkills: EquipmentSkills): DecoPermutation => {
|
||||
const skillMap: EquipmentSkills = new EquipmentSkills()
|
||||
decos.forEach(deco => skillMap.addSkills(deco.skills))
|
||||
|
||||
// get max of default and computed score
|
||||
// default score can only be higher than computed when the decos of 2 wanted skills cancel each other out (e.g. handicraft and sharpness)
|
||||
const computedScore = getScoreFromSkillMap(skillMap, wantedSkills)
|
||||
const defaultScore = Math.max(...Array.from(skillMap.values()))
|
||||
const score = (Math.max(computedScore, defaultScore))
|
||||
|
||||
return {
|
||||
skills: skillMap,
|
||||
decos,
|
||||
score,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* checks if deco permutation is the same or better than comparison in respect to wanted skills
|
||||
* returns 0 if better/different, returns 1 if same, returns 2 if worse
|
||||
*/
|
||||
const decoPermWorseOrSameAsComparison = (perm: DecoPermutation, comparison: DecoPermutation, wantedSkills: EquipmentSkills) => {
|
||||
const arr = []
|
||||
for (const w of Array.from(wantedSkills.entries())) {
|
||||
const wId = w[0]
|
||||
const a = perm.skills.get(wId)
|
||||
const b = comparison.skills.get(wId)
|
||||
|
||||
if (a > b) return 0
|
||||
if (a === b) arr.push(1)
|
||||
else arr.push(2)
|
||||
}
|
||||
return Math.max(...arr)
|
||||
}
|
||||
|
||||
/** returns a mapping of slot level to the amount of score it is worth */
|
||||
const getDecoSlotScoreMap = (decoPermutationsPerSlotLevel: Map<Slots, DecoPermutation[]>): Map<number, number> => {
|
||||
const m = new Map(Array.from(decoPermutationsPerSlotLevel.entries()).map(([slotLevel, permList]) => {
|
||||
return [slotLevel, Math.max(...permList.map(x => x.score))]
|
||||
}))
|
||||
m.set(0, 0)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
/** prune a list of deco permutations of all duplicates and downgrades */
|
||||
const pruneDecoPermutations = (permList: DecoPermutation[], wantedSkills: EquipmentSkills): DecoPermutation[] => {
|
||||
// we go through entire list left through right => x
|
||||
// for each ele, we check the entire list again => y
|
||||
// if y is an upgrade of x, then x will be filtered out
|
||||
// if y is the same as x, and y is further right in the list, then x will be filtered
|
||||
// only if x has no upgrade, and no element right of it that is the same will it remain in the list
|
||||
const res = permList
|
||||
.filter((x, i) => {
|
||||
let shouldBeFiltered: boolean = false
|
||||
|
||||
for (let j = 0; j < permList.length; j++) {
|
||||
if (i === j) continue
|
||||
|
||||
const y = permList[j]
|
||||
const v = decoPermWorseOrSameAsComparison(x, y, wantedSkills)
|
||||
|
||||
if (v === 2) {
|
||||
shouldBeFiltered = true
|
||||
break
|
||||
}
|
||||
|
||||
if (j > i && v === 1) {
|
||||
shouldBeFiltered = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return !shouldBeFiltered
|
||||
})
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
export {
|
||||
getScoreFromSkillMap,
|
||||
scoreTorsoUpPieces,
|
||||
evaluateListOfDecos,
|
||||
decoPermWorseOrSameAsComparison,
|
||||
getDecoSlotScoreMap,
|
||||
pruneDecoPermutations,
|
||||
}
|
||||
109
src/searcher/models/ArmorSet.ts
Normal file
109
src/searcher/models/ArmorSet.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import Defense from '../../data-provider/models/equipment/Defense'
|
||||
import EquipmentSkills from '../../data-provider/models/equipment/EquipmentSkills'
|
||||
import Resistance from '../../data-provider/models/equipment/Resistance'
|
||||
import Evaluation from './Evaluation'
|
||||
import ArmorPiece from '../../data-provider/models/equipment/ArmorPiece'
|
||||
import Decoration from '../../data-provider/models/equipment/Decoration'
|
||||
import Charm from '../../data-provider/models/equipment/Charm'
|
||||
import SkillActivationMap from '../../data-provider/models/skills/SkillActivationMap'
|
||||
import SkillActivation from '../../data-provider/models/skills/SkillActivation'
|
||||
import ArmorEvaluation from '../../scorer/models/ArmorEvaluation'
|
||||
import DecoEvaluation from '../../scorer/models/DecoEvaluation'
|
||||
import EquipmentCategory from '../../data-provider/models/equipment/EquipmentCategory'
|
||||
|
||||
export default class ArmorSet {
|
||||
readonly head: ArmorPiece
|
||||
readonly chest: ArmorPiece
|
||||
readonly arms: ArmorPiece
|
||||
readonly waist: ArmorPiece
|
||||
readonly legs: ArmorPiece
|
||||
readonly charm: Charm
|
||||
readonly decos: Decoration[]
|
||||
|
||||
evaluation: Evaluation
|
||||
|
||||
constructor (
|
||||
armorEval: ArmorEvaluation,
|
||||
decoEval: DecoEvaluation,
|
||||
skillActivations: SkillActivationMap,
|
||||
) {
|
||||
const chest = armorEval.equipment[EquipmentCategory.CHEST] as unknown as ArmorPiece
|
||||
|
||||
this.chest = armorEval.torsoUp > 0 ? ArmorSet.applyTorsoUpToChest(chest, armorEval.torsoUp) : chest
|
||||
this.head = armorEval.equipment[EquipmentCategory.HEAD] as unknown as ArmorPiece
|
||||
this.arms = armorEval.equipment[EquipmentCategory.ARMS] as unknown as ArmorPiece
|
||||
this.waist = armorEval.equipment[EquipmentCategory.WAIST] as unknown as ArmorPiece
|
||||
this.legs = armorEval.equipment[EquipmentCategory.LEGS] as unknown as ArmorPiece
|
||||
this.charm = armorEval.equipment[EquipmentCategory.CHARM] as unknown as Charm
|
||||
this.decos = decoEval.decos
|
||||
this.evaluation = this.evaluate(armorEval, decoEval, skillActivations)
|
||||
}
|
||||
|
||||
private static applyTorsoUpToChest (chest: ArmorPiece, torsoUp: number): ArmorPiece {
|
||||
const newSkills = new EquipmentSkills(chest.skills)
|
||||
newSkills.multiply(torsoUp + 1)
|
||||
return {
|
||||
...chest,
|
||||
skills: newSkills,
|
||||
}
|
||||
}
|
||||
|
||||
getPieces (): ArmorPiece[] {
|
||||
return [
|
||||
this.head,
|
||||
this.chest,
|
||||
this.arms,
|
||||
this.waist,
|
||||
this.legs,
|
||||
]
|
||||
}
|
||||
|
||||
evaluate (
|
||||
armorEval: ArmorEvaluation,
|
||||
decoEval: DecoEvaluation,
|
||||
activations: SkillActivationMap,
|
||||
): Evaluation {
|
||||
const totalDefense: Defense = { base: 0, max: 0 }
|
||||
let totalResistance: Resistance = [0, 0, 0, 0, 0]
|
||||
|
||||
// iterate over all armor pieces
|
||||
for (const piece of this.getPieces()) {
|
||||
totalDefense.base += piece.defense.base
|
||||
totalDefense.max += piece.defense.max
|
||||
totalResistance = piece.resistance.map((res, i) => res + totalResistance[i])
|
||||
}
|
||||
|
||||
// get total skills
|
||||
const decoSkills = new EquipmentSkills()
|
||||
decoEval.decos.forEach(d => decoSkills.addSkills(d.skills))
|
||||
const skills = new EquipmentSkills(armorEval.skills)
|
||||
skills.addSkills(new EquipmentSkills(decoSkills))
|
||||
|
||||
// get activations
|
||||
const a: SkillActivation[] = []
|
||||
for (const [sId, sVal] of skills) {
|
||||
if (Math.abs(sVal) < 10) {
|
||||
continue
|
||||
}
|
||||
|
||||
const activationsOfSkill = activations.get(sId)!
|
||||
.filter(act => {
|
||||
return act.isPositive
|
||||
? sVal >= act.requiredPoints
|
||||
: sVal <= act.requiredPoints
|
||||
})
|
||||
a.push(...activationsOfSkill)
|
||||
}
|
||||
|
||||
// build, save and return model
|
||||
const thisEval: Evaluation = {
|
||||
defense: totalDefense,
|
||||
resistance: totalResistance,
|
||||
activations: a,
|
||||
skills,
|
||||
torsoUp: armorEval.torsoUp,
|
||||
}
|
||||
this.evaluation = thisEval
|
||||
return thisEval
|
||||
}
|
||||
}
|
||||
12
src/searcher/models/Evaluation.ts
Normal file
12
src/searcher/models/Evaluation.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import Defense from '../../data-provider/models/equipment/Defense'
|
||||
import EquipmentSkills from '../../data-provider/models/equipment/EquipmentSkills'
|
||||
import Resistance from '../../data-provider/models/equipment/Resistance'
|
||||
import SkillActivation from '../../data-provider/models/skills/SkillActivation'
|
||||
|
||||
export default interface Evaluation {
|
||||
skills: EquipmentSkills;
|
||||
activations: SkillActivation[];
|
||||
defense: Defense;
|
||||
resistance: Resistance;
|
||||
torsoUp: number;
|
||||
}
|
||||
16
src/searcher/models/SearchConstraints.ts
Normal file
16
src/searcher/models/SearchConstraints.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import ArmorType from '../../data-provider/models/equipment/ArmorType'
|
||||
import EquipmentMin from '../../data-provider/models/equipment/EquipmentMin'
|
||||
import Rarity from '../../data-provider/models/equipment/Rarity'
|
||||
import Slots from '../../data-provider/models/equipment/Slots'
|
||||
import SkillActivation from '../../data-provider/models/skills/SkillActivation'
|
||||
|
||||
export default interface SearchConstraints {
|
||||
weaponSlots: Slots;
|
||||
armorType: ArmorType;
|
||||
armorRarity: Rarity,
|
||||
decoRarity: Rarity,
|
||||
skillActivations: SkillActivation[];
|
||||
limit: number;
|
||||
pins: (EquipmentMin | undefined)[];
|
||||
exclusions: EquipmentMin[][];
|
||||
}
|
||||
370
src/searcher/searcher.module.ts
Normal file
370
src/searcher/searcher.module.ts
Normal file
@ -0,0 +1,370 @@
|
||||
import { DUMMY_PIECE } from '../data-provider/data-provider.module'
|
||||
import ArmorPiece from '../data-provider/models/equipment/ArmorPiece'
|
||||
import ArmorType from '../data-provider/models/equipment/ArmorType'
|
||||
import Charm from '../data-provider/models/equipment/Charm'
|
||||
import Decoration from '../data-provider/models/equipment/Decoration'
|
||||
import EquipmentCategory from '../data-provider/models/equipment/EquipmentCategory'
|
||||
import EquipmentSkills from '../data-provider/models/equipment/EquipmentSkills'
|
||||
import SkilledEquipment from '../data-provider/models/equipment/SkilledEquipment'
|
||||
import Slots from '../data-provider/models/equipment/Slots'
|
||||
import StaticSkillData from '../data-provider/models/skills/StaticSkillData'
|
||||
import ArmorEvaluation from '../scorer/models/ArmorEvaluation'
|
||||
import ArmorSet from './models/ArmorSet'
|
||||
import DecoPermutation from '../scorer/models/DecoPermutation'
|
||||
import SearchConstraints from './models/SearchConstraints'
|
||||
import ScoredSkilledEquipment from '../scorer/models/ScoredSkilledEquipment'
|
||||
import { applyArmorFilter, applyCharmFilter, applyRarityFilter, filterHasSkill } from '../data-filter/data-filter.module'
|
||||
import { pruneDecoPermutations, evaluateListOfDecos, getDecoSlotScoreMap, getScoreFromSkillMap, scoreTorsoUpPieces } from '../scorer/scorer.module'
|
||||
import DecoEvaluation from '../scorer/models/DecoEvaluation'
|
||||
import DecoMinSlotMap from '../scorer/models/DecoMinSlotMap'
|
||||
|
||||
// #region initial search data
|
||||
/** get initial armor eval with all dummy pieces */
|
||||
const getIntiailArmorEval = (type: ArmorType) => {
|
||||
const categoryArray = [
|
||||
EquipmentCategory.HEAD,
|
||||
EquipmentCategory.CHEST,
|
||||
EquipmentCategory.ARMS,
|
||||
EquipmentCategory.WAIST,
|
||||
EquipmentCategory.LEGS,
|
||||
EquipmentCategory.CHARM,
|
||||
]
|
||||
|
||||
const pieces: ScoredSkilledEquipment[] = categoryArray.map((x) => {
|
||||
return {
|
||||
...DUMMY_PIECE,
|
||||
type,
|
||||
category: x,
|
||||
score: 0,
|
||||
}
|
||||
})
|
||||
|
||||
return new ArmorEvaluation(pieces)
|
||||
}
|
||||
|
||||
/** returns all the ways you can possibly arrange the viable decorations on a given slot level (1, 2, 3) */
|
||||
const getDecorationVariationsPerSlotLevel = (
|
||||
decorations: Decoration[],
|
||||
wantedSkills: EquipmentSkills,
|
||||
): Map<Slots, DecoPermutation[]> => {
|
||||
// get all decorations of specific slot
|
||||
const rawOneSlots = decorations.filter(d => d.requiredSlots === 1)
|
||||
const rawTwoSlots = decorations.filter(d => d.requiredSlots === 2)
|
||||
const rawThreeSlots = decorations.filter(d => d.requiredSlots === 3)
|
||||
|
||||
// create dummy for unused slots
|
||||
const dummy: Decoration = {
|
||||
name: 'None',
|
||||
rarity: 0,
|
||||
requiredSlots: 0,
|
||||
skills: new EquipmentSkills(),
|
||||
}
|
||||
|
||||
// get all variations for 1 slot
|
||||
const oneSlotVariations = rawOneSlots.map(x => [x]).concat([[dummy]])
|
||||
const oneSlotEvaluated = pruneDecoPermutations(oneSlotVariations.map(x => evaluateListOfDecos(x, wantedSkills)), wantedSkills)
|
||||
const prunedOneSlotVariations = oneSlotEvaluated.map(x => x.decos)
|
||||
|
||||
// get all variations for 2 slots
|
||||
const twoOneSlotDecoVariations = []
|
||||
for (let i = 0; i < prunedOneSlotVariations.length; i++) {
|
||||
const x = prunedOneSlotVariations[i]
|
||||
for (let j = Math.abs(i); j < prunedOneSlotVariations.length; j++) {
|
||||
const y = prunedOneSlotVariations[j]
|
||||
twoOneSlotDecoVariations.push(x.concat(y))
|
||||
}
|
||||
}
|
||||
const twoSlotVariations = rawTwoSlots
|
||||
.map(x => [x])
|
||||
.concat(twoOneSlotDecoVariations)
|
||||
const twoSlotEvaluated = pruneDecoPermutations(twoSlotVariations.map(x => evaluateListOfDecos(x, wantedSkills)), wantedSkills)
|
||||
|
||||
// get all variations for 3 slots
|
||||
const threeOneSlotDecoVariations = []
|
||||
for (let i = 0; i < prunedOneSlotVariations.length; i++) {
|
||||
const x = prunedOneSlotVariations[i]
|
||||
for (let j = Math.abs(i); j < twoOneSlotDecoVariations.length; j++) {
|
||||
const y = twoOneSlotDecoVariations[j]
|
||||
threeOneSlotDecoVariations.push(x.concat(y))
|
||||
}
|
||||
}
|
||||
const oneAndTwoSlotDecoVariations = []
|
||||
for (const oneSlot of rawOneSlots) {
|
||||
for (const twoSlot of rawTwoSlots) {
|
||||
oneAndTwoSlotDecoVariations.push([oneSlot, twoSlot])
|
||||
}
|
||||
}
|
||||
const threeSlotVariations = rawThreeSlots
|
||||
.map(x => [x])
|
||||
.concat(oneAndTwoSlotDecoVariations)
|
||||
.concat(threeOneSlotDecoVariations)
|
||||
const threeSlotEvaluated = pruneDecoPermutations(threeSlotVariations.map(x => evaluateListOfDecos(x, wantedSkills)), wantedSkills)
|
||||
|
||||
// return pruned evaluations
|
||||
return new Map([
|
||||
[0, []],
|
||||
[1, oneSlotEvaluated],
|
||||
[2, twoSlotEvaluated],
|
||||
[3, threeSlotEvaluated],
|
||||
])
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region search logic
|
||||
function * getArmorPermutations (
|
||||
equipment: ScoredSkilledEquipment[][],
|
||||
previousEval: ArmorEvaluation,
|
||||
maximumRemainingScore: number[],
|
||||
requiredScore: number,
|
||||
categoryIndex: number,
|
||||
): Generator<ArmorEvaluation, void, undefined> {
|
||||
for (const piece of equipment[categoryIndex]) {
|
||||
// create and eval new set
|
||||
const thisEval = previousEval.copy()
|
||||
thisEval.addPiece(piece)
|
||||
|
||||
// yield it if score is sufficient
|
||||
if (thisEval.score >= requiredScore) yield thisEval
|
||||
// otherwise check if its possible to still find sets on this branch and break if not
|
||||
else {
|
||||
if ((thisEval.score + maximumRemainingScore[categoryIndex]) < requiredScore) break
|
||||
}
|
||||
|
||||
// then yield the next loop if there is one
|
||||
if (categoryIndex > 0) {
|
||||
yield * getArmorPermutations(
|
||||
equipment,
|
||||
thisEval,
|
||||
maximumRemainingScore,
|
||||
requiredScore,
|
||||
categoryIndex - 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function * getDecoPermutations (
|
||||
decoPermutationsPerSlotLevel: Map<Slots, DecoPermutation[]>,
|
||||
slotsOfArmor: Slots[],
|
||||
previousEval: DecoEvaluation,
|
||||
slotIndex: number,
|
||||
): Generator<DecoEvaluation, void, undefined> {
|
||||
const slotLevel = slotsOfArmor[slotIndex]
|
||||
for (const perm of decoPermutationsPerSlotLevel.get(slotLevel)!) {
|
||||
// create and eval new set
|
||||
const thisEval = previousEval.copy()
|
||||
thisEval.addPerm(perm, slotLevel)
|
||||
|
||||
// yield it if score is sufficient
|
||||
if (thisEval.requiredSlots <= 0) yield thisEval
|
||||
// otherwise check if its possible to still find sets on this branch and break if not
|
||||
else {
|
||||
if (thisEval.unusedSlotsSum < thisEval.requiredSlots) continue
|
||||
}
|
||||
|
||||
// then yield the next loop if there is one
|
||||
if (slotIndex > 0) {
|
||||
yield * getDecoPermutations(
|
||||
decoPermutationsPerSlotLevel,
|
||||
slotsOfArmor,
|
||||
thisEval,
|
||||
slotIndex - 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const transformTorsoUpDecoPermutation = (perm: DecoPermutation, torsoUp: number): DecoPermutation => {
|
||||
const factor = torsoUp + 1
|
||||
|
||||
const score = perm.score * factor
|
||||
const decos = perm.decos.map(d => {
|
||||
const newSkills = new EquipmentSkills(d.skills)
|
||||
newSkills.multiply(factor)
|
||||
const newDeco: Decoration = {
|
||||
...d,
|
||||
affectedByTorsoUp: true,
|
||||
name: d.name.concat(' (TorsoUp)'),
|
||||
skills: newSkills,
|
||||
}
|
||||
|
||||
return newDeco
|
||||
})
|
||||
const newTotalSkills = new EquipmentSkills(perm.skills)
|
||||
newTotalSkills.multiply(factor)
|
||||
const skills = newTotalSkills
|
||||
|
||||
return {
|
||||
score,
|
||||
decos,
|
||||
skills,
|
||||
}
|
||||
}
|
||||
|
||||
const findSufficientDecoPermutation = (
|
||||
armorEval: ArmorEvaluation,
|
||||
constraints: SearchConstraints,
|
||||
wantedSkills: EquipmentSkills,
|
||||
decoMinSlotMap: DecoMinSlotMap,
|
||||
decoPermutationsPerSlotLevel: Map<Slots, DecoPermutation[]>,
|
||||
): DecoEvaluation | undefined => {
|
||||
const _inner = (
|
||||
_slotList: Slots[],
|
||||
_initialEval: DecoEvaluation,
|
||||
): DecoEvaluation | undefined => {
|
||||
if (_initialEval.requiredSlots <= 0) return _initialEval
|
||||
if (_initialEval.unusedSlotsSum < _initialEval.requiredSlots) return undefined
|
||||
if (_slotList.length === 0) return undefined
|
||||
|
||||
const decoEvaluation = getDecoPermutations(
|
||||
decoPermutationsPerSlotLevel,
|
||||
_slotList,
|
||||
_initialEval,
|
||||
_slotList.length - 1,
|
||||
).next().value
|
||||
|
||||
if (decoEvaluation) return decoEvaluation
|
||||
return undefined
|
||||
}
|
||||
|
||||
let r: DecoEvaluation | undefined
|
||||
const torsoSlots = armorEval.equipment[EquipmentCategory.CHEST].slots
|
||||
const missingSkills = new EquipmentSkills(Array.from(wantedSkills).map(([sId, sVal]) => {
|
||||
return [sId, sVal - armorEval.skills.get(sId)]
|
||||
}))
|
||||
const slotSum = armorEval.totalSlots + constraints.weaponSlots
|
||||
|
||||
if (armorEval.torsoUp > 0 && torsoSlots > 0) {
|
||||
// if torso up, fill the chest slots and then iterate over permutations from there
|
||||
const slotList = armorEval.getSlotsExceptChest().concat(constraints.weaponSlots ? constraints.weaponSlots : [])
|
||||
const slotSumWithoutTorso = slotSum - torsoSlots
|
||||
const initialEval = new DecoEvaluation(decoMinSlotMap, slotSumWithoutTorso, missingSkills)
|
||||
for (const chestPerm of decoPermutationsPerSlotLevel.get(torsoSlots)!) {
|
||||
const transformedPerm = transformTorsoUpDecoPermutation(chestPerm, armorEval.torsoUp)
|
||||
const copiedEval = initialEval.copy()
|
||||
copiedEval.addPerm(transformedPerm, torsoSlots)
|
||||
const temp = _inner(slotList, copiedEval)
|
||||
if (temp) {
|
||||
r = temp
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// otherwise just iterate over permutations
|
||||
const slotList = armorEval.getSlots().concat(constraints.weaponSlots ? constraints.weaponSlots : [])
|
||||
r = _inner(slotList, new DecoEvaluation(decoMinSlotMap, armorEval.totalSlots + constraints.weaponSlots, missingSkills))
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
const findSets = (
|
||||
armorPieces: ArmorPiece[][],
|
||||
decorations: Decoration[],
|
||||
charms: Charm[],
|
||||
constraints: SearchConstraints,
|
||||
skillData: StaticSkillData,
|
||||
) => {
|
||||
const wantedSkills: EquipmentSkills = new EquipmentSkills(constraints.skillActivations.map(x => [x.requiredSkill, x.requiredPoints]))
|
||||
const decoPermutationsPerSlotLevel = getDecorationVariationsPerSlotLevel(decorations, wantedSkills)
|
||||
const slotScoreMap = getDecoSlotScoreMap(decoPermutationsPerSlotLevel)
|
||||
const initialArmorEval = getIntiailArmorEval(constraints.armorType)
|
||||
const wantedScore = getScoreFromSkillMap(wantedSkills, wantedSkills) - slotScoreMap.get(constraints.weaponSlots)!
|
||||
const decoMinSlotMap = new DecoMinSlotMap(decorations, wantedSkills)
|
||||
|
||||
const skilledEquipment: SkilledEquipment[][] = armorPieces
|
||||
skilledEquipment.push(charms)
|
||||
|
||||
// score equipment
|
||||
const scoredEquipment: ScoredSkilledEquipment[][] = skilledEquipment
|
||||
.map(equList => equList.map((equ) => {
|
||||
const score = slotScoreMap.get(equ.slots)! + getScoreFromSkillMap(equ.skills, wantedSkills)
|
||||
return {
|
||||
...equ,
|
||||
score,
|
||||
}
|
||||
}))
|
||||
|
||||
// reorder equipment and manually rescore torso up pieces
|
||||
const maxTorsoScore = Math.max(...scoredEquipment[1].map(x => x.score))
|
||||
const readjustedEquipment = [
|
||||
scoredEquipment[1], // chest first to simplify torso up calculation
|
||||
scoredEquipment[0].map(x => scoreTorsoUpPieces(x, maxTorsoScore)),
|
||||
scoredEquipment[2], // arms cant have torso up
|
||||
scoredEquipment[3].map(x => scoreTorsoUpPieces(x, maxTorsoScore)),
|
||||
scoredEquipment[4].map(x => scoreTorsoUpPieces(x, maxTorsoScore)),
|
||||
scoredEquipment[5], // charm cant have torso up
|
||||
]
|
||||
|
||||
// sort equipment by score
|
||||
const sorted = readjustedEquipment.map(l => l.sort((a, b) => b.score - a.score))
|
||||
|
||||
// get list of maximum score of remaining iterations
|
||||
const maximumRemainingScore = [0]
|
||||
let sumOfAllIterations = 0
|
||||
sorted.map(x => x[0].score).forEach((m) => {
|
||||
sumOfAllIterations += m
|
||||
maximumRemainingScore.push(sumOfAllIterations)
|
||||
})
|
||||
|
||||
let length = 0
|
||||
const validSets: ArmorSet[] = []
|
||||
// try all viable armor permuations
|
||||
for (const armorEvaluation of getArmorPermutations(
|
||||
sorted,
|
||||
initialArmorEval,
|
||||
maximumRemainingScore,
|
||||
wantedScore,
|
||||
sorted.length - 1,
|
||||
)) {
|
||||
// find first sufficient deco eval
|
||||
const decoEvaluation = findSufficientDecoPermutation(
|
||||
armorEvaluation,
|
||||
constraints,
|
||||
wantedSkills,
|
||||
decoMinSlotMap,
|
||||
decoPermutationsPerSlotLevel,
|
||||
)
|
||||
|
||||
// build and append set if there is any deco eval
|
||||
if (decoEvaluation) {
|
||||
const set = new ArmorSet(armorEvaluation, decoEvaluation, skillData.skillActivation)
|
||||
validSets.push(set)
|
||||
|
||||
// exit if enough sets found
|
||||
if (length === constraints.limit - 1) break
|
||||
length++
|
||||
}
|
||||
}
|
||||
|
||||
return validSets
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region entrypoint
|
||||
const search = (
|
||||
armorPieces: ArmorPiece[][],
|
||||
decorations: Decoration[],
|
||||
charms: Charm[],
|
||||
constraints: SearchConstraints,
|
||||
skillData: StaticSkillData,
|
||||
) => {
|
||||
const a = armorPieces
|
||||
.map((piecesOfCategory, i) => {
|
||||
return applyArmorFilter(piecesOfCategory, constraints.armorRarity, constraints.armorType, i, constraints.pins[i], constraints.exclusions[i], constraints.skillActivations)
|
||||
})
|
||||
const c = applyCharmFilter(charms, constraints.skillActivations)
|
||||
const d = applyRarityFilter(decorations, constraints.decoRarity)
|
||||
.filter(x => filterHasSkill(x, constraints.skillActivations))
|
||||
|
||||
return findSets(
|
||||
a,
|
||||
d as Decoration[],
|
||||
c,
|
||||
constraints,
|
||||
skillData,
|
||||
)
|
||||
}
|
||||
// #endregion
|
||||
|
||||
export { search }
|
||||
30
tsconfig.json
Normal file
30
tsconfig.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"newLine": "LF",
|
||||
"outDir": "./lib/",
|
||||
"target": "es2015",
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"jsx": "preserve",
|
||||
"downlevelIteration": true,
|
||||
"lib": [
|
||||
"es2019",
|
||||
"es2017",
|
||||
"dom"
|
||||
],
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
".git",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user