diff --git a/src/application/gui/ApplicationView.ts b/src/application/gui/ApplicationView.ts index a8e270f2..6b10d180 100644 --- a/src/application/gui/ApplicationView.ts +++ b/src/application/gui/ApplicationView.ts @@ -2,20 +2,28 @@ import { NavigationView } from "./NavigationView"; import { MainContentView } from "./MainContentView"; import { el } from "../../core/gui/dom"; import { ResizableWidget } from "../../core/gui/ResizableWidget"; +import { GuiStore, GuiTool } from "../../core/stores/GuiStore"; +/** + * The top-level view which contains all other views. + */ export class ApplicationView extends ResizableWidget { - private menu_view = this.disposable(new NavigationView()); - private main_content_view = this.disposable(new MainContentView()); + private menu_view: NavigationView; + private main_content_view: MainContentView; - readonly element = el.div( - { class: "application_ApplicationView" }, - this.menu_view.element, - this.main_content_view.element, - ); + readonly element: HTMLElement; - constructor() { + constructor(gui_store: GuiStore, tool_views: [GuiTool, () => Promise][]) { super(); + this.menu_view = this.disposable(new NavigationView(gui_store)); + this.main_content_view = this.disposable(new MainContentView(gui_store, tool_views)); + + this.element = el.div( + { class: "application_ApplicationView" }, + this.menu_view.element, + this.main_content_view.element, + ); this.element.id = "root"; this.finalize_construction(); diff --git a/src/application/gui/MainContentView.ts b/src/application/gui/MainContentView.ts index 56a7f6cd..0f74a602 100644 --- a/src/application/gui/MainContentView.ts +++ b/src/application/gui/MainContentView.ts @@ -1,32 +1,24 @@ import { el } from "../../core/gui/dom"; -import { gui_store, GuiTool } from "../../core/stores/GuiStore"; +import { GuiStore, GuiTool } from "../../core/stores/GuiStore"; import { LazyWidget } from "../../core/gui/LazyWidget"; import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { ChangeEvent } from "../../core/observable/Observable"; -const TOOLS: [GuiTool, () => Promise][] = [ - [GuiTool.Viewer, async () => new (await import("../../viewer/gui/ViewerView")).ViewerView()], - [ - GuiTool.QuestEditor, - async () => new (await import("../../quest_editor/gui/QuestEditorView")).QuestEditorView(), - ], - [ - GuiTool.HuntOptimizer, - async () => - new (await import("../../hunt_optimizer/gui/HuntOptimizerView")).HuntOptimizerView(), - ], -]; - export class MainContentView extends ResizableWidget { readonly element = el.div({ class: "application_MainContentView" }); - private tool_views = new Map( - TOOLS.map(([tool, create_view]) => [tool, this.disposable(new LazyWidget(create_view))]), - ); + private tool_views: Map; - constructor() { + constructor(gui_store: GuiStore, tool_views: [GuiTool, () => Promise][]) { super(); + this.tool_views = new Map( + tool_views.map(([tool, create_view]) => [ + tool, + this.disposable(new LazyWidget(create_view)), + ]), + ); + for (const tool_view of this.tool_views.values()) { this.element.append(tool_view.element); } diff --git a/src/application/gui/NavigationView.ts b/src/application/gui/NavigationView.ts index 90b2b2aa..d5f9a99d 100644 --- a/src/application/gui/NavigationView.ts +++ b/src/application/gui/NavigationView.ts @@ -1,6 +1,6 @@ import { el, icon, Icon } from "../../core/gui/dom"; import "./NavigationView.css"; -import { gui_store, GuiTool } from "../../core/stores/GuiStore"; +import { GuiStore, GuiTool } from "../../core/stores/GuiStore"; import { Widget } from "../../core/gui/Widget"; import { NavigationButton } from "./NavigationButton"; import { Select } from "../../core/gui/Select"; @@ -50,7 +50,7 @@ export class NavigationView extends Widget { readonly height = 30; - constructor() { + constructor(private readonly gui_store: GuiStore) { super(); this.element.style.height = `${this.height}px`; @@ -62,11 +62,11 @@ export class NavigationView extends Widget { this.finalize_construction(); } - private mousedown(e: MouseEvent): void { + private mousedown = (e: MouseEvent): void => { if (e.target instanceof HTMLLabelElement && e.target.control instanceof HTMLInputElement) { - gui_store.tool.val = (GuiTool as any)[e.target.control.value]; + this.gui_store.tool.val = (GuiTool as any)[e.target.control.value]; } - } + }; private mark_tool_button = (tool: GuiTool): void => { const button = this.buttons.get(tool); diff --git a/src/core/stores/GuiStore.ts b/src/core/stores/GuiStore.ts index abaed8df..2d46726d 100644 --- a/src/core/stores/GuiStore.ts +++ b/src/core/stores/GuiStore.ts @@ -17,7 +17,7 @@ const GUI_TOOL_TO_STRING = new Map([ ]); const STRING_TO_GUI_TOOL = new Map([...GUI_TOOL_TO_STRING.entries()].map(([k, v]) => [v, k])); -class GuiStore implements Disposable { +export class GuiStore implements Disposable { readonly tool: WritableProperty = property(GuiTool.Viewer); readonly server: Property; @@ -106,8 +106,6 @@ class GuiStore implements Disposable { } } -export const gui_store = new GuiStore(); - function string_to_gui_tool(tool: string): GuiTool | undefined { return STRING_TO_GUI_TOOL.get(tool); } diff --git a/src/core/stores/ItemTypeStore.ts b/src/core/stores/ItemTypeStore.ts index a3be6c68..38e3c139 100644 --- a/src/core/stores/ItemTypeStore.ts +++ b/src/core/stores/ItemTypeStore.ts @@ -9,6 +9,11 @@ import { import { ServerMap } from "./ServerMap"; import { Server } from "../model"; import { ItemTypeDto } from "../dto/ItemTypeDto"; +import { GuiStore } from "./GuiStore"; + +export function load_item_type_stores(gui_store: GuiStore): ServerMap { + return new ServerMap(gui_store, load); +} export class ItemTypeStore { readonly item_types: ItemType[]; @@ -91,5 +96,3 @@ async function load(server: Server): Promise { 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 2a14cfe9..02f142fe 100644 --- a/src/core/stores/ServerMap.ts +++ b/src/core/stores/ServerMap.ts @@ -1,20 +1,20 @@ import { Server } from "../model"; import { Property } from "../observable/property/Property"; -import { gui_store } from "./GuiStore"; import { memoize } from "lodash"; import { sequential } from "../sequential"; import { Disposable } from "../observable/Disposable"; +import { GuiStore } from "./GuiStore"; /** * Map with a lazily-loaded, guaranteed value per server. */ export class ServerMap { /** - * The value for the current server as set in {@link gui_store}. + * The value for the current server as set in the {@link GuiStore}. */ get current(): Property> { if (!this._current) { - this._current = gui_store.server.map(server => this.get(server)); + this._current = this.gui_store.server.map(server => this.get(server)); } return this._current; @@ -23,7 +23,7 @@ export class ServerMap { private readonly get_value: (server: Server) => Promise; private _current?: Property>; - constructor(get_value: (server: Server) => Promise) { + constructor(private readonly gui_store: GuiStore, get_value: (server: Server) => Promise) { this.get_value = memoize(get_value); } diff --git a/src/dps_calc/stores/DpsCalcStore.ts b/src/dps_calc/stores/DpsCalcStore.ts index def007f1..9cedde2a 100644 --- a/src/dps_calc/stores/DpsCalcStore.ts +++ b/src/dps_calc/stores/DpsCalcStore.ts @@ -1,5 +1,5 @@ -import { WeaponItem, WeaponItemType, ArmorItemType, ShieldItemType } from "../../core/model/items"; -import { item_type_stores, ItemTypeStore } from "../../core/stores/ItemTypeStore"; +import { ArmorItemType, ShieldItemType, WeaponItem, WeaponItemType } from "../../core/model/items"; +import { ItemTypeStore } from "../../core/stores/ItemTypeStore"; import { Property } from "../../core/observable/property/Property"; import { list_property, map, property } from "../../core/observable"; import { WritableProperty } from "../../core/observable/property/WritableProperty"; @@ -7,6 +7,7 @@ import { ListProperty } from "../../core/observable/property/list/ListProperty"; import { WritableListProperty } from "../../core/observable/property/list/WritableListProperty"; import { sequential } from "../../core/sequential"; import { Disposable } from "../../core/observable/Disposable"; +import { ServerMap } from "../../core/stores/ServerMap"; const NORMAL_DAMAGE_FACTOR = 0.2 * 0.9; const HEAVY_DAMAGE_FACTOR = NORMAL_DAMAGE_FACTOR * 1.89; @@ -14,7 +15,7 @@ const HEAVY_DAMAGE_FACTOR = NORMAL_DAMAGE_FACTOR * 1.89; // const VJAYA_DAMAGE_FACTOR = NORMAL_DAMAGE_FACTOR * 5.56; // const CRIT_FACTOR = 1.5; -class Weapon { +export class Weapon { readonly shifta_atp: Property = this.store.shifta_factor.map(shifta_factor => { if (this.item.type.min_atp === this.item.type.max_atp) { return 0; @@ -92,7 +93,7 @@ class Weapon { constructor(private readonly store: DpsCalcStore, readonly item: WeaponItem) {} } -class DpsCalcStore implements Disposable { +export class DpsCalcStore implements Disposable { private readonly _weapon_types: WritableListProperty = list_property(); private readonly _armor_types: WritableListProperty = list_property(); private readonly _shield_types: WritableListProperty = list_property(); @@ -154,7 +155,7 @@ class DpsCalcStore implements Disposable { readonly enemy_dfp: Property = this._enemy_dfp; - constructor() { + constructor(item_type_stores: ServerMap) { this.disposable = item_type_stores.current.observe( sequential(async ({ value: item_type_store }: { value: Promise }) => { const weapon_types: WeaponItemType[] = []; @@ -186,5 +187,3 @@ class DpsCalcStore implements Disposable { this._weapons.push(new Weapon(this, new WeaponItem(type))); }; } - -export const dps_calc_store = new DpsCalcStore(); diff --git a/src/hunt_optimizer/gui/HuntOptimizerView.ts b/src/hunt_optimizer/gui/HuntOptimizerView.ts index fa96e2f4..2bb42fd1 100644 --- a/src/hunt_optimizer/gui/HuntOptimizerView.ts +++ b/src/hunt_optimizer/gui/HuntOptimizerView.ts @@ -1,7 +1,13 @@ import { TabContainer } from "../../core/gui/TabContainer"; +import { ServerMap } from "../../core/stores/ServerMap"; +import { HuntOptimizerStore } from "../stores/HuntOptimizerStore"; +import { HuntMethodStore } from "../stores/HuntMethodStore"; export class HuntOptimizerView extends TabContainer { - constructor() { + constructor( + hunt_optimizer_stores: ServerMap, + hunt_method_stores: ServerMap, + ) { super({ class: "hunt_optimizer_HuntOptimizerView", tabs: [ @@ -9,14 +15,16 @@ export class HuntOptimizerView extends TabContainer { title: "Optimize", key: "optimize", create_view: async function() { - return new (await import("./OptimizerView")).OptimizerView(); + return new (await import("./OptimizerView")).OptimizerView( + hunt_optimizer_stores, + ); }, }, { title: "Methods", key: "methods", create_view: async function() { - return new (await import("./MethodsView")).MethodsView(); + return new (await import("./MethodsView")).MethodsView(hunt_method_stores); }, }, { diff --git a/src/hunt_optimizer/gui/MethodsForEpisodeView.ts b/src/hunt_optimizer/gui/MethodsForEpisodeView.ts index 7755fa6f..e225cc1e 100644 --- a/src/hunt_optimizer/gui/MethodsForEpisodeView.ts +++ b/src/hunt_optimizer/gui/MethodsForEpisodeView.ts @@ -1,7 +1,6 @@ import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { Episode } from "../../core/data_formats/parsing/quest/Episode"; import { el } from "../../core/gui/dom"; -import { hunt_method_stores } from "../stores/HuntMethodStore"; import { HuntMethodModel } from "../model/HuntMethodModel"; import { ENEMY_NPC_TYPES, @@ -14,6 +13,8 @@ import { DurationInput } from "../../core/gui/DurationInput"; import { Disposable } from "../../core/observable/Disposable"; import { SortDirection, Table } from "../../core/gui/Table"; import { list_property } from "../../core/observable"; +import { ServerMap } from "../../core/stores/ServerMap"; +import { HuntMethodStore } from "../stores/HuntMethodStore"; export class MethodsForEpisodeView extends ResizableWidget { readonly element = el.div({ class: "hunt_optimizer_MethodsForEpisodeView" }); @@ -22,7 +23,7 @@ export class MethodsForEpisodeView extends ResizableWidget { private readonly enemy_types: NpcType[]; private hunt_methods_observer?: Disposable; - constructor(episode: Episode) { + constructor(hunt_method_stores: ServerMap, episode: Episode) { super(); this.episode = episode; diff --git a/src/hunt_optimizer/gui/MethodsView.ts b/src/hunt_optimizer/gui/MethodsView.ts index c87639d1..ea8f211b 100644 --- a/src/hunt_optimizer/gui/MethodsView.ts +++ b/src/hunt_optimizer/gui/MethodsView.ts @@ -1,9 +1,11 @@ import { TabContainer } from "../../core/gui/TabContainer"; import { Episode } from "../../core/data_formats/parsing/quest/Episode"; import { MethodsForEpisodeView } from "./MethodsForEpisodeView"; +import { ServerMap } from "../../core/stores/ServerMap"; +import { HuntMethodStore } from "../stores/HuntMethodStore"; export class MethodsView extends TabContainer { - constructor() { + constructor(hunt_method_stores: ServerMap) { super({ class: "hunt_optimizer_MethodsView", tabs: [ @@ -11,21 +13,21 @@ export class MethodsView extends TabContainer { title: "Episode I", key: "episode_1", create_view: async function() { - return new MethodsForEpisodeView(Episode.I); + return new MethodsForEpisodeView(hunt_method_stores, Episode.I); }, }, { title: "Episode II", key: "episode_2", create_view: async function() { - return new MethodsForEpisodeView(Episode.II); + return new MethodsForEpisodeView(hunt_method_stores, Episode.II); }, }, { title: "Episode IV", key: "episode_4", create_view: async function() { - return new MethodsForEpisodeView(Episode.IV); + return new MethodsForEpisodeView(hunt_method_stores, Episode.IV); }, }, ], diff --git a/src/hunt_optimizer/gui/OptimizationResultView.ts b/src/hunt_optimizer/gui/OptimizationResultView.ts index ff9a8140..3db42f05 100644 --- a/src/hunt_optimizer/gui/OptimizationResultView.ts +++ b/src/hunt_optimizer/gui/OptimizationResultView.ts @@ -1,7 +1,6 @@ import { Widget } from "../../core/gui/Widget"; import { el, section_id_icon } from "../../core/gui/dom"; import { Column, Table } from "../../core/gui/Table"; -import { hunt_optimizer_stores } from "../stores/HuntOptimizerStore"; import { Disposable } from "../../core/observable/Disposable"; import { list_property } from "../../core/observable"; import { OptimalMethodModel, OptimalResultModel } from "../model"; @@ -9,6 +8,8 @@ import { Difficulty } from "../../core/model"; import { Episode } from "../../core/data_formats/parsing/quest/Episode"; import "./OptimizationResultView.css"; import { Duration } from "luxon"; +import { ServerMap } from "../../core/stores/ServerMap"; +import { HuntOptimizerStore } from "../stores/HuntOptimizerStore"; export class OptimizationResultView extends Widget { readonly element = el.div( @@ -19,7 +20,7 @@ export class OptimizationResultView extends Widget { private results_observer?: Disposable; private table?: Table; - constructor() { + constructor(hunt_optimizer_stores: ServerMap) { super(); this.disposable( diff --git a/src/hunt_optimizer/gui/OptimizerView.ts b/src/hunt_optimizer/gui/OptimizerView.ts index aee6556f..5fefaf24 100644 --- a/src/hunt_optimizer/gui/OptimizerView.ts +++ b/src/hunt_optimizer/gui/OptimizerView.ts @@ -3,16 +3,18 @@ import { el } from "../../core/gui/dom"; import { WantedItemsView } from "./WantedItemsView"; import "./OptimizerView.css"; import { OptimizationResultView } from "./OptimizationResultView"; +import { ServerMap } from "../../core/stores/ServerMap"; +import { HuntOptimizerStore } from "../stores/HuntOptimizerStore"; export class OptimizerView extends ResizableWidget { readonly element = el.div({ class: "hunt_optimizer_OptimizerView" }); - constructor() { + constructor(hunt_optimizer_stores: ServerMap) { super(); this.element.append( - this.disposable(new WantedItemsView()).element, - this.disposable(new OptimizationResultView()).element, + this.disposable(new WantedItemsView(hunt_optimizer_stores)).element, + this.disposable(new OptimizationResultView(hunt_optimizer_stores)).element, ); this.finalize_construction(); diff --git a/src/hunt_optimizer/gui/WantedItemsView.ts b/src/hunt_optimizer/gui/WantedItemsView.ts index 22c43059..111dfab4 100644 --- a/src/hunt_optimizer/gui/WantedItemsView.ts +++ b/src/hunt_optimizer/gui/WantedItemsView.ts @@ -5,11 +5,12 @@ import { Disposer } from "../../core/observable/Disposer"; import { Widget } from "../../core/gui/Widget"; import { WantedItemModel } from "../model"; import { NumberInput } from "../../core/gui/NumberInput"; -import { hunt_optimizer_stores } from "../stores/HuntOptimizerStore"; import { ComboBox } from "../../core/gui/ComboBox"; import { list_property } from "../../core/observable"; import { ItemType } from "../../core/model/items"; import { Disposable } from "../../core/observable/Disposable"; +import { ServerMap } from "../../core/stores/ServerMap"; +import { HuntOptimizerStore } from "../stores/HuntOptimizerStore"; export class WantedItemsView extends Widget { readonly element = el.div({ class: "hunt_optimizer_WantedItemsView" }); @@ -17,7 +18,7 @@ export class WantedItemsView extends Widget { private readonly tbody_element = el.tbody(); private readonly store_disposer = this.disposable(new Disposer()); - constructor() { + constructor(private readonly hunt_optimizer_stores: ServerMap) { super(); const huntable_items = list_property(); @@ -94,7 +95,7 @@ export class WantedItemsView extends Widget { row_disposer.add( remove_button.click.observe(async () => - (await hunt_optimizer_stores.current.val).remove_wanted_item(wanted_item), + (await this.hunt_optimizer_stores.current.val).remove_wanted_item(wanted_item), ), ); diff --git a/src/hunt_optimizer/index.ts b/src/hunt_optimizer/index.ts new file mode 100644 index 00000000..d1d927e9 --- /dev/null +++ b/src/hunt_optimizer/index.ts @@ -0,0 +1,29 @@ +import { HuntOptimizerView } from "./gui/HuntOptimizerView"; +import { ServerMap } from "../core/stores/ServerMap"; +import { HuntMethodStore, load_hunt_method_stores } from "./stores/HuntMethodStore"; +import { GuiStore } from "../core/stores/GuiStore"; +import { HuntOptimizerStore, load_hunt_optimizer_stores } from "./stores/HuntOptimizerStore"; +import { ItemTypeStore } from "../core/stores/ItemTypeStore"; +import { HuntMethodPersister } from "./persistence/HuntMethodPersister"; +import { HuntOptimizerPersister } from "./persistence/HuntOptimizerPersister"; +import { ItemDropStore } from "./stores/ItemDropStore"; + +export function initialize_hunt_optimizer( + gui_store: GuiStore, + item_type_stores: ServerMap, + item_drop_stores: ServerMap, +): HuntOptimizerView { + const hunt_method_stores: ServerMap = load_hunt_method_stores( + gui_store, + new HuntMethodPersister(), + ); + const hunt_optimizer_stores: ServerMap = load_hunt_optimizer_stores( + gui_store, + new HuntOptimizerPersister(item_type_stores), + item_type_stores, + item_drop_stores, + hunt_method_stores, + ); + + return new HuntOptimizerView(hunt_optimizer_stores, hunt_method_stores); +} diff --git a/src/hunt_optimizer/persistence/HuntMethodPersister.ts b/src/hunt_optimizer/persistence/HuntMethodPersister.ts index f843071e..1a519877 100644 --- a/src/hunt_optimizer/persistence/HuntMethodPersister.ts +++ b/src/hunt_optimizer/persistence/HuntMethodPersister.ts @@ -5,7 +5,7 @@ import { Duration } from "luxon"; const METHOD_USER_TIMES_KEY = "HuntMethodStore.methodUserTimes"; -class HuntMethodPersister extends Persister { +export class HuntMethodPersister extends Persister { persist_method_user_times(hunt_methods: readonly HuntMethodModel[], server: Server): void { const user_times: PersistedUserTimes = {}; @@ -39,5 +39,3 @@ class HuntMethodPersister extends Persister { } type PersistedUserTimes = { [method_id: string]: number }; - -export const hunt_method_persister = new HuntMethodPersister(); diff --git a/src/hunt_optimizer/persistence/HuntOptimizerPersister.ts b/src/hunt_optimizer/persistence/HuntOptimizerPersister.ts index 7fe761ab..1451ee6c 100644 --- a/src/hunt_optimizer/persistence/HuntOptimizerPersister.ts +++ b/src/hunt_optimizer/persistence/HuntOptimizerPersister.ts @@ -1,11 +1,16 @@ import { Server } from "../../core/model"; -import { item_type_stores } from "../../core/stores/ItemTypeStore"; import { Persister } from "../../core/persistence"; import { WantedItemModel } from "../model"; +import { ItemTypeStore } from "../../core/stores/ItemTypeStore"; +import { ServerMap } from "../../core/stores/ServerMap"; const WANTED_ITEMS_KEY = "HuntOptimizerStore.wantedItems"; -class HuntOptimizerPersister extends Persister { +export class HuntOptimizerPersister extends Persister { + constructor(private readonly item_type_stores: ServerMap) { + super(); + } + persist_wanted_items(server: Server, wanted_items: readonly WantedItemModel[]): void { this.persist_for_server( server, @@ -20,7 +25,7 @@ class HuntOptimizerPersister extends Persister { } async load_wanted_items(server: Server): Promise { - const item_store = await item_type_stores.get(server); + const item_store = await this.item_type_stores.get(server); const persisted_wanted_items = await this.load_for_server( server, @@ -50,5 +55,3 @@ type PersistedWantedItem = { itemKindId?: number; // Legacy name, not persisted, only checked when loading. amount: number; }; - -export const hunt_optimizer_persister = new HuntOptimizerPersister(); diff --git a/src/hunt_optimizer/stores/HuntMethodStore.ts b/src/hunt_optimizer/stores/HuntMethodStore.ts index 55d57911..6e742976 100644 --- a/src/hunt_optimizer/stores/HuntMethodStore.ts +++ b/src/hunt_optimizer/stores/HuntMethodStore.ts @@ -4,12 +4,13 @@ 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 { HuntMethodPersister } 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 { GuiStore } from "../../core/stores/GuiStore"; import { ServerMap } from "../../core/stores/ServerMap"; const logger = Logger.get("hunt_optimizer/stores/HuntMethodStore"); @@ -18,12 +19,23 @@ 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 }); +export function load_hunt_method_stores( + gui_store: GuiStore, + hunt_method_persister: HuntMethodPersister, +): ServerMap { + return new ServerMap(gui_store, create_loader(hunt_method_persister)); +} + export class HuntMethodStore implements Disposable { readonly methods: ListProperty; private readonly disposer = new Disposer(); - constructor(server: Server, methods: HuntMethodModel[]) { + constructor( + hunt_method_persister: HuntMethodPersister, + server: Server, + methods: HuntMethodModel[], + ) { this.methods = list_property(method => [method.user_time], ...methods); this.disposer.add( @@ -38,62 +50,64 @@ export class HuntMethodStore implements Disposable { } } -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, - ), +function create_loader( + hunt_method_persister: HuntMethodPersister, +): (server: Server) => Promise { + return async server => { + const response = await fetch( + `${process.env.PUBLIC_URL}/quests.${Server[server].toLowerCase()}.json`, ); - } + const quests = (await response.json()) as QuestDto[]; + const methods: HuntMethodModel[] = []; - await hunt_method_persister.load_method_user_times(methods, server); + for (const quest of quests) { + let total_enemy_count = 0; + const enemy_counts = new Map(); - return new HuntMethodStore(server, methods); + 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(hunt_method_persister, 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 7d97f335..c19f1c17 100644 --- a/src/hunt_optimizer/stores/HuntOptimizerStore.ts +++ b/src/hunt_optimizer/stores/HuntOptimizerStore.ts @@ -16,13 +16,32 @@ import { OptimalMethodModel, OptimalResultModel, WantedItemModel } from "../mode import { ListProperty } from "../../core/observable/property/list/ListProperty"; import { list_property, map } from "../../core/observable"; import { WritableListProperty } from "../../core/observable/property/list/WritableListProperty"; -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 { HuntMethodStore } from "./HuntMethodStore"; +import { ItemDropStore } from "./ItemDropStore"; +import { ItemTypeStore } from "../../core/stores/ItemTypeStore"; import { Disposable } from "../../core/observable/Disposable"; import { Disposer } from "../../core/observable/Disposer"; +import { ServerMap } from "../../core/stores/ServerMap"; +import { GuiStore } from "../../core/stores/GuiStore"; +import { HuntOptimizerPersister } from "../persistence/HuntOptimizerPersister"; + +export function load_hunt_optimizer_stores( + gui_store: GuiStore, + hunt_optimizer_persister: HuntOptimizerPersister, + item_type_stores: ServerMap, + item_drop_stores: ServerMap, + hunt_method_stores: ServerMap, +): ServerMap { + return new ServerMap( + gui_store, + create_loader( + hunt_optimizer_persister, + item_type_stores, + item_drop_stores, + hunt_method_stores, + ), + ); +} // TODO: take into account mothmants spawned from mothverts. // TODO: take into account split slimes. @@ -31,7 +50,7 @@ import { Disposer } from "../../core/observable/Disposer"; // TODO: Show expected value or probability per item per method. // Can be useful when deciding which item to hunt first. // TODO: boxes. -class HuntOptimizerStore implements Disposable { +export class HuntOptimizerStore implements Disposable { readonly huntable_item_types: ItemType[]; // TODO: wanted items per server. readonly wanted_items: ListProperty; @@ -43,6 +62,7 @@ class HuntOptimizerStore implements Disposable { private readonly disposer = new Disposer(); constructor( + private readonly hunt_optimizer_persister: HuntOptimizerPersister, private readonly server: Server, item_type_store: ItemTypeStore, private readonly item_drop_store: ItemDropStore, @@ -314,23 +334,28 @@ class HuntOptimizerStore implements Disposable { } private initialize_persistence = async (): Promise => { - this._wanted_items.val = await hunt_optimizer_persister.load_wanted_items(this.server); + this._wanted_items.val = await this.hunt_optimizer_persister.load_wanted_items(this.server); this.disposer.add( this._wanted_items.observe(({ value }) => { - hunt_optimizer_persister.persist_wanted_items(this.server, value); + this.hunt_optimizer_persister.persist_wanted_items(this.server, value); }), ); }; } -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), - ); +function create_loader( + hunt_optimizer_persister: HuntOptimizerPersister, + item_type_stores: ServerMap, + item_drop_stores: ServerMap, + hunt_method_stores: ServerMap, +): (server: Server) => Promise { + return async server => + new HuntOptimizerStore( + hunt_optimizer_persister, + 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 7116c893..4ef402c9 100644 --- a/src/hunt_optimizer/stores/ItemDropStore.ts +++ b/src/hunt_optimizer/stores/ItemDropStore.ts @@ -1,13 +1,21 @@ 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 { EnemyDropDto } from "../dto/drops"; +import { GuiStore } from "../../core/stores/GuiStore"; +import { ItemTypeStore } from "../../core/stores/ItemTypeStore"; const logger = Logger.get("stores/ItemDropStore"); +export function load_item_drop_stores( + gui_store: GuiStore, + item_type_stores: ServerMap, +): ServerMap { + return new ServerMap(gui_store, create_loader(item_type_stores)); +} + export class ItemDropStore { readonly enemy_drops: EnemyDropTable; @@ -65,55 +73,57 @@ export class 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(); +function create_loader( + item_type_stores: ServerMap, +): (server: Server) => Promise { + return async server => { + 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(); - for (const drop_dto of data) { - const npc_type = (NpcType as any)[drop_dto.enemy]; + for (const drop_dto of data) { + const npc_type = (NpcType as any)[drop_dto.enemy]; - if (!npc_type) { - logger.warn( - `Couldn't determine NpcType of episode ${drop_dto.episode} ${drop_dto.enemy}.`, - ); - continue; - } + if (!npc_type) { + logger.warn( + `Couldn't determine NpcType of episode ${drop_dto.episode} ${drop_dto.enemy}.`, + ); + continue; + } - const difficulty = (Difficulty as any)[drop_dto.difficulty]; - const item_type = item_type_store.get_by_id(drop_dto.itemTypeId); + const difficulty = (Difficulty as any)[drop_dto.difficulty]; + const item_type = item_type_store.get_by_id(drop_dto.itemTypeId); - if (!item_type) { - logger.warn(`Couldn't find item kind ${drop_dto.itemTypeId}.`); - continue; - } + if (!item_type) { + logger.warn(`Couldn't find item kind ${drop_dto.itemTypeId}.`); + continue; + } - const section_id = (SectionId as any)[drop_dto.sectionId]; + const section_id = (SectionId as any)[drop_dto.sectionId]; - if (section_id == null) { - logger.warn(`Couldn't find section ID ${drop_dto.sectionId}.`); - continue; - } + if (section_id == null) { + logger.warn(`Couldn't find section ID ${drop_dto.sectionId}.`); + continue; + } - enemy_drops.set_drop( - difficulty, - section_id, - npc_type, - new EnemyDrop( + enemy_drops.set_drop( difficulty, section_id, npc_type, - item_type, - drop_dto.dropRate, - drop_dto.rareRate, - ), - ); - } + new EnemyDrop( + difficulty, + section_id, + npc_type, + item_type, + drop_dto.dropRate, + drop_dto.rareRate, + ), + ); + } - return new ItemDropStore(enemy_drops); + return new ItemDropStore(enemy_drops); + }; } - -export const item_drop_stores: ServerMap = new ServerMap(load); diff --git a/src/index.ts b/src/index.ts index a4a4686e..51528440 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,9 +7,12 @@ import "@fortawesome/fontawesome-free/js/fontawesome"; import "@fortawesome/fontawesome-free/js/solid"; import "@fortawesome/fontawesome-free/js/regular"; import "@fortawesome/fontawesome-free/js/brands"; +import { GuiStore, GuiTool } from "./core/stores/GuiStore"; +import { load_item_type_stores } from "./core/stores/ItemTypeStore"; +import { load_item_drop_stores } from "./hunt_optimizer/stores/ItemDropStore"; Logger.useDefaults({ - defaultLevel: (Logger as any)[process.env["LOG_LEVEL"] || "OFF"], + defaultLevel: (Logger as any)[process.env["LOG_LEVEL"] ?? "OFF"], }); function initialize(): Disposable { @@ -23,8 +26,36 @@ function initialize(): Disposable { document.addEventListener("dragover", dragover); document.addEventListener("drop", drop); - // Initialize view. - const application_view = new ApplicationView(); + // Initialize core stores shared by several submodules. + const gui_store = new GuiStore(); + const item_type_stores = load_item_type_stores(gui_store); + const item_drop_stores = load_item_drop_stores(gui_store, item_type_stores); + + // Initialize application view. + const application_view = new ApplicationView(gui_store, [ + [ + GuiTool.Viewer, + async () => { + return (await import("./viewer/index")).initialize_viewer(gui_store); + }, + ], + [ + GuiTool.QuestEditor, + async () => { + return (await import("./quest_editor/index")).initialize_quest_editor(gui_store); + }, + ], + [ + GuiTool.HuntOptimizer, + async () => { + return (await import("./hunt_optimizer/index")).initialize_hunt_optimizer( + gui_store, + item_type_stores, + item_drop_stores, + ); + }, + ], + ]); // Resize the view on window resize. const resize = throttle( diff --git a/src/quest_editor/actions/CreateEntityAction.ts b/src/quest_editor/actions/CreateEntityAction.ts index 10c6f205..56961ddc 100644 --- a/src/quest_editor/actions/CreateEntityAction.ts +++ b/src/quest_editor/actions/CreateEntityAction.ts @@ -1,17 +1,20 @@ import { Action } from "../../core/undo/Action"; import { QuestEntityModel } from "../model/QuestEntityModel"; import { entity_data } from "../../core/data_formats/parsing/quest/entities"; -import { quest_editor_store } from "../stores/QuestEditorStore"; +import { QuestEditorStore } from "../stores/QuestEditorStore"; export class CreateEntityAction implements Action { readonly description: string; - constructor(private entity: QuestEntityModel) { + constructor( + private readonly quest_editor_store: QuestEditorStore, + private readonly entity: QuestEntityModel, + ) { this.description = `Create ${entity_data(entity.type).name}`; } undo(): void { - const quest = quest_editor_store.current_quest.val; + const quest = this.quest_editor_store.current_quest.val; if (quest) { quest.remove_entity(this.entity); @@ -19,12 +22,12 @@ export class CreateEntityAction implements Action { } redo(): void { - const quest = quest_editor_store.current_quest.val; + const quest = this.quest_editor_store.current_quest.val; if (quest) { quest.add_entity(this.entity); - quest_editor_store.set_selected_entity(this.entity); + this.quest_editor_store.set_selected_entity(this.entity); } } } diff --git a/src/quest_editor/actions/QuestEditAction.ts b/src/quest_editor/actions/QuestEditAction.ts index 193d9def..9d6629c6 100644 --- a/src/quest_editor/actions/QuestEditAction.ts +++ b/src/quest_editor/actions/QuestEditAction.ts @@ -5,10 +5,10 @@ import { PropertyChangeEvent } from "../../core/observable/property/Property"; export abstract class QuestEditAction implements Action { abstract readonly description: string; - protected new: T; - protected old: T; + protected readonly new: T; + protected readonly old: T; - constructor(protected quest: QuestModel, event: PropertyChangeEvent) { + constructor(protected readonly quest: QuestModel, event: PropertyChangeEvent) { this.new = event.value; this.old = event.old_value; } diff --git a/src/quest_editor/actions/RemoveEntityAction.ts b/src/quest_editor/actions/RemoveEntityAction.ts index 2da2c8f7..5b38a5d2 100644 --- a/src/quest_editor/actions/RemoveEntityAction.ts +++ b/src/quest_editor/actions/RemoveEntityAction.ts @@ -1,27 +1,30 @@ import { Action } from "../../core/undo/Action"; import { QuestEntityModel } from "../model/QuestEntityModel"; import { entity_data } from "../../core/data_formats/parsing/quest/entities"; -import { quest_editor_store } from "../stores/QuestEditorStore"; +import { QuestEditorStore } from "../stores/QuestEditorStore"; export class RemoveEntityAction implements Action { readonly description: string; - constructor(private entity: QuestEntityModel) { + constructor( + private readonly quest_editor_store: QuestEditorStore, + private readonly entity: QuestEntityModel, + ) { this.description = `Delete ${entity_data(entity.type).name}`; } undo(): void { - const quest = quest_editor_store.current_quest.val; + const quest = this.quest_editor_store.current_quest.val; if (quest) { quest.add_entity(this.entity); - quest_editor_store.set_selected_entity(this.entity); + this.quest_editor_store.set_selected_entity(this.entity); } } redo(): void { - const quest = quest_editor_store.current_quest.val; + const quest = this.quest_editor_store.current_quest.val; if (quest) { quest.remove_entity(this.entity); diff --git a/src/quest_editor/actions/RotateEntityAction.ts b/src/quest_editor/actions/RotateEntityAction.ts index aeb178ac..7946c5bf 100644 --- a/src/quest_editor/actions/RotateEntityAction.ts +++ b/src/quest_editor/actions/RotateEntityAction.ts @@ -1,23 +1,24 @@ import { Action } from "../../core/undo/Action"; import { QuestEntityModel } from "../model/QuestEntityModel"; import { entity_data } from "../../core/data_formats/parsing/quest/entities"; -import { quest_editor_store } from "../stores/QuestEditorStore"; import { Euler } from "three"; +import { QuestEditorStore } from "../stores/QuestEditorStore"; export class RotateEntityAction implements Action { readonly description: string; constructor( - private entity: QuestEntityModel, - private old_rotation: Euler, - private new_rotation: Euler, - private world: boolean, + private readonly quest_editor_store: QuestEditorStore, + private readonly entity: QuestEntityModel, + private readonly old_rotation: Euler, + private readonly new_rotation: Euler, + private readonly world: boolean, ) { this.description = `Rotate ${entity_data(entity.type).name}`; } undo(): void { - quest_editor_store.set_selected_entity(this.entity); + this.quest_editor_store.set_selected_entity(this.entity); if (this.world) { this.entity.set_world_rotation(this.old_rotation); @@ -27,7 +28,7 @@ export class RotateEntityAction implements Action { } redo(): void { - quest_editor_store.set_selected_entity(this.entity); + this.quest_editor_store.set_selected_entity(this.entity); if (this.world) { this.entity.set_world_rotation(this.new_rotation); diff --git a/src/quest_editor/actions/TranslateEntityAction.ts b/src/quest_editor/actions/TranslateEntityAction.ts index 25b71b93..21e9d13d 100644 --- a/src/quest_editor/actions/TranslateEntityAction.ts +++ b/src/quest_editor/actions/TranslateEntityAction.ts @@ -1,26 +1,27 @@ import { Action } from "../../core/undo/Action"; import { QuestEntityModel } from "../model/QuestEntityModel"; import { entity_data } from "../../core/data_formats/parsing/quest/entities"; -import { quest_editor_store } from "../stores/QuestEditorStore"; import { SectionModel } from "../model/SectionModel"; import { Vector3 } from "three"; +import { QuestEditorStore } from "../stores/QuestEditorStore"; export class TranslateEntityAction implements Action { readonly description: string; constructor( - private entity: QuestEntityModel, - private old_section: SectionModel | undefined, - private new_section: SectionModel | undefined, - private old_position: Vector3, - private new_position: Vector3, - private world: boolean, + private readonly quest_editor_store: QuestEditorStore, + private readonly entity: QuestEntityModel, + private readonly old_section: SectionModel | undefined, + private readonly new_section: SectionModel | undefined, + private readonly old_position: Vector3, + private readonly new_position: Vector3, + private readonly world: boolean, ) { this.description = `Move ${entity_data(entity.type).name}`; } undo(): void { - quest_editor_store.set_selected_entity(this.entity); + this.quest_editor_store.set_selected_entity(this.entity); if (this.old_section) { this.entity.set_section(this.old_section); @@ -34,7 +35,7 @@ export class TranslateEntityAction implements Action { } redo(): void { - quest_editor_store.set_selected_entity(this.entity); + this.quest_editor_store.set_selected_entity(this.entity); if (this.new_section) { this.entity.set_section(this.new_section); diff --git a/src/quest_editor/gui/AsmEditorToolBar.ts b/src/quest_editor/gui/AsmEditorToolBar.ts index b9f9ddb2..3eb8c0ba 100644 --- a/src/quest_editor/gui/AsmEditorToolBar.ts +++ b/src/quest_editor/gui/AsmEditorToolBar.ts @@ -1,9 +1,9 @@ import { ToolBar } from "../../core/gui/ToolBar"; import { CheckBox } from "../../core/gui/CheckBox"; -import { asm_editor_store } from "../stores/AsmEditorStore"; +import { AsmEditorStore } from "../stores/AsmEditorStore"; export class AsmEditorToolBar extends ToolBar { - constructor() { + constructor(asm_editor_store: AsmEditorStore) { const inline_args_mode_checkbox = new CheckBox(true, { label: "Inline args mode", tooltip: asm_editor_store.has_issues.map(has_issues => { diff --git a/src/quest_editor/gui/AsmEditorView.ts b/src/quest_editor/gui/AsmEditorView.ts index 3a9c0e1d..9d63c508 100644 --- a/src/quest_editor/gui/AsmEditorView.ts +++ b/src/quest_editor/gui/AsmEditorView.ts @@ -1,14 +1,14 @@ import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { el } from "../../core/gui/dom"; import { editor, KeyCode, KeyMod, Range } from "monaco-editor"; -import { asm_editor_store } from "../stores/AsmEditorStore"; import { AsmEditorToolBar } from "./AsmEditorToolBar"; import { EditorHistory } from "./EditorHistory"; -import IStandaloneCodeEditor = editor.IStandaloneCodeEditor; import "./AsmEditorView.css"; import { ListChangeType } from "../../core/observable/property/list/ListProperty"; -import { quest_editor_store } from "../stores/QuestEditorStore"; -import { gui_store } from "../../core/stores/GuiStore"; +import { GuiStore } from "../../core/stores/GuiStore"; +import { AsmEditorStore } from "../stores/AsmEditorStore"; +import { QuestRunner } from "../QuestRunner"; +import IStandaloneCodeEditor = editor.IStandaloneCodeEditor; editor.defineTheme("phantasmal-world", { base: "vs-dark", @@ -32,7 +32,7 @@ editor.defineTheme("phantasmal-world", { const DUMMY_MODEL = editor.createModel("", "psoasm"); export class AsmEditorView extends ResizableWidget { - private readonly tool_bar_view = this.disposable(new AsmEditorToolBar()); + private readonly tool_bar_view: AsmEditorToolBar; private readonly editor: IStandaloneCodeEditor; private readonly history: EditorHistory; private breakpoint_decoration_ids: string[] = []; @@ -40,9 +40,15 @@ export class AsmEditorView extends ResizableWidget { readonly element = el.div(); - constructor() { + constructor( + gui_store: GuiStore, + quest_runner: QuestRunner, + private readonly asm_editor_store: AsmEditorStore, + ) { super(); + this.tool_bar_view = this.disposable(new AsmEditorToolBar(asm_editor_store)); + this.element.append(this.tool_bar_view.element); this.editor = this.disposable( @@ -99,7 +105,7 @@ export class AsmEditorView extends ResizableWidget { this.breakpoint_decoration_ids = []; this.execloc_decoration_id = ""; - quest_editor_store.quest_runner.clear_breakpoints(); + quest_runner.clear_breakpoints(); }, { call_now: true }, ), @@ -203,7 +209,7 @@ export class AsmEditorView extends ResizableWidget { if (!pos) { return; } - quest_editor_store.quest_runner.toggle_breakpoint(pos.lineNumber); + quest_runner.toggle_breakpoint(pos.lineNumber); } break; default: @@ -211,7 +217,7 @@ export class AsmEditorView extends ResizableWidget { } }), - this.enabled.bind_to(quest_editor_store.quest_runner.running.map(r => !r)), + this.enabled.bind_to(quest_runner.running.map(r => !r)), ); this.finalize_construction(); @@ -231,6 +237,6 @@ export class AsmEditorView extends ResizableWidget { super.set_enabled(enabled); this.tool_bar_view.enabled.val = enabled; - this.editor.updateOptions({ readOnly: !enabled || !asm_editor_store.model.val }); + this.editor.updateOptions({ readOnly: !enabled || !this.asm_editor_store.model.val }); } } diff --git a/src/quest_editor/gui/EntityInfoView.ts b/src/quest_editor/gui/EntityInfoView.ts index a7bf467f..08dc887b 100644 --- a/src/quest_editor/gui/EntityInfoView.ts +++ b/src/quest_editor/gui/EntityInfoView.ts @@ -1,7 +1,6 @@ import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { el } from "../../core/gui/dom"; import { DisabledView } from "./DisabledView"; -import { quest_editor_store } from "../stores/QuestEditorStore"; import { QuestNpcModel } from "../model/QuestNpcModel"; import { entity_data } from "../../core/data_formats/parsing/quest/entities"; import "./EntityInfoView.css"; @@ -10,6 +9,7 @@ import { Disposer } from "../../core/observable/Disposer"; import { QuestEntityModel } from "../model/QuestEntityModel"; import { Euler, Vector3 } from "three"; import { deg_to_rad, rad_to_deg } from "../../core/math"; +import { QuestEditorStore } from "../stores/QuestEditorStore"; export class EntityInfoView extends ResizableWidget { readonly element = el.div({ class: "quest_editor_EntityInfoView", tab_index: -1 }); @@ -43,7 +43,7 @@ export class EntityInfoView extends ResizableWidget { private readonly entity_disposer = new Disposer(); - constructor() { + constructor(private readonly quest_editor_store: QuestEditorStore) { super(); const entity = quest_editor_store.selected_entity; @@ -174,7 +174,7 @@ export class EntityInfoView extends ResizableWidget { ), this.pos_x_element.value.observe(({ value }) => - quest_editor_store.translate_entity( + this.quest_editor_store.translate_entity( entity, entity.section.val, entity.section.val, @@ -185,7 +185,7 @@ export class EntityInfoView extends ResizableWidget { ), this.pos_y_element.value.observe(({ value }) => - quest_editor_store.translate_entity( + this.quest_editor_store.translate_entity( entity, entity.section.val, entity.section.val, @@ -196,7 +196,7 @@ export class EntityInfoView extends ResizableWidget { ), this.pos_z_element.value.observe(({ value }) => - quest_editor_store.translate_entity( + this.quest_editor_store.translate_entity( entity, entity.section.val, entity.section.val, @@ -220,7 +220,7 @@ export class EntityInfoView extends ResizableWidget { ), this.rot_x_element.value.observe(({ value }) => - quest_editor_store.rotate_entity( + this.quest_editor_store.rotate_entity( entity, rot.val, new Euler(deg_to_rad(value), rot.val.y, rot.val.z, "ZXY"), @@ -229,7 +229,7 @@ export class EntityInfoView extends ResizableWidget { ), this.rot_y_element.value.observe(({ value }) => - quest_editor_store.rotate_entity( + this.quest_editor_store.rotate_entity( entity, rot.val, new Euler(rot.val.x, deg_to_rad(value), rot.val.z, "ZXY"), @@ -238,7 +238,7 @@ export class EntityInfoView extends ResizableWidget { ), this.rot_z_element.value.observe(({ value }) => - quest_editor_store.rotate_entity( + this.quest_editor_store.rotate_entity( entity, rot.val, new Euler(rot.val.x, rot.val.y, deg_to_rad(value), "ZXY"), diff --git a/src/quest_editor/gui/EntityListView.ts b/src/quest_editor/gui/EntityListView.ts index 83e756f2..dc3d0bfc 100644 --- a/src/quest_editor/gui/EntityListView.ts +++ b/src/quest_editor/gui/EntityListView.ts @@ -6,14 +6,14 @@ import { entity_dnd_source } from "./entity_dnd"; import { render_entity_to_image } from "../rendering/render_entity_to_image"; import { WritableListProperty } from "../../core/observable/property/list/WritableListProperty"; import { list_property } from "../../core/observable"; -import { quest_editor_store } from "../stores/QuestEditorStore"; +import { QuestEditorStore } from "../stores/QuestEditorStore"; export abstract class EntityListView extends ResizableWidget { readonly element: HTMLElement; protected readonly entities: WritableListProperty = list_property(); - protected constructor(class_name: string) { + protected constructor(quest_editor_store: QuestEditorStore, class_name: string) { super(); const list_element = el.div({ class: "quest_editor_EntityListView_entity_list" }); diff --git a/src/quest_editor/gui/EventsView.ts b/src/quest_editor/gui/EventsView.ts index e1211e56..c2b0814c 100644 --- a/src/quest_editor/gui/EventsView.ts +++ b/src/quest_editor/gui/EventsView.ts @@ -1,6 +1,5 @@ import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { el } from "../../core/gui/dom"; -import { quest_editor_store } from "../stores/QuestEditorStore"; import { QuestEventDagModel } from "../model/QuestEventDagModel"; import { Disposer } from "../../core/observable/Disposer"; import { NumberInput } from "../../core/gui/NumberInput"; @@ -11,6 +10,7 @@ import { ListChangeType, ListPropertyChangeEvent, } from "../../core/observable/property/list/ListProperty"; +import { QuestEditorStore } from "../stores/QuestEditorStore"; type DagGuiData = { dag: QuestEventDagModel; @@ -29,7 +29,7 @@ export class EventsView extends ResizableWidget { readonly element = el.div({ class: "quest_editor_EventsView" }); - constructor() { + constructor(private readonly quest_editor_store: QuestEditorStore) { super(); this.disposables( @@ -69,8 +69,8 @@ export class EventsView extends ResizableWidget { this.event_dags_observer.dispose(); } - const quest = quest_editor_store.current_quest.val; - const area = quest_editor_store.current_area.val; + const quest = this.quest_editor_store.current_quest.val; + const area = this.quest_editor_store.current_area.val; if (quest && area) { const event_dags = quest.event_dags.filtered(dag => dag.area_id === area.id); @@ -169,6 +169,10 @@ export class EventsView extends ResizableWidget { }; }; + /** + * This method does measurements of the event elements. So it should be called after the event + * elements have been added to the DOM and have been *laid out* by the browser. + */ private update_edges = (): void => { const SPACING = 8; let max_depth = 0; diff --git a/src/quest_editor/gui/NpcCountsView.ts b/src/quest_editor/gui/NpcCountsView.ts index 7a7e795b..fef852d8 100644 --- a/src/quest_editor/gui/NpcCountsView.ts +++ b/src/quest_editor/gui/NpcCountsView.ts @@ -1,11 +1,11 @@ import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { el } from "../../core/gui/dom"; -import { quest_editor_store } from "../stores/QuestEditorStore"; import { npc_data, NpcType } from "../../core/data_formats/parsing/quest/npc_types"; import "./NpcCountsView.css"; import { DisabledView } from "./DisabledView"; import { property } from "../../core/observable"; import { QuestNpcModel } from "../model/QuestNpcModel"; +import { QuestEditorStore } from "../stores/QuestEditorStore"; export class NpcCountsView extends ResizableWidget { readonly element = el.div({ class: "quest_editor_NpcCountsView" }); @@ -14,7 +14,7 @@ export class NpcCountsView extends ResizableWidget { private readonly no_quest_view = new DisabledView("No quest loaded."); - constructor() { + constructor(quest_editor_store: QuestEditorStore) { super(); this.element.append(this.table_element, this.no_quest_view.element); diff --git a/src/quest_editor/gui/NpcListView.ts b/src/quest_editor/gui/NpcListView.ts index 6b2b534f..7c449e1c 100644 --- a/src/quest_editor/gui/NpcListView.ts +++ b/src/quest_editor/gui/NpcListView.ts @@ -1,11 +1,11 @@ import { EntityListView } from "./EntityListView"; import { npc_data, NPC_TYPES, NpcType } from "../../core/data_formats/parsing/quest/npc_types"; -import { quest_editor_store } from "../stores/QuestEditorStore"; +import { QuestEditorStore } from "../stores/QuestEditorStore"; import { Episode } from "../../core/data_formats/parsing/quest/Episode"; export class NpcListView extends EntityListView { - constructor() { - super("quest_editor_NpcListView"); + constructor(private readonly quest_editor_store: QuestEditorStore) { + super(quest_editor_store, "quest_editor_NpcListView"); this.disposables( quest_editor_store.current_quest.observe(this.filter_npcs), @@ -17,8 +17,8 @@ export class NpcListView extends EntityListView { } private filter_npcs = (): void => { - const quest = quest_editor_store.current_quest.val; - const area = quest_editor_store.current_area.val; + const quest = this.quest_editor_store.current_quest.val; + const area = this.quest_editor_store.current_area.val; const episode = quest ? quest.episode : Episode.I; const area_id = area ? area.id : 0; diff --git a/src/quest_editor/gui/ObjectListView.ts b/src/quest_editor/gui/ObjectListView.ts index a3298c8d..a5c718b5 100644 --- a/src/quest_editor/gui/ObjectListView.ts +++ b/src/quest_editor/gui/ObjectListView.ts @@ -4,12 +4,12 @@ import { OBJECT_TYPES, ObjectType, } from "../../core/data_formats/parsing/quest/object_types"; -import { quest_editor_store } from "../stores/QuestEditorStore"; import { Episode } from "../../core/data_formats/parsing/quest/Episode"; +import { QuestEditorStore } from "../stores/QuestEditorStore"; export class ObjectListView extends EntityListView { - constructor() { - super("quest_editor_ObjectListView"); + constructor(private readonly quest_editor_store: QuestEditorStore) { + super(quest_editor_store, "quest_editor_ObjectListView"); this.disposables( quest_editor_store.current_quest.observe(this.filter_objects), @@ -21,8 +21,8 @@ export class ObjectListView extends EntityListView { } private filter_objects = (): void => { - const quest = quest_editor_store.current_quest.val; - const area = quest_editor_store.current_area.val; + const quest = this.quest_editor_store.current_quest.val; + const area = this.quest_editor_store.current_area.val; const episode = quest ? quest.episode : Episode.I; const area_id = area ? area.id : 0; diff --git a/src/quest_editor/gui/QuestEditorRendererView.ts b/src/quest_editor/gui/QuestEditorRendererView.ts index fa360f79..95ead4e6 100644 --- a/src/quest_editor/gui/QuestEditorRendererView.ts +++ b/src/quest_editor/gui/QuestEditorRendererView.ts @@ -1,18 +1,33 @@ import { QuestRenderer } from "../rendering/QuestRenderer"; -import { quest_editor_store } from "../stores/QuestEditorStore"; +import { QuestEditorStore } from "../stores/QuestEditorStore"; import { QuestEditorModelManager } from "../rendering/QuestEditorModelManager"; import { QuestRendererView } from "./QuestRendererView"; import { QuestEntityControls } from "../rendering/QuestEntityControls"; +import { GuiStore } from "../../core/stores/GuiStore"; export class QuestEditorRendererView extends QuestRendererView { private readonly entity_controls: QuestEntityControls; - constructor() { - super("quest_editor_QuestEditorRendererView", new QuestRenderer(QuestEditorModelManager)); + constructor(gui_store: GuiStore, quest_editor_store: QuestEditorStore) { + super( + gui_store, + quest_editor_store, + "quest_editor_QuestEditorRendererView", + new QuestRenderer( + renderer => + new QuestEditorModelManager( + quest_editor_store.current_quest, + quest_editor_store.current_area, + renderer, + ), + ), + ); this.element.addEventListener("focus", () => quest_editor_store.undo.make_current(), true); - this.entity_controls = this.disposable(new QuestEntityControls(this.renderer)); + this.entity_controls = this.disposable( + new QuestEntityControls(quest_editor_store, this.renderer), + ); this.disposables( quest_editor_store.selected_entity.observe( diff --git a/src/quest_editor/gui/QuestEditorToolBar.ts b/src/quest_editor/gui/QuestEditorToolBar.ts index 866d4381..188aa5be 100644 --- a/src/quest_editor/gui/QuestEditorToolBar.ts +++ b/src/quest_editor/gui/QuestEditorToolBar.ts @@ -1,7 +1,6 @@ import { ToolBar } from "../../core/gui/ToolBar"; import { FileButton } from "../../core/gui/FileButton"; import { Button } from "../../core/gui/Button"; -import { quest_editor_store } from "../stores/QuestEditorStore"; import { undo_manager } from "../../core/undo/UndoManager"; import { Select } from "../../core/gui/Select"; import { list_property, map } from "../../core/observable"; @@ -10,10 +9,11 @@ import { Icon } from "../../core/gui/dom"; import { DropDown } from "../../core/gui/DropDown"; import { Episode } from "../../core/data_formats/parsing/quest/Episode"; import { area_store } from "../stores/AreaStore"; -import { gui_store, GuiTool } from "../../core/stores/GuiStore"; +import { GuiStore, GuiTool } from "../../core/stores/GuiStore"; +import { QuestEditorStore } from "../stores/QuestEditorStore"; export class QuestEditorToolBar extends ToolBar { - constructor() { + constructor(gui_store: GuiStore, quest_editor_store: QuestEditorStore) { const new_quest_button = new DropDown( "New quest", [Episode.I], diff --git a/src/quest_editor/gui/QuestEditorView.ts b/src/quest_editor/gui/QuestEditorView.ts index 3b04fdd5..4afe0864 100644 --- a/src/quest_editor/gui/QuestEditorView.ts +++ b/src/quest_editor/gui/QuestEditorView.ts @@ -10,41 +10,19 @@ import { NpcCountsView } from "./NpcCountsView"; import { QuestEditorRendererView } from "./QuestEditorRendererView"; import { AsmEditorView } from "./AsmEditorView"; import { EntityInfoView } from "./EntityInfoView"; -import { gui_store, GuiTool } from "../../core/stores/GuiStore"; -import { quest_editor_store } from "../stores/QuestEditorStore"; +import { GuiStore, GuiTool } from "../../core/stores/GuiStore"; import { NpcListView } from "./NpcListView"; import { ObjectListView } from "./ObjectListView"; import { EventsView } from "./EventsView"; import { RegistersView } from "./RegistersView"; import { LogView } from "./LogView"; import { QuestRunnerRendererView } from "./QuestRunnerRendererView"; +import { QuestEditorStore } from "../stores/QuestEditorStore"; import Logger = require("js-logger"); +import { AsmEditorStore } from "../stores/AsmEditorStore"; const logger = Logger.get("quest_editor/gui/QuestEditorView"); -// Don't change the values of this map, as they are persisted in the user's browser. -const VIEW_TO_NAME = new Map ResizableWidget, string>([ - [QuestInfoView, "quest_info"], - [NpcCountsView, "npc_counts"], - [QuestEditorRendererView, "quest_renderer"], - [AsmEditorView, "asm_editor"], - [EntityInfoView, "entity_info"], - [NpcListView, "npc_list_view"], - [ObjectListView, "object_list_view"], -]); - -if (gui_store.feature_active("events")) { - VIEW_TO_NAME.set(EventsView, "events_view"); -} - -if (gui_store.feature_active("vm")) { - VIEW_TO_NAME.set(QuestRunnerRendererView, "quest_runner"); - VIEW_TO_NAME.set(LogView, "log_view"); - VIEW_TO_NAME.set(RegistersView, "registers_view"); -} - -const VIEW_WHITE_LIST = [...VIEW_TO_NAME.values()].filter(view => view !== "quest_runner"); - const DEFAULT_LAYOUT_CONFIG = { settings: { showPopoutIcon: false, @@ -62,110 +40,20 @@ const DEFAULT_LAYOUT_CONFIG = { }, }; -const DEFAULT_LAYOUT_CONTENT: ItemConfigType[] = [ - { - type: "row", - content: [ - { - type: "column", - width: 2, - content: [ - { - type: "stack", - content: [ - { - title: "Info", - type: "component", - componentName: VIEW_TO_NAME.get(QuestInfoView), - isClosable: false, - }, - { - title: "NPC Counts", - type: "component", - componentName: VIEW_TO_NAME.get(NpcCountsView), - isClosable: false, - }, - ], - }, - { - title: "Entity", - type: "component", - componentName: VIEW_TO_NAME.get(EntityInfoView), - isClosable: false, - }, - ], - }, - { - type: "stack", - width: 9, - content: [ - { - id: VIEW_TO_NAME.get(QuestEditorRendererView), - title: "3D View", - type: "component", - componentName: VIEW_TO_NAME.get(QuestEditorRendererView), - isClosable: false, - }, - { - title: "Script", - type: "component", - componentName: VIEW_TO_NAME.get(AsmEditorView), - isClosable: false, - }, - ...(gui_store.feature_active("vm") - ? [ - { - title: "Log", - type: "component", - componentName: VIEW_TO_NAME.get(LogView), - isClosable: false, - }, - { - title: "Registers", - type: "component", - componentName: VIEW_TO_NAME.get(RegistersView), - isClosable: false, - }, - ] - : []), - ], - }, - { - type: "stack", - width: 2, - content: [ - { - title: "NPCs", - type: "component", - componentName: VIEW_TO_NAME.get(NpcListView), - isClosable: false, - }, - { - title: "Objects", - type: "component", - componentName: VIEW_TO_NAME.get(ObjectListView), - isClosable: false, - }, - ...(gui_store.feature_active("events") - ? [ - { - title: "Events", - type: "component", - componentName: VIEW_TO_NAME.get(EventsView), - isClosable: false, - }, - ] - : []), - ], - }, - ], - }, -]; - export class QuestEditorView extends ResizableWidget { readonly element = el.div({ class: "quest_editor_QuestEditorView" }); - private readonly tool_bar_view = this.disposable(new QuestEditorToolBar()); + /** + * Maps views to names and creation functions. + */ + private readonly view_map: Map< + new (...args: never) => ResizableWidget, + { name: string; create(): ResizableWidget } + >; + + private readonly view_white_list: readonly string[]; + + private readonly tool_bar: QuestEditorToolBar; private readonly layout_element = create_element("div", { class: "quest_editor_gl_container" }); private readonly layout: Promise; @@ -173,10 +61,85 @@ export class QuestEditorView extends ResizableWidget { private readonly sub_views = new Map(); - constructor() { + constructor( + private readonly gui_store: GuiStore, + quest_editor_store: QuestEditorStore, + asm_editor_store: AsmEditorStore, + ) { super(); - this.element.append(this.tool_bar_view.element, this.layout_element); + // Don't change the values of this map, as they are persisted in the user's browser. + this.view_map = new Map< + new (...args: never) => ResizableWidget, + { name: string; create(): ResizableWidget } + >([ + [ + QuestInfoView, + { name: "quest_info", create: () => new QuestInfoView(quest_editor_store) }, + ], + [ + NpcCountsView, + { name: "npc_counts", create: () => new NpcCountsView(quest_editor_store) }, + ], + [ + QuestEditorRendererView, + { + name: "quest_renderer", + create: () => new QuestEditorRendererView(gui_store, quest_editor_store), + }, + ], + [ + AsmEditorView, + { + name: "asm_editor", + create: () => + new AsmEditorView( + gui_store, + quest_editor_store.quest_runner, + asm_editor_store, + ), + }, + ], + [ + EntityInfoView, + { name: "entity_info", create: () => new EntityInfoView(quest_editor_store) }, + ], + [ + NpcListView, + { name: "npc_list_view", create: () => new NpcListView(quest_editor_store) }, + ], + [ + ObjectListView, + { name: "object_list_view", create: () => new ObjectListView(quest_editor_store) }, + ], + ]); + + if (gui_store.feature_active("events")) { + this.view_map.set(EventsView, { + name: "events_view", + create: () => new EventsView(quest_editor_store), + }); + } + + if (gui_store.feature_active("vm")) { + this.view_map.set(QuestRunnerRendererView, { + name: "quest_runner", + create: () => new QuestRunnerRendererView(gui_store, quest_editor_store), + }); + this.view_map.set(LogView, { name: "log_view", create: () => new LogView() }); + this.view_map.set(RegistersView, { + name: "registers_view", + create: () => new RegistersView(quest_editor_store.quest_runner), + }); + } + + this.view_white_list = [...this.view_map.values()] + .map(({ name }) => name) + .filter(name => name !== "quest_runner"); + + this.tool_bar = this.disposable(new QuestEditorToolBar(gui_store, quest_editor_store)); + + this.element.append(this.tool_bar.element, this.layout_element); this.layout = this.init_golden_layout(); @@ -194,20 +157,20 @@ export class QuestEditorView extends ResizableWidget { if (quest_editor_store.quest_runner.running.val === running) { const runner_items = layout.root.getItemsById( - VIEW_TO_NAME.get(QuestRunnerRendererView)!, + this.view_map.get(QuestRunnerRendererView)!.name, ); if (running) { if (runner_items.length === 0) { const renderer_item = layout.root.getItemsById( - VIEW_TO_NAME.get(QuestEditorRendererView)!, + this.view_map.get(QuestEditorRendererView)!.name, )[0]; renderer_item.parent.addChild({ - id: VIEW_TO_NAME.get(QuestRunnerRendererView), + id: this.view_map.get(QuestRunnerRendererView)!.name, title: "Quest", type: "component", - componentName: VIEW_TO_NAME.get(QuestRunnerRendererView), + componentName: this.view_map.get(QuestRunnerRendererView)!.name, isClosable: false, }); } @@ -234,7 +197,7 @@ export class QuestEditorView extends ResizableWidget { resize(width: number, height: number): this { super.resize(width, height); - const layout_height = Math.max(0, height - this.tool_bar_view.height); + const layout_height = Math.max(0, height - this.tool_bar.height); this.layout_element.style.width = `${width}px`; this.layout_element.style.height = `${layout_height}px`; this.layout.then(layout => layout.updateSize(width, layout_height)); @@ -254,9 +217,11 @@ export class QuestEditorView extends ResizableWidget { } private async init_golden_layout(): Promise { + const default_layout_content = this.get_default_layout_content(); + const content = await quest_editor_ui_persister.load_layout_config( - VIEW_WHITE_LIST, - DEFAULT_LAYOUT_CONTENT, + this.view_white_list, + default_layout_content, ); try { @@ -269,7 +234,7 @@ export class QuestEditorView extends ResizableWidget { return this.attempt_gl_init({ ...DEFAULT_LAYOUT_CONFIG, - content: DEFAULT_LAYOUT_CONTENT, + content: default_layout_content, }); } } @@ -280,15 +245,16 @@ export class QuestEditorView extends ResizableWidget { const self = this; try { - for (const [view_ctor, name] of VIEW_TO_NAME) { - // registerComponent expects a regular function and not an arrow function. - // This function will be called with new. + for (const { name, create } of this.view_map.values()) { + // registerComponent expects a regular function and not an arrow function. This + // function will be called with new. layout.registerComponent(name, function(container: Container) { - const view = new view_ctor(); + const view = create(); container.on("close", () => view.dispose()); container.on("resize", () => - // Subtract 4 from height to work around bug in Golden Layout related to headerHeight. + // Subtract 4 from height to work around bug in Golden Layout related to + // headerHeight. view.resize(container.width, container.height - 4), ); @@ -320,4 +286,106 @@ export class QuestEditorView extends ResizableWidget { throw e; } } + + private get_default_layout_content(): ItemConfigType[] { + return [ + { + type: "row", + content: [ + { + type: "column", + width: 2, + content: [ + { + type: "stack", + content: [ + { + title: "Info", + type: "component", + componentName: this.view_map.get(QuestInfoView)!.name, + isClosable: false, + }, + { + title: "NPC Counts", + type: "component", + componentName: this.view_map.get(NpcCountsView)!.name, + isClosable: false, + }, + ], + }, + { + title: "Entity", + type: "component", + componentName: this.view_map.get(EntityInfoView)!.name, + isClosable: false, + }, + ], + }, + { + type: "stack", + width: 9, + content: [ + { + id: this.view_map.get(QuestEditorRendererView)!.name, + title: "3D View", + type: "component", + componentName: this.view_map.get(QuestEditorRendererView)!.name, + isClosable: false, + }, + { + title: "Script", + type: "component", + componentName: this.view_map.get(AsmEditorView)!.name, + isClosable: false, + }, + ...(this.gui_store.feature_active("vm") + ? [ + { + title: "Log", + type: "component", + componentName: this.view_map.get(LogView)!.name, + isClosable: false, + }, + { + title: "Registers", + type: "component", + componentName: this.view_map.get(RegistersView)!.name, + isClosable: false, + }, + ] + : []), + ], + }, + { + type: "stack", + width: 2, + content: [ + { + title: "NPCs", + type: "component", + componentName: this.view_map.get(NpcListView)!.name, + isClosable: false, + }, + { + title: "Objects", + type: "component", + componentName: this.view_map.get(ObjectListView)!.name, + isClosable: false, + }, + ...(this.gui_store.feature_active("events") + ? [ + { + title: "Events", + type: "component", + componentName: this.view_map.get(EventsView)!.name, + isClosable: false, + }, + ] + : []), + ], + }, + ], + }, + ]; + } } diff --git a/src/quest_editor/gui/QuestInfoView.ts b/src/quest_editor/gui/QuestInfoView.ts index a6c7e6d1..54d94be4 100644 --- a/src/quest_editor/gui/QuestInfoView.ts +++ b/src/quest_editor/gui/QuestInfoView.ts @@ -1,6 +1,5 @@ import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { el } from "../../core/gui/dom"; -import { quest_editor_store } from "../stores/QuestEditorStore"; import { Episode } from "../../core/data_formats/parsing/quest/Episode"; import { NumberInput } from "../../core/gui/NumberInput"; import { Disposer } from "../../core/observable/Disposer"; @@ -8,6 +7,7 @@ import { TextInput } from "../../core/gui/TextInput"; import { TextArea } from "../../core/gui/TextArea"; import "./QuestInfoView.css"; import { DisabledView } from "./DisabledView"; +import { QuestEditorStore } from "../stores/QuestEditorStore"; export class QuestInfoView extends ResizableWidget { readonly element = el.div({ class: "quest_editor_QuestInfoView", tab_index: -1 }); @@ -41,7 +41,7 @@ export class QuestInfoView extends ResizableWidget { private readonly quest_disposer = this.disposable(new Disposer()); - constructor() { + constructor(quest_editor_store: QuestEditorStore) { super(); const quest = quest_editor_store.current_quest; diff --git a/src/quest_editor/gui/QuestRendererView.ts b/src/quest_editor/gui/QuestRendererView.ts index 462a9514..7f4d67ff 100644 --- a/src/quest_editor/gui/QuestRendererView.ts +++ b/src/quest_editor/gui/QuestRendererView.ts @@ -1,9 +1,9 @@ import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { RendererWidget } from "../../core/gui/RendererWidget"; import { QuestRenderer } from "../rendering/QuestRenderer"; -import { gui_store, GuiTool } from "../../core/stores/GuiStore"; -import { quest_editor_store } from "../stores/QuestEditorStore"; +import { GuiStore, GuiTool } from "../../core/stores/GuiStore"; import { el } from "../../core/gui/dom"; +import { QuestEditorStore } from "../stores/QuestEditorStore"; export abstract class QuestRendererView extends ResizableWidget { private readonly renderer_view: RendererWidget; @@ -12,7 +12,12 @@ export abstract class QuestRendererView extends ResizableWidget { readonly element: HTMLElement; - protected constructor(className: string, renderer: QuestRenderer) { + protected constructor( + gui_store: GuiStore, + quest_editor_store: QuestEditorStore, + className: string, + renderer: QuestRenderer, + ) { super(); this.element = el.div({ class: className, tab_index: -1 }); diff --git a/src/quest_editor/gui/QuestRunnerRendererView.ts b/src/quest_editor/gui/QuestRunnerRendererView.ts index b63681a0..08f081b6 100644 --- a/src/quest_editor/gui/QuestRunnerRendererView.ts +++ b/src/quest_editor/gui/QuestRunnerRendererView.ts @@ -1,10 +1,20 @@ import { QuestRenderer } from "../rendering/QuestRenderer"; import { QuestRunnerModelManager } from "../rendering/QuestRunnerModelManager"; import { QuestRendererView } from "./QuestRendererView"; +import { QuestRunner } from "../QuestRunner"; +import { GuiStore } from "../../core/stores/GuiStore"; +import { QuestEditorStore } from "../stores/QuestEditorStore"; export class QuestRunnerRendererView extends QuestRendererView { - constructor() { - super("quest_editor_QuestRunnerRendererView", new QuestRenderer(QuestRunnerModelManager)); + constructor(gui_store: GuiStore, quest_editor_store: QuestEditorStore) { + super( + gui_store, + quest_editor_store, + "quest_editor_QuestRunnerRendererView", + new QuestRenderer( + renderer => new QuestRunnerModelManager(quest_editor_store.quest_runner, renderer), + ), + ); this.renderer.init_camera_controls(); diff --git a/src/quest_editor/gui/RegistersView.ts b/src/quest_editor/gui/RegistersView.ts index 022f9a84..13ce5b5c 100644 --- a/src/quest_editor/gui/RegistersView.ts +++ b/src/quest_editor/gui/RegistersView.ts @@ -1,5 +1,4 @@ import { ResizableWidget } from "../../core/gui/ResizableWidget"; -import { quest_editor_store } from "../stores/QuestEditorStore"; import { el } from "../../core/gui/dom"; import { REGISTER_COUNT } from "../scripting/vm/VirtualMachine"; import { TextInput } from "../../core/gui/TextInput"; @@ -8,6 +7,7 @@ import { CheckBox } from "../../core/gui/CheckBox"; import { number_to_hex_string } from "../../core/util"; import "./RegistersView.css"; import { Select } from "../../core/gui/Select"; +import { QuestRunner } from "../QuestRunner"; enum RegisterDisplayType { Signed, @@ -65,7 +65,7 @@ export class RegistersView extends ResizableWidget { this.container_element, ); - constructor() { + constructor(private readonly quest_runner: QuestRunner) { super(); this.type_select.selected.val = RegisterDisplayType.Unsigned; @@ -96,8 +96,7 @@ export class RegistersView extends ResizableWidget { // predicate that indicates whether to display // placeholder text or the actual register values const should_use_placeholders = (): boolean => - !quest_editor_store.quest_runner.paused.val || - !quest_editor_store.quest_runner.running.val; + !this.quest_runner.paused.val || !this.quest_runner.running.val; // set initial values this.update(should_use_placeholders(), this.hex_checkbox.checked.val); @@ -105,10 +104,10 @@ export class RegistersView extends ResizableWidget { this.disposables( // check if values need to be updated // when QuestRunner execution state changes - quest_editor_store.quest_runner.running.observe(() => + this.quest_runner.running.observe(() => this.update(should_use_placeholders(), this.hex_checkbox.checked.val), ), - quest_editor_store.quest_runner.paused.observe(() => + this.quest_runner.paused.observe(() => this.update(should_use_placeholders(), this.hex_checkbox.checked.val), ), @@ -132,23 +131,23 @@ export class RegistersView extends ResizableWidget { switch (type) { case RegisterDisplayType.Signed: - getter = quest_editor_store.quest_runner.vm.get_register_signed; + getter = this.quest_runner.vm.get_register_signed; break; case RegisterDisplayType.Unsigned: - getter = quest_editor_store.quest_runner.vm.get_register_unsigned; + getter = this.quest_runner.vm.get_register_unsigned; break; case RegisterDisplayType.Word: - getter = quest_editor_store.quest_runner.vm.get_register_word; + getter = this.quest_runner.vm.get_register_word; break; case RegisterDisplayType.Byte: - getter = quest_editor_store.quest_runner.vm.get_register_byte; + getter = this.quest_runner.vm.get_register_byte; break; case RegisterDisplayType.Float: - getter = quest_editor_store.quest_runner.vm.get_register_float; + getter = this.quest_runner.vm.get_register_float; break; } - return getter.bind(quest_editor_store.quest_runner.vm); + return getter.bind(this.quest_runner.vm); } private update(use_placeholders: boolean, use_hex: boolean): void { @@ -162,7 +161,7 @@ export class RegistersView extends ResizableWidget { } else if (use_hex) { for (let i = 0; i < REGISTER_COUNT; i++) { const reg_el = this.register_els[i]; - const reg_val = quest_editor_store.quest_runner.vm.get_register_unsigned(i); + const reg_val = this.quest_runner.vm.get_register_unsigned(i); reg_el.value.set_val(number_to_hex_string(reg_val), { silent: true }); } diff --git a/src/quest_editor/index.ts b/src/quest_editor/index.ts new file mode 100644 index 00000000..2f042532 --- /dev/null +++ b/src/quest_editor/index.ts @@ -0,0 +1,11 @@ +import { QuestEditorView } from "./gui/QuestEditorView"; +import { GuiStore } from "../core/stores/GuiStore"; +import { QuestEditorStore } from "./stores/QuestEditorStore"; +import { AsmEditorStore } from "./stores/AsmEditorStore"; + +export function initialize_quest_editor(gui_store: GuiStore): QuestEditorView { + const quest_editor_store = new QuestEditorStore(gui_store); + const asm_editor_store = new AsmEditorStore(quest_editor_store); + + return new QuestEditorView(gui_store, quest_editor_store, asm_editor_store); +} diff --git a/src/quest_editor/persistence/QuestEditorUiPersister.ts b/src/quest_editor/persistence/QuestEditorUiPersister.ts index b090e206..f04a8e0b 100644 --- a/src/quest_editor/persistence/QuestEditorUiPersister.ts +++ b/src/quest_editor/persistence/QuestEditorUiPersister.ts @@ -14,7 +14,7 @@ export class QuestEditorUiPersister extends Persister { ); async load_layout_config( - components: string[], + components: readonly string[], default_config: GoldenLayout.ItemConfigType[], ): Promise { const config = await this.load(LAYOUT_CONFIG_KEY); @@ -28,7 +28,7 @@ export class QuestEditorUiPersister extends Persister { private verify_layout_config( config: GoldenLayout.ItemConfigType[], - components: string[], + components: readonly string[], ): boolean { const set = new Set(components); diff --git a/src/quest_editor/rendering/QuestEditorModelManager.ts b/src/quest_editor/rendering/QuestEditorModelManager.ts index dee3bcec..599fba1d 100644 --- a/src/quest_editor/rendering/QuestEditorModelManager.ts +++ b/src/quest_editor/rendering/QuestEditorModelManager.ts @@ -1,28 +1,34 @@ import { QuestRenderer } from "./QuestRenderer"; -import { quest_editor_store } from "../stores/QuestEditorStore"; import { AreaVariantDetails, QuestModelManager } from "./QuestModelManager"; import { AreaVariantModel } from "../model/AreaVariantModel"; import { QuestNpcModel } from "../model/QuestNpcModel"; import { QuestObjectModel } from "../model/QuestObjectModel"; import { ListProperty } from "../../core/observable/property/list/ListProperty"; import { list_property } from "../../core/observable"; +import { Property } from "../../core/observable/property/Property"; +import { QuestModel } from "../model/QuestModel"; +import { AreaModel } from "../model/AreaModel"; /** * Model loader used while editing a quest. */ export class QuestEditorModelManager extends QuestModelManager { - constructor(renderer: QuestRenderer) { + constructor( + private readonly current_quest: Property, + private readonly current_area: Property, + renderer: QuestRenderer, + ) { super(renderer); this.disposer.add_all( - quest_editor_store.current_quest.observe(this.area_variant_changed), - quest_editor_store.current_area.observe(this.area_variant_changed), + current_quest.observe(this.area_variant_changed), + current_area.observe(this.area_variant_changed), ); } protected get_area_variant_details(): AreaVariantDetails { - const quest = quest_editor_store.current_quest.val; - const area = quest_editor_store.current_area.val; + const quest = this.current_quest.val; + const area = this.current_area.val; let area_variant: AreaVariantModel | undefined; let npcs: ListProperty; diff --git a/src/quest_editor/rendering/QuestEntityControls.ts b/src/quest_editor/rendering/QuestEntityControls.ts index e834a489..96fda185 100644 --- a/src/quest_editor/rendering/QuestEntityControls.ts +++ b/src/quest_editor/rendering/QuestEntityControls.ts @@ -1,7 +1,6 @@ import { QuestEntityModel } from "../model/QuestEntityModel"; import { Euler, Intersection, Mesh, Plane, Quaternion, Raycaster, Vector2, Vector3 } from "three"; import { QuestRenderer } from "./QuestRenderer"; -import { quest_editor_store } from "../stores/QuestEditorStore"; import { EntityUserData } from "./conversion/entities"; import { QuestNpcModel } from "../model/QuestNpcModel"; import { AreaUserData } from "./conversion/areas"; @@ -18,6 +17,7 @@ import { import { QuestObjectModel } from "../model/QuestObjectModel"; import { AreaModel } from "../model/AreaModel"; import { QuestModel } from "../model/QuestModel"; +import { QuestEditorStore } from "../stores/QuestEditorStore"; const ZERO_VECTOR = Object.freeze(new Vector3(0, 0, 0)); const UP_VECTOR = Object.freeze(new Vector3(0, 1, 0)); @@ -42,10 +42,13 @@ export class QuestEntityControls implements Disposable { set enabled(enabled: boolean) { this._enabled = enabled; this.state.cancel(); - this.state = new IdleState(this.renderer, this._enabled); + this.state = new IdleState(this.quest_editor_store, this.renderer, this._enabled); } - constructor(private readonly renderer: QuestRenderer) { + constructor( + private readonly quest_editor_store: QuestEditorStore, + private readonly renderer: QuestRenderer, + ) { this.disposer.add(quest_editor_store.selected_entity.observe(this.selected_entity_changed)); renderer.dom_element.addEventListener("keydown", this.keydown); @@ -57,7 +60,7 @@ export class QuestEntityControls implements Disposable { add_entity_dnd_listener(renderer.dom_element, "dragleave", this.dragleave); add_entity_dnd_listener(renderer.dom_element, "drop", this.drop); - this.state = new IdleState(renderer, this._enabled); + this.state = new IdleState(this.quest_editor_store, renderer, this._enabled); } dispose = (): void => { @@ -304,16 +307,20 @@ interface State { } class IdleState implements State { - constructor(private readonly renderer: QuestRenderer, private readonly enabled: boolean) {} + constructor( + private readonly quest_editor_store: QuestEditorStore, + private readonly renderer: QuestRenderer, + private readonly enabled: boolean, + ) {} process_event(evt: Evt): State { switch (evt.type) { case EvtType.KeyDown: { if (this.enabled) { - const entity = quest_editor_store.selected_entity.val; + const entity = this.quest_editor_store.selected_entity.val; if (entity && evt.key === "Delete") { - quest_editor_store.remove_entity(entity); + this.quest_editor_store.remove_entity(entity); } } @@ -325,10 +332,11 @@ class IdleState implements State { if (pick) { if (evt.buttons === 1) { - quest_editor_store.set_selected_entity(pick.entity); + this.quest_editor_store.set_selected_entity(pick.entity); if (this.enabled) { return new TranslationState( + this.quest_editor_store, this.renderer, pick.entity, pick.drag_adjust, @@ -336,10 +344,11 @@ class IdleState implements State { ); } } else if (evt.buttons === 2) { - quest_editor_store.set_selected_entity(pick.entity); + this.quest_editor_store.set_selected_entity(pick.entity); if (this.enabled) { return new RotationState( + this.quest_editor_store, this.renderer, pick.entity, pick.mesh, @@ -365,7 +374,7 @@ class IdleState implements State { !evt.moved_since_last_pointer_down && !this.pick_entity(evt.pointer_device_position) ) { - quest_editor_store.set_selected_entity(undefined); + this.quest_editor_store.set_selected_entity(undefined); } return this; @@ -378,12 +387,18 @@ class IdleState implements State { case EvtType.EntityDragEnter: { if (this.enabled) { - const area = quest_editor_store.current_area.val; - const quest = quest_editor_store.current_quest.val; + const area = this.quest_editor_store.current_area.val; + const quest = this.quest_editor_store.current_quest.val; if (!area || !quest) return this; - return new CreationState(this.renderer, evt, quest, area); + return new CreationState( + this.quest_editor_store, + this.renderer, + evt, + quest, + area, + ); } else { return this; } @@ -440,6 +455,7 @@ class TranslationState implements State { private cancelled = false; constructor( + private readonly quest_editor_store: QuestEditorStore, private readonly renderer: QuestRenderer, private readonly entity: QuestEntityModel, private readonly drag_adjust: Vector3, @@ -454,7 +470,7 @@ class TranslationState implements State { switch (evt.type) { case EvtType.MouseMove: { if (this.cancelled) { - return new IdleState(this.renderer, true); + return new IdleState(this.quest_editor_store, this.renderer, true); } if (evt.moved_since_last_pointer_down) { @@ -475,7 +491,7 @@ class TranslationState implements State { this.renderer.controls.enabled = true; if (!this.cancelled && evt.moved_since_last_pointer_down) { - quest_editor_store.translate_entity( + this.quest_editor_store.translate_entity( this.entity, this.initial_section, this.entity.section.val, @@ -485,11 +501,13 @@ class TranslationState implements State { ); } - return new IdleState(this.renderer, true); + return new IdleState(this.quest_editor_store, this.renderer, true); } default: - return this.cancelled ? new IdleState(this.renderer, true) : this; + return this.cancelled + ? new IdleState(this.quest_editor_store, this.renderer, true) + : this; } } @@ -512,6 +530,7 @@ class RotationState implements State { private cancelled = false; constructor( + private readonly quest_editor_store: QuestEditorStore, private readonly renderer: QuestRenderer, private readonly entity: QuestEntityModel, private readonly mesh: Mesh, @@ -526,7 +545,7 @@ class RotationState implements State { switch (evt.type) { case EvtType.MouseMove: { if (this.cancelled) { - return new IdleState(this.renderer, true); + return new IdleState(this.quest_editor_store, this.renderer, true); } if (evt.moved_since_last_pointer_down) { @@ -547,7 +566,7 @@ class RotationState implements State { this.renderer.controls.enabled = true; if (!this.cancelled && evt.moved_since_last_pointer_down) { - quest_editor_store.rotate_entity( + this.quest_editor_store.rotate_entity( this.entity, this.initial_rotation, this.entity.world_rotation.val, @@ -555,11 +574,13 @@ class RotationState implements State { ); } - return new IdleState(this.renderer, true); + return new IdleState(this.quest_editor_store, this.renderer, true); } default: - return this.cancelled ? new IdleState(this.renderer, true) : this; + return this.cancelled + ? new IdleState(this.quest_editor_store, this.renderer, true) + : this; } } @@ -577,7 +598,13 @@ class CreationState implements State { private readonly drag_adjust = new Vector3(0, 0, 0); private cancelled = false; - constructor(renderer: QuestRenderer, evt: EntityDragEvt, quest: QuestModel, area: AreaModel) { + constructor( + private readonly quest_editor_store: QuestEditorStore, + renderer: QuestRenderer, + evt: EntityDragEvt, + quest: QuestModel, + area: AreaModel, + ) { this.renderer = renderer; evt.drag_element.style.display = "none"; @@ -637,7 +664,7 @@ class CreationState implements State { ); quest.add_entity(this.entity); - quest_editor_store.set_selected_entity(this.entity); + this.quest_editor_store.set_selected_entity(this.entity); } process_event(evt: Evt): State { @@ -650,7 +677,7 @@ class CreationState implements State { evt.data_transfer.dropEffect = "none"; } - return new IdleState(this.renderer, true); + return new IdleState(this.quest_editor_store, this.renderer, true); } evt.stop_propagation(); @@ -676,21 +703,21 @@ class CreationState implements State { case EvtType.EntityDragLeave: { evt.drag_element.style.display = "flex"; - const quest = quest_editor_store.current_quest.val; + const quest = this.quest_editor_store.current_quest.val; if (quest) { quest.remove_entity(this.entity); } - return new IdleState(this.renderer, true); + return new IdleState(this.quest_editor_store, this.renderer, true); } case EvtType.EntityDrop: { if (!this.cancelled) { - quest_editor_store.push_create_entity_action(this.entity); + this.quest_editor_store.push_create_entity_action(this.entity); } - return new IdleState(this.renderer, true); + return new IdleState(this.quest_editor_store, this.renderer, true); } default: @@ -701,7 +728,7 @@ class CreationState implements State { cancel(): void { this.cancelled = true; - const quest = quest_editor_store.current_quest.val; + const quest = this.quest_editor_store.current_quest.val; if (quest) { quest.remove_entity(this.entity); diff --git a/src/quest_editor/rendering/QuestRenderer.ts b/src/quest_editor/rendering/QuestRenderer.ts index b474043c..5a7fdcea 100644 --- a/src/quest_editor/rendering/QuestRenderer.ts +++ b/src/quest_editor/rendering/QuestRenderer.ts @@ -52,10 +52,10 @@ export class QuestRenderer extends Renderer { selected_entity: QuestEntityModel | undefined; - constructor(ModelManager: new (renderer: QuestRenderer) => QuestModelManager) { + constructor(create_model_manager: (renderer: QuestRenderer) => QuestModelManager) { super(); - this.disposer.add(new ModelManager(this)); + this.disposer.add(create_model_manager(this)); } /** diff --git a/src/quest_editor/rendering/QuestRunnerModelManager.ts b/src/quest_editor/rendering/QuestRunnerModelManager.ts index cff4d23e..57e5fac1 100644 --- a/src/quest_editor/rendering/QuestRunnerModelManager.ts +++ b/src/quest_editor/rendering/QuestRunnerModelManager.ts @@ -1,24 +1,23 @@ import { QuestRenderer } from "./QuestRenderer"; -import { quest_editor_store } from "../stores/QuestEditorStore"; import { AreaVariantDetails, QuestModelManager } from "./QuestModelManager"; +import { QuestRunner } from "../QuestRunner"; /** * Model loader used while running a quest. */ export class QuestRunnerModelManager extends QuestModelManager { - constructor(renderer: QuestRenderer) { + constructor(private readonly quest_runner: QuestRunner, renderer: QuestRenderer) { super(renderer); this.disposer.add_all( - quest_editor_store.quest_runner.game_state.current_area_variant.observe( - this.area_variant_changed, - { call_now: true }, - ), + this.quest_runner.game_state.current_area_variant.observe(this.area_variant_changed, { + call_now: true, + }), ); } protected get_area_variant_details(): AreaVariantDetails { - const game_state = quest_editor_store.quest_runner.game_state; + const game_state = this.quest_runner.game_state; return { episode: game_state.episode, diff --git a/src/quest_editor/stores/AsmEditorStore.ts b/src/quest_editor/stores/AsmEditorStore.ts index f3870fc6..a7086549 100644 --- a/src/quest_editor/stores/AsmEditorStore.ts +++ b/src/quest_editor/stores/AsmEditorStore.ts @@ -3,7 +3,6 @@ import { AssemblyAnalyser } from "../scripting/AssemblyAnalyser"; import { Disposable } from "../../core/observable/Disposable"; import { Disposer } from "../../core/observable/Disposer"; import { SimpleUndo } from "../../core/undo/SimpleUndo"; -import { quest_editor_store } from "./QuestEditorStore"; import { ASM_SYNTAX } from "./asm_syntax"; import { AssemblyError, AssemblyWarning } from "../scripting/assembly"; import { Observable } from "../../core/observable/Observable"; @@ -18,6 +17,7 @@ import SignatureHelpResult = languages.SignatureHelpResult; import LocationLink = languages.LocationLink; import IModelContentChange = editor.IModelContentChange; import { Breakpoint } from "../scripting/vm/Debugger"; +import { QuestEditorStore } from "./QuestEditorStore"; const assembly_analyser = new AssemblyAnalyser(); @@ -104,11 +104,13 @@ export class AsmEditorStore implements Disposable { readonly has_issues: Property = assembly_analyser.issues.map( issues => issues.warnings.length + issues.errors.length > 0, ); - readonly breakpoints: ListProperty = quest_editor_store.quest_runner.breakpoints; - readonly execution_location: Property = - quest_editor_store.quest_runner.pause_location; + readonly breakpoints: ListProperty; + readonly execution_location: Property; + + constructor(private readonly quest_editor_store: QuestEditorStore) { + this.breakpoints = quest_editor_store.quest_runner.breakpoints; + this.execution_location = quest_editor_store.quest_runner.pause_location; - constructor() { this.disposer.add_all( quest_editor_store.current_quest.observe(this.quest_changed, { call_now: true, @@ -231,7 +233,7 @@ export class AsmEditorStore implements Disposable { this.undo.reset(); this.model_disposer.dispose_all(); - const quest = quest_editor_store.current_quest.val; + const quest = this.quest_editor_store.current_quest.val; if (quest) { const manual_stack = !this.inline_args_mode.val; @@ -272,7 +274,7 @@ export class AsmEditorStore implements Disposable { // Line numbers can't go lower than 1. const new_line_num = Math.max(line_num - num_removed_lines, 1); - if (quest_editor_store.quest_runner.remove_breakpoint(line_num)) { + if (this.quest_editor_store.quest_runner.remove_breakpoint(line_num)) { new_breakpoints.push(new_line_num); } } @@ -281,7 +283,9 @@ export class AsmEditorStore implements Disposable { // number of removed lines. for (const breakpoint of this.breakpoints.val) { if (breakpoint.line_no > change.range.endLineNumber) { - quest_editor_store.quest_runner.remove_breakpoint(breakpoint.line_no); + this.quest_editor_store.quest_runner.remove_breakpoint( + breakpoint.line_no, + ); new_breakpoints.push(breakpoint.line_no - num_removed_lines); } } @@ -294,7 +298,9 @@ export class AsmEditorStore implements Disposable { // forwards by the number of added lines for (const breakpoint of this.breakpoints.val) { if (breakpoint.line_no > change.range.endLineNumber) { - quest_editor_store.quest_runner.remove_breakpoint(breakpoint.line_no); + this.quest_editor_store.quest_runner.remove_breakpoint( + breakpoint.line_no, + ); new_breakpoints.push(breakpoint.line_no + num_added_lines); } } @@ -302,10 +308,8 @@ export class AsmEditorStore implements Disposable { } for (const breakpoint of new_breakpoints) { - quest_editor_store.quest_runner.set_breakpoint(breakpoint); + this.quest_editor_store.quest_runner.set_breakpoint(breakpoint); } } } } - -export const asm_editor_store = new AsmEditorStore(); diff --git a/src/quest_editor/stores/QuestEditorStore.ts b/src/quest_editor/stores/QuestEditorStore.ts index 2a7dcc69..4c4da60e 100644 --- a/src/quest_editor/stores/QuestEditorStore.ts +++ b/src/quest_editor/stores/QuestEditorStore.ts @@ -13,7 +13,7 @@ import { SectionModel } from "../model/SectionModel"; import { QuestEntityModel } from "../model/QuestEntityModel"; import { Disposable } from "../../core/observable/Disposable"; import { Disposer } from "../../core/observable/Disposer"; -import { gui_store, GuiTool } from "../../core/stores/GuiStore"; +import { GuiStore, GuiTool } from "../../core/stores/GuiStore"; import { UndoStack } from "../../core/undo/UndoStack"; import { TranslateEntityAction } from "../actions/TranslateEntityAction"; import { EditShortDescriptionAction } from "../actions/EditShortDescriptionAction"; @@ -48,7 +48,7 @@ export class QuestEditorStore implements Disposable { readonly current_area: Property = this._current_area; readonly selected_entity: Property = this._selected_entity; - constructor() { + constructor(gui_store: GuiStore) { this.disposer.add_all( gui_store.tool.observe( ({ value: tool }) => { @@ -181,6 +181,7 @@ export class QuestEditorStore implements Disposable { this.undo .push( new TranslateEntityAction( + this, entity, old_section, new_section, @@ -198,15 +199,17 @@ export class QuestEditorStore implements Disposable { new_rotation: Euler, world: boolean, ): void => { - this.undo.push(new RotateEntityAction(entity, old_rotation, new_rotation, world)).redo(); + this.undo + .push(new RotateEntityAction(this, entity, old_rotation, new_rotation, world)) + .redo(); }; push_create_entity_action = (entity: QuestEntityModel): void => { - this.undo.push(new CreateEntityAction(entity)); + this.undo.push(new CreateEntityAction(this, entity)); }; remove_entity = (entity: QuestEntityModel): void => { - this.undo.push(new RemoveEntityAction(entity)).redo(); + this.undo.push(new RemoveEntityAction(this, entity)).redo(); }; private async set_quest(quest?: QuestModel, filename?: string): Promise { @@ -268,5 +271,3 @@ export class QuestEditorStore implements Disposable { } }; } - -export const quest_editor_store = new QuestEditorStore(); diff --git a/src/viewer/gui/TextureView.ts b/src/viewer/gui/TextureView.ts index 0b826db8..40b1ef2c 100644 --- a/src/viewer/gui/TextureView.ts +++ b/src/viewer/gui/TextureView.ts @@ -2,10 +2,10 @@ import { el, Icon } from "../../core/gui/dom"; import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { FileButton } from "../../core/gui/FileButton"; import { ToolBar } from "../../core/gui/ToolBar"; -import { texture_store } from "../stores/TextureStore"; import { RendererWidget } from "../../core/gui/RendererWidget"; import { TextureRenderer } from "../rendering/TextureRenderer"; -import { gui_store, GuiTool } from "../../core/stores/GuiStore"; +import { GuiStore, GuiTool } from "../../core/stores/GuiStore"; +import { TextureStore } from "../stores/TextureStore"; export class TextureView extends ResizableWidget { readonly element = el.div({ class: "viewer_TextureView" }); @@ -17,11 +17,15 @@ export class TextureView extends ResizableWidget { private readonly tool_bar = this.disposable(new ToolBar({ children: [this.open_file_button] })); - private readonly renderer_view = this.disposable(new RendererWidget(new TextureRenderer())); + private readonly renderer_view: RendererWidget; - constructor() { + constructor(gui_store: GuiStore, texture_store: TextureStore) { super(); + this.renderer_view = this.disposable( + new RendererWidget(new TextureRenderer(texture_store)), + ); + this.element.append(this.tool_bar.element, this.renderer_view.element); this.disposable( diff --git a/src/viewer/gui/ViewerView.ts b/src/viewer/gui/ViewerView.ts index 7b0f933f..3f660f67 100644 --- a/src/viewer/gui/ViewerView.ts +++ b/src/viewer/gui/ViewerView.ts @@ -1,23 +1,24 @@ import { TabContainer } from "../../core/gui/TabContainer"; +import { Model3DView } from "./model_3d/Model3DView"; +import { TextureView } from "./TextureView"; export class ViewerView extends TabContainer { - constructor() { + constructor( + create_model_3d_view: () => Promise, + create_texture_view: () => Promise, + ) { super({ class: "viewer_ViewerView", tabs: [ { title: "Models", key: "model", - create_view: async function() { - return new (await import("./model_3d/Model3DView")).Model3DView(); - }, + create_view: create_model_3d_view, }, { title: "Textures", key: "texture", - create_view: async function() { - return new (await import("./TextureView")).TextureView(); - }, + create_view: create_texture_view, }, ], }); diff --git a/src/viewer/gui/model_3d/Model3DToolBar.ts b/src/viewer/gui/model_3d/Model3DToolBar.ts index 77d25176..b8f4a446 100644 --- a/src/viewer/gui/model_3d/Model3DToolBar.ts +++ b/src/viewer/gui/model_3d/Model3DToolBar.ts @@ -3,12 +3,12 @@ import { FileButton } from "../../../core/gui/FileButton"; import { CheckBox } from "../../../core/gui/CheckBox"; import { NumberInput } from "../../../core/gui/NumberInput"; import { PSO_FRAME_RATE } from "../../../core/rendering/conversion/ninja_animation"; -import { model_store } from "../../stores/Model3DStore"; import { Label } from "../../../core/gui/Label"; import { Icon } from "../../../core/gui/dom"; +import { Model3DStore } from "../../stores/Model3DStore"; export class Model3DToolBar extends ToolBar { - constructor() { + constructor(model_3d_store: Model3DStore) { const open_file_button = new FileButton("Open file...", { icon_left: Icon.File, accept: ".nj, .njm, .xj, .xvm", @@ -24,11 +24,11 @@ export class Model3DToolBar extends ToolBar { const animation_frame_input = new NumberInput(1, { label: "Frame:", min: 1, - max: model_store.animation_frame_count, + max: model_3d_store.animation_frame_count, step: 1, }); const animation_frame_count_label = new Label( - model_store.animation_frame_count.map(count => `/ ${count}`), + model_3d_store.animation_frame_count.map(count => `/ ${count}`), ); super({ @@ -45,33 +45,35 @@ export class Model3DToolBar extends ToolBar { // Always-enabled controls. this.disposables( open_file_button.files.observe(({ value: files }) => { - if (files.length) model_store.load_file(files[0]); + if (files.length) model_3d_store.load_file(files[0]); }), - skeleton_checkbox.checked.observe(({ value }) => model_store.set_show_skeleton(value)), + skeleton_checkbox.checked.observe(({ value }) => + model_3d_store.set_show_skeleton(value), + ), ); // Controls that are only enabled when an animation is selected. - const enabled = model_store.current_nj_motion.map(njm => njm != undefined); + const enabled = model_3d_store.current_nj_motion.map(njm => njm != undefined); this.disposables( play_animation_checkbox.enabled.bind_to(enabled), - play_animation_checkbox.checked.bind_to(model_store.animation_playing), + play_animation_checkbox.checked.bind_to(model_3d_store.animation_playing), play_animation_checkbox.checked.observe(({ value }) => - model_store.set_animation_playing(value), + model_3d_store.set_animation_playing(value), ), animation_frame_rate_input.enabled.bind_to(enabled), animation_frame_rate_input.value.observe(({ value }) => - model_store.set_animation_frame_rate(value), + model_3d_store.set_animation_frame_rate(value), ), animation_frame_input.enabled.bind_to(enabled), animation_frame_input.value.bind_to( - model_store.animation_frame.map(v => Math.round(v)), + model_3d_store.animation_frame.map(v => Math.round(v)), ), animation_frame_input.value.observe(({ value }) => - model_store.set_animation_frame(value), + model_3d_store.set_animation_frame(value), ), animation_frame_count_label.enabled.bind_to(enabled), diff --git a/src/viewer/gui/model_3d/Model3DView.ts b/src/viewer/gui/model_3d/Model3DView.ts index 9a8ca9de..0afa1a07 100644 --- a/src/viewer/gui/model_3d/Model3DView.ts +++ b/src/viewer/gui/model_3d/Model3DView.ts @@ -1,14 +1,14 @@ import { el } from "../../../core/gui/dom"; import { ResizableWidget } from "../../../core/gui/ResizableWidget"; import "./Model3DView.css"; -import { gui_store, GuiTool } from "../../../core/stores/GuiStore"; +import { GuiStore, GuiTool } from "../../../core/stores/GuiStore"; import { RendererWidget } from "../../../core/gui/RendererWidget"; -import { model_store } from "../../stores/Model3DStore"; import { Model3DRenderer } from "../../rendering/Model3DRenderer"; import { Model3DToolBar } from "./Model3DToolBar"; import { Model3DSelectListView } from "./Model3DSelectListView"; import { CharacterClassModel } from "../../model/CharacterClassModel"; import { CharacterClassAnimationModel } from "../../model/CharacterClassAnimationModel"; +import { Model3DStore } from "../../stores/Model3DStore"; const MODEL_LIST_WIDTH = 100; const ANIMATION_LIST_WIDTH = 140; @@ -21,25 +21,27 @@ export class Model3DView extends ResizableWidget { private animation_list_view: Model3DSelectListView; private renderer_view: RendererWidget; - constructor() { + constructor(gui_store: GuiStore, model_3d_store: Model3DStore) { super(); - this.tool_bar_view = this.disposable(new Model3DToolBar()); + this.tool_bar_view = this.disposable(new Model3DToolBar(model_3d_store)); this.model_list_view = this.disposable( new Model3DSelectListView( - model_store.models, - model_store.current_model, - model_store.set_current_model, + model_3d_store.models, + model_3d_store.current_model, + model_3d_store.set_current_model, ), ); this.animation_list_view = this.disposable( new Model3DSelectListView( - model_store.animations, - model_store.current_animation, - model_store.set_current_animation, + model_3d_store.animations, + model_3d_store.current_animation, + model_3d_store.set_current_animation, ), ); - this.renderer_view = this.disposable(new RendererWidget(new Model3DRenderer())); + this.renderer_view = this.disposable( + new RendererWidget(new Model3DRenderer(model_3d_store)), + ); this.animation_list_view.borders = true; @@ -53,7 +55,7 @@ export class Model3DView extends ResizableWidget { ), ); - model_store.set_current_model(model_store.models[5]); + model_3d_store.set_current_model(model_3d_store.models[5]); this.renderer_view.start_rendering(); diff --git a/src/viewer/index.ts b/src/viewer/index.ts new file mode 100644 index 00000000..2562e479 --- /dev/null +++ b/src/viewer/index.ts @@ -0,0 +1,18 @@ +import { ViewerView } from "./gui/ViewerView"; +import { GuiStore } from "../core/stores/GuiStore"; + +export function initialize_viewer(gui_store: GuiStore): ViewerView { + return new ViewerView( + async () => { + const { Model3DStore } = await import("./stores/Model3DStore"); + const { Model3DView } = await import("./gui/model_3d/Model3DView"); + return new Model3DView(gui_store, new Model3DStore()); + }, + + async () => { + const { TextureStore } = await import("./stores/TextureStore"); + const { TextureView } = await import("./gui/TextureView"); + return new TextureView(gui_store, new TextureStore()); + }, + ); +} diff --git a/src/viewer/rendering/Model3DRenderer.ts b/src/viewer/rendering/Model3DRenderer.ts index bee16c55..5ecb943d 100644 --- a/src/viewer/rendering/Model3DRenderer.ts +++ b/src/viewer/rendering/Model3DRenderer.ts @@ -12,7 +12,6 @@ import { SkinnedMesh, Vector3, } from "three"; -import { model_store } from "../stores/Model3DStore"; import { Disposable } from "../../core/observable/Disposable"; import { NjMotion } from "../../core/data_formats/parsing/ninja/motion"; import { xvm_to_textures } from "../../core/rendering/conversion/ninja_textures"; @@ -25,6 +24,7 @@ import { import { Renderer } from "../../core/rendering/Renderer"; import { Disposer } from "../../core/observable/Disposer"; import { ChangeEvent } from "../../core/observable/Observable"; +import { Model3DStore } from "../stores/Model3DStore"; export class Model3DRenderer extends Renderer implements Disposable { private readonly disposer = new Disposer(); @@ -40,17 +40,17 @@ export class Model3DRenderer extends Renderer implements Disposable { readonly camera = new PerspectiveCamera(75, 1, 1, 200); - constructor() { + constructor(private readonly model_3d_store: Model3DStore) { super(); this.disposer.add_all( - model_store.current_nj_data.observe(this.nj_data_or_xvm_changed), - model_store.current_xvm.observe(this.nj_data_or_xvm_changed), - model_store.current_nj_motion.observe(this.nj_motion_changed), - model_store.show_skeleton.observe(this.show_skeleton_changed), - model_store.animation_playing.observe(this.animation_playing_changed), - model_store.animation_frame_rate.observe(this.animation_frame_rate_changed), - model_store.animation_frame.observe(this.animation_frame_changed), + model_3d_store.current_nj_data.observe(this.nj_data_or_xvm_changed), + model_3d_store.current_xvm.observe(this.nj_data_or_xvm_changed), + model_3d_store.current_nj_motion.observe(this.nj_motion_changed), + model_3d_store.show_skeleton.observe(this.show_skeleton_changed), + model_3d_store.animation_playing.observe(this.animation_playing_changed), + model_3d_store.animation_frame_rate.observe(this.animation_frame_rate_changed), + model_3d_store.animation_frame.observe(this.animation_frame_changed), ); this.init_camera_controls(); @@ -95,14 +95,14 @@ export class Model3DRenderer extends Renderer implements Disposable { this.animation = undefined; } - const nj_data = model_store.current_nj_data.val; + const nj_data = this.model_3d_store.current_nj_data.val; if (nj_data) { const { nj_object, has_skeleton } = nj_data; let mesh: Mesh; - const xvm = model_store.current_xvm.val; + const xvm = this.model_3d_store.current_xvm.val; const textures = xvm ? xvm_to_textures(xvm) : undefined; const materials = @@ -132,7 +132,7 @@ export class Model3DRenderer extends Renderer implements Disposable { this.scene.add(mesh); this.skeleton_helper = new SkeletonHelper(mesh); - this.skeleton_helper.visible = model_store.show_skeleton.val; + this.skeleton_helper.visible = this.model_3d_store.show_skeleton.val; (this.skeleton_helper.material as any).linewidth = 3; this.scene.add(this.skeleton_helper); @@ -150,7 +150,7 @@ export class Model3DRenderer extends Renderer implements Disposable { mixer = this.animation.mixer; } - const nj_data = model_store.current_nj_data.val; + const nj_data = this.model_3d_store.current_nj_data.val; if (!this.mesh || !(this.mesh instanceof SkinnedMesh) || !nj_motion || !nj_data) return; @@ -198,7 +198,7 @@ export class Model3DRenderer extends Renderer implements Disposable { }; private animation_frame_changed = ({ value: frame }: ChangeEvent): void => { - const nj_motion = model_store.current_nj_motion.val; + const nj_motion = this.model_3d_store.current_nj_motion.val; if (this.animation && nj_motion) { const frame_count = nj_motion.frame_count; @@ -217,7 +217,7 @@ export class Model3DRenderer extends Renderer implements Disposable { if (this.animation && !this.animation.action.paused) { const time = this.animation.action.time; this.update_animation_time = false; - model_store.set_animation_frame(time * PSO_FRAME_RATE + 1); + this.model_3d_store.set_animation_frame(time * PSO_FRAME_RATE + 1); this.update_animation_time = true; } } diff --git a/src/viewer/rendering/TextureRenderer.ts b/src/viewer/rendering/TextureRenderer.ts index 975237f8..5546ea67 100644 --- a/src/viewer/rendering/TextureRenderer.ts +++ b/src/viewer/rendering/TextureRenderer.ts @@ -12,7 +12,7 @@ import { Renderer } from "../../core/rendering/Renderer"; import { Disposer } from "../../core/observable/Disposer"; import { Xvm } from "../../core/data_formats/parsing/ninja/texture"; import { xvm_texture_to_texture } from "../../core/rendering/conversion/ninja_textures"; -import { texture_store } from "../stores/TextureStore"; +import { TextureStore } from "../stores/TextureStore"; import Logger = require("js-logger"); const logger = Logger.get("viewer/rendering/TextureRenderer"); @@ -23,7 +23,7 @@ export class TextureRenderer extends Renderer implements Disposable { readonly camera = new OrthographicCamera(-400, 400, 300, -300, 1, 10); - constructor() { + constructor(texture_store: TextureStore) { super(); this.disposer.add_all( diff --git a/src/viewer/stores/Model3DStore.ts b/src/viewer/stores/Model3DStore.ts index 767c710d..f16dd399 100644 --- a/src/viewer/stores/Model3DStore.ts +++ b/src/viewer/stores/Model3DStore.ts @@ -278,5 +278,3 @@ export class Model3DStore implements Disposable { } } } - -export const model_store = new Model3DStore(); diff --git a/src/viewer/stores/TextureStore.ts b/src/viewer/stores/TextureStore.ts index 614c81a6..5223e66d 100644 --- a/src/viewer/stores/TextureStore.ts +++ b/src/viewer/stores/TextureStore.ts @@ -21,5 +21,3 @@ export class TextureStore { } }; } - -export const texture_store = new TextureStore();