Compare commits

..

18 Commits
v0.13.7 ... dev

Author SHA1 Message Date
Kvan7
c067f4c17b Merge pull request #801 from Kvan7/dev
v0.13.8
2025-12-22 21:23:30 -06:00
Kvan7
b50584c913 version bump 2025-12-22 21:15:30 -06:00
Kvan7
d9d28c2564 update workflow 2025-12-22 21:05:21 -06:00
Kvan7
8ff70d39ac Disable foil by default (nobody opening relequaries) 2025-12-22 21:02:34 -06:00
Kvan7
51febcd66e Merge pull request #795 from chrisheib/add_level_filter
Add level filter
2025-12-22 21:01:42 -06:00
Kvan7
1fde68ef46 shorten english name 2025-12-22 14:58:09 -06:00
kvan7
c41c2925dc update app_i18n 2025-12-22 20:39:52 -06:00
kvan7
abf3e67c1b Add max level & drop att filters 2025-12-22 20:29:56 -06:00
kvan7
403d9fb4bf localize parsing 2025-12-22 13:31:41 -06:00
Kvan7
211b031633 Merge branch 'dev' into add_level_filter 2025-12-21 19:55:32 -06:00
kvan7
95c3c7cfcd fix fracture detection 2025-12-21 19:52:51 -06:00
STSchiff
538c209e6d more linter happyness (safely access requires.level) 2025-12-21 21:34:30 +01:00
kvan7
c23d9b5eb3 temp detection for fractured items 2025-12-20 20:29:39 -06:00
STSchiff
532a615d0f try to make linter happy 2025-12-20 20:12:06 +01:00
kvan7
73cd5a4712 remove split mac 2025-12-20 13:02:50 -06:00
STSchiff
eb05596dd0 formatter 2025-12-20 19:56:54 +01:00
STSchiff
1928fd47da level filter: renderer & trade query 2025-12-20 19:56:50 +01:00
STSchiff
019ee881ea add requirements parser 2025-12-20 19:56:46 +01:00
33 changed files with 484 additions and 98 deletions

View File

@@ -59,6 +59,7 @@ body:
label: Version label: Version
description: What version of EE2 are you running? You can see this in Settings -> About description: What version of EE2 are you running? You can see this in Settings -> About
options: options:
- 0.13.8
- 0.13.7 - 0.13.7
- 0.13.6 - 0.13.6
- 0.13.5 - 0.13.5
@@ -71,7 +72,7 @@ body:
- 0.11.x - 0.11.x
- 0.10.x - 0.10.x
- Change me - Change me
default: 11 default: 12
validations: validations:
required: true required: true
- type: textarea - type: textarea

View File

@@ -1,4 +1,8 @@
on: on:
push:
branches:
- 'master'
- 'dev'
pull_request: pull_request:
branches: branches:
- master - master

View File

