diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 976b3ed..3dcb80d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,8 +2,8 @@ name: release on: push: - tags: - - v* + branches: + - main jobs: release: diff --git a/frontend/src/lib/components/SearchResult.svelte b/frontend/src/lib/components/SearchResult.svelte index e4cf03d..645f628 100644 --- a/frontend/src/lib/components/SearchResult.svelte +++ b/frontend/src/lib/components/SearchResult.svelte @@ -1,27 +1,35 @@ -
+ const handleOnClick = () => highlight( set.seed, set.skills.map((s) => s.passive) - )}> + ); + + +
Seed {set.seed} (weight {set.weight})
- +
{#each set.skills as skill}
diff --git a/frontend/src/lib/components/TradeButton.svelte b/frontend/src/lib/components/TradeButton.svelte new file mode 100644 index 0000000..bef2e67 --- /dev/null +++ b/frontend/src/lib/components/TradeButton.svelte @@ -0,0 +1,27 @@ + + + diff --git a/frontend/src/lib/components/TradeLinks.svelte b/frontend/src/lib/components/TradeLinks.svelte new file mode 100644 index 0000000..6ff8731 --- /dev/null +++ b/frontend/src/lib/components/TradeLinks.svelte @@ -0,0 +1,14 @@ + + +
+ {#each queries as query, index} + + {/each} +
diff --git a/frontend/src/lib/skill_tree.ts b/frontend/src/lib/skill_tree.ts index cc222c7..5e258d8 100644 --- a/frontend/src/lib/skill_tree.ts +++ b/frontend/src/lib/skill_tree.ts @@ -1,5 +1,7 @@ import type { Translation, Node, SkillTreeData, Group, Sprite, TranslationFile } from './skill_tree_types'; import { data } from './types'; +import { type Filter, type Query, filterGroupsToQuery, filtersToFilterGroup } from './utils/trade_utils'; +import { chunkArray } from './utils/utils'; export let skillTree: SkillTreeData; @@ -384,91 +386,58 @@ const tradeStatNames: { [key: number]: { [key: string]: string } } = { } }; -export const constructQuery = (jewel: number, conqueror: string, result: SearchWithSeed[]) => { - const max_filter_length = 50; - const max_filters = 4; - const max_query_length = max_filter_length * max_filters; - const final_query = []; - const stat = { - type: 'count', - value: { min: 1 }, - filters: [], - disabled: false - }; +export const constructSingleResultQuery = (jewel: number, conqueror: string | null, result: SearchWithSeed): Query => { + const anyConqueror = conqueror === null; - // single seed case - if (result.length == 1) { - for (const conq of Object.keys(tradeStatNames[jewel])) { - stat.filters.push({ - id: tradeStatNames[jewel][conq], - value: { - min: result[0].seed, - max: result[0].seed - }, - disabled: conq != conqueror - }); - } + const filters: Filter[] = Object.keys(tradeStatNames[jewel]).map((conq) => ({ + id: tradeStatNames[jewel][conq], + value: { + min: result.seed, + max: result.seed + }, + disabled: anyConqueror ? false : conq != conqueror + })); - final_query.push(stat); - // too many results case - } else if (result.length > max_query_length) { - for (let i = 0; i < max_filters; i++) { - final_query.push({ - type: 'count', - value: { min: 1 }, - filters: [], - disabled: i == 0 ? false : true - }); - } + const filterGroup = filtersToFilterGroup(filters, false); + const query: Query = filterGroupsToQuery([filterGroup]); + return query; +}; - for (const [i, r] of result.slice(0, max_query_length).entries()) { - const index = Math.floor(i / max_filter_length); +const constructSearchFilter = (jewel: number, conqueror: string | null, result: SearchWithSeed): Filter[] => { + // null conqueror indicates to search for any conqueror + const anyConqueror = conqueror === null; + const conquerors = anyConqueror ? Object.keys(tradeStatNames[jewel]) : [conqueror]; - final_query[index].filters.push({ - id: tradeStatNames[jewel][conqueror], - value: { - min: r.seed, - max: r.seed - } - }); + return conquerors.map((conq) => ({ + id: tradeStatNames[jewel][conq], + value: { + min: result.seed, + max: result.seed } - } else { - for (const conq of Object.keys(tradeStatNames[jewel])) { - stat.disabled = conq != conqueror; - - for (const r of result) { - stat.filters.push({ - id: tradeStatNames[jewel][conq], - value: { - min: r.seed, - max: r.seed - } - }); - } + })); +}; - if (stat.filters.length > max_filter_length) { - stat.filters = stat.filters.slice(0, max_filter_length); - } +export const constructQueries = (jewel: number, conqueror: string | null, results: SearchWithSeed[]) => { + const max_filter_length = 50; + const max_filters = 4; + const max_query_length = max_filter_length * max_filters; - final_query.push(stat); - } - } + // convert all results into filters + const allFilters = results.flatMap((result) => constructSearchFilter(jewel, conqueror, result)); - return { - query: { - status: { - option: 'online' - }, - stats: final_query - }, - sort: { - price: 'asc' - } - }; -}; + // group filters into groups of max_query_length, where each group is further grouped into chunks of max_filter_length + // this represents multiple trade links, where each trade link has multiple filter groups, and each filter group has multiple filters + const queryFilterGroups = chunkArray(allFilters, max_query_length).map((chunk) => + chunkArray(chunk, max_filter_length) + ); + + // map filters groups to queries + const tradeQueries = queryFilterGroups.map((queryFilterGroup) => { + // for each query, map the chunks within it to filter groups + const tradeFilterGroups = queryFilterGroup.map((filters, index) => filtersToFilterGroup(filters, index !== 0)); + const tradeQuery: Query = filterGroupsToQuery(tradeFilterGroups); + return tradeQuery; + }); -export const openTrade = (jewel: number, conqueror: string, results: SearchWithSeed[]) => { - const url = new URL('https://www.pathofexile.com/trade/search/Necropolis'); - url.searchParams.set('q', JSON.stringify(constructQuery(jewel, conqueror, results))); - window.open(url, '_blank'); + return tradeQueries; }; diff --git a/frontend/src/lib/utils/trade_utils.ts b/frontend/src/lib/utils/trade_utils.ts new file mode 100644 index 0000000..229a1ee --- /dev/null +++ b/frontend/src/lib/utils/trade_utils.ts @@ -0,0 +1,47 @@ +export type Filter = { + id: string; + value: { min: number; max: number }; + disabled?: boolean; +}; +export type FilterGroup = { + type: string; + value: { min: number }; + filters: Filter[]; + disabled: boolean; +}; +export type Query = { + query: { + status: { + option: string; + }; + stats: FilterGroup[]; + }; + sort: { + price: string; + }; +}; + +export const filtersToFilterGroup = (filters: Filter[], disabled: boolean): FilterGroup => ({ + type: 'count', + value: { min: 1 }, + filters: filters, + disabled: disabled +}); + +export const filterGroupsToQuery = (FilterGroups: FilterGroup[]): Query => ({ + query: { + status: { + option: 'online' + }, + stats: FilterGroups + }, + sort: { + price: 'asc' + } +}); + +export const openQueryTrade = (query: Query) => { + const url = new URL('https://www.pathofexile.com/trade/search/Necropolis'); + url.searchParams.set('q', JSON.stringify(query)); + window.open(url, '_blank'); +}; diff --git a/frontend/src/lib/utils/utils.ts b/frontend/src/lib/utils/utils.ts new file mode 100644 index 0000000..01f16a6 --- /dev/null +++ b/frontend/src/lib/utils/utils.ts @@ -0,0 +1,19 @@ +/** + * Break an array into chunks of the given size + * + * e.g. for chunk size 2: + * + * [1, 2, 3, 4, 5] => [[1, 2], [3, 4], [5]] + */ +export const chunkArray = (inputArray: Array, chunkSize: number): Array[] => + inputArray.reduce((resultArray, item, index) => { + const chunkIndex = Math.floor(index / chunkSize); + + if (!resultArray[chunkIndex]) { + resultArray[chunkIndex] = []; // start a new chunk + } + + resultArray[chunkIndex].push(item); + + return resultArray; + }, [] as Array[]); diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 9eb628c..870c1d0 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -117,8 +117,9 @@ {#if result.AlternatePassiveSkill}

Alternate Passive Skill

- {result.AlternatePassiveSkill.Name} ({result.AlternatePassiveSkill.ID}) ({result.AlternatePassiveSkill}) + + {result.AlternatePassiveSkill.Name} ({result.AlternatePassiveSkill.ID}) +
{#if result.StatRolls && Object.keys(result.StatRolls).length > 0} diff --git a/frontend/src/routes/tree/+page.svelte b/frontend/src/routes/tree/+page.svelte index 531b8f0..1d0ae35 100644 --- a/frontend/src/routes/tree/+page.svelte +++ b/frontend/src/routes/tree/+page.svelte @@ -4,13 +4,15 @@ import { page } from '$app/stores'; import { goto } from '$app/navigation'; import type { Node } from '../../lib/skill_tree_types'; - import { getAffectedNodes, skillTree, translateStat, openTrade } from '../../lib/skill_tree'; + import { getAffectedNodes, skillTree, translateStat, constructQueries } from '../../lib/skill_tree'; import { syncWrap } from '../../lib/worker'; import { proxy } from 'comlink'; - import type { ReverseSearchConfig, StatConfig } from '../../lib/skill_tree'; + import type { Query, ReverseSearchConfig, StatConfig } from '../../lib/skill_tree'; import SearchResults from '../../lib/components/SearchResults.svelte'; import { statValues } from '../../lib/values'; import { data, calculator } from '../../lib/types'; + import TradeButton from '$lib/components/TradeButton.svelte'; + import TradeLinks from '$lib/components/TradeLinks.svelte'; const searchParams = $page.url.searchParams; @@ -28,13 +30,19 @@ })) : []; - let selectedConqueror = searchParams.has('conqueror') + $: dropdownConqs = conquerors.concat([{ value: 'Any', label: 'Any' }]); + + let dropdownConqueror = searchParams.has('conqueror') ? { value: searchParams.get('conqueror'), label: searchParams.get('conqueror') } : undefined; + $: anyConqueror = dropdownConqueror?.value === 'Any'; + + $: selectedConqueror = dropdownConqueror?.value === 'Any' ? conquerors[0] : dropdownConqueror; + let seed: number = searchParams.has('seed') ? parseInt(searchParams.get('seed')) : 0; let circledNode: number | undefined = searchParams.has('location') @@ -80,7 +88,7 @@ const updateUrl = () => { const url = new URL(window.location.origin + window.location.pathname); selectedJewel && url.searchParams.append('jewel', selectedJewel.value.toString()); - selectedConqueror && url.searchParams.append('conqueror', selectedConqueror.value); + dropdownConqueror && url.searchParams.append('conqueror', dropdownConqueror.value); seed && url.searchParams.append('seed', seed.toString()); circledNode && url.searchParams.append('location', circledNode.toString()); mode && url.searchParams.append('mode', mode); @@ -156,14 +164,14 @@ let currentSeed = 0; let searchResults: SearchResults; let searchJewel = 1; - let searchConqueror = ''; + let searchConqueror: string | null = null; const search = () => { if (!circledNode) { return; } searchJewel = selectedJewel.value; - searchConqueror = selectedConqueror.value; + searchConqueror = anyConqueror ? null : selectedConqueror.value; searching = true; searchResults = undefined; @@ -421,6 +429,20 @@ }; let collapsed = false; + + let showTradeLinks = false; + + let queries: Query[]; + + // reconstruct queries if search results change + $: if (searchResults && results) { + queries = constructQueries(searchJewel, searchConqueror, searchResults.raw); + + // reset showTradeLinks to hidden if new queries is only length of 1 + if (queries.length === 1) { + showTradeLinks = false; + } + } @@ -457,12 +479,7 @@ {#if searchResults}
{#if results} - +
{#if selectedConqueror && Object.keys(data.TimelessJewelConquerors[selectedJewel.value]).indexOf(selectedConqueror.value) >= 0} @@ -661,6 +678,9 @@ {/if} {#if searchResults && results} + {#if showTradeLinks} + + {/if} {/if}
@@ -698,7 +718,8 @@ } .grouped { - @apply bg-pink-500/40 disabled:bg-pink-900/40; + @apply bg-pink-500/40; + disabled: bg-pink-900/40; } .rainbow {