From 1c2473c24f1dffdb7e8ac2ba7fa4f1f12067adb2 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Thu, 5 Sep 2019 20:30:11 +0200 Subject: [PATCH] Improved loading of store data. --- assets_generation/update_drops_ephinea.ts | 24 +-- assets_generation/update_ephinea_data.ts | 34 ++-- src/core/model/index.ts | 12 +- src/core/observable/Disposer.ts | 30 ++-- src/core/observable/index.ts | 31 +++- ...eListProperty.ts => SimpleListProperty.ts} | 4 +- .../property/loadable/LoadableProperty.ts | 37 ++++ .../property/loadable/LoadableState.ts | 26 +++ .../SimpleLoadableProperty.ts} | 71 +------- .../observable/property/loadable/Store.ts | 41 +++++ src/core/persistence.ts | 12 +- src/core/stores/GuiStore.ts | 6 +- src/core/stores/ItemTypeStore.ts | 162 ++++++++---------- src/core/stores/ServerMap.ts | 36 +++- src/core/util.ts | 37 ++++ .../gui/MethodsForEpisodeView.ts | 34 +++- src/hunt_optimizer/gui/WantedItemsView.ts | 34 +++- src/hunt_optimizer/model/ItemDrop.ts | 6 +- src/hunt_optimizer/model/index.ts | 6 +- .../persistence/HuntMethodPersister.ts | 6 +- .../persistence/HuntOptimizerPersister.ts | 8 +- src/hunt_optimizer/stores/HuntMethodStore.ts | 158 ++++++++--------- .../stores/HuntOptimizerStore.ts | 125 +++++++------- src/hunt_optimizer/stores/ItemDropStore.ts | 124 ++++++-------- src/old/core/ui/SectionIdIcon.tsx | 6 +- .../ui/HuntOptimizerComponent.css | 27 --- .../ui/HuntOptimizerComponent.tsx | 22 --- .../ui/OptimizationResultComponent.tsx | 6 +- .../hunt_optimizer/ui/OptimizerComponent.css | 11 -- .../hunt_optimizer/ui/OptimizerComponent.tsx | 13 -- src/quest_editor/gui/QuestEditorToolBar.ts | 5 +- src/quest_editor/model/QuestModel.ts | 4 +- webpack.dev.js | 6 +- webpack.prod.js | 6 +- 34 files changed, 618 insertions(+), 552 deletions(-) rename src/core/observable/property/list/{SimpleWritableListProperty.ts => SimpleListProperty.ts} (98%) create mode 100644 src/core/observable/property/loadable/LoadableProperty.ts create mode 100644 src/core/observable/property/loadable/LoadableState.ts rename src/core/observable/property/{LoadableProperty.ts => loadable/SimpleLoadableProperty.ts} (52%) create mode 100644 src/core/observable/property/loadable/Store.ts create mode 100644 src/core/util.ts delete mode 100644 src/old/hunt_optimizer/ui/HuntOptimizerComponent.css delete mode 100644 src/old/hunt_optimizer/ui/HuntOptimizerComponent.tsx delete mode 100644 src/old/hunt_optimizer/ui/OptimizerComponent.css delete mode 100644 src/old/hunt_optimizer/ui/OptimizerComponent.tsx diff --git a/assets_generation/update_drops_ephinea.ts b/assets_generation/update_drops_ephinea.ts index be54036a..e0726079 100644 --- a/assets_generation/update_drops_ephinea.ts +++ b/assets_generation/update_drops_ephinea.ts @@ -3,7 +3,7 @@ import { writeFileSync } from "fs"; import "isomorphic-fetch"; import Logger from "js-logger"; import { ASSETS_DIR } from "."; -import { DifficultyModel, SectionIdModel, SectionIdModels } from "../src/core/model"; +import { Difficulty, SectionId, SectionIds } from "../src/core/model"; import { name_and_episode_to_npc_type, NpcType, @@ -16,10 +16,10 @@ const logger = Logger.get("assets_generation/update_drops_ephinea"); export async function update_drops_from_website(item_types: ItemTypeDto[]): Promise { logger.info("Updating item drops."); - const normal = await download(item_types, DifficultyModel.Normal); - const hard = await download(item_types, DifficultyModel.Hard); - const vhard = await download(item_types, DifficultyModel.VHard, "very-hard"); - const ultimate = await download(item_types, DifficultyModel.Ultimate); + const normal = await download(item_types, Difficulty.Normal); + const hard = await download(item_types, Difficulty.Hard); + const vhard = await download(item_types, Difficulty.VHard, "very-hard"); + const ultimate = await download(item_types, Difficulty.Ultimate); const enemy_json = JSON.stringify( [...normal.enemy_drops, ...hard.enemy_drops, ...vhard.enemy_drops, ...ultimate.enemy_drops], @@ -42,8 +42,8 @@ export async function update_drops_from_website(item_types: ItemTypeDto[]): Prom async function download( item_types: ItemTypeDto[], - difficulty: DifficultyModel, - difficulty_url: string = DifficultyModel[difficulty].toLowerCase(), + difficulty: Difficulty, + difficulty_url: string = Difficulty[difficulty].toLowerCase(), ): Promise<{ enemy_drops: EnemyDropDto[]; box_drops: BoxDropDto[]; items: Set }> { const response = await fetch(`https://ephinea.pioneer2.net/drop-charts/${difficulty_url}/`); const body = await response.text(); @@ -75,7 +75,7 @@ async function download( try { let enemy_or_box = - enemy_or_box_text.split("/")[difficulty === DifficultyModel.Ultimate ? 1 : 0] || + enemy_or_box_text.split("/")[difficulty === Difficulty.Ultimate ? 1 : 0] || enemy_or_box_text; if (enemy_or_box === "Halo Rappy") { @@ -95,7 +95,7 @@ async function download( return; } - const section_id = SectionIdModels[td_i - 1]; + const section_id = SectionIds[td_i - 1]; if (is_box) { // TODO: @@ -151,9 +151,9 @@ async function download( ] = /Rare Rate: (\d+)\/(\d+(\.\d+)?)/g.exec(title)!.map(parseFloat); data.enemy_drops.push({ - difficulty: DifficultyModel[difficulty], + difficulty: Difficulty[difficulty], episode, - sectionId: SectionIdModel[section_id], + sectionId: SectionId[section_id], enemy: NpcType[npc_type], itemTypeId: item_type.id, dropRate: drop_rate_num / drop_rate_denom, @@ -163,7 +163,7 @@ async function download( data.items.add(item); } catch (e) { logger.error( - `Error while processing item ${item} of ${enemy_or_box} in episode ${episode} ${DifficultyModel[difficulty]}.`, + `Error while processing item ${item} of ${enemy_or_box} in episode ${episode} ${Difficulty[difficulty]}.`, e, ); } diff --git a/assets_generation/update_ephinea_data.ts b/assets_generation/update_ephinea_data.ts index 29925f13..8357d0cd 100644 --- a/assets_generation/update_ephinea_data.ts +++ b/assets_generation/update_ephinea_data.ts @@ -5,7 +5,7 @@ import { BufferCursor } from "../src/core/data_formats/cursor/BufferCursor"; import { ItemPmt, parse_item_pmt } from "../src/core/data_formats/parsing/itempmt"; import { parse_quest } from "../src/core/data_formats/parsing/quest"; import { parse_unitxt, Unitxt } from "../src/core/data_formats/parsing/unitxt"; -import { DifficultyModels, DifficultyModel, SectionIdModel, SectionIdModels } from "../src/core/model"; +import { Difficulties, Difficulty, SectionId, SectionIds } from "../src/core/model"; import { update_drops_from_website } from "./update_drops_ephinea"; import { Episode, EPISODES } from "../src/core/data_formats/parsing/quest/Episode"; import { npc_data, NPC_TYPES, NpcType } from "../src/core/data_formats/parsing/quest/npc_types"; @@ -267,9 +267,9 @@ function update_drops(item_pt: ItemPt): void { const enemy_drops = new Array(); - for (const diff of DifficultyModels) { + for (const diff of Difficulties) { for (const ep of EPISODES) { - for (const sid of SectionIdModels) { + for (const sid of SectionIds) { enemy_drops.push(...load_enemy_drops(item_pt, diff, ep, sid)); } } @@ -279,9 +279,9 @@ function update_drops(item_pt: ItemPt): void { const box_drops = new Array(); - for (const diff of DifficultyModels) { + for (const diff of Difficulties) { for (const ep of EPISODES) { - for (const sid of SectionIdModels) { + for (const sid of SectionIds) { box_drops.push(...load_box_drops(diff, ep, sid)); } } @@ -311,10 +311,10 @@ function load_item_pt(): ItemPt { for (const episode of [Episode.I, Episode.II]) { table[episode] = []; - for (const diff of DifficultyModels) { + for (const diff of Difficulties) { table[episode][diff] = []; - for (const sid of SectionIdModels) { + for (const sid of SectionIds) { const dar_table = new Map(); table[episode][diff][sid] = { @@ -358,10 +358,10 @@ function load_item_pt(): ItemPt { table[Episode.IV] = []; - for (const diff of DifficultyModels) { + for (const diff of Difficulties) { table[Episode.IV][diff] = []; - for (const sid of SectionIdModels) { + for (const sid of SectionIds) { const dar_table = new Map(); table[Episode.IV][diff][sid] = { @@ -509,9 +509,9 @@ function load_item_pt(): ItemPt { function load_enemy_drops( item_pt: ItemPt, - difficulty: DifficultyModel, + difficulty: Difficulty, episode: Episode, - section_id: SectionIdModel, + section_id: SectionId, ): EnemyDropDto[] { const drops: EnemyDropDto[] = []; const drops_buf = readFileSync( @@ -537,9 +537,9 @@ function load_enemy_drops( logger.error(`No DAR found for ${NpcType[enemy]}.`); } else if (rare_rate > 0 && item_type_id) { drops.push({ - difficulty: DifficultyModel[difficulty], + difficulty: Difficulty[difficulty], episode, - sectionId: SectionIdModel[section_id], + sectionId: SectionId[section_id], enemy: NpcType[enemy], itemTypeId: item_type_id, dropRate: dar, @@ -557,9 +557,9 @@ function load_enemy_drops( } function load_box_drops( - difficulty: DifficultyModel, + difficulty: Difficulty, episode: Episode, - section_id: SectionIdModel, + section_id: SectionId, ): BoxDropDto[] { const drops: BoxDropDto[] = []; const drops_buf = readFileSync( @@ -581,9 +581,9 @@ function load_box_drops( if (drop_rate > 0 && item_type_id) { drops.push({ - difficulty: DifficultyModel[difficulty], + difficulty: Difficulty[difficulty], episode, - sectionId: SectionIdModel[section_id], + sectionId: SectionId[section_id], areaId: area_id, itemTypeId: item_type_id, dropRate: drop_rate, diff --git a/src/core/model/index.ts b/src/core/model/index.ts index bf11b867..804a058c 100644 --- a/src/core/model/index.ts +++ b/src/core/model/index.ts @@ -3,13 +3,13 @@ import { enum_values } from "../enums"; export const RARE_ENEMY_PROB = 1 / 512; export const KONDRIEU_PROB = 1 / 10; -export enum ServerModel { +export enum Server { Ephinea = "Ephinea", } -export const ServerModels: ServerModel[] = enum_values(ServerModel); +export const Servers: Server[] = enum_values(Server); -export enum SectionIdModel { +export enum SectionId { Viridia, Greenill, Skyly, @@ -22,13 +22,13 @@ export enum SectionIdModel { Whitill, } -export const SectionIdModels: SectionIdModel[] = enum_values(SectionIdModel); +export const SectionIds: SectionId[] = enum_values(SectionId); -export enum DifficultyModel { +export enum Difficulty { Normal, Hard, VHard, Ultimate, } -export const DifficultyModels: DifficultyModel[] = enum_values(DifficultyModel); +export const Difficulties: Difficulty[] = enum_values(Difficulty); diff --git a/src/core/observable/Disposer.ts b/src/core/observable/Disposer.ts index a78c286f..984b8ff3 100644 --- a/src/core/observable/Disposer.ts +++ b/src/core/observable/Disposer.ts @@ -7,9 +7,6 @@ const logger = Logger.get("core/observable/Disposer"); * Container for disposables. */ export class Disposer implements Disposable { - private readonly disposables: Disposable[] = []; - private disposed = false; - /** * The amount of disposables contained in this disposer. */ @@ -17,12 +14,21 @@ export class Disposer implements Disposable { return this.disposables.length; } + get disposed(): boolean { + return this._disposed; + } + + private _disposed = false; + private readonly disposables: Disposable[] = []; + /** * Add a single disposable and return the given disposable. */ add(disposable: T): T { - this.check_not_disposed(); - this.disposables.push(disposable); + if (!this._disposed) { + this.disposables.push(disposable); + } + return disposable; } @@ -30,8 +36,10 @@ export class Disposer implements Disposable { * Add 0 or more disposables. */ add_all(...disposable: Disposable[]): this { - this.check_not_disposed(); - this.disposables.push(...disposable); + if (!this._disposed) { + this.disposables.push(...disposable); + } + return this; } @@ -53,12 +61,6 @@ export class Disposer implements Disposable { */ dispose(): void { this.dispose_all(); - this.disposed = true; - } - - private check_not_disposed(): void { - if (this.disposed) { - throw new Error("This disposer has been disposed."); - } + this._disposed = true; } } diff --git a/src/core/observable/index.ts b/src/core/observable/index.ts index c05dba11..cea01308 100644 --- a/src/core/observable/index.ts +++ b/src/core/observable/index.ts @@ -5,7 +5,7 @@ import { Emitter } from "./Emitter"; import { Property } from "./property/Property"; import { DependentProperty } from "./property/DependentProperty"; import { WritableListProperty } from "./property/list/WritableListProperty"; -import { SimpleWritableListProperty } from "./property/list/SimpleWritableListProperty"; +import { SimpleListProperty } from "./property/list/SimpleListProperty"; import { Observable } from "./Observable"; export function emitter(): Emitter { @@ -20,7 +20,7 @@ export function list_property( extract_observables?: (element: T) => Observable[], ...elements: T[] ): WritableListProperty { - return new SimpleWritableListProperty(extract_observables, ...elements); + return new SimpleListProperty(extract_observables, ...elements); } export function add(left: Property, right: number): Property { @@ -31,10 +31,27 @@ export function sub(left: Property, right: number): Property { return left.map(l => l - right); } -export function map( - f: (prop_1: S, prop_2: T) => R, - prop_1: Property, - prop_2: Property, +export function map( + f: (prop_1: P1, prop_2: P2) => R, + prop_1: Property, + prop_2: Property, +): Property; +export function map( + f: (prop_1: P1, prop_2: P2, prop_3: P3) => R, + prop_1: Property, + prop_2: Property, + prop_3: Property, +): Property; +export function map( + f: (prop_1: P1, prop_2: P2, prop_3: P3, prop_4: P4) => R, + prop_1: Property, + prop_2: Property, + prop_3: Property, + prop_4: Property, +): Property; +export function map( + f: (...props: Property[]) => R, + ...props: Property[] ): Property { - return new DependentProperty([prop_1, prop_2], () => f(prop_1.val, prop_2.val)); + return new DependentProperty(props, () => f(...props.map(p => p.val))); } diff --git a/src/core/observable/property/list/SimpleWritableListProperty.ts b/src/core/observable/property/list/SimpleListProperty.ts similarity index 98% rename from src/core/observable/property/list/SimpleWritableListProperty.ts rename to src/core/observable/property/list/SimpleListProperty.ts index 88d1bb32..5fbccc3e 100644 --- a/src/core/observable/property/list/SimpleWritableListProperty.ts +++ b/src/core/observable/property/list/SimpleListProperty.ts @@ -8,9 +8,9 @@ import { Property } from "../Property"; import { ListChangeType, ListPropertyChangeEvent } from "./ListProperty"; import Logger from "js-logger"; -const logger = Logger.get("core/observable/property/list/SimpleWritableListProperty"); +const logger = Logger.get("core/observable/property/list/SimpleListProperty"); -export class SimpleWritableListProperty extends AbstractProperty +export class SimpleListProperty extends AbstractProperty implements WritableListProperty { readonly length: Property; diff --git a/src/core/observable/property/loadable/LoadableProperty.ts b/src/core/observable/property/loadable/LoadableProperty.ts new file mode 100644 index 00000000..199a7248 --- /dev/null +++ b/src/core/observable/property/loadable/LoadableProperty.ts @@ -0,0 +1,37 @@ +import { Property } from "../Property"; +import { LoadableState } from "./LoadableState"; + +/** + * Represents a value that can be loaded asynchronously. + * [state]{@link LoadableProperty#state} represents the current state of this Loadable's value. + */ +export interface LoadableProperty extends Property { + readonly state: Property; + + /** + * True if the initial data load has happened. It may or may not have succeeded. + * Check [error]{@link LoadableProperty#error} to know whether an error occurred. + */ + readonly is_initialized: Property; + + /** + * True if a data load is underway. This may be the initializing load or a later reload. + */ + readonly is_loading: Property; + + /** + * This property returns valid data as soon as possible. + * If the Loadable is uninitialized a data load will be triggered, otherwise the current value will be returned. + */ + readonly promise: Promise; + + /** + * Contains the {@link Error} object if an error occurred during the most recent data load. + */ + readonly error: Property; + + /** + * Load the data. Initializes the Loadable if it is uninitialized. + */ + load(): Promise; +} diff --git a/src/core/observable/property/loadable/LoadableState.ts b/src/core/observable/property/loadable/LoadableState.ts new file mode 100644 index 00000000..1cd1a5fb --- /dev/null +++ b/src/core/observable/property/loadable/LoadableState.ts @@ -0,0 +1,26 @@ +export enum LoadableState { + /** + * No attempt has been made to load data. + */ + Uninitialized, + + /** + * The first data load is underway. + */ + Initializing, + + /** + * Data was loaded at least once. The most recent load was successful. + */ + Nominal, + + /** + * Data was loaded at least once. The most recent load failed. + */ + Error, + + /** + * Data was loaded at least once. Another data load is underway. + */ + Reloading, +} diff --git a/src/core/observable/property/LoadableProperty.ts b/src/core/observable/property/loadable/SimpleLoadableProperty.ts similarity index 52% rename from src/core/observable/property/LoadableProperty.ts rename to src/core/observable/property/loadable/SimpleLoadableProperty.ts index 68363a47..ef29eee3 100644 --- a/src/core/observable/property/LoadableProperty.ts +++ b/src/core/observable/property/loadable/SimpleLoadableProperty.ts @@ -1,65 +1,19 @@ -import { Property } from "./Property"; -import { WritableProperty } from "./WritableProperty"; -import { property } from "../index"; -import { AbstractProperty } from "./AbstractProperty"; +import { Property } from "../Property"; +import { WritableProperty } from "../WritableProperty"; +import { property } from "../../index"; +import { AbstractProperty } from "../AbstractProperty"; +import { LoadableState } from "./LoadableState"; +import { LoadableProperty } from "./LoadableProperty"; -export enum LoadableState { - /** - * No attempt has been made to load data. - */ - Uninitialized, - - /** - * The first data load is underway. - */ - Initializing, - - /** - * Data was loaded at least once. The most recent load was successful. - */ - Nominal, - - /** - * Data was loaded at least once. The most recent load failed. - */ - Error, - - /** - * Data was loaded at least once. Another data load is underway. - */ - Reloading, -} - -/** - * Represents a value that can be loaded asynchronously. - * [state]{@link Loadable#state} represents the current state of this Loadable's value. - */ -export class LoadableProperty extends AbstractProperty implements Property { - /** - * When value is accessed and this Loadable is uninitialized, a load will be triggered. - * Will return the initial value until a load has succeeded. - */ +export class SimpleLoadableProperty extends AbstractProperty implements LoadableProperty { get val(): T { return this.get_val(); } readonly state: Property; - - /** - * True if the initial data load has happened. It may or may not have succeeded. - * Check [error]{@link Loadable#error} to know whether an error occurred. - */ readonly is_initialized: Property; - - /** - * True if a data load is underway. This may be the initializing load or a later reload. - */ readonly is_loading: Property; - /** - * This property returns valid data as soon as possible. - * If the Loadable is uninitialized a data load will be triggered, otherwise the current value will be returned. - */ get promise(): Promise { // Load value on first use. if (this._state.val === LoadableState.Uninitialized) { @@ -69,9 +23,6 @@ export class LoadableProperty extends AbstractProperty implements Property } } - /** - * Contains the {@link Error} object if an error occurred during the most recent data load. - */ readonly error: Property; private _val: T; @@ -100,17 +51,9 @@ export class LoadableProperty extends AbstractProperty implements Property } get_val(): T { - // Load value on first use. - if (this._state.val === LoadableState.Uninitialized) { - this.load_value(); - } - return this._val; } - /** - * Load the data. Initializes the Loadable if it is uninitialized. - */ load(): Promise { return this.load_value(); } diff --git a/src/core/observable/property/loadable/Store.ts b/src/core/observable/property/loadable/Store.ts new file mode 100644 index 00000000..648b4a27 --- /dev/null +++ b/src/core/observable/property/loadable/Store.ts @@ -0,0 +1,41 @@ +import { LoadableState } from "./LoadableState"; +import { Property } from "../Property"; +import { WritableProperty } from "../WritableProperty"; +import { property } from "../../index"; + +export class Store { + readonly state: Property; + + /** + * True if the initial data load has happened. It may or may not have succeeded. + * Check [error]{@link LoadableProperty#error} to know whether an error occurred. + */ + readonly is_initialized: Property; + + /** + * True if a data load is underway. This may be the initializing load or a later reload. + */ + readonly is_loading: Property; + + /** + * Contains the {@link Error} object if an error occurred during the most recent data load. + */ + readonly error: Property; + + private readonly _state: WritableProperty = property( + LoadableState.Uninitialized, + ); + private readonly _error: WritableProperty = property(undefined); + + constructor() { + this.state = this._state; + + this.is_initialized = this.state.map(state => state !== LoadableState.Uninitialized); + + this.is_loading = this.state.map( + state => state === LoadableState.Initializing || state === LoadableState.Reloading, + ); + + this.error = this._error; + } +} diff --git a/src/core/persistence.ts b/src/core/persistence.ts index e0c54e45..9c4be88e 100644 --- a/src/core/persistence.ts +++ b/src/core/persistence.ts @@ -1,10 +1,10 @@ import Logger from "js-logger"; -import { ServerModel } from "./model"; +import { Server } from "./model"; const logger = Logger.get("core/persistence/Persister"); export abstract class Persister { - protected persist_for_server(server: ServerModel, key: string, data: any): void { + protected persist_for_server(server: Server, key: string, data: any): void { this.persist(this.server_key(server, key), data); } @@ -16,7 +16,7 @@ export abstract class Persister { } } - protected async load_for_server(server: ServerModel, key: string): Promise { + protected async load_for_server(server: Server, key: string): Promise { return this.load(this.server_key(server, key)); } @@ -30,15 +30,15 @@ export abstract class Persister { } } - private server_key(server: ServerModel, key: string): string { + private server_key(server: Server, key: string): string { let k = key + "."; switch (server) { - case ServerModel.Ephinea: + case Server.Ephinea: k += "Ephinea"; break; default: - throw new Error(`Server ${ServerModel[server]} not supported.`); + throw new Error(`Server ${Server[server]} not supported.`); } return k; diff --git a/src/core/stores/GuiStore.ts b/src/core/stores/GuiStore.ts index b30a934b..7ecb245c 100644 --- a/src/core/stores/GuiStore.ts +++ b/src/core/stores/GuiStore.ts @@ -2,7 +2,7 @@ import { WritableProperty } from "../observable/property/WritableProperty"; import { Disposable } from "../observable/Disposable"; import { property } from "../observable"; import { Property } from "../observable/property/Property"; -import { ServerModel } from "../model"; +import { Server } from "../model"; export enum GuiTool { Viewer, @@ -19,9 +19,9 @@ const STRING_TO_GUI_TOOL = new Map([...GUI_TOOL_TO_STRING.entries()].map(([k, v] class GuiStore implements Disposable { readonly tool: WritableProperty = property(GuiTool.Viewer); - readonly server: Property; + readonly server: Property; - private readonly _server: WritableProperty = property(ServerModel.Ephinea); + private readonly _server: WritableProperty = property(Server.Ephinea); private readonly hash_disposer = this.tool.observe(({ value: tool }) => { window.location.hash = `#/${gui_tool_to_string(tool)}`; }); diff --git a/src/core/stores/ItemTypeStore.ts b/src/core/stores/ItemTypeStore.ts index b2789bbb..a3be6c68 100644 --- a/src/core/stores/ItemTypeStore.ts +++ b/src/core/stores/ItemTypeStore.ts @@ -7,99 +7,89 @@ import { WeaponItemType, } from "../model/items"; import { ServerMap } from "./ServerMap"; -import { ServerModel } from "../model"; -import { LoadableProperty } from "../observable/property/LoadableProperty"; +import { Server } from "../model"; import { ItemTypeDto } from "../dto/ItemTypeDto"; export class ItemTypeStore { - readonly item_types: ItemType[] = []; + readonly item_types: ItemType[]; - private readonly server: ServerModel; - private readonly id_to_item_type: ItemType[] = []; - - constructor(server: ServerModel) { - this.server = server; + constructor(item_types: ItemType[], private readonly id_to_item_type: ItemType[]) { + this.item_types = item_types; } - get_by_id(id: number): ItemType | undefined { + get_by_id = (id: number): ItemType | undefined => { return this.id_to_item_type[id]; - } - - load = async (): Promise => { - const response = await fetch( - `${process.env.PUBLIC_URL}/itemTypes.${ServerModel[this.server].toLowerCase()}.json`, - ); - const data: ItemTypeDto[] = await response.json(); - - this.item_types.splice(0, Infinity); - - for (const item_type_dto of data) { - let item_type: ItemType; - - switch (item_type_dto.class) { - case "weapon": - item_type = new WeaponItemType( - item_type_dto.id, - item_type_dto.name, - item_type_dto.minAtp, - item_type_dto.maxAtp, - item_type_dto.ata, - item_type_dto.maxGrind, - item_type_dto.requiredAtp, - ); - break; - case "armor": - item_type = new ArmorItemType( - item_type_dto.id, - item_type_dto.name, - item_type_dto.atp, - item_type_dto.ata, - item_type_dto.minEvp, - item_type_dto.maxEvp, - item_type_dto.minDfp, - item_type_dto.maxDfp, - item_type_dto.mst, - item_type_dto.hp, - item_type_dto.lck, - ); - break; - case "shield": - item_type = new ShieldItemType( - item_type_dto.id, - item_type_dto.name, - item_type_dto.atp, - item_type_dto.ata, - item_type_dto.minEvp, - item_type_dto.maxEvp, - item_type_dto.minDfp, - item_type_dto.maxDfp, - item_type_dto.mst, - item_type_dto.hp, - item_type_dto.lck, - ); - break; - case "unit": - item_type = new UnitItemType(item_type_dto.id, item_type_dto.name); - break; - case "tool": - item_type = new ToolItemType(item_type_dto.id, item_type_dto.name); - break; - default: - continue; - } - - this.id_to_item_type[item_type.id] = item_type; - this.item_types.push(item_type); - } }; } -export const item_type_stores: ServerMap> = new ServerMap( - server => { - const store = new ItemTypeStore(server); - return new LoadableProperty(store, async () => { - await store.load(); - return store; - }); - }, -); +async function load(server: Server): Promise { + const response = await fetch( + `${process.env.PUBLIC_URL}/itemTypes.${Server[server].toLowerCase()}.json`, + ); + const data: ItemTypeDto[] = await response.json(); + const item_types: ItemType[] = []; + const id_to_item_type: ItemType[] = []; + + for (const item_type_dto of data) { + let item_type: ItemType; + + switch (item_type_dto.class) { + case "weapon": + item_type = new WeaponItemType( + item_type_dto.id, + item_type_dto.name, + item_type_dto.minAtp, + item_type_dto.maxAtp, + item_type_dto.ata, + item_type_dto.maxGrind, + item_type_dto.requiredAtp, + ); + break; + case "armor": + item_type = new ArmorItemType( + item_type_dto.id, + item_type_dto.name, + item_type_dto.atp, + item_type_dto.ata, + item_type_dto.minEvp, + item_type_dto.maxEvp, + item_type_dto.minDfp, + item_type_dto.maxDfp, + item_type_dto.mst, + item_type_dto.hp, + item_type_dto.lck, + ); + break; + case "shield": + item_type = new ShieldItemType( + item_type_dto.id, + item_type_dto.name, + item_type_dto.atp, + item_type_dto.ata, + item_type_dto.minEvp, + item_type_dto.maxEvp, + item_type_dto.minDfp, + item_type_dto.maxDfp, + item_type_dto.mst, + item_type_dto.hp, + item_type_dto.lck, + ); + break; + case "unit": + item_type = new UnitItemType(item_type_dto.id, item_type_dto.name); + break; + case "tool": + item_type = new ToolItemType(item_type_dto.id, item_type_dto.name); + break; + default: + continue; + } + + id_to_item_type[item_type.id] = item_type; + item_types.push(item_type); + } + + return new ItemTypeStore(item_types, id_to_item_type); +} + +export const item_type_stores: ServerMap = new ServerMap(load); diff --git a/src/core/stores/ServerMap.ts b/src/core/stores/ServerMap.ts index 069662bf..fcb20a47 100644 --- a/src/core/stores/ServerMap.ts +++ b/src/core/stores/ServerMap.ts @@ -1,20 +1,38 @@ -import { ServerModel } from "../model"; -import { EnumMap } from "../enums"; +import { Server } from "../model"; import { Property } from "../observable/property/Property"; import { gui_store } from "./GuiStore"; +import { memoize } from "lodash"; +import { sequential } from "../util"; +import { Disposable } from "../observable/Disposable"; /** - * Map with a guaranteed value per server. + * Map with a lazily-loaded, guaranteed value per server. */ -export class ServerMap extends EnumMap { +export class ServerMap { /** - * @returns the value for the current server as set in {@link gui_store}. + * The value for the current server as set in {@link gui_store}. */ - readonly current: Property; + get current(): Property> { + if (!this._current) { + this._current = gui_store.server.map(server => this.get(server)); + } - constructor(initial_value: (server: ServerModel) => V) { - super(ServerModel, initial_value); + return this._current; + } - this.current = gui_store.server.map(server => this.get(server)); + private readonly get_value: (server: Server) => Promise; + private _current?: Property>; + + constructor(get_value: (server: Server) => Promise) { + this.get_value = memoize(get_value); + } + + get(server: Server): Promise { + return this.get_value(server); + } + + observe_current(f: (current: T) => void, options?: { call_now?: boolean }): Disposable { + const seq_f = sequential(async ({ value }: { value: Promise }) => f(await value)); + return this.current.observe(seq_f, options); } } diff --git a/src/core/util.ts b/src/core/util.ts new file mode 100644 index 00000000..16bac21d --- /dev/null +++ b/src/core/util.ts @@ -0,0 +1,37 @@ +/** + * Takes a function f that returns a promise and returns a function that forwards calls to f + * sequentially. So f will never be called while a call to f is underway. + */ +export function sequential Promise>(f: F): F { + const queue: { + args: any[]; + resolve: (value: any) => void; + reject: (reason: any) => void; + }[] = []; + + async function process_queue(): Promise { + while (queue.length) { + const { args, resolve, reject } = queue[0]; + + try { + resolve(await f(...args)); + } catch (e) { + reject(e); + } finally { + queue.shift(); + } + } + } + + function g(...args: any[]): Promise { + const promise = new Promise((resolve, reject) => queue.push({ args, resolve, reject })); + + if (queue.length === 1) { + process_queue(); + } + + return promise; + } + + return g as F; +} diff --git a/src/hunt_optimizer/gui/MethodsForEpisodeView.ts b/src/hunt_optimizer/gui/MethodsForEpisodeView.ts index 314f3aba..36a6c0eb 100644 --- a/src/hunt_optimizer/gui/MethodsForEpisodeView.ts +++ b/src/hunt_optimizer/gui/MethodsForEpisodeView.ts @@ -11,12 +11,14 @@ import { import "./MethodsForEpisodeView.css"; import { Disposer } from "../../core/observable/Disposer"; import { DurationInput } from "../../core/gui/DurationInput"; +import { Disposable } from "../../core/observable/Disposable"; export class MethodsForEpisodeView extends ResizableWidget { private readonly episode: Episode; private readonly enemy_types: NpcType[]; private readonly tbody_element: HTMLTableSectionElement; private readonly time_disposer = this.disposable(new Disposer()); + private hunt_methods_observer?: Disposable; constructor(episode: Episode) { super(el.div({ class: "hunt_optimizer_MethodsForEpisodeView" })); @@ -45,16 +47,34 @@ export class MethodsForEpisodeView extends ResizableWidget { table_element.append(thead_element, this.tbody_element); this.element.append(table_element); - hunt_method_stores.current.val.methods.load(); + this.disposable( + hunt_method_stores.observe_current( + hunt_method_store => { + if (this.hunt_methods_observer) { + this.hunt_methods_observer.dispose(); + } - this.disposables( - hunt_method_stores.current - .flat_map(current => current.methods) - .observe(({ value }) => this.update_table(value)), + this.hunt_methods_observer = hunt_method_store.methods.observe( + this.update_table, + { + call_now: true, + }, + ); + }, + { call_now: true }, + ), ); } - private update_table(methods: HuntMethodModel[]): void { + dispose(): void { + super.dispose(); + + if (this.hunt_methods_observer) { + this.hunt_methods_observer.dispose(); + } + } + + private update_table = ({ value: methods }: { value: HuntMethodModel[] }) => { this.time_disposer.dispose_all(); const frag = document.createDocumentFragment(); @@ -83,5 +103,5 @@ export class MethodsForEpisodeView extends ResizableWidget { this.tbody_element.innerHTML = ""; this.tbody_element.append(frag); - } + }; } diff --git a/src/hunt_optimizer/gui/WantedItemsView.ts b/src/hunt_optimizer/gui/WantedItemsView.ts index d2f9e500..83c87117 100644 --- a/src/hunt_optimizer/gui/WantedItemsView.ts +++ b/src/hunt_optimizer/gui/WantedItemsView.ts @@ -1,6 +1,5 @@ import { el, Icon } from "../../core/gui/dom"; import "./WantedItemsView.css"; -import { hunt_optimizer_store } from "../stores/HuntOptimizerStore"; import { Button } from "../../core/gui/Button"; import { Disposer } from "../../core/observable/Disposer"; import { Widget } from "../../core/gui/Widget"; @@ -10,23 +9,52 @@ import { } from "../../core/observable/property/list/ListProperty"; import { WantedItemModel } from "../model"; import { NumberInput } from "../../core/gui/NumberInput"; +import { ToolBar } from "../../core/gui/ToolBar"; +import { hunt_optimizer_stores } from "../stores/HuntOptimizerStore"; +import { Disposable } from "../../core/observable/Disposable"; export class WantedItemsView extends Widget { private readonly tbody_element = el.tbody(); private readonly table_disposer = this.disposable(new Disposer()); + private wanted_items_observer?: Disposable; constructor() { super(el.div({ class: "hunt_optimizer_WantedItemsView" })); this.element.append( el.h2({ text: "Wanted Items" }), + this.disposable(new ToolBar({ children: [new Button("Optimize")] })).element, el.div( { class: "hunt_optimizer_WantedItemsView_table_wrapper" }, el.table({}, this.tbody_element), ), ); - hunt_optimizer_store.wanted_items.observe_list(this.update_table, { call_now: true }); + this.disposable( + hunt_optimizer_stores.observe_current( + hunt_optimizer_store => { + if (this.wanted_items_observer) { + this.wanted_items_observer.dispose(); + } + + this.wanted_items_observer = hunt_optimizer_store.wanted_items.observe_list( + this.update_table, + { + call_now: true, + }, + ); + }, + { call_now: true }, + ), + ); + } + + dispose(): void { + super.dispose(); + + if (this.wanted_items_observer) { + this.wanted_items_observer.dispose(); + } } private update_table = (change: ListPropertyChangeEvent): void => { @@ -79,7 +107,7 @@ export class WantedItemsView extends Widget { private create_row = (wanted_item: WantedItemModel): HTMLTableRowElement => { const amount_input = this.table_disposer.add( - new NumberInput(wanted_item.amount.val, { min: 1, step: 1 }), + new NumberInput(wanted_item.amount.val, { min: 0, step: 1 }), ); this.table_disposer.add_all( diff --git a/src/hunt_optimizer/model/ItemDrop.ts b/src/hunt_optimizer/model/ItemDrop.ts index af51cbab..afd1e363 100644 --- a/src/hunt_optimizer/model/ItemDrop.ts +++ b/src/hunt_optimizer/model/ItemDrop.ts @@ -1,5 +1,5 @@ import { ItemType } from "../../core/model/items"; -import { DifficultyModel, SectionIdModel } from "../../core/model"; +import { Difficulty, SectionId } from "../../core/model"; import { NpcType } from "../../core/data_formats/parsing/quest/npc_types"; interface ItemDrop { @@ -12,8 +12,8 @@ export class EnemyDrop implements ItemDrop { readonly rate: number; constructor( - readonly difficulty: DifficultyModel, - readonly section_id: SectionIdModel, + readonly difficulty: Difficulty, + readonly section_id: SectionId, readonly npc_type: NpcType, readonly item_type: ItemType, readonly anything_rate: number, diff --git a/src/hunt_optimizer/model/index.ts b/src/hunt_optimizer/model/index.ts index 74e80fae..97669827 100644 --- a/src/hunt_optimizer/model/index.ts +++ b/src/hunt_optimizer/model/index.ts @@ -1,5 +1,5 @@ import { ItemType } from "../../core/model/items"; -import { DifficultyModel, SectionIdModel } from "../../core/model"; +import { Difficulty, SectionId } from "../../core/model"; import { Episode } from "../../core/data_formats/parsing/quest/Episode"; import { Duration } from "luxon"; import { Property } from "../../core/observable/property/Property"; @@ -33,8 +33,8 @@ export class OptimalResultModel { export class OptimalMethodModel { constructor( - readonly difficulty: DifficultyModel, - readonly section_ids: SectionIdModel[], + readonly difficulty: Difficulty, + readonly section_ids: SectionId[], readonly method_name: string, readonly method_episode: Episode, readonly method_time: Duration, diff --git a/src/hunt_optimizer/persistence/HuntMethodPersister.ts b/src/hunt_optimizer/persistence/HuntMethodPersister.ts index f218c7ea..8efd3898 100644 --- a/src/hunt_optimizer/persistence/HuntMethodPersister.ts +++ b/src/hunt_optimizer/persistence/HuntMethodPersister.ts @@ -1,12 +1,12 @@ import { Persister } from "../../core/persistence"; -import { ServerModel } from "../../core/model"; +import { Server } from "../../core/model"; import { HuntMethodModel } from "../model/HuntMethodModel"; import { Duration } from "luxon"; const METHOD_USER_TIMES_KEY = "HuntMethodStore.methodUserTimes"; class HuntMethodPersister extends Persister { - persist_method_user_times(hunt_methods: HuntMethodModel[], server: ServerModel): void { + persist_method_user_times(hunt_methods: HuntMethodModel[], server: Server): void { const user_times: PersistedUserTimes = {}; for (const method of hunt_methods) { @@ -20,7 +20,7 @@ class HuntMethodPersister extends Persister { async load_method_user_times( hunt_methods: HuntMethodModel[], - server: ServerModel, + server: Server, ): Promise { const user_times = await this.load_for_server( server, diff --git a/src/hunt_optimizer/persistence/HuntOptimizerPersister.ts b/src/hunt_optimizer/persistence/HuntOptimizerPersister.ts index 7170ea68..3ddb424d 100644 --- a/src/hunt_optimizer/persistence/HuntOptimizerPersister.ts +++ b/src/hunt_optimizer/persistence/HuntOptimizerPersister.ts @@ -1,4 +1,4 @@ -import { ServerModel } from "../../core/model"; +import { Server } from "../../core/model"; import { item_type_stores } from "../../core/stores/ItemTypeStore"; import { Persister } from "../../core/persistence"; import { WantedItemModel } from "../model"; @@ -6,7 +6,7 @@ import { WantedItemModel } from "../model"; const WANTED_ITEMS_KEY = "HuntOptimizerStore.wantedItems"; class HuntOptimizerPersister extends Persister { - persist_wanted_items(server: ServerModel, wanted_items: WantedItemModel[]): void { + persist_wanted_items(server: Server, wanted_items: WantedItemModel[]): void { this.persist_for_server( server, WANTED_ITEMS_KEY, @@ -19,8 +19,8 @@ class HuntOptimizerPersister extends Persister { ); } - async load_wanted_items(server: ServerModel): Promise { - const item_store = await item_type_stores.get(server).promise; + async load_wanted_items(server: Server): Promise { + const item_store = await item_type_stores.get(server); const persisted_wanted_items = await this.load_for_server( server, diff --git a/src/hunt_optimizer/stores/HuntMethodStore.ts b/src/hunt_optimizer/stores/HuntMethodStore.ts index ec59f2c3..7617208f 100644 --- a/src/hunt_optimizer/stores/HuntMethodStore.ts +++ b/src/hunt_optimizer/stores/HuntMethodStore.ts @@ -1,15 +1,16 @@ import Logger from "js-logger"; -import { ServerMap } from "../../core/stores/ServerMap"; -import { LoadableProperty } from "../../core/observable/property/LoadableProperty"; -import { Disposable } from "../../core/observable/Disposable"; -import { Disposer } from "../../core/observable/Disposer"; -import { ServerModel } from "../../core/model"; +import { Server } from "../../core/model"; import { QuestDto } from "../dto/QuestDto"; import { NpcType } from "../../core/data_formats/parsing/quest/npc_types"; import { SimpleQuestModel } from "../model/SimpleQuestModel"; import { HuntMethodModel } from "../model/HuntMethodModel"; import { hunt_method_persister } from "../persistence/HuntMethodPersister"; import { Duration } from "luxon"; +import { ListProperty } from "../../core/observable/property/list/ListProperty"; +import { list_property } from "../../core/observable"; +import { Disposable } from "../../core/observable/Disposable"; +import { Disposer } from "../../core/observable/Disposer"; +import { ServerMap } from "../../core/stores/ServerMap"; const logger = Logger.get("hunt_optimizer/stores/HuntMethodStore"); @@ -17,91 +18,82 @@ const DEFAULT_DURATION = Duration.fromObject({ minutes: 30 }); const DEFAULT_GOVERNMENT_TEST_DURATION = Duration.fromObject({ minutes: 45 }); const DEFAULT_LARGE_ENEMY_COUNT_DURATION = Duration.fromObject({ minutes: 45 }); -class HuntMethodStore implements Disposable { - readonly methods: LoadableProperty; +export class HuntMethodStore implements Disposable { + readonly methods: ListProperty; private readonly disposer = new Disposer(); - private readonly server: ServerModel; - constructor(server: ServerModel) { - this.methods = new LoadableProperty([], () => this.load_hunt_methods(server)); - this.server = server; + constructor(server: Server, methods: HuntMethodModel[]) { + this.methods = list_property(undefined, ...methods); + + this.disposer.add( + this.methods.observe_list(() => + hunt_method_persister.persist_method_user_times(this.methods.val, server), + ), + ); } dispose(): void { this.disposer.dispose(); } - - private async load_hunt_methods(server: ServerModel): Promise { - const response = await fetch( - `${process.env.PUBLIC_URL}/quests.${ServerModel[server].toLowerCase()}.json`, - ); - const quests = (await response.json()) as QuestDto[]; - const methods = new Array(); - - for (const quest of quests) { - let total_enemy_count = 0; - const enemy_counts = new Map(); - - for (const [code, count] of Object.entries(quest.enemyCounts)) { - const npc_type = (NpcType as any)[code]; - - if (!npc_type) { - logger.error(`No NpcType found for code ${code}.`); - } else { - enemy_counts.set(npc_type, count); - total_enemy_count += count; - } - } - - // Filter out some quests. - /* eslint-disable no-fallthrough */ - switch (quest.id) { - // The following quests are left out because their enemies don't drop anything. - case 31: // Black Paper's Dangerous Deal - case 34: // Black Paper's Dangerous Deal 2 - case 1305: // Maximum Attack S (Ep. 1) - case 1306: // Maximum Attack S (Ep. 2) - case 1307: // Maximum Attack S (Ep. 4) - case 313: // Beyond the Horizon - - // MAXIMUM ATTACK 3 Ver2 is filtered out because its actual enemy count depends on the path taken. - // TODO: generate a method per path. - case 314: - continue; - } - - methods.push( - new HuntMethodModel( - `q${quest.id}`, - quest.name, - new SimpleQuestModel(quest.id, quest.name, quest.episode, enemy_counts), - /^\d-\d.*/.test(quest.name) - ? DEFAULT_GOVERNMENT_TEST_DURATION - : total_enemy_count > 400 - ? DEFAULT_LARGE_ENEMY_COUNT_DURATION - : DEFAULT_DURATION, - ), - ); - } - - await this.load_user_times(methods, server); - return methods; - } - - private load_user_times = async (methods: HuntMethodModel[], server: ServerModel) => { - await hunt_method_persister.load_method_user_times(methods, server); - - this.disposer.dispose_all(); - - this.disposer.add_all( - ...methods.map(method => method.user_time.observe(this.persist_user_times)), - ); - }; - - private persist_user_times = () => { - hunt_method_persister.persist_method_user_times(this.methods.val, this.server); - }; } -export const hunt_method_stores = new ServerMap(server => new HuntMethodStore(server)); +async function load(server: Server): Promise { + const response = await fetch( + `${process.env.PUBLIC_URL}/quests.${Server[server].toLowerCase()}.json`, + ); + const quests = (await response.json()) as QuestDto[]; + const methods: HuntMethodModel[] = []; + + for (const quest of quests) { + let total_enemy_count = 0; + const enemy_counts = new Map(); + + for (const [code, count] of Object.entries(quest.enemyCounts)) { + const npc_type = (NpcType as any)[code]; + + if (!npc_type) { + logger.error(`No NpcType found for code ${code}.`); + } else { + enemy_counts.set(npc_type, count); + total_enemy_count += count; + } + } + + // Filter out some quests. + /* eslint-disable no-fallthrough */ + switch (quest.id) { + // The following quests are left out because their enemies don't drop anything. + case 31: // Black Paper's Dangerous Deal + case 34: // Black Paper's Dangerous Deal 2 + case 1305: // Maximum Attack S (Ep. 1) + case 1306: // Maximum Attack S (Ep. 2) + case 1307: // Maximum Attack S (Ep. 4) + case 313: // Beyond the Horizon + + // MAXIMUM ATTACK 3 Ver2 is filtered out because its actual enemy count depends on the path taken. + // TODO: generate a method per path. + case 314: + continue; + } + + methods.push( + new HuntMethodModel( + `q${quest.id}`, + quest.name, + new SimpleQuestModel(quest.id, quest.name, quest.episode, enemy_counts), + /^\d-\d.*/.test(quest.name) + ? DEFAULT_GOVERNMENT_TEST_DURATION + : total_enemy_count > 400 + ? DEFAULT_LARGE_ENEMY_COUNT_DURATION + : DEFAULT_DURATION, + ), + ); + } + + await hunt_method_persister.load_method_user_times(methods, server); + + return new HuntMethodStore(server, methods); +} + +export const hunt_method_stores: ServerMap = new ServerMap(load); diff --git a/src/hunt_optimizer/stores/HuntOptimizerStore.ts b/src/hunt_optimizer/stores/HuntOptimizerStore.ts index c74d330e..a1a3690e 100644 --- a/src/hunt_optimizer/stores/HuntOptimizerStore.ts +++ b/src/hunt_optimizer/stores/HuntOptimizerStore.ts @@ -1,26 +1,28 @@ import solver from "javascript-lp-solver"; import { ItemType } from "../../core/model/items"; import { - DifficultyModel, - DifficultyModels, + Difficulties, + Difficulty, KONDRIEU_PROB, RARE_ENEMY_PROB, - SectionIdModel, - SectionIdModels, - ServerModel, + SectionId, + SectionIds, + Server, } from "../../core/model"; import { npc_data, NpcType } from "../../core/data_formats/parsing/quest/npc_types"; import { HuntMethodModel } from "../model/HuntMethodModel"; import { Property } from "../../core/observable/property/Property"; -import { WritableProperty } from "../../core/observable/property/WritableProperty"; import { OptimalMethodModel, OptimalResultModel, WantedItemModel } from "../model"; import { ListProperty } from "../../core/observable/property/list/ListProperty"; -import { list_property, map, property } from "../../core/observable"; +import { list_property, map } from "../../core/observable"; import { WritableListProperty } from "../../core/observable/property/list/WritableListProperty"; -import { hunt_method_stores } from "./HuntMethodStore"; -import { item_drop_stores } from "./ItemDropStore"; -import { item_type_stores } from "../../core/stores/ItemTypeStore"; +import { hunt_method_stores, HuntMethodStore } from "./HuntMethodStore"; +import { item_drop_stores, ItemDropStore } from "./ItemDropStore"; +import { item_type_stores, ItemTypeStore } from "../../core/stores/ItemTypeStore"; import { hunt_optimizer_persister } from "../persistence/HuntOptimizerPersister"; +import { ServerMap } from "../../core/stores/ServerMap"; +import { Disposable } from "../../core/observable/Disposable"; +import { Disposer } from "../../core/observable/Disposer"; // TODO: take into account mothmants spawned from mothverts. // TODO: take into account split slimes. @@ -29,8 +31,8 @@ import { hunt_optimizer_persister } from "../persistence/HuntOptimizerPersister" // TODO: Show expected value or probability per item per method. // Can be useful when deciding which item to hunt first. // TODO: boxes. -class HuntOptimizerStore { - readonly huntable_item_types: Property; +class HuntOptimizerStore implements Disposable { + readonly huntable_item_types: ItemType[]; // TODO: wanted items per server. readonly wanted_items: ListProperty; readonly result: Property; @@ -38,42 +40,42 @@ class HuntOptimizerStore { private readonly _wanted_items: WritableListProperty = list_property( wanted_item => [wanted_item.amount], ); - private readonly _result: WritableProperty = property( - undefined, - ); + private readonly disposer = new Disposer(); - constructor() { - this.huntable_item_types = map( - (item_type_store, item_drop_store) => { - return item_type_store.item_types.filter( - item_type => - item_drop_store.enemy_drops.get_drops_for_item_type(item_type.id).length, - ); - }, - item_type_stores.current.flat_map(current => current), - item_drop_stores.current.flat_map(current => current), + constructor( + private readonly server: Server, + item_type_store: ItemTypeStore, + private readonly item_drop_store: ItemDropStore, + hunt_method_store: HuntMethodStore, + ) { + this.huntable_item_types = item_type_store.item_types.filter( + item_type => item_drop_store.enemy_drops.get_drops_for_item_type(item_type.id).length, ); this.wanted_items = this._wanted_items; - this.result = this._result; + + this.result = map(this.optimize, this.wanted_items, hunt_method_store.methods); this.initialize_persistence(); } - optimize = async () => { - if (!this.wanted_items.length) { - this._result.val = undefined; - return; + dispose(): void { + this.disposer.dispose(); + } + + private optimize = ( + wanted_items: WantedItemModel[], + methods: HuntMethodModel[], + ): OptimalResultModel | undefined => { + if (!wanted_items.length) { + return undefined; } - // Initialize this set before awaiting data, so user changes don't affect this optimization - // run from this point on. - const wanted_items = new Set( - this.wanted_items.val.filter(w => w.amount.val > 0).map(w => w.item_type), + const filtered_wanted_items = new Set( + wanted_items.filter(w => w.amount.val > 0).map(w => w.item_type), ); - const methods = await hunt_method_stores.current.val.methods.promise; - const drop_table = (await item_drop_stores.current.val.promise).enemy_drops; + const drop_table = this.item_drop_store.enemy_drops; // Add a constraint per wanted item. const constraints: { [item_name: string]: { min: number } } = {}; @@ -95,8 +97,8 @@ class HuntOptimizerStore { type VariableDetails = { method: HuntMethodModel; - difficulty: DifficultyModel; - section_id: SectionIdModel; + difficulty: Difficulty; + section_id: SectionId; split_pan_arms: boolean; }; const variable_details: Map = new Map(); @@ -161,8 +163,8 @@ class HuntOptimizerStore { const counts = counts_list[i]; const split_pan_arms = i === 1; - for (const difficulty of DifficultyModels) { - for (const section_id of SectionIdModels) { + for (const difficulty of Difficulties) { + for (const section_id of SectionIds) { // Will contain an entry per wanted item dropped by enemies in this method/ // difficulty/section ID combo. const variable: Variable = { @@ -174,7 +176,7 @@ class HuntOptimizerStore { for (const [npc_type, count] of counts.entries()) { const drop = drop_table.get_drop(difficulty, section_id, npc_type); - if (drop && wanted_items.has(drop.item_type)) { + if (drop && filtered_wanted_items.has(drop.item_type)) { const value = variable[drop.item_type.name] || 0; variable[drop.item_type.name] = value + count * drop.rate; add_variable = true; @@ -217,8 +219,7 @@ class HuntOptimizerStore { }); if (!result.feasible) { - this._result.val = undefined; - return; + return undefined; } const optimal_methods: OptimalMethodModel[] = []; @@ -235,7 +236,7 @@ class HuntOptimizerStore { const items = new Map(); for (const [item_name, expected_amount] of Object.entries(variable)) { - for (const item of wanted_items) { + for (const item of filtered_wanted_items) { if (item_name === item.name) { items.set(item, runs * expected_amount); break; @@ -246,9 +247,9 @@ class HuntOptimizerStore { // Find all section IDs that provide the same items with the same expected amount. // E.g. if you need a spread needle and a bringer's right arm, using either // purplenum or yellowboze will give you the exact same probabilities. - const section_ids: SectionIdModel[] = []; + const section_ids: SectionId[] = []; - for (const sid of SectionIdModels) { + for (const sid of SectionIds) { let match_found = true; if (sid !== section_id) { @@ -288,12 +289,12 @@ class HuntOptimizerStore { } } - this._result.val = new OptimalResultModel([...wanted_items], optimal_methods); + return new OptimalResultModel([...filtered_wanted_items], optimal_methods); }; private full_method_name( - difficulty: DifficultyModel, - section_id: SectionIdModel, + difficulty: Difficulty, + section_id: SectionId, method: HuntMethodModel, split_pan_arms: boolean, ): string { @@ -303,17 +304,23 @@ class HuntOptimizerStore { } private initialize_persistence = async () => { - this._wanted_items.val = await hunt_optimizer_persister.load_wanted_items( - ServerModel.Ephinea, - ); + this._wanted_items.val = await hunt_optimizer_persister.load_wanted_items(this.server); - this._wanted_items.observe_list(() => { - hunt_optimizer_persister.persist_wanted_items( - ServerModel.Ephinea, - this.wanted_items.val, - ); - }); + this.disposer.add( + this._wanted_items.observe(({ value }) => { + hunt_optimizer_persister.persist_wanted_items(this.server, value); + }), + ); }; } -export const hunt_optimizer_store = new HuntOptimizerStore(); +async function load(server: Server): Promise { + return new HuntOptimizerStore( + server, + await item_type_stores.get(server), + await item_drop_stores.get(server), + await hunt_method_stores.get(server), + ); +} + +export const hunt_optimizer_stores: ServerMap = new ServerMap(load); diff --git a/src/hunt_optimizer/stores/ItemDropStore.ts b/src/hunt_optimizer/stores/ItemDropStore.ts index b75ad713..7116c893 100644 --- a/src/hunt_optimizer/stores/ItemDropStore.ts +++ b/src/hunt_optimizer/stores/ItemDropStore.ts @@ -1,20 +1,21 @@ -import { - DifficultyModel, - DifficultyModels, - SectionIdModel, - SectionIdModels, - ServerModel, -} from "../../core/model"; +import { Difficulties, Difficulty, SectionId, SectionIds, Server } from "../../core/model"; import { item_type_stores } from "../../core/stores/ItemTypeStore"; import { ServerMap } from "../../core/stores/ServerMap"; import Logger from "js-logger"; import { NpcType } from "../../core/data_formats/parsing/quest/npc_types"; import { EnemyDrop } from "../model/ItemDrop"; -import { LoadableProperty } from "../../core/observable/property/LoadableProperty"; import { EnemyDropDto } from "../dto/drops"; const logger = Logger.get("stores/ItemDropStore"); +export class ItemDropStore { + readonly enemy_drops: EnemyDropTable; + + constructor(enemy_drops: EnemyDropTable) { + this.enemy_drops = enemy_drops; + } +} + export class EnemyDropTable { // Mapping of difficulties to section IDs to NpcTypes to EnemyDrops. private table: EnemyDrop[][][] = []; @@ -23,27 +24,27 @@ export class EnemyDropTable { private item_type_to_drops: EnemyDrop[][] = []; constructor() { - for (let i = 0; i < DifficultyModels.length; i++) { + for (let i = 0; i < Difficulties.length; i++) { const diff_array: EnemyDrop[][] = []; this.table.push(diff_array); - for (let j = 0; j < SectionIdModels.length; j++) { + for (let j = 0; j < SectionIds.length; j++) { diff_array.push([]); } } } get_drop( - difficulty: DifficultyModel, - section_id: SectionIdModel, + difficulty: Difficulty, + section_id: SectionId, npc_type: NpcType, ): EnemyDrop | undefined { return this.table[difficulty][section_id][npc_type]; } set_drop( - difficulty: DifficultyModel, - section_id: SectionIdModel, + difficulty: Difficulty, + section_id: SectionId, npc_type: NpcType, drop: EnemyDrop, ): void { @@ -64,70 +65,55 @@ export class EnemyDropTable { } } -export class ItemDropStore { - readonly enemy_drops: EnemyDropTable = new EnemyDropTable(); +async function load(server: Server): Promise { + const item_type_store = await item_type_stores.get(server); + const response = await fetch( + `${process.env.PUBLIC_URL}/enemyDrops.${Server[server].toLowerCase()}.json`, + ); + const data: EnemyDropDto[] = await response.json(); + const enemy_drops = new EnemyDropTable(); - private readonly server: ServerModel; + for (const drop_dto of data) { + const npc_type = (NpcType as any)[drop_dto.enemy]; - constructor(server: ServerModel) { - this.server = server; - } + if (!npc_type) { + logger.warn( + `Couldn't determine NpcType of episode ${drop_dto.episode} ${drop_dto.enemy}.`, + ); + continue; + } - load = async (): Promise => { - const item_type_store = await item_type_stores.get(this.server).promise; - const response = await fetch( - `${process.env.PUBLIC_URL}/enemyDrops.${ServerModel[this.server].toLowerCase()}.json`, - ); - const data: EnemyDropDto[] = await response.json(); + const difficulty = (Difficulty as any)[drop_dto.difficulty]; + const item_type = item_type_store.get_by_id(drop_dto.itemTypeId); - for (const drop_dto of data) { - const npc_type = (NpcType as any)[drop_dto.enemy]; + if (!item_type) { + logger.warn(`Couldn't find item kind ${drop_dto.itemTypeId}.`); + continue; + } - if (!npc_type) { - logger.warn( - `Couldn't determine NpcType of episode ${drop_dto.episode} ${drop_dto.enemy}.`, - ); - continue; - } + const section_id = (SectionId as any)[drop_dto.sectionId]; - const difficulty = (DifficultyModel as any)[drop_dto.difficulty]; - const item_type = item_type_store.get_by_id(drop_dto.itemTypeId); + if (section_id == null) { + logger.warn(`Couldn't find section ID ${drop_dto.sectionId}.`); + continue; + } - if (!item_type) { - logger.warn(`Couldn't find item kind ${drop_dto.itemTypeId}.`); - continue; - } - - const section_id = (SectionIdModel as any)[drop_dto.sectionId]; - - if (section_id == null) { - logger.warn(`Couldn't find section ID ${drop_dto.sectionId}.`); - continue; - } - - this.enemy_drops.set_drop( + enemy_drops.set_drop( + difficulty, + section_id, + npc_type, + new EnemyDrop( difficulty, section_id, npc_type, - new EnemyDrop( - difficulty, - section_id, - npc_type, - item_type, - drop_dto.dropRate, - drop_dto.rareRate, - ), - ); - } - }; + item_type, + drop_dto.dropRate, + drop_dto.rareRate, + ), + ); + } + + return new ItemDropStore(enemy_drops); } -export const item_drop_stores: ServerMap> = new ServerMap( - server => { - const store = new ItemDropStore(server); - return new LoadableProperty(store, async () => { - await store.load(); - return store; - }); - }, -); +export const item_drop_stores: ServerMap = new ServerMap(load); diff --git a/src/old/core/ui/SectionIdIcon.tsx b/src/old/core/ui/SectionIdIcon.tsx index 74d5a882..d1941724 100644 --- a/src/old/core/ui/SectionIdIcon.tsx +++ b/src/old/core/ui/SectionIdIcon.tsx @@ -1,12 +1,12 @@ import React from "react"; -import { SectionIdModel } from "../../../core/model"; +import { SectionId } from "../../../core/model"; export function SectionIdIcon({ section_id, size = 28, title, }: { - section_id: SectionIdModel; + section_id: SectionId; size?: number; title?: string; }): JSX.Element { @@ -17,7 +17,7 @@ export function SectionIdIcon({ display: "inline-block", width: size, height: size, - backgroundImage: `url(${process.env.PUBLIC_URL}/images/sectionids/${SectionIdModel[section_id]}.png)`, + backgroundImage: `url(${process.env.PUBLIC_URL}/images/sectionids/${SectionId[section_id]}.png)`, backgroundSize: size, }} /> diff --git a/src/old/hunt_optimizer/ui/HuntOptimizerComponent.css b/src/old/hunt_optimizer/ui/HuntOptimizerComponent.css deleted file mode 100644 index d8bb7928..00000000 --- a/src/old/hunt_optimizer/ui/HuntOptimizerComponent.css +++ /dev/null @@ -1,27 +0,0 @@ -.main { - display: flex; - align-items: stretch; - overflow: hidden; - margin-top: 10px; -} - -.main > :global(.ant-tabs) { - flex: 1; - display: flex; - flex-direction: column; - align-items: stretch; -} - -.main > :global(.ant-tabs > .ant-tabs-content) { - flex: 1; - display: flex; - flex-direction: column; - align-items: stretch; -} - -.main > :global(.ant-tabs > .ant-tabs-content > .ant-tabs-tabpane-active) { - flex: 1; - display: flex; - flex-direction: column; - align-items: stretch; -} diff --git a/src/old/hunt_optimizer/ui/HuntOptimizerComponent.tsx b/src/old/hunt_optimizer/ui/HuntOptimizerComponent.tsx deleted file mode 100644 index 9d4a6db0..00000000 --- a/src/old/hunt_optimizer/ui/HuntOptimizerComponent.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Tabs } from "antd"; -import React from "react"; -import styles from "./HuntOptimizerComponent.css"; -import { MethodsComponent } from "./MethodsComponent"; -import { OptimizerComponent } from "./OptimizerComponent"; - -const TabPane = Tabs.TabPane; - -export function HuntOptimizerComponent(): JSX.Element { - return ( -
- - - - - - - - -
- ); -} diff --git a/src/old/hunt_optimizer/ui/OptimizationResultComponent.tsx b/src/old/hunt_optimizer/ui/OptimizationResultComponent.tsx index 7d83ab08..31a369e0 100644 --- a/src/old/hunt_optimizer/ui/OptimizationResultComponent.tsx +++ b/src/old/hunt_optimizer/ui/OptimizationResultComponent.tsx @@ -2,7 +2,7 @@ import { computed } from "mobx"; import { observer } from "mobx-react"; import React, { Component, ReactNode } from "react"; import { AutoSizer, Index } from "react-virtualized"; -import { DifficultyModel, SectionIdModel } from "../../../core/model"; +import { Difficulty, SectionId } from "../../../core/model"; import { hunt_optimizer_store, OptimalMethod } from "../stores/HuntOptimizerStore"; import { BigTable, Column } from "../../core/ui/BigTable"; import { SectionIdIcon } from "../../core/ui/SectionIdIcon"; @@ -28,7 +28,7 @@ export class OptimizationResultComponent extends Component { { name: "Difficulty", width: 75, - cell_renderer: result => DifficultyModel[result.difficulty], + cell_renderer: result => Difficulty[result.difficulty], footer_value: "Totals:", }, { @@ -52,7 +52,7 @@ export class OptimizationResultComponent extends Component { ))} ), - tooltip: result => result.section_ids.map(sid => SectionIdModel[sid]).join(", "), + tooltip: result => result.section_ids.map(sid => SectionId[sid]).join(", "), }, { name: "Time/Run", diff --git a/src/old/hunt_optimizer/ui/OptimizerComponent.css b/src/old/hunt_optimizer/ui/OptimizerComponent.css deleted file mode 100644 index cab2988f..00000000 --- a/src/old/hunt_optimizer/ui/OptimizerComponent.css +++ /dev/null @@ -1,11 +0,0 @@ -.main { - flex: 1; - display: flex; - align-items: stretch; - padding-top: 5px; -} - -.main > *:nth-child(2) { - flex: 1; - overflow: hidden; -} diff --git a/src/old/hunt_optimizer/ui/OptimizerComponent.tsx b/src/old/hunt_optimizer/ui/OptimizerComponent.tsx deleted file mode 100644 index 0d5e61aa..00000000 --- a/src/old/hunt_optimizer/ui/OptimizerComponent.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from "react"; -import { OptimizationResultComponent } from "./OptimizationResultComponent"; -import styles from "./OptimizerComponent.css"; -import { WantedItemsComponent } from "./WantedItemsComponent"; - -export function OptimizerComponent(): JSX.Element { - return ( -
- - -
- ); -} diff --git a/src/quest_editor/gui/QuestEditorToolBar.ts b/src/quest_editor/gui/QuestEditorToolBar.ts index a8610c5c..66724746 100644 --- a/src/quest_editor/gui/QuestEditorToolBar.ts +++ b/src/quest_editor/gui/QuestEditorToolBar.ts @@ -50,7 +50,10 @@ export class QuestEditorToolBar extends ToolBar { const area_select = new Select( quest_editor_store.current_quest.flat_map(quest => { if (quest) { - return list_property(...area_store.get_areas_for_episode(quest.episode)); + return list_property( + undefined, + ...area_store.get_areas_for_episode(quest.episode), + ); } else { return list_property(); } diff --git a/src/quest_editor/model/QuestModel.ts b/src/quest_editor/model/QuestModel.ts index 3194226c..d956dec9 100644 --- a/src/quest_editor/model/QuestModel.ts +++ b/src/quest_editor/model/QuestModel.ts @@ -148,8 +148,8 @@ export class QuestModel { this.episode = episode; this._map_designations = property(map_designations); this.map_designations = this._map_designations; - this.objects = list_property(...objects); - this.npcs = list_property(...npcs); + this.objects = list_property(undefined, ...objects); + this.npcs = list_property(undefined, ...npcs); this.dat_unknowns = dat_unknowns; this.object_code = object_code; this.shop_items = shop_items; diff --git a/webpack.dev.js b/webpack.dev.js index 39db93d0..f871ca20 100644 --- a/webpack.dev.js +++ b/webpack.dev.js @@ -11,7 +11,7 @@ module.exports = merge(common, { module: { rules: [ { - test: /\.tsx?$/, + test: /\.ts$/, use: [ { loader: "ts-loader", @@ -58,10 +58,6 @@ module.exports = merge(common, { test: /\.(png|svg|jpg|gif)$/, use: ["file-loader"], }, - { - test: /\.worker\.js$/, - use: { loader: "worker-loader" }, - }, ], }, plugins: [ diff --git a/webpack.prod.js b/webpack.prod.js index b68f9de6..4d13b672 100644 --- a/webpack.prod.js +++ b/webpack.prod.js @@ -33,7 +33,7 @@ module.exports = merge(common, { module: { rules: [ { - test: /\.tsx?$/, + test: /\.ts$/, use: "ts-loader", include: path.resolve(__dirname, "src"), }, @@ -45,10 +45,6 @@ module.exports = merge(common, { test: /\.(png|svg|jpg|gif)$/, use: ["file-loader"], }, - { - test: /\.worker\.js$/, - use: { loader: "worker-loader" }, - }, ], }, plugins: [