@@ -1,6 +1,6 @@
# ![Perfect Jewelers Orb](./renderer/public/images/jeweler.png) Exiled Exchange 2 # ![Perfect Jewelers Orb](./renderer/public/images/jeweler.png) Exiled Exchange 2
![GitHub Downloads (specific asset, latest release)](https://img.shields.io/github/downloads/kvan7/exiled-exchange-2/latest/Exiled-Exchange-2-Setup-0.13.7.exe?style=plastic&link=https%3A%2F%2Ftooomm.github.io%2Fgithub-release-stats%2F%3Fusername%3Dkvan7%26repository%3DExiled-Exchange-2) ![GitHub Downloads (specific asset, latest release)](https://img.shields.io/github/downloads/kvan7/exiled-exchange-2/latest/Exiled-Exchange-2-Setup-0.13.8.exe?style=plastic&link=https%3A%2F%2Ftooomm.github.io%2Fgithub-release-stats%2F%3Fusername%3Dkvan7%26repository%3DExiled-Exchange-2)
![GitHub Tag](https://img.shields.io/github/v/tag/kvan7/exiled-exchange-2?style=plastic&label=latest%20version) ![GitHub Tag](https://img.shields.io/github/v/tag/kvan7/exiled-exchange-2?style=plastic&label=latest%20version)
![GitHub commits since latest release (branch)](https://img.shields.io/github/commits-since/kvan7/exiled-exchange-2/latest/dev?style=plastic) ![GitHub commits since latest release (branch)](https://img.shields.io/github/commits-since/kvan7/exiled-exchange-2/latest/dev?style=plastic)

View File

@@ -20,7 +20,7 @@ export default defineConfig({
}, },
themeConfig: { themeConfig: {
// logo: 'TODO', https://github.com/vuejs/vitepress/issues/1401 // logo: 'TODO', https://github.com/vuejs/vitepress/issues/1401
appVersion: '0.13.7', appVersion: '0.13.8',
github: { github: {
releasesUrl: 'https://github.com/Kvan7/Exiled-Exchange-2/releases' releasesUrl: 'https://github.com/Kvan7/Exiled-Exchange-2/releases'
}, },

View File

@@ -1,12 +1,12 @@
{ {
"name": "exiled-exchange-2", "name": "exiled-exchange-2",
"version": "0.13.7", "version": "0.13.8",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "exiled-exchange-2", "name": "exiled-exchange-2",
"version": "0.13.7", "version": "0.13.8",
"dependencies": { "dependencies": {
"electron-overlay-window": "4.0.2", "electron-overlay-window": "4.0.2",
"uiohook-napi": "1.5.x" "uiohook-napi": "1.5.x"

View File

@@ -1,6 +1,6 @@
{ {
"name": "exiled-exchange-2", "name": "exiled-exchange-2",
"version": "0.13.7", "version": "0.13.8",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "node build/script.mjs", "dev": "node build/script.mjs",

View File

@@ -26,9 +26,8 @@ if (process.platform !== "darwin") {
app.enableSandbox(); app.enableSandbox();
let tray: AppTray; let tray: AppTray;
// Ensure accessibility permissions on MacOS. (async () => {
if (process.platform === "darwin") { if (process.platform === "darwin") {
(async () => {
async function ensureAccessibilityPermission(): Promise<boolean> { async function ensureAccessibilityPermission(): Promise<boolean> {
if (systemPreferences.isTrustedAccessibilityClient(false)) return true; if (systemPreferences.isTrustedAccessibilityClient(false)) return true;
@@ -60,77 +59,8 @@ if (process.platform === "darwin") {
return; return;
} }
console.log("Accessibility permission granted, starting app"); console.log("Accessibility permission granted, starting app");
app.on("ready", async () => { }
tray = new AppTray(eventPipe);
const logger = new Logger(eventPipe);
const gameLogWatcher = new GameLogWatcher(eventPipe, logger);
const gameConfig = new GameConfig(eventPipe, logger);
const poeWindow = new GameWindow();
const appUpdater = new AppUpdater(eventPipe);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _httpProxy = new HttpProxy(server, logger);
if (process.env.VITE_DEV_SERVER_URL) {
try {
await installExtension(VUEJS_DEVTOOLS);
logger.write("info Vue Devtools installed");
} catch (error) {
logger.write(`error installing Vue Devtools: ${error}`);
console.log(`error installing Vue Devtools: ${error}`);
}
}
process.addListener("uncaughtException", (err) => {
logger.write(`error [uncaughtException] ${err.message}, ${err.stack}`);
});
process.addListener("unhandledRejection", (reason) => {
logger.write(`error [unhandledRejection] ${(reason as Error).stack}`);
});
setTimeout(
async () => {
const overlay = new OverlayWindow(eventPipe, logger, poeWindow);
// eslint-disable-next-line no-new
new OverlayVisibility(eventPipe, overlay, gameConfig);
const shortcuts = await Shortcuts.create(
logger,
overlay,
poeWindow,
gameConfig,
eventPipe,
);
eventPipe.onEventAnyClient(
"CLIENT->MAIN::update-host-config",
(cfg) => {
overlay.updateOpts(cfg.overlayKey, cfg.windowTitle);
shortcuts.updateActions(
cfg.shortcuts,
cfg.stashScroll,
cfg.logKeys,
cfg.restoreClipboard,
cfg.language,
);
gameLogWatcher.restart(cfg.clientLog ?? "", cfg.readClientLog);
gameConfig.readConfig(cfg.gameConfig ?? "");
appUpdater.checkAtStartup();
tray.overlayKey = cfg.overlayKey;
},
);
uIOhook.start();
console.log("uIOhook started");
const port = await startServer(appUpdater, logger);
// TODO: move up (currently crashes)
logger.write(
`info ${os.type()} ${os.release} / v${app.getVersion()}`,
);
overlay.loadAppPage(port);
tray.serverPort = port;
},
// fixes(linux): window is black instead of transparent
process.platform === "linux" ? 1000 : 0,
);
});
})();
} else {
app.on("ready", async () => { app.on("ready", async () => {
tray = new AppTray(eventPipe); tray = new AppTray(eventPipe);
const logger = new Logger(eventPipe); const logger = new Logger(eventPipe);
@@ -150,7 +80,6 @@ if (process.platform === "darwin") {
console.log(`error installing Vue Devtools: ${error}`); console.log(`error installing Vue Devtools: ${error}`);
} }
} }
process.addListener("uncaughtException", (err) => { process.addListener("uncaughtException", (err) => {
logger.write(`error [uncaughtException] ${err.message}, ${err.stack}`); logger.write(`error [uncaughtException] ${err.message}, ${err.stack}`);
}); });
@@ -199,4 +128,4 @@ if (process.platform === "darwin") {
process.platform === "linux" ? 1000 : 0, process.platform === "linux" ? 1000 : 0,
); );
}); });
} })();

View File

@@ -57,6 +57,7 @@
"has_empty_prefix": "前綴", "has_empty_prefix": "前綴",
"has_empty_suffix": "後綴", "has_empty_suffix": "後綴",
"item_level": "物品等級:{0}", "item_level": "物品等級:{0}",
"requires_level": "需求 等級: {0}",
"stock": "庫存:{0}", "stock": "庫存:{0}",
"map_tier": "地圖階級:{0}", "map_tier": "地圖階級:{0}",
"area_level": "區域等級:{0}", "area_level": "區域等級:{0}",

View File

@@ -1,4 +1,5 @@
// @ts-check // @ts-check
// autogenerated file, do not edit
/** @type{import('../../../src/assets/data/interfaces').TranslationDict} */ /** @type{import('../../../src/assets/data/interfaces').TranslationDict} */
export default { export default {
RARITY_NORMAL: '中', RARITY_NORMAL: '中',
@@ -159,4 +160,5 @@ export default {
LOG_LEVEL_UP: /^(.*) 現在等級 (?<level>\d+)$/, LOG_LEVEL_UP: /^(.*) 現在等級 (?<level>\d+)$/,
// [Manual] // [Manual]
LOG_ZONE_GEN: /^Generating level (?<area_level>\d+) area "(?<zone>.*)" with seed (?<seed>\d+)$/, LOG_ZONE_GEN: /^Generating level (?<area_level>\d+) area "(?<zone>.*)" with seed (?<seed>\d+)$/,
REQUIRES_LINE: /^需求: \s*(?:等級[^\d,]*(?<level>\d+))?\D*(?:(?<str>\d+)[^\d,]*力量)?\D*(?:(?<dex>\d+)[^\d,]*敏捷)?\D*(?:(?<int>\d+)[^\d,]*智慧)?$/,
} }

View File

@@ -57,6 +57,7 @@
"has_empty_prefix": "Präfix", "has_empty_prefix": "Präfix",
"has_empty_suffix": "Suffix", "has_empty_suffix": "Suffix",
"item_level": "Gegenstandsstufe: {0}", "item_level": "Gegenstandsstufe: {0}",
"requires_level": "Erfordert Stufe: {0}",
"stock": "Bestand: {0}", "stock": "Bestand: {0}",
"map_tier": "Kartenstufe: {0}", "map_tier": "Kartenstufe: {0}",
"area_level": "Gebietsstufe: {0}", "area_level": "Gebietsstufe: {0}",

View File

@@ -1,4 +1,5 @@
// @ts-check // @ts-check
// autogenerated file, do not edit
/** @type{import('../../../src/assets/data/interfaces').TranslationDict} */ /** @type{import('../../../src/assets/data/interfaces').TranslationDict} */
export default { export default {
RARITY_NORMAL: 'Normal', RARITY_NORMAL: 'Normal',
@@ -159,4 +160,5 @@ export default {
LOG_LEVEL_UP: /^(.*) ist jetzt Stufe (?<level>\d+)$/, LOG_LEVEL_UP: /^(.*) ist jetzt Stufe (?<level>\d+)$/,
// [Manual] // [Manual]
LOG_ZONE_GEN: /^Generating level (?<area_level>\d+) area "(?<zone>.*)" with seed (?<seed>\d+)$/, LOG_ZONE_GEN: /^Generating level (?<area_level>\d+) area "(?<zone>.*)" with seed (?<seed>\d+)$/,
REQUIRES_LINE: /^Erfordert: \s*(?:Stufe[^\d,]*(?<level>\d+))?\D*(?:(?<str>\d+)[^\d,]*Str)?\D*(?:(?<dex>\d+)[^\d,]*Ges )?\D*(?:(?<int>\d+)[^\d,]*Int)?$/,
} }

View File

@@ -57,6 +57,7 @@
"has_empty_prefix": "Prefix", "has_empty_prefix": "Prefix",
"has_empty_suffix": "Suffix", "has_empty_suffix": "Suffix",
"item_level": "Item Level: {0}", "item_level": "Item Level: {0}",
"requires_level": "Req. Level: {0}",
"stock": "Stock: {0}", "stock": "Stock: {0}",
"map_tier": "Map Tier: {0}", "map_tier": "Map Tier: {0}",
"area_level": "Area Level: {0}", "area_level": "Area Level: {0}",

View File

@@ -1,4 +1,5 @@
// @ts-check // @ts-check
// autogenerated file, do not edit
/** @type{import('../../../src/assets/data/interfaces').TranslationDict} */ /** @type{import('../../../src/assets/data/interfaces').TranslationDict} */
export default { export default {
RARITY_NORMAL: 'Normal', RARITY_NORMAL: 'Normal',
@@ -159,4 +160,5 @@ export default {
LOG_LEVEL_UP: /^(.*) is now level (?<level>\d+)$/, LOG_LEVEL_UP: /^(.*) is now level (?<level>\d+)$/,
// [Manual] // [Manual]
LOG_ZONE_GEN: /^Generating level (?<area_level>\d+) area "(?<zone>.*)" with seed (?<seed>\d+)$/, LOG_ZONE_GEN: /^Generating level (?<area_level>\d+) area "(?<zone>.*)" with seed (?<seed>\d+)$/,
REQUIRES_LINE: /^Requires: \s*(?:Level[^\d,]*(?<level>\d+))?\D*(?:(?<str>\d+)[^\d,]*Str)?\D*(?:(?<dex>\d+)[^\d,]*Dex)?\D*(?:(?<int>\d+)[^\d,]*Int)?$/,
} }

View File

@@ -57,6 +57,7 @@
"has_empty_prefix": "Prefijo", "has_empty_prefix": "Prefijo",
"has_empty_suffix": "Sufijo", "has_empty_suffix": "Sufijo",
"item_level": "Nivel del objeto: {0}", "item_level": "Nivel del objeto: {0}",
"requires_level": "Requiere Nivel: {0}",
"stock": "Inventario: {0}", "stock": "Inventario: {0}",
"map_tier": "Nivel del mapa: {0}", "map_tier": "Nivel del mapa: {0}",
"area_level": "Nivel del área: {0}", "area_level": "Nivel del área: {0}",

View File

@@ -1,4 +1,5 @@
// @ts-check // @ts-check
// autogenerated file, do not edit
/** @type{import('../../../src/assets/data/interfaces').TranslationDict} */ /** @type{import('../../../src/assets/data/interfaces').TranslationDict} */
export default { export default {
RARITY_NORMAL: 'Normal', RARITY_NORMAL: 'Normal',
@@ -159,4 +160,5 @@ export default {
LOG_LEVEL_UP: /^(.*) es ahora nivel (?<level>\d+)$/, LOG_LEVEL_UP: /^(.*) es ahora nivel (?<level>\d+)$/,
// [Manual] // [Manual]
LOG_ZONE_GEN: /^Generating level (?<area_level>\d+) area "(?<zone>.*)" with seed (?<seed>\d+)$/, LOG_ZONE_GEN: /^Generating level (?<area_level>\d+) area "(?<zone>.*)" with seed (?<seed>\d+)$/,
REQUIRES_LINE: /^Requiere: \s*(?:Nivel[^\d,]*(?<level>\d+))?\D*(?:(?<str>\d+)[^\d,]*Fue)?\D*(?:(?<dex>\d+)[^\d,]*Des)?\D*(?:(?<int>\d+)[^\d,]*Int)?$/,
} }

View File

@@ -57,6 +57,7 @@
"has_empty_prefix": "プレフィックス", "has_empty_prefix": "プレフィックス",
"has_empty_suffix": "サフィックス", "has_empty_suffix": "サフィックス",
"item_level": "アイテムレベル: {0}", "item_level": "アイテムレベル: {0}",
"requires_level": "装備条件 レベル: {0}",
"stock": "在庫: {0}", "stock": "在庫: {0}",
"map_tier": "マップティア: {0}", "map_tier": "マップティア: {0}",
"area_level": "エリアレベル: {0}", "area_level": "エリアレベル: {0}",

View File

@@ -1,4 +1,5 @@
// @ts-check // @ts-check
// autogenerated file, do not edit
/** @type{import('../../../src/assets/data/interfaces').TranslationDict} */ /** @type{import('../../../src/assets/data/interfaces').TranslationDict} */
export default { export default {
RARITY_NORMAL: 'ノーマル', RARITY_NORMAL: 'ノーマル',
@@ -159,4 +160,5 @@ export default {
LOG_LEVEL_UP: /^(.*)は現在レベル(?<level>\d+)です$/, LOG_LEVEL_UP: /^(.*)は現在レベル(?<level>\d+)です$/,
// [Manual] // [Manual]
LOG_ZONE_GEN: /^Generating level (?<area_level>\d+) area "(?<zone>.*)" with seed (?<seed>\d+)$/, LOG_ZONE_GEN: /^Generating level (?<area_level>\d+) area "(?<zone>.*)" with seed (?<seed>\d+)$/,
REQUIRES_LINE: /^装備条件:\s*(?:レベル[^\d,]*(?<level>\d+))?\D*(?:(?<str>\d+)[^\d,]*筋力)?\D*(?:(?<dex>\d+)[^\d,]*器用さ)?\D*(?:(?<int>\d+)[^\d,]*知性)?$/,
} }

View File

@@ -54,6 +54,7 @@
"has_empty_suffix": "빈 접미어", "has_empty_suffix": "빈 접미어",
"spirit": "정신력: {0}", "spirit": "정신력: {0}",
"item_level": "아이템 레벨: {0}", "item_level": "아이템 레벨: {0}",
"requires_level": "요구 사항 레벨: {0}",
"stock": "홈: {0}", "stock": "홈: {0}",
"map_tier": "지도 등급: {0}", "map_tier": "지도 등급: {0}",
"area_level": "지역 레벨: {0}", "area_level": "지역 레벨: {0}",

View File

@@ -1,4 +1,5 @@
// @ts-check // @ts-check
// autogenerated file, do not edit
/** @type{import('../../../src/assets/data/interfaces').TranslationDict} */ /** @type{import('../../../src/assets/data/interfaces').TranslationDict} */
export default { export default {
RARITY_NORMAL: '일반', RARITY_NORMAL: '일반',
@@ -159,4 +160,5 @@ export default {
LOG_LEVEL_UP: /^(.*)의 레벨이 (?<level>\d+)이(가) 되었습니다$/, LOG_LEVEL_UP: /^(.*)의 레벨이 (?<level>\d+)이(가) 되었습니다$/,
// [Manual] // [Manual]
LOG_ZONE_GEN: /^Generating level (?<area_level>\d+) area "(?<zone>.*)" with seed (?<seed>\d+)$/, LOG_ZONE_GEN: /^Generating level (?<area_level>\d+) area "(?<zone>.*)" with seed (?<seed>\d+)$/,
REQUIRES_LINE: /^요구 사항: \s*(?:레벨[^\d,]*(?<level>\d+))?\D*(?:(?<str>\d+)[^\d,]*힘)?\D*(?:(?<dex>\d+)[^\d,]*민첩)?\D*(?:(?<int>\d+)[^\d,]*지능)?$/,
} }

View File

@@ -56,6 +56,7 @@
"has_empty_prefix": "Prefixo", "has_empty_prefix": "Prefixo",
"has_empty_suffix": "Sufixo", "has_empty_suffix": "Sufixo",
"item_level": "Nível do item: {0}", "item_level": "Nível do item: {0}",
"requires_level": "Requer Nível: {0}",
"stock": "Inventário: {0}", "stock": "Inventário: {0}",
"map_tier": "Nível do mapa: {0}", "map_tier": "Nível do mapa: {0}",
"area_level": "Nível da área: {0}", "area_level": "Nível da área: {0}",

View File

@@ -1,4 +1,5 @@
// @ts-check // @ts-check
// autogenerated file, do not edit
/** @type{import('../../../src/assets/data/interfaces').TranslationDict} */ /** @type{import('../../../src/assets/data/interfaces').TranslationDict} */
export default { export default {
RARITY_NORMAL: 'Normal', RARITY_NORMAL: 'Normal',
@@ -159,4 +160,5 @@ export default {
LOG_LEVEL_UP: /^(.*) agora está no nível (?<level>\d+)$/, LOG_LEVEL_UP: /^(.*) agora está no nível (?<level>\d+)$/,
// [Manual] // [Manual]
LOG_ZONE_GEN: /^Generating level (?<area_level>\d+) area "(?<zone>.*)" with seed (?<seed>\d+)$/, LOG_ZONE_GEN: /^Generating level (?<area_level>\d+) area "(?<zone>.*)" with seed (?<seed>\d+)$/,
REQUIRES_LINE: /^Requer: \s*(?:Nível[^\d,]*(?<level>\d+))?\D*(?:(?<str>\d+)[^\d,]*For)?\D*(?:(?<dex>\d+)[^\d,]*Des)?\D*(?:(?<int>\d+)[^\d,]*Int)?$/,
} }

View File

@@ -71,6 +71,7 @@
"has_empty_prefix": "Префикс", "has_empty_prefix": "Префикс",
"has_empty_suffix": "Суффикс", "has_empty_suffix": "Суффикс",
"item_level": "Ур. предмета: {0}", "item_level": "Ур. предмета: {0}",
"requires_level": "Требуется Уровень: {0}",
"spirit": "Дух: {0}", "spirit": "Дух: {0}",
"reload_time": "Время перезарядки: {0}", "reload_time": "Время перезарядки: {0}",
"stock": "Запас: {0}", "stock": "Запас: {0}",

View File

@@ -1,4 +1,5 @@
// @ts-check // @ts-check
// autogenerated file, do not edit
/** @type{import('../../../src/assets/data/interfaces').TranslationDict} */ /** @type{import('../../../src/assets/data/interfaces').TranslationDict} */
export default { export default {
RARITY_NORMAL: 'Обычный', RARITY_NORMAL: 'Обычный',
@@ -159,4 +160,5 @@ export default {
LOG_LEVEL_UP: /^(.*) теперь (?<level>\d+) уровня$/, LOG_LEVEL_UP: /^(.*) теперь (?<level>\d+) уровня$/,
// [Manual] // [Manual]
LOG_ZONE_GEN: /^Generating level (?<area_level>\d+) area "(?<zone>.*)" with seed (?<seed>\d+)$/, LOG_ZONE_GEN: /^Generating level (?<area_level>\d+) area "(?<zone>.*)" with seed (?<seed>\d+)$/,
REQUIRES_LINE: /^Требуется: \s*(?:Уровень[^\d,]*(?<level>\d+))?\D*(?:(?<str>\d+)[^\d,]*Сила)?\D*(?:(?<dex>\d+)[^\d,]*Ловк)?\D*(?:(?<int>\d+)[^\d,]*Инт)?$/,
} }

View File

@@ -0,0 +1,73 @@
import { __testExports, ParserState } from "@/parser/Parser";
import { beforeEach, describe, expect, it } from "vitest";
import { setupTests } from "@specs/vitest.setup";
import {
FracturedItem,
FracturedItemNoModMarked,
RareItem,
TestItem,
} from "./items";
import { loadForLang } from "@/assets/data";
import { ParsedItem } from "@/parser/ParsedItem";
import { ModifierType } from "@/parser/modifiers";
describe("Parse Fractured Items", () => {
beforeEach(async () => {
setupTests();
await loadForLang("en");
});
it.each([
[FracturedItem, true],
[FracturedItemNoModMarked, true],
[RareItem, undefined],
])(
"%#, Each mod section is recognized",
(item: TestItem, isFractured: boolean | undefined) => {
const sections = __testExports.itemTextToSections(item.rawText);
const parsedItem = {} as ParsedItem;
__testExports.parseFracturedText(
sections[sections.length - 1],
parsedItem,
);
expect(parsedItem.isFractured).toBe(isFractured);
},
);
it("adds fractured if some mod is fractured", () => {
const parsedItem = {
newMods: [
{
info: { type: ModifierType.Fractured, tags: [] },
stats: [],
},
{
info: { type: ModifierType.Explicit, tags: [] },
stats: [],
},
{
info: { type: ModifierType.Explicit, tags: [] },
stats: [],
},
],
} as unknown as ParserState;
__testExports.parseFractured(parsedItem);
expect(parsedItem.isFractured).toBe(true);
});
it("does nothing if no mod is fractured", () => {
const parsedItem = {
newMods: [
{
info: { type: ModifierType.Implicit, tags: [] },
stats: [],
},
{
info: { type: ModifierType.Explicit, tags: [] },
stats: [],
},
],
} as unknown as ParserState;
__testExports.parseFractured(parsedItem);
expect(parsedItem.isFractured).toBeUndefined();
});
});

View File

@@ -66,6 +66,14 @@ export class TestItem implements ParsedItem {
note?: string; note?: string;
category?: ItemCategory | undefined; category?: ItemCategory | undefined;
requires?: {
level: number;
str: number;
dex: number;
int: number;
};
info: BaseType = { info: BaseType = {
name: "test", name: "test",
refName: "test", refName: "test",
@@ -125,6 +133,12 @@ NormalItem.quality = 9;
NormalItem.armourAR = 174; NormalItem.armourAR = 174;
NormalItem.armourES = 60; NormalItem.armourES = 60;
NormalItem.itemLevel = 81; NormalItem.itemLevel = 81;
NormalItem.requires = {
level: 75,
str: 67,
dex: 0,
int: 67,
};
NormalItem.info.refName = "Divine Crown"; NormalItem.info.refName = "Divine Crown";
NormalItem.sectionCount = 4; NormalItem.sectionCount = 4;
@@ -157,6 +171,12 @@ MagicItem.weaponELEMENTAL = MagicItem.weaponLIGHTNING;
MagicItem.weaponCRIT = 5; MagicItem.weaponCRIT = 5;
MagicItem.weaponAS = 1.2; MagicItem.weaponAS = 1.2;
MagicItem.itemLevel = 32; MagicItem.itemLevel = 32;
MagicItem.requires = {
level: 28,
str: 57,
dex: 0,
int: 0,
};
MagicItem.info.refName = "Temple Maul"; MagicItem.info.refName = "Temple Maul";
MagicItem.sectionCount = 5; MagicItem.sectionCount = 5;
@@ -200,6 +220,12 @@ RareItem.weaponELEMENTAL =
RareItem.weaponAS = 1.2; RareItem.weaponAS = 1.2;
RareItem.weaponCRIT = 5; RareItem.weaponCRIT = 5;
RareItem.itemLevel = 80; RareItem.itemLevel = 80;
RareItem.requires = {
level: 51,
str: 0,
dex: 103,
int: 0,
};
RareItem.info.refName = "Rider Bow"; RareItem.info.refName = "Rider Bow";
RareItem.sectionCount = 5; RareItem.sectionCount = 5;
@@ -237,6 +263,12 @@ UniqueItem.category = ItemCategory.Focus;
UniqueItem.rarity = ItemRarity.Unique; UniqueItem.rarity = ItemRarity.Unique;
UniqueItem.armourES = 44; UniqueItem.armourES = 44;
UniqueItem.itemLevel = 81; UniqueItem.itemLevel = 81;
UniqueItem.requires = {
level: 26,
str: 0,
dex: 0,
int: 43,
};
// NOTE: requires step through to verify use of Name here is right // NOTE: requires step through to verify use of Name here is right
UniqueItem.info.refName = "The Eternal Spark"; UniqueItem.info.refName = "The Eternal Spark";
@@ -271,6 +303,12 @@ Item Level: 79
RareWithImplicit.category = ItemCategory.Ring; RareWithImplicit.category = ItemCategory.Ring;
RareWithImplicit.rarity = ItemRarity.Rare; RareWithImplicit.rarity = ItemRarity.Rare;
RareWithImplicit.itemLevel = 79; RareWithImplicit.itemLevel = 79;
RareWithImplicit.requires = {
level: 45,
str: 0,
dex: 0,
int: 0,
};
RareWithImplicit.info.refName = "Prismatic Ring"; RareWithImplicit.info.refName = "Prismatic Ring";
RareWithImplicit.sectionCount = 5; RareWithImplicit.sectionCount = 5;
@@ -401,6 +439,12 @@ HighDamageRareItem.sectionCount = 9;
HighDamageRareItem.prefixCount = 3; HighDamageRareItem.prefixCount = 3;
HighDamageRareItem.suffixCount = 3; HighDamageRareItem.suffixCount = 3;
HighDamageRareItem.implicitCount = 1; HighDamageRareItem.implicitCount = 1;
HighDamageRareItem.requires = {
level: 79,
str: 89,
dex: 89,
int: 0,
};
HighDamageRareItem.runeSockets = { HighDamageRareItem.runeSockets = {
empty: 0, empty: 0,
@@ -447,6 +491,12 @@ ArmourHighValueRareItem.rarity = ItemRarity.Rare;
ArmourHighValueRareItem.quality = 20; ArmourHighValueRareItem.quality = 20;
ArmourHighValueRareItem.armourAR = 3075; ArmourHighValueRareItem.armourAR = 3075;
ArmourHighValueRareItem.itemLevel = 80; ArmourHighValueRareItem.itemLevel = 80;
ArmourHighValueRareItem.requires = {
level: 65,
str: 121,
dex: 0,
int: 0,
};
ArmourHighValueRareItem.info.refName = "Soldier Cuirass"; ArmourHighValueRareItem.info.refName = "Soldier Cuirass";
ArmourHighValueRareItem.sectionCount = 8; ArmourHighValueRareItem.sectionCount = 8;
@@ -488,6 +538,12 @@ Note: ~b/o 5 exalted
WandRareItem.category = ItemCategory.Wand; WandRareItem.category = ItemCategory.Wand;
WandRareItem.rarity = ItemRarity.Rare; WandRareItem.rarity = ItemRarity.Rare;
WandRareItem.itemLevel = 82; WandRareItem.itemLevel = 82;
WandRareItem.requires = {
level: 90,
str: 0,
dex: 0,
int: 125,
};
WandRareItem.info.refName = "Withered Wand"; WandRareItem.info.refName = "Withered Wand";
WandRareItem.sectionCount = 6; WandRareItem.sectionCount = 6;
@@ -521,6 +577,12 @@ NormalShield.itemLevel = 82;
NormalShield.armourAR = 71; NormalShield.armourAR = 71;
NormalShield.armourEV = 64; NormalShield.armourEV = 64;
NormalShield.armourBLOCK = 25; NormalShield.armourBLOCK = 25;
NormalShield.requires = {
level: 54,
str: 42,
dex: 42,
int: 0,
};
NormalShield.info.refName = "Polished Targe"; NormalShield.info.refName = "Polished Targe";
NormalShield.sectionCount = 6; NormalShield.sectionCount = 6;
@@ -558,6 +620,12 @@ Has 2(1-3) Charm Slots
TwoImplicitItem.category = ItemCategory.Belt; TwoImplicitItem.category = ItemCategory.Belt;
TwoImplicitItem.rarity = ItemRarity.Rare; TwoImplicitItem.rarity = ItemRarity.Rare;
TwoImplicitItem.itemLevel = 80; TwoImplicitItem.itemLevel = 80;
TwoImplicitItem.requires = {
level: 59,
str: 0,
dex: 0,
int: 0,
};
TwoImplicitItem.info.refName = "Ornate Belt"; TwoImplicitItem.info.refName = "Ornate Belt";
TwoImplicitItem.sectionCount = 5; TwoImplicitItem.sectionCount = 5;
@@ -686,5 +754,147 @@ RareMapFakeAllProps.mapMagicMonsters = 30;
RareMapFakeAllProps.mapRareMonsters = 71; RareMapFakeAllProps.mapRareMonsters = 71;
RareMapFakeAllProps.mapDropChance = 90; RareMapFakeAllProps.mapDropChance = 90;
RareMapFakeAllProps.mapItemRarity = 17; RareMapFakeAllProps.mapItemRarity = 17;
RareMapFakeAllProps.sectionCount = 5; RareMapFakeAllProps.sectionCount = 6;
// #endregion
// #region FracturedItem
export const FracturedItem = new TestItem(`Item Class: Bows
Rarity: Rare
Miracle Siege
Obliterator Bow
--------
Quality: +25% (augmented)
Physical Damage: 381-705 (augmented)
Critical Hit Chance: 9.40% (augmented)
Attacks per Second: 1.15
--------
Requires: Level 78, 163 (unmet) Dex
--------
Sockets: S S
--------
Item Level: 81
--------
36% increased Physical Damage (rune)
--------
{ Implicit Modifier }
50% reduced Projectile Range
--------
{ Prefix Modifier "Flaring" (Tier: 1) — Damage, Physical, Attack }
Adds 32(26-39) to 59(44-66) Physical Damage (fractured)
{ Prefix Modifier "Bloodthirsty" (Tier: 4) — Damage, Physical, Attack }
134(110-134)% increased Physical Damage
{ Prefix Modifier "Champion's" (Tier: 4) — Damage, Physical, Attack }
54(45-54)% increased Physical Damage
+113(98-123) to Accuracy Rating
{ Suffix Modifier "of the Essence" — Speed }
20(20-25)% chance to gain Onslaught on Killing Hits with this Weapon
{ Suffix Modifier "of the Essence" — Attack }
+3 to Level of all Attack Skills
{ Suffix Modifier "of Ruin" (Tier: 2) — Attack, Critical }
+4.4(3.81-4.4)% to Critical Hit Chance
--------
Fractured Item
`);
FracturedItem.category = ItemCategory.Bow;
FracturedItem.rarity = ItemRarity.Rare;
FracturedItem.quality = 25;
FracturedItem.weaponPHYSICAL = 624;
FracturedItem.weaponAS = 1.15;
FracturedItem.weaponCRIT = 9.4;
FracturedItem.itemLevel = 81;
FracturedItem.requires = {
level: 78,
str: 163,
dex: 0,
int: 0,
};
FracturedItem.info.refName = "Obliterator Bow";
FracturedItem.isFractured = true;
FracturedItem.prefixCount = 3;
FracturedItem.suffixCount = 3;
FracturedItem.implicitCount = 1;
FracturedItem.sectionCount = 9;
FracturedItem.runeSockets = {
empty: 0,
current: 2,
normal: 2,
};
// #endregion
// #region FracturedItemNoModMarked
export const FracturedItemNoModMarked = new TestItem(`Item Class: Bows
Rarity: Rare
Miracle Siege
Obliterator Bow
--------
Quality: +25% (augmented)
Physical Damage: 381-705 (augmented)
Critical Hit Chance: 9.40% (augmented)
Attacks per Second: 1.15
--------
Requires: Level 78, 163 (unmet) Dex
--------
Sockets: S S
--------
Item Level: 81
--------
36% increased Physical Damage (rune)
--------
{ Implicit Modifier }
50% reduced Projectile Range
--------
{ Prefix Modifier "Flaring" (Tier: 1) — Damage, Physical, Attack }
Adds 32(26-39) to 59(44-66) Physical Damage
{ Prefix Modifier "Bloodthirsty" (Tier: 4) — Damage, Physical, Attack }
134(110-134)% increased Physical Damage
{ Prefix Modifier "Champion's" (Tier: 4) — Damage, Physical, Attack }
54(45-54)% increased Physical Damage
+113(98-123) to Accuracy Rating
{ Suffix Modifier "of the Essence" — Speed }
20(20-25)% chance to gain Onslaught on Killing Hits with this Weapon
{ Suffix Modifier "of the Essence" — Attack }
+3 to Level of all Attack Skills
{ Suffix Modifier "of Ruin" (Tier: 2) — Attack, Critical }
+4.4(3.81-4.4)% to Critical Hit Chance
--------
Fractured Item
`);
FracturedItemNoModMarked.category = ItemCategory.Bow;
FracturedItemNoModMarked.rarity = ItemRarity.Rare;
FracturedItemNoModMarked.quality = 25;
FracturedItemNoModMarked.weaponPHYSICAL = 381.5;
FracturedItemNoModMarked.weaponAS = 1.15;
FracturedItemNoModMarked.weaponCRIT = 9.4;
FracturedItemNoModMarked.itemLevel = 81;
FracturedItemNoModMarked.requires = {
level: 78,
str: 163,
dex: 0,
int: 0,
};
FracturedItemNoModMarked.info.refName = "Obliterator Bow";
FracturedItemNoModMarked.isFractured = true;
FracturedItemNoModMarked.prefixCount = 3;
FracturedItemNoModMarked.suffixCount = 3;
FracturedItemNoModMarked.implicitCount = 1;
FracturedItemNoModMarked.sectionCount = 9;
FracturedItemNoModMarked.runeSockets = {
empty: 0,
current: 2,
normal: 2,
};
// #endregion // #endregion

View File

@@ -1,3 +1,4 @@
import { CLIENT_STRINGS as _$ } from "@/assets/data";
import { __testExports } from "@/parser/Parser"; import { __testExports } from "@/parser/Parser";
import { beforeEach, describe, expect, it, test } from "vitest"; import { beforeEach, describe, expect, it, test } from "vitest";
import { setupTests } from "@specs/vitest.setup"; import { setupTests } from "@specs/vitest.setup";
@@ -8,6 +9,7 @@ import {
NormalItem, NormalItem,
RareItem, RareItem,
RareWithImplicit, RareWithImplicit,
TestItem,
UniqueItem, UniqueItem,
WandRareItem, WandRareItem,
} from "./items"; } from "./items";
@@ -49,6 +51,8 @@ describe("parseWeapon", () => {
const res = __testExports.parseWeapon(sections[1], parsedItem); const res = __testExports.parseWeapon(sections[1], parsedItem);
// console.log(sections);
expect(res).toBe("SECTION_PARSED"); expect(res).toBe("SECTION_PARSED");
expect(parsedItem.weaponPHYSICAL).toBe(MagicItem.weaponPHYSICAL); expect(parsedItem.weaponPHYSICAL).toBe(MagicItem.weaponPHYSICAL);
expect(parsedItem.weaponELEMENTAL).toBe(MagicItem.weaponELEMENTAL); expect(parsedItem.weaponELEMENTAL).toBe(MagicItem.weaponELEMENTAL);
@@ -134,3 +138,86 @@ describe("parseArmour", () => {
expect(parsedItem.armourBLOCK).toBe(ArmourHighValueRareItem.armourBLOCK); expect(parsedItem.armourBLOCK).toBe(ArmourHighValueRareItem.armourBLOCK);
}); });
}); });
describe("parseRequirements", () => {
it.each([
["Normal", NormalItem],
["Magic", MagicItem],
["Rare", RareItem],
["Unique", UniqueItem],
["RareWithImplicit", RareWithImplicit],
["HighDamageRare", HighDamageRareItem],
["ArmourHighValueRare", ArmourHighValueRareItem],
["WandRare", WandRareItem],
])(
"%s, items parse requirements",
async (testName: string, item: TestItem) => {
setupTests();
await loadForLang("en");
const sections = __testExports.itemTextToSections(item.rawText);
const parsedItem = {} as ParsedItem;
const res = __testExports.parseRequirements(
sections.find((s) => s.some((l) => l.startsWith(_$.REQUIRES)))!,
parsedItem,
);
expect(res).toBe("SECTION_PARSED");
expect(parsedItem.requires).toEqual(item.requires);
},
);
it.each([
[
"en",
"Requires: Level 28, 57 (augmented) Str",
{ level: 28, str: 57, dex: 0, int: 0 },
],
[
"cmn-Hant",
"需求: 等級 80, 108 (unmet) 智慧",
{ level: 80, str: 0, dex: 0, int: 108 },
],
[
"ja",
"装備条件:レベル 72, 70 筋力, 70 知性",
{ level: 72, str: 70, dex: 0, int: 70 },
],
[
"ko",
"요구 사항: 레벨 78, 89 힘, 89 (unmet) 민첩",
{ level: 78, str: 89, dex: 89, int: 0 },
],
[
"cmn-Hant",
"需求: 等級 78, 54 力量, 138 智慧",
{ level: 78, str: 54, dex: 0, int: 138 },
],
[
"ru",
"Требуется: Уровень 80, 59 (unmet) Ловк, 59 Инт",
{
level: 80,
str: 0,
dex: 59,
int: 59,
},
],
])(
"%s requires regex works",
async (
lang: string,
str: string,
expectedResult: ParsedItem["requires"],
) => {
setupTests();
await loadForLang(lang);
const parsedItem = {} as ParsedItem;
const res = __testExports.parseRequirements([str], parsedItem);
expect(res).toBe("SECTION_PARSED");
expect(parsedItem.requires).toEqual(expectedResult);
},
);
});

View File

@@ -245,6 +245,7 @@ export interface TranslationDict {
LOG_ZONE_GEN: RegExp; LOG_ZONE_GEN: RegExp;
DOUBLE_CORRUPTED: string; DOUBLE_CORRUPTED: string;
IMPLICIT_MODIFIER: string; IMPLICIT_MODIFIER: string;
REQUIRES_LINE: RegExp;
} }
export interface Filter { export interface Filter {

View File

@@ -94,6 +94,12 @@ export interface ParsedItem {
}; };
note?: string; note?: string;
category?: ItemCategory; category?: ItemCategory;
requires?: {
level: number;
str: number;
dex: number;
int: number;
};
info: BaseType; info: BaseType;
rawText: string; rawText: string;
} }

View File

@@ -48,7 +48,7 @@ type SectionParseResult =
type ParserFn = (section: string[], item: ParserState) => SectionParseResult; type ParserFn = (section: string[], item: ParserState) => SectionParseResult;
type VirtualParserFn = (item: ParserState) => Result<never, string> | void; type VirtualParserFn = (item: ParserState) => Result<never, string> | void;
interface ParserState extends ParsedItem { export interface ParserState extends ParsedItem {
name: string; name: string;
baseType: string | undefined; baseType: string | undefined;
infoVariants: BaseType[]; infoVariants: BaseType[];
@@ -379,6 +379,7 @@ function parseBlightedMap(item: ParsedItem) {
} }
function parseFractured(item: ParserState) { function parseFractured(item: ParserState) {
// NOTE: partially also controlled by parseFracturedText
if (item.newMods.some((mod) => mod.info.type === ModifierType.Fractured)) { if (item.newMods.some((mod) => mod.info.type === ModifierType.Fractured)) {
item.isFractured = true; item.isFractured = true;
} }
@@ -580,13 +581,24 @@ function parseItemLevel(section: string[], item: ParsedItem) {
} }
function parseRequirements(section: string[], item: ParsedItem) { function parseRequirements(section: string[], item: ParsedItem) {
if ( if (!section[0].startsWith(_$.REQUIRES)) {
section[0].startsWith(_$.REQUIREMENTS) || return "SECTION_SKIPPED";
section[0].startsWith(_$.REQUIRES)
) {
return "SECTION_PARSED";
} }
return "SECTION_SKIPPED";
const match = section[0].match(_$.REQUIRES_LINE);
// TODO: remove once validated in other langs
if (!match) {
throw new Error("Failed to parse requirements");
}
item.requires = {
level: parseInt(match.groups!.level ?? "0"),
str: parseInt(match.groups!.str ?? "0"),
dex: parseInt(match.groups!.dex ?? "0"),
int: parseInt(match.groups!.int ?? "0"),
};
return "SECTION_PARSED";
} }
function parseTalismanTier(section: string[], item: ParsedItem) { function parseTalismanTier(section: string[], item: ParsedItem) {
@@ -1222,9 +1234,11 @@ function parsePriceNote(section: string[], item: ParsedItem) {
return "SECTION_SKIPPED"; return "SECTION_SKIPPED";
} }
function parseFracturedText(section: string[], _item: ParsedItem) { function parseFracturedText(section: string[], item: ParsedItem) {
for (const line of section) { for (const line of section) {
if (line === _$.FRACTURED_ITEM) { if (line === _$.FRACTURED_ITEM) {
// HACK: remove once bug is fixed (https://www.pathofexile.com/forum/view-thread/3891367)
item.isFractured = true;
return "SECTION_PARSED"; return "SECTION_PARSED";
} }
} }
@@ -1754,4 +1768,7 @@ export const __testExports = {
parseArmour, parseArmour,
parseModifiers, parseModifiers,
parseWaystone, parseWaystone,
parseRequirements,
parseFractured,
parseFracturedText,
}; };

View File

@@ -61,6 +61,11 @@
:filter="filters.itemLevel" :filter="filters.itemLevel"
:name="t('item.item_level')" :name="t('item.item_level')"
/> />
<filter-btn-numeric
v-if="filters.requires?.level"
:filter="filters.requires?.level"
:name="t('item.requires_level')"
/>
<filter-btn-numeric <filter-btn-numeric
v-if="filters.stackSize" v-if="filters.stackSize"
:filter="filters.stackSize" :filter="filters.stackSize"

View File

@@ -1,10 +1,5 @@
import type { ItemFilters } from "./interfaces"; import type { ItemFilters } from "./interfaces";
import { import { ParsedItem, ItemCategory, ItemRarity } from "@/parser";
ParsedItem,
ItemCategory,
ItemRarity,
itemIsModifiable,
} from "@/parser";
import { tradeTag } from "../trade/common"; import { tradeTag } from "../trade/common";
import { ModifierType } from "@/parser/modifiers"; import { ModifierType } from "@/parser/modifiers";
import { BaseType, ITEM_BY_REF } from "@/assets/data"; import { BaseType, ITEM_BY_REF } from "@/assets/data";
@@ -294,6 +289,22 @@ export function createFilters(
}; };
} }
if (item.requires && item.rarity === ItemRarity.Rare && !opts.exact) {
if (
item.requires.level &&
item.requires.level <= 75 &&
item.itemLevel &&
item.itemLevel <= 75
) {
filters.requires = {
level: {
value: item.requires.level,
disabled: true,
},
};
}
}
const forAdornedJewel = const forAdornedJewel =
item.rarity === ItemRarity.Magic && item.rarity === ItemRarity.Magic &&
// item.isCorrupted && -- let the buyer corrupt // item.isCorrupted && -- let the buyer corrupt
@@ -355,12 +366,12 @@ export function createFilters(
filters.sanctified = { disabled: false }; filters.sanctified = { disabled: false };
} }
if (!item.isFractured && itemIsModifiable(item)) { if (!item.isFractured && opts.exact) {
filters.fractured = { value: false }; filters.fractured = { value: false };
} }
if (item.isFoil) { if (item.isFoil) {
filters.foil = { disabled: false }; filters.foil = { disabled: true };
} }
if (item.influences.length && item.influences.length <= 2) { if (item.influences.length && item.influences.length <= 2) {

View File

@@ -89,6 +89,12 @@ export interface ItemFilters {
collapseListings: "api" | "app"; collapseListings: "api" | "app";
}; };
tempRuneStorage?: StatFilter[]; tempRuneStorage?: StatFilter[];
requires?: {
level?: FilterNumeric;
str?: FilterNumeric;
dex?: FilterNumeric;
int?: FilterNumeric;
};
} }
export interface FilterNumeric { export interface FilterNumeric {

View File

@@ -433,6 +433,18 @@ export function createTradeRequest(
} }
} }
if (
filters.requires &&
filters.requires.level &&
!filters.requires.level.disabled
) {
propSet(
query.filters,
"req_filters.filters.lvl.max",
filters.requires.level.value,
);
}
if (filters.quality && !filters.quality.disabled) { if (filters.quality && !filters.quality.disabled) {
propSet( propSet(
query.filters, query.filters,