Improved loading of store data.

This commit is contained in:
Daan Vanden Bosch 2019-09-05 20:30:11 +02:00
parent 46ba5bb018
commit 1c2473c24f
34 changed files with 618 additions and 552 deletions

View File

@ -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<void> {
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<string> }> {
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,
);
}

View File

@ -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<EnemyDropDto>();
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<BoxDropDto>();
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<NpcType, number>();
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<NpcType, number>();
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,

View File

@ -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);

View File

@ -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<T extends Disposable>(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;
}
}

View File

@ -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<E>(): Emitter<E> {
@ -20,7 +20,7 @@ export function list_property<T>(
extract_observables?: (element: T) => Observable<any>[],
...elements: T[]
): WritableListProperty<T> {
return new SimpleWritableListProperty(extract_observables, ...elements);
return new SimpleListProperty(extract_observables, ...elements);
}
export function add(left: Property<number>, right: number): Property<number> {
@ -31,10 +31,27 @@ export function sub(left: Property<number>, right: number): Property<number> {
return left.map(l => l - right);
}
export function map<R, S, T>(
f: (prop_1: S, prop_2: T) => R,
prop_1: Property<S>,
prop_2: Property<T>,
export function map<R, P1, P2>(
f: (prop_1: P1, prop_2: P2) => R,
prop_1: Property<P1>,
prop_2: Property<P2>,
): Property<R>;
export function map<R, P1, P2, P3>(
f: (prop_1: P1, prop_2: P2, prop_3: P3) => R,
prop_1: Property<P1>,
prop_2: Property<P2>,
prop_3: Property<P3>,
): Property<R>;
export function map<R, P1, P2, P3, P4>(
f: (prop_1: P1, prop_2: P2, prop_3: P3, prop_4: P4) => R,
prop_1: Property<P1>,
prop_2: Property<P2>,
prop_3: Property<P3>,
prop_4: Property<P4>,
): Property<R>;
export function map<R>(
f: (...props: Property<any>[]) => R,
...props: Property<any>[]
): Property<R> {
return new DependentProperty([prop_1, prop_2], () => f(prop_1.val, prop_2.val));
return new DependentProperty(props, () => f(...props.map(p => p.val)));
}

View File

@ -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<T> extends AbstractProperty<T[]>
export class SimpleListProperty<T> extends AbstractProperty<T[]>
implements WritableListProperty<T> {
readonly length: Property<number>;

View File

@ -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<T> extends Property<T> {
readonly state: Property<LoadableState>;
/**
* 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<boolean>;
/**
* True if a data load is underway. This may be the initializing load or a later reload.
*/
readonly is_loading: Property<boolean>;
/**
* 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<T>;
/**
* Contains the {@link Error} object if an error occurred during the most recent data load.
*/
readonly error: Property<Error | undefined>;
/**
* Load the data. Initializes the Loadable if it is uninitialized.
*/
load(): Promise<T>;
}

View File

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

View File

@ -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<T> extends AbstractProperty<T> implements Property<T> {
/**
* 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<T> extends AbstractProperty<T> implements LoadableProperty<T> {
get val(): T {
return this.get_val();
}
readonly state: Property<LoadableState>;
/**
* 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<boolean>;
/**
* True if a data load is underway. This may be the initializing load or a later reload.
*/
readonly is_loading: Property<boolean>;
/**
* 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<T> {
// Load value on first use.
if (this._state.val === LoadableState.Uninitialized) {
@ -69,9 +23,6 @@ export class LoadableProperty<T> extends AbstractProperty<T> implements Property
}
}
/**
* Contains the {@link Error} object if an error occurred during the most recent data load.
*/
readonly error: Property<Error | undefined>;
private _val: T;
@ -100,17 +51,9 @@ export class LoadableProperty<T> extends AbstractProperty<T> 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<T> {
return this.load_value();
}

View File

@ -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<LoadableState>;
/**
* 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<boolean>;
/**
* True if a data load is underway. This may be the initializing load or a later reload.
*/
readonly is_loading: Property<boolean>;
/**
* Contains the {@link Error} object if an error occurred during the most recent data load.
*/
readonly error: Property<Error | undefined>;
private readonly _state: WritableProperty<LoadableState> = property(
LoadableState.Uninitialized,
);
private readonly _error: WritableProperty<Error | undefined> = 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;
}
}

View File

@ -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<T>(server: ServerModel, key: string): Promise<T | undefined> {
protected async load_for_server<T>(server: Server, key: string): Promise<T | undefined> {
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;

View File

@ -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<GuiTool> = property(GuiTool.Viewer);
readonly server: Property<ServerModel>;
readonly server: Property<Server>;
private readonly _server: WritableProperty<ServerModel> = property(ServerModel.Ephinea);
private readonly _server: WritableProperty<Server> = property(Server.Ephinea);
private readonly hash_disposer = this.tool.observe(({ value: tool }) => {
window.location.hash = `#/${gui_tool_to_string(tool)}`;
});

View File

@ -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<void> => {
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<LoadableProperty<ItemTypeStore>> = new ServerMap(
server => {
const store = new ItemTypeStore(server);
return new LoadableProperty(store, async () => {
await store.load();
return store;
});
},
);
async function load(server: Server): Promise<ItemTypeStore> {
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<ItemTypeStore> = new ServerMap(load);

View File

@ -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<V> extends EnumMap<ServerModel, V> {
export class ServerMap<T> {
/**
* @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<V>;
get current(): Property<Promise<T>> {
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<T>;
private _current?: Property<Promise<T>>;
constructor(get_value: (server: Server) => Promise<T>) {
this.get_value = memoize(get_value);
}
get(server: Server): Promise<T> {
return this.get_value(server);
}
observe_current(f: (current: T) => void, options?: { call_now?: boolean }): Disposable {
const seq_f = sequential(async ({ value }: { value: Promise<T> }) => f(await value));
return this.current.observe(seq_f, options);
}
}

37
src/core/util.ts Normal file
View File

@ -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<F extends (...args: any[]) => Promise<any>>(f: F): F {
const queue: {
args: any[];
resolve: (value: any) => void;
reject: (reason: any) => void;
}[] = [];
async function process_queue(): Promise<void> {
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<any> {
const promise = new Promise((resolve, reject) => queue.push({ args, resolve, reject }));
if (queue.length === 1) {
process_queue();
}
return promise;
}
return g as F;
}

View File

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

View File

@ -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<WantedItemModel>): 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(

View File

@ -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,

View File

@ -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,

View File

@ -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<void> {
const user_times = await this.load_for_server<PersistedUserTimes>(
server,

View File

@ -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<WantedItemModel[]> {
const item_store = await item_type_stores.get(server).promise;
async load_wanted_items(server: Server): Promise<WantedItemModel[]> {
const item_store = await item_type_stores.get(server);
const persisted_wanted_items = await this.load_for_server<PersistedWantedItem[]>(
server,

View File

@ -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<HuntMethodModel[]>;
export class HuntMethodStore implements Disposable {
readonly methods: ListProperty<HuntMethodModel>;
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<HuntMethodModel[]> {
const response = await fetch(
`${process.env.PUBLIC_URL}/quests.${ServerModel[server].toLowerCase()}.json`,
);
const quests = (await response.json()) as QuestDto[];
const methods = new Array<HuntMethodModel>();
for (const quest of quests) {
let total_enemy_count = 0;
const enemy_counts = new Map<NpcType, number>();
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<HuntMethodStore> {
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<NpcType, number>();
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<HuntMethodStore> = new ServerMap(load);

View File

@ -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<ItemType[]>;
class HuntOptimizerStore implements Disposable {
readonly huntable_item_types: ItemType[];
// TODO: wanted items per server.
readonly wanted_items: ListProperty<WantedItemModel>;
readonly result: Property<OptimalResultModel | undefined>;
@ -38,42 +40,42 @@ class HuntOptimizerStore {
private readonly _wanted_items: WritableListProperty<WantedItemModel> = list_property(
wanted_item => [wanted_item.amount],
);
private readonly _result: WritableProperty<OptimalResultModel | undefined> = 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<string, VariableDetails> = 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<ItemType, number>();
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<HuntOptimizerStore> {
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<HuntOptimizerStore> = new ServerMap(load);

View File

@ -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<ItemDropStore> {
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<void> => {
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<LoadableProperty<ItemDropStore>> = new ServerMap(
server => {
const store = new ItemDropStore(server);
return new LoadableProperty(store, async () => {
await store.load();
return store;
});
},
);
export const item_drop_stores: ServerMap<ItemDropStore> = new ServerMap(load);

View File

@ -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,
}}
/>

View File

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

View File

@ -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 (
<section className={styles.main}>
<Tabs type="card">
<TabPane tab="Optimize" key="optimize">
<OptimizerComponent />
</TabPane>
<TabPane tab="Methods" key="methods">
<MethodsComponent />
</TabPane>
</Tabs>
</section>
);
}

View File

@ -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 {
))}
</div>
),
tooltip: result => result.section_ids.map(sid => SectionIdModel[sid]).join(", "),
tooltip: result => result.section_ids.map(sid => SectionId[sid]).join(", "),
},
{
name: "Time/Run",

View File

@ -1,11 +0,0 @@
.main {
flex: 1;
display: flex;
align-items: stretch;
padding-top: 5px;
}
.main > *:nth-child(2) {
flex: 1;
overflow: hidden;
}

View File

@ -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 (
<section className={styles.main}>
<WantedItemsComponent />
<OptimizationResultComponent />
</section>
);
}

View File

@ -50,7 +50,10 @@ export class QuestEditorToolBar extends ToolBar {
const area_select = new Select<AreaModel>(
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<AreaModel>();
}

View File

@ -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;

View File

@ -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: [

View File

@ -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: [