init from mhtri-armor-set-searcher

This commit is contained in:
TimHasert 2023-08-20 18:48:36 +02:00
commit 21086eb0d9
57 changed files with 13184 additions and 0 deletions

3
.eslintignore Normal file
View File

@ -0,0 +1,3 @@
/dist
/node_modules
/.cache

22
.eslintrc.json Normal file
View 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
View File

@ -0,0 +1,4 @@
/dist
/node_modules
/.cache
/venv

21
LICENSE Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

41
package.json Normal file
View 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"
}

View 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
View 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
View 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
View 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);
}

View 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)
}
}

View 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)
})
}

View 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),
}
}

View 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])
}

View 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,
}

View 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()
})
}

View 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))
}

View 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,
}

View 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,
}

View File

@ -0,0 +1,3 @@
type GameID = number;
export default GameID

View 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;
}

View File

@ -0,0 +1,8 @@
/* eslint-disable no-unused-vars */
enum ArmorType {
ALL = 0,
BLADEMASTER = 1,
GUNNER = 2,
}
export default ArmorType

View File

@ -0,0 +1,4 @@
import SkilledEquipment from './SkilledEquipment'
export default interface Charm extends SkilledEquipment {
}

View 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;
}

View File

@ -0,0 +1,5 @@
export default interface Defense {
base: number,
max: number,
maxLr?: number, // only for low rank pieces
}

View 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

View 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;
}

View 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

View File

@ -0,0 +1,7 @@
import EquipmentCategory from './EquipmentCategory'
export default interface EquipmentMin {
name: string;
category: EquipmentCategory;
isGeneric?: boolean;
}

View 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)
}
}
}

View File

@ -0,0 +1,3 @@
type Rarity = 0|1|2|3|4|5|6|7;
export default Rarity

View File

@ -0,0 +1,3 @@
type Resistance = number[];
export default Resistance

View File

@ -0,0 +1,5 @@
import Equipment from './Equipment'
import SkilledItem from './SkilledItem'
export default interface SkilledEquipment extends Equipment, SkilledItem {
}

View File

@ -0,0 +1,7 @@
import EquipmentSkills from './EquipmentSkills'
import Rarity from './Rarity'
export default interface SkilledItem {
rarity: Rarity,
skills: EquipmentSkills,
}

View File

@ -0,0 +1,3 @@
type Slots = 0|1|2|3;
export default Slots

View File

@ -0,0 +1,7 @@
import ArmorPiece from './ArmorPiece'
import Decoration from './Decoration'
export default interface StaticEquipmentData {
armor: ArmorPiece[][];
decorations: Decoration[];
}

View File

@ -0,0 +1,3 @@
type Skill = number;
export default Skill

View 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,
}

View File

@ -0,0 +1,6 @@
import GameID from '../GameId'
import SkillActivation from './SkillActivation'
type SkillActivationMap = Map<GameID, SkillActivation[]>;
export default SkillActivationMap

View File

@ -0,0 +1,5 @@
import GameID from '../GameId'
type SkillNameMap = Map<GameID, string>
export default SkillNameMap

View File

@ -0,0 +1,8 @@
import SkillActivationMap from './SkillActivationMap'
import SkillNameMap from './SkillNameMap'
export default interface StaticSkillData {
skillName: SkillNameMap,
skillActivation: SkillActivationMap,
skillCategories: string[],
}

View 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
}
}

View 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
}
}

View 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
}

View File

@ -0,0 +1 @@
export const range = (start: number, end: number) => Array.from({ length: (end - start) }, (_, k) => k + start)

View 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
}
}

View 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
}
}

View 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
}
}

View 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,
}

View 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
View 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,
}

View 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
}
}

View 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;
}

View 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[][];
}

View 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
View 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"
]
}