Most dependencies are now injected to improve testability.

This commit is contained in:
Daan Vanden Bosch 2019-12-21 19:40:42 +01:00
parent 063d524a7b
commit 8ce19fac62
57 changed files with 857 additions and 546 deletions

View File

@ -2,20 +2,28 @@ import { NavigationView } from "./NavigationView";
import { MainContentView } from "./MainContentView"; import { MainContentView } from "./MainContentView";
import { el } from "../../core/gui/dom"; import { el } from "../../core/gui/dom";
import { ResizableWidget } from "../../core/gui/ResizableWidget"; 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 { export class ApplicationView extends ResizableWidget {
private menu_view = this.disposable(new NavigationView()); private menu_view: NavigationView;
private main_content_view = this.disposable(new MainContentView()); private main_content_view: MainContentView;
readonly element = el.div( readonly element: HTMLElement;
{ class: "application_ApplicationView" },
this.menu_view.element,
this.main_content_view.element,
);
constructor() { constructor(gui_store: GuiStore, tool_views: [GuiTool, () => Promise<ResizableWidget>][]) {
super(); 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.element.id = "root";
this.finalize_construction(); this.finalize_construction();

View File

@ -1,32 +1,24 @@
import { el } from "../../core/gui/dom"; 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 { LazyWidget } from "../../core/gui/LazyWidget";
import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { ChangeEvent } from "../../core/observable/Observable"; import { ChangeEvent } from "../../core/observable/Observable";
const TOOLS: [GuiTool, () => Promise<ResizableWidget>][] = [
[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 { export class MainContentView extends ResizableWidget {
readonly element = el.div({ class: "application_MainContentView" }); readonly element = el.div({ class: "application_MainContentView" });
private tool_views = new Map( private tool_views: Map<GuiTool, LazyWidget>;
TOOLS.map(([tool, create_view]) => [tool, this.disposable(new LazyWidget(create_view))]),
);
constructor() { constructor(gui_store: GuiStore, tool_views: [GuiTool, () => Promise<ResizableWidget>][]) {
super(); 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()) { for (const tool_view of this.tool_views.values()) {
this.element.append(tool_view.element); this.element.append(tool_view.element);
} }

View File

@ -1,6 +1,6 @@
import { el, icon, Icon } from "../../core/gui/dom"; import { el, icon, Icon } from "../../core/gui/dom";
import "./NavigationView.css"; 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 { Widget } from "../../core/gui/Widget";
import { NavigationButton } from "./NavigationButton"; import { NavigationButton } from "./NavigationButton";
import { Select } from "../../core/gui/Select"; import { Select } from "../../core/gui/Select";
@ -50,7 +50,7 @@ export class NavigationView extends Widget {
readonly height = 30; readonly height = 30;
constructor() { constructor(private readonly gui_store: GuiStore) {
super(); super();
this.element.style.height = `${this.height}px`; this.element.style.height = `${this.height}px`;
@ -62,11 +62,11 @@ export class NavigationView extends Widget {
this.finalize_construction(); this.finalize_construction();
} }
private mousedown(e: MouseEvent): void { private mousedown = (e: MouseEvent): void => {
if (e.target instanceof HTMLLabelElement && e.target.control instanceof HTMLInputElement) { 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 => { private mark_tool_button = (tool: GuiTool): void => {
const button = this.buttons.get(tool); const button = this.buttons.get(tool);

View File

@ -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])); 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<GuiTool> = property(GuiTool.Viewer); readonly tool: WritableProperty<GuiTool> = property(GuiTool.Viewer);
readonly server: Property<Server>; readonly server: Property<Server>;
@ -106,8 +106,6 @@ class GuiStore implements Disposable {
} }
} }
export const gui_store = new GuiStore();
function string_to_gui_tool(tool: string): GuiTool | undefined { function string_to_gui_tool(tool: string): GuiTool | undefined {
return STRING_TO_GUI_TOOL.get(tool); return STRING_TO_GUI_TOOL.get(tool);
} }

View File

@ -9,6 +9,11 @@ import {
import { ServerMap } from "./ServerMap"; import { ServerMap } from "./ServerMap";
import { Server } from "../model"; import { Server } from "../model";
import { ItemTypeDto } from "../dto/ItemTypeDto"; import { ItemTypeDto } from "../dto/ItemTypeDto";
import { GuiStore } from "./GuiStore";
export function load_item_type_stores(gui_store: GuiStore): ServerMap<ItemTypeStore> {
return new ServerMap(gui_store, load);
}
export class ItemTypeStore { export class ItemTypeStore {
readonly item_types: ItemType[]; readonly item_types: ItemType[];
@ -91,5 +96,3 @@ async function load(server: Server): Promise<ItemTypeStore> {
return new ItemTypeStore(item_types, id_to_item_type); return new ItemTypeStore(item_types, id_to_item_type);
} }
export const item_type_stores: ServerMap<ItemTypeStore> = new ServerMap(load);

View File

@ -1,20 +1,20 @@
import { Server } from "../model"; import { Server } from "../model";
import { Property } from "../observable/property/Property"; import { Property } from "../observable/property/Property";
import { gui_store } from "./GuiStore";
import { memoize } from "lodash"; import { memoize } from "lodash";
import { sequential } from "../sequential"; import { sequential } from "../sequential";
import { Disposable } from "../observable/Disposable"; import { Disposable } from "../observable/Disposable";
import { GuiStore } from "./GuiStore";
/** /**
* Map with a lazily-loaded, guaranteed value per server. * Map with a lazily-loaded, guaranteed value per server.
*/ */
export class ServerMap<T> { export class ServerMap<T> {
/** /**
* 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<Promise<T>> { get current(): Property<Promise<T>> {
if (!this._current) { 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; return this._current;
@ -23,7 +23,7 @@ export class ServerMap<T> {
private readonly get_value: (server: Server) => Promise<T>; private readonly get_value: (server: Server) => Promise<T>;
private _current?: Property<Promise<T>>; private _current?: Property<Promise<T>>;
constructor(get_value: (server: Server) => Promise<T>) { constructor(private readonly gui_store: GuiStore, get_value: (server: Server) => Promise<T>) {
this.get_value = memoize(get_value); this.get_value = memoize(get_value);
} }

View File

@ -1,5 +1,5 @@
import { WeaponItem, WeaponItemType, ArmorItemType, ShieldItemType } from "../../core/model/items"; import { ArmorItemType, ShieldItemType, WeaponItem, WeaponItemType } from "../../core/model/items";
import { item_type_stores, ItemTypeStore } from "../../core/stores/ItemTypeStore"; import { ItemTypeStore } from "../../core/stores/ItemTypeStore";
import { Property } from "../../core/observable/property/Property"; import { Property } from "../../core/observable/property/Property";
import { list_property, map, property } from "../../core/observable"; import { list_property, map, property } from "../../core/observable";
import { WritableProperty } from "../../core/observable/property/WritableProperty"; 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 { WritableListProperty } from "../../core/observable/property/list/WritableListProperty";
import { sequential } from "../../core/sequential"; import { sequential } from "../../core/sequential";
import { Disposable } from "../../core/observable/Disposable"; import { Disposable } from "../../core/observable/Disposable";
import { ServerMap } from "../../core/stores/ServerMap";
const NORMAL_DAMAGE_FACTOR = 0.2 * 0.9; const NORMAL_DAMAGE_FACTOR = 0.2 * 0.9;
const HEAVY_DAMAGE_FACTOR = NORMAL_DAMAGE_FACTOR * 1.89; 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 VJAYA_DAMAGE_FACTOR = NORMAL_DAMAGE_FACTOR * 5.56;
// const CRIT_FACTOR = 1.5; // const CRIT_FACTOR = 1.5;
class Weapon { export class Weapon {
readonly shifta_atp: Property<number> = this.store.shifta_factor.map(shifta_factor => { readonly shifta_atp: Property<number> = this.store.shifta_factor.map(shifta_factor => {
if (this.item.type.min_atp === this.item.type.max_atp) { if (this.item.type.min_atp === this.item.type.max_atp) {
return 0; return 0;
@ -92,7 +93,7 @@ class Weapon {
constructor(private readonly store: DpsCalcStore, readonly item: WeaponItem) {} constructor(private readonly store: DpsCalcStore, readonly item: WeaponItem) {}
} }
class DpsCalcStore implements Disposable { export class DpsCalcStore implements Disposable {
private readonly _weapon_types: WritableListProperty<WeaponItemType> = list_property(); private readonly _weapon_types: WritableListProperty<WeaponItemType> = list_property();
private readonly _armor_types: WritableListProperty<ArmorItemType> = list_property(); private readonly _armor_types: WritableListProperty<ArmorItemType> = list_property();
private readonly _shield_types: WritableListProperty<ShieldItemType> = list_property(); private readonly _shield_types: WritableListProperty<ShieldItemType> = list_property();
@ -154,7 +155,7 @@ class DpsCalcStore implements Disposable {
readonly enemy_dfp: Property<number> = this._enemy_dfp; readonly enemy_dfp: Property<number> = this._enemy_dfp;
constructor() { constructor(item_type_stores: ServerMap<ItemTypeStore>) {
this.disposable = item_type_stores.current.observe( this.disposable = item_type_stores.current.observe(
sequential(async ({ value: item_type_store }: { value: Promise<ItemTypeStore> }) => { sequential(async ({ value: item_type_store }: { value: Promise<ItemTypeStore> }) => {
const weapon_types: WeaponItemType[] = []; const weapon_types: WeaponItemType[] = [];
@ -186,5 +187,3 @@ class DpsCalcStore implements Disposable {
this._weapons.push(new Weapon(this, new WeaponItem(type))); this._weapons.push(new Weapon(this, new WeaponItem(type)));
}; };
} }
export const dps_calc_store = new DpsCalcStore();

View File

@ -1,7 +1,13 @@
import { TabContainer } from "../../core/gui/TabContainer"; 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 { export class HuntOptimizerView extends TabContainer {
constructor() { constructor(
hunt_optimizer_stores: ServerMap<HuntOptimizerStore>,
hunt_method_stores: ServerMap<HuntMethodStore>,
) {
super({ super({
class: "hunt_optimizer_HuntOptimizerView", class: "hunt_optimizer_HuntOptimizerView",
tabs: [ tabs: [
@ -9,14 +15,16 @@ export class HuntOptimizerView extends TabContainer {
title: "Optimize", title: "Optimize",
key: "optimize", key: "optimize",
create_view: async function() { create_view: async function() {
return new (await import("./OptimizerView")).OptimizerView(); return new (await import("./OptimizerView")).OptimizerView(
hunt_optimizer_stores,
);
}, },
}, },
{ {
title: "Methods", title: "Methods",
key: "methods", key: "methods",
create_view: async function() { create_view: async function() {
return new (await import("./MethodsView")).MethodsView(); return new (await import("./MethodsView")).MethodsView(hunt_method_stores);
}, },
}, },
{ {

View File

@ -1,7 +1,6 @@
import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { Episode } from "../../core/data_formats/parsing/quest/Episode"; import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { el } from "../../core/gui/dom"; import { el } from "../../core/gui/dom";
import { hunt_method_stores } from "../stores/HuntMethodStore";
import { HuntMethodModel } from "../model/HuntMethodModel"; import { HuntMethodModel } from "../model/HuntMethodModel";
import { import {
ENEMY_NPC_TYPES, ENEMY_NPC_TYPES,
@ -14,6 +13,8 @@ import { DurationInput } from "../../core/gui/DurationInput";
import { Disposable } from "../../core/observable/Disposable"; import { Disposable } from "../../core/observable/Disposable";
import { SortDirection, Table } from "../../core/gui/Table"; import { SortDirection, Table } from "../../core/gui/Table";
import { list_property } from "../../core/observable"; import { list_property } from "../../core/observable";
import { ServerMap } from "../../core/stores/ServerMap";
import { HuntMethodStore } from "../stores/HuntMethodStore";
export class MethodsForEpisodeView extends ResizableWidget { export class MethodsForEpisodeView extends ResizableWidget {
readonly element = el.div({ class: "hunt_optimizer_MethodsForEpisodeView" }); readonly element = el.div({ class: "hunt_optimizer_MethodsForEpisodeView" });
@ -22,7 +23,7 @@ export class MethodsForEpisodeView extends ResizableWidget {
private readonly enemy_types: NpcType[]; private readonly enemy_types: NpcType[];
private hunt_methods_observer?: Disposable; private hunt_methods_observer?: Disposable;
constructor(episode: Episode) { constructor(hunt_method_stores: ServerMap<HuntMethodStore>, episode: Episode) {
super(); super();
this.episode = episode; this.episode = episode;

View File

@ -1,9 +1,11 @@
import { TabContainer } from "../../core/gui/TabContainer"; import { TabContainer } from "../../core/gui/TabContainer";
import { Episode } from "../../core/data_formats/parsing/quest/Episode"; import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { MethodsForEpisodeView } from "./MethodsForEpisodeView"; import { MethodsForEpisodeView } from "./MethodsForEpisodeView";
import { ServerMap } from "../../core/stores/ServerMap";
import { HuntMethodStore } from "../stores/HuntMethodStore";
export class MethodsView extends TabContainer { export class MethodsView extends TabContainer {
constructor() { constructor(hunt_method_stores: ServerMap<HuntMethodStore>) {
super({ super({
class: "hunt_optimizer_MethodsView", class: "hunt_optimizer_MethodsView",
tabs: [ tabs: [
@ -11,21 +13,21 @@ export class MethodsView extends TabContainer {
title: "Episode I", title: "Episode I",
key: "episode_1", key: "episode_1",
create_view: async function() { create_view: async function() {
return new MethodsForEpisodeView(Episode.I); return new MethodsForEpisodeView(hunt_method_stores, Episode.I);
}, },
}, },
{ {
title: "Episode II", title: "Episode II",
key: "episode_2", key: "episode_2",
create_view: async function() { create_view: async function() {
return new MethodsForEpisodeView(Episode.II); return new MethodsForEpisodeView(hunt_method_stores, Episode.II);
}, },
}, },
{ {
title: "Episode IV", title: "Episode IV",
key: "episode_4", key: "episode_4",
create_view: async function() { create_view: async function() {
return new MethodsForEpisodeView(Episode.IV); return new MethodsForEpisodeView(hunt_method_stores, Episode.IV);
}, },
}, },
], ],

View File

@ -1,7 +1,6 @@
import { Widget } from "../../core/gui/Widget"; import { Widget } from "../../core/gui/Widget";
import { el, section_id_icon } from "../../core/gui/dom"; import { el, section_id_icon } from "../../core/gui/dom";
import { Column, Table } from "../../core/gui/Table"; import { Column, Table } from "../../core/gui/Table";
import { hunt_optimizer_stores } from "../stores/HuntOptimizerStore";
import { Disposable } from "../../core/observable/Disposable"; import { Disposable } from "../../core/observable/Disposable";
import { list_property } from "../../core/observable"; import { list_property } from "../../core/observable";
import { OptimalMethodModel, OptimalResultModel } from "../model"; import { OptimalMethodModel, OptimalResultModel } from "../model";
@ -9,6 +8,8 @@ import { Difficulty } from "../../core/model";
import { Episode } from "../../core/data_formats/parsing/quest/Episode"; import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import "./OptimizationResultView.css"; import "./OptimizationResultView.css";
import { Duration } from "luxon"; import { Duration } from "luxon";
import { ServerMap } from "../../core/stores/ServerMap";
import { HuntOptimizerStore } from "../stores/HuntOptimizerStore";
export class OptimizationResultView extends Widget { export class OptimizationResultView extends Widget {
readonly element = el.div( readonly element = el.div(
@ -19,7 +20,7 @@ export class OptimizationResultView extends Widget {
private results_observer?: Disposable; private results_observer?: Disposable;
private table?: Table<OptimalMethodModel>; private table?: Table<OptimalMethodModel>;
constructor() { constructor(hunt_optimizer_stores: ServerMap<HuntOptimizerStore>) {
super(); super();
this.disposable( this.disposable(

View File

@ -3,16 +3,18 @@ import { el } from "../../core/gui/dom";
import { WantedItemsView } from "./WantedItemsView"; import { WantedItemsView } from "./WantedItemsView";
import "./OptimizerView.css"; import "./OptimizerView.css";
import { OptimizationResultView } from "./OptimizationResultView"; import { OptimizationResultView } from "./OptimizationResultView";
import { ServerMap } from "../../core/stores/ServerMap";
import { HuntOptimizerStore } from "../stores/HuntOptimizerStore";
export class OptimizerView extends ResizableWidget { export class OptimizerView extends ResizableWidget {
readonly element = el.div({ class: "hunt_optimizer_OptimizerView" }); readonly element = el.div({ class: "hunt_optimizer_OptimizerView" });
constructor() { constructor(hunt_optimizer_stores: ServerMap<HuntOptimizerStore>) {
super(); super();
this.element.append( this.element.append(
this.disposable(new WantedItemsView()).element, this.disposable(new WantedItemsView(hunt_optimizer_stores)).element,
this.disposable(new OptimizationResultView()).element, this.disposable(new OptimizationResultView(hunt_optimizer_stores)).element,
); );
this.finalize_construction(); this.finalize_construction();

View File

@ -5,11 +5,12 @@ import { Disposer } from "../../core/observable/Disposer";
import { Widget } from "../../core/gui/Widget"; import { Widget } from "../../core/gui/Widget";
import { WantedItemModel } from "../model"; import { WantedItemModel } from "../model";
import { NumberInput } from "../../core/gui/NumberInput"; import { NumberInput } from "../../core/gui/NumberInput";
import { hunt_optimizer_stores } from "../stores/HuntOptimizerStore";
import { ComboBox } from "../../core/gui/ComboBox"; import { ComboBox } from "../../core/gui/ComboBox";
import { list_property } from "../../core/observable"; import { list_property } from "../../core/observable";
import { ItemType } from "../../core/model/items"; import { ItemType } from "../../core/model/items";
import { Disposable } from "../../core/observable/Disposable"; import { Disposable } from "../../core/observable/Disposable";
import { ServerMap } from "../../core/stores/ServerMap";
import { HuntOptimizerStore } from "../stores/HuntOptimizerStore";
export class WantedItemsView extends Widget { export class WantedItemsView extends Widget {
readonly element = el.div({ class: "hunt_optimizer_WantedItemsView" }); 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 tbody_element = el.tbody();
private readonly store_disposer = this.disposable(new Disposer()); private readonly store_disposer = this.disposable(new Disposer());
constructor() { constructor(private readonly hunt_optimizer_stores: ServerMap<HuntOptimizerStore>) {
super(); super();
const huntable_items = list_property<ItemType>(); const huntable_items = list_property<ItemType>();
@ -94,7 +95,7 @@ export class WantedItemsView extends Widget {
row_disposer.add( row_disposer.add(
remove_button.click.observe(async () => 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),
), ),
); );

View File

@ -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<ItemTypeStore>,
item_drop_stores: ServerMap<ItemDropStore>,
): HuntOptimizerView {
const hunt_method_stores: ServerMap<HuntMethodStore> = load_hunt_method_stores(
gui_store,
new HuntMethodPersister(),
);
const hunt_optimizer_stores: ServerMap<HuntOptimizerStore> = 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);
}

View File

@ -5,7 +5,7 @@ import { Duration } from "luxon";
const METHOD_USER_TIMES_KEY = "HuntMethodStore.methodUserTimes"; 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 { persist_method_user_times(hunt_methods: readonly HuntMethodModel[], server: Server): void {
const user_times: PersistedUserTimes = {}; const user_times: PersistedUserTimes = {};
@ -39,5 +39,3 @@ class HuntMethodPersister extends Persister {
} }
type PersistedUserTimes = { [method_id: string]: number }; type PersistedUserTimes = { [method_id: string]: number };
export const hunt_method_persister = new HuntMethodPersister();

View File

@ -1,11 +1,16 @@
import { Server } from "../../core/model"; import { Server } from "../../core/model";
import { item_type_stores } from "../../core/stores/ItemTypeStore";
import { Persister } from "../../core/persistence"; import { Persister } from "../../core/persistence";
import { WantedItemModel } from "../model"; import { WantedItemModel } from "../model";
import { ItemTypeStore } from "../../core/stores/ItemTypeStore";
import { ServerMap } from "../../core/stores/ServerMap";
const WANTED_ITEMS_KEY = "HuntOptimizerStore.wantedItems"; const WANTED_ITEMS_KEY = "HuntOptimizerStore.wantedItems";
class HuntOptimizerPersister extends Persister { export class HuntOptimizerPersister extends Persister {
constructor(private readonly item_type_stores: ServerMap<ItemTypeStore>) {
super();
}
persist_wanted_items(server: Server, wanted_items: readonly WantedItemModel[]): void { persist_wanted_items(server: Server, wanted_items: readonly WantedItemModel[]): void {
this.persist_for_server( this.persist_for_server(
server, server,
@ -20,7 +25,7 @@ class HuntOptimizerPersister extends Persister {
} }
async load_wanted_items(server: Server): Promise<WantedItemModel[]> { async load_wanted_items(server: Server): Promise<WantedItemModel[]> {
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<PersistedWantedItem[]>( const persisted_wanted_items = await this.load_for_server<PersistedWantedItem[]>(
server, server,
@ -50,5 +55,3 @@ type PersistedWantedItem = {
itemKindId?: number; // Legacy name, not persisted, only checked when loading. itemKindId?: number; // Legacy name, not persisted, only checked when loading.
amount: number; amount: number;
}; };
export const hunt_optimizer_persister = new HuntOptimizerPersister();

View File

@ -4,12 +4,13 @@ import { QuestDto } from "../dto/QuestDto";
import { NpcType } from "../../core/data_formats/parsing/quest/npc_types"; import { NpcType } from "../../core/data_formats/parsing/quest/npc_types";
import { SimpleQuestModel } from "../model/SimpleQuestModel"; import { SimpleQuestModel } from "../model/SimpleQuestModel";
import { HuntMethodModel } from "../model/HuntMethodModel"; import { HuntMethodModel } from "../model/HuntMethodModel";
import { hunt_method_persister } from "../persistence/HuntMethodPersister"; import { HuntMethodPersister } from "../persistence/HuntMethodPersister";
import { Duration } from "luxon"; import { Duration } from "luxon";
import { ListProperty } from "../../core/observable/property/list/ListProperty"; import { ListProperty } from "../../core/observable/property/list/ListProperty";
import { list_property } from "../../core/observable"; import { list_property } from "../../core/observable";
import { Disposable } from "../../core/observable/Disposable"; import { Disposable } from "../../core/observable/Disposable";
import { Disposer } from "../../core/observable/Disposer"; import { Disposer } from "../../core/observable/Disposer";
import { GuiStore } from "../../core/stores/GuiStore";
import { ServerMap } from "../../core/stores/ServerMap"; import { ServerMap } from "../../core/stores/ServerMap";
const logger = Logger.get("hunt_optimizer/stores/HuntMethodStore"); 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_GOVERNMENT_TEST_DURATION = Duration.fromObject({ minutes: 45 });
const DEFAULT_LARGE_ENEMY_COUNT_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<HuntMethodStore> {
return new ServerMap(gui_store, create_loader(hunt_method_persister));
}
export class HuntMethodStore implements Disposable { export class HuntMethodStore implements Disposable {
readonly methods: ListProperty<HuntMethodModel>; readonly methods: ListProperty<HuntMethodModel>;
private readonly disposer = new Disposer(); 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.methods = list_property(method => [method.user_time], ...methods);
this.disposer.add( this.disposer.add(
@ -38,62 +50,64 @@ export class HuntMethodStore implements Disposable {
} }
} }
async function load(server: Server): Promise<HuntMethodStore> { function create_loader(
const response = await fetch( hunt_method_persister: HuntMethodPersister,
`${process.env.PUBLIC_URL}/quests.${Server[server].toLowerCase()}.json`, ): (server: Server) => Promise<HuntMethodStore> {
); return async server => {
const quests = (await response.json()) as QuestDto[]; const response = await fetch(
const methods: HuntMethodModel[] = []; `${process.env.PUBLIC_URL}/quests.${Server[server].toLowerCase()}.json`,
for (const quest of quests) {
let total_enemy_count = 0;
const enemy_counts = new Map<NpcType, number>();
for (const [code, count] of Object.entries(quest.enemyCounts)) {
const npc_type = (NpcType as any)[code];
if (!npc_type) {
logger.error(`No NpcType found for code ${code}.`);
} else {
enemy_counts.set(npc_type, count);
total_enemy_count += count;
}
}
// Filter out some quests.
/* eslint-disable no-fallthrough */
switch (quest.id) {
// The following quests are left out because their enemies don't drop anything.
case 31: // Black Paper's Dangerous Deal
case 34: // Black Paper's Dangerous Deal 2
case 1305: // Maximum Attack S (Ep. 1)
case 1306: // Maximum Attack S (Ep. 2)
case 1307: // Maximum Attack S (Ep. 4)
case 313: // Beyond the Horizon
// MAXIMUM ATTACK 3 Ver2 is filtered out because its actual enemy count depends on the path taken.
// TODO: generate a method per path.
case 314:
continue;
}
methods.push(
new HuntMethodModel(
`q${quest.id}`,
quest.name,
new SimpleQuestModel(quest.id, quest.name, quest.episode, enemy_counts),
/^\d-\d.*/.test(quest.name)
? DEFAULT_GOVERNMENT_TEST_DURATION
: total_enemy_count > 400
? DEFAULT_LARGE_ENEMY_COUNT_DURATION
: DEFAULT_DURATION,
),
); );
} 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<NpcType, number>();
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<HuntMethodStore> = new ServerMap(load);

View File

@ -16,13 +16,32 @@ import { OptimalMethodModel, OptimalResultModel, WantedItemModel } from "../mode
import { ListProperty } from "../../core/observable/property/list/ListProperty"; import { ListProperty } from "../../core/observable/property/list/ListProperty";
import { list_property, map } from "../../core/observable"; import { list_property, map } from "../../core/observable";
import { WritableListProperty } from "../../core/observable/property/list/WritableListProperty"; import { WritableListProperty } from "../../core/observable/property/list/WritableListProperty";
import { hunt_method_stores, HuntMethodStore } from "./HuntMethodStore"; import { HuntMethodStore } from "./HuntMethodStore";
import { item_drop_stores, ItemDropStore } from "./ItemDropStore"; import { ItemDropStore } from "./ItemDropStore";
import { item_type_stores, ItemTypeStore } from "../../core/stores/ItemTypeStore"; import { ItemTypeStore } from "../../core/stores/ItemTypeStore";
import { hunt_optimizer_persister } from "../persistence/HuntOptimizerPersister";
import { ServerMap } from "../../core/stores/ServerMap";
import { Disposable } from "../../core/observable/Disposable"; import { Disposable } from "../../core/observable/Disposable";
import { Disposer } from "../../core/observable/Disposer"; 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<ItemTypeStore>,
item_drop_stores: ServerMap<ItemDropStore>,
hunt_method_stores: ServerMap<HuntMethodStore>,
): ServerMap<HuntOptimizerStore> {
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 mothmants spawned from mothverts.
// TODO: take into account split slimes. // 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. // TODO: Show expected value or probability per item per method.
// Can be useful when deciding which item to hunt first. // Can be useful when deciding which item to hunt first.
// TODO: boxes. // TODO: boxes.
class HuntOptimizerStore implements Disposable { export class HuntOptimizerStore implements Disposable {
readonly huntable_item_types: ItemType[]; readonly huntable_item_types: ItemType[];
// TODO: wanted items per server. // TODO: wanted items per server.
readonly wanted_items: ListProperty<WantedItemModel>; readonly wanted_items: ListProperty<WantedItemModel>;
@ -43,6 +62,7 @@ class HuntOptimizerStore implements Disposable {
private readonly disposer = new Disposer(); private readonly disposer = new Disposer();
constructor( constructor(
private readonly hunt_optimizer_persister: HuntOptimizerPersister,
private readonly server: Server, private readonly server: Server,
item_type_store: ItemTypeStore, item_type_store: ItemTypeStore,
private readonly item_drop_store: ItemDropStore, private readonly item_drop_store: ItemDropStore,
@ -314,23 +334,28 @@ class HuntOptimizerStore implements Disposable {
} }
private initialize_persistence = async (): Promise<void> => { private initialize_persistence = async (): Promise<void> => {
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.disposer.add(
this._wanted_items.observe(({ value }) => { 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<HuntOptimizerStore> { function create_loader(
return new HuntOptimizerStore( hunt_optimizer_persister: HuntOptimizerPersister,
server, item_type_stores: ServerMap<ItemTypeStore>,
await item_type_stores.get(server), item_drop_stores: ServerMap<ItemDropStore>,
await item_drop_stores.get(server), hunt_method_stores: ServerMap<HuntMethodStore>,
await hunt_method_stores.get(server), ): (server: Server) => Promise<HuntOptimizerStore> {
); 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<HuntOptimizerStore> = new ServerMap(load);

View File

@ -1,13 +1,21 @@
import { Difficulties, Difficulty, SectionId, SectionIds, Server } from "../../core/model"; import { Difficulties, Difficulty, SectionId, SectionIds, Server } from "../../core/model";
import { item_type_stores } from "../../core/stores/ItemTypeStore";
import { ServerMap } from "../../core/stores/ServerMap"; import { ServerMap } from "../../core/stores/ServerMap";
import Logger from "js-logger"; import Logger from "js-logger";
import { NpcType } from "../../core/data_formats/parsing/quest/npc_types"; import { NpcType } from "../../core/data_formats/parsing/quest/npc_types";
import { EnemyDrop } from "../model/ItemDrop"; import { EnemyDrop } from "../model/ItemDrop";
import { EnemyDropDto } from "../dto/drops"; import { EnemyDropDto } from "../dto/drops";
import { GuiStore } from "../../core/stores/GuiStore";
import { ItemTypeStore } from "../../core/stores/ItemTypeStore";
const logger = Logger.get("stores/ItemDropStore"); const logger = Logger.get("stores/ItemDropStore");
export function load_item_drop_stores(
gui_store: GuiStore,
item_type_stores: ServerMap<ItemTypeStore>,
): ServerMap<ItemDropStore> {
return new ServerMap(gui_store, create_loader(item_type_stores));
}
export class ItemDropStore { export class ItemDropStore {
readonly enemy_drops: EnemyDropTable; readonly enemy_drops: EnemyDropTable;
@ -65,55 +73,57 @@ export class EnemyDropTable {
} }
} }
async function load(server: Server): Promise<ItemDropStore> { function create_loader(
const item_type_store = await item_type_stores.get(server); item_type_stores: ServerMap<ItemTypeStore>,
const response = await fetch( ): (server: Server) => Promise<ItemDropStore> {
`${process.env.PUBLIC_URL}/enemyDrops.${Server[server].toLowerCase()}.json`, return async server => {
); const item_type_store = await item_type_stores.get(server);
const data: EnemyDropDto[] = await response.json(); const response = await fetch(
const enemy_drops = new EnemyDropTable(); `${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) { for (const drop_dto of data) {
const npc_type = (NpcType as any)[drop_dto.enemy]; const npc_type = (NpcType as any)[drop_dto.enemy];
if (!npc_type) { if (!npc_type) {
logger.warn( logger.warn(
`Couldn't determine NpcType of episode ${drop_dto.episode} ${drop_dto.enemy}.`, `Couldn't determine NpcType of episode ${drop_dto.episode} ${drop_dto.enemy}.`,
); );
continue; continue;
} }
const difficulty = (Difficulty as any)[drop_dto.difficulty]; const difficulty = (Difficulty as any)[drop_dto.difficulty];
const item_type = item_type_store.get_by_id(drop_dto.itemTypeId); const item_type = item_type_store.get_by_id(drop_dto.itemTypeId);
if (!item_type) { if (!item_type) {
logger.warn(`Couldn't find item kind ${drop_dto.itemTypeId}.`); logger.warn(`Couldn't find item kind ${drop_dto.itemTypeId}.`);
continue; continue;
} }
const section_id = (SectionId as any)[drop_dto.sectionId]; const section_id = (SectionId as any)[drop_dto.sectionId];
if (section_id == null) { if (section_id == null) {
logger.warn(`Couldn't find section ID ${drop_dto.sectionId}.`); logger.warn(`Couldn't find section ID ${drop_dto.sectionId}.`);
continue; continue;
} }
enemy_drops.set_drop( enemy_drops.set_drop(
difficulty,
section_id,
npc_type,
new EnemyDrop(
difficulty, difficulty,
section_id, section_id,
npc_type, npc_type,
item_type, new EnemyDrop(
drop_dto.dropRate, difficulty,
drop_dto.rareRate, 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<ItemDropStore> = new ServerMap(load);

View File

@ -7,9 +7,12 @@ import "@fortawesome/fontawesome-free/js/fontawesome";
import "@fortawesome/fontawesome-free/js/solid"; import "@fortawesome/fontawesome-free/js/solid";
import "@fortawesome/fontawesome-free/js/regular"; import "@fortawesome/fontawesome-free/js/regular";
import "@fortawesome/fontawesome-free/js/brands"; 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({ Logger.useDefaults({
defaultLevel: (Logger as any)[process.env["LOG_LEVEL"] || "OFF"], defaultLevel: (Logger as any)[process.env["LOG_LEVEL"] ?? "OFF"],
}); });
function initialize(): Disposable { function initialize(): Disposable {
@ -23,8 +26,36 @@ function initialize(): Disposable {
document.addEventListener("dragover", dragover); document.addEventListener("dragover", dragover);
document.addEventListener("drop", drop); document.addEventListener("drop", drop);
// Initialize view. // Initialize core stores shared by several submodules.
const application_view = new ApplicationView(); 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. // Resize the view on window resize.
const resize = throttle( const resize = throttle(

View File

@ -1,17 +1,20 @@
import { Action } from "../../core/undo/Action"; import { Action } from "../../core/undo/Action";
import { QuestEntityModel } from "../model/QuestEntityModel"; import { QuestEntityModel } from "../model/QuestEntityModel";
import { entity_data } from "../../core/data_formats/parsing/quest/entities"; 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 { export class CreateEntityAction implements Action {
readonly description: string; 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}`; this.description = `Create ${entity_data(entity.type).name}`;
} }
undo(): void { undo(): void {
const quest = quest_editor_store.current_quest.val; const quest = this.quest_editor_store.current_quest.val;
if (quest) { if (quest) {
quest.remove_entity(this.entity); quest.remove_entity(this.entity);
@ -19,12 +22,12 @@ export class CreateEntityAction implements Action {
} }
redo(): void { redo(): void {
const quest = quest_editor_store.current_quest.val; const quest = this.quest_editor_store.current_quest.val;
if (quest) { if (quest) {
quest.add_entity(this.entity); quest.add_entity(this.entity);
quest_editor_store.set_selected_entity(this.entity); this.quest_editor_store.set_selected_entity(this.entity);
} }
} }
} }

View File

@ -5,10 +5,10 @@ import { PropertyChangeEvent } from "../../core/observable/property/Property";
export abstract class QuestEditAction<T> implements Action { export abstract class QuestEditAction<T> implements Action {
abstract readonly description: string; abstract readonly description: string;
protected new: T; protected readonly new: T;
protected old: T; protected readonly old: T;
constructor(protected quest: QuestModel, event: PropertyChangeEvent<T>) { constructor(protected readonly quest: QuestModel, event: PropertyChangeEvent<T>) {
this.new = event.value; this.new = event.value;
this.old = event.old_value; this.old = event.old_value;
} }

View File

@ -1,27 +1,30 @@
import { Action } from "../../core/undo/Action"; import { Action } from "../../core/undo/Action";
import { QuestEntityModel } from "../model/QuestEntityModel"; import { QuestEntityModel } from "../model/QuestEntityModel";
import { entity_data } from "../../core/data_formats/parsing/quest/entities"; 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 { export class RemoveEntityAction implements Action {
readonly description: string; 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}`; this.description = `Delete ${entity_data(entity.type).name}`;
} }
undo(): void { undo(): void {
const quest = quest_editor_store.current_quest.val; const quest = this.quest_editor_store.current_quest.val;
if (quest) { if (quest) {
quest.add_entity(this.entity); quest.add_entity(this.entity);
quest_editor_store.set_selected_entity(this.entity); this.quest_editor_store.set_selected_entity(this.entity);
} }
} }
redo(): void { redo(): void {
const quest = quest_editor_store.current_quest.val; const quest = this.quest_editor_store.current_quest.val;
if (quest) { if (quest) {
quest.remove_entity(this.entity); quest.remove_entity(this.entity);

View File

@ -1,23 +1,24 @@
import { Action } from "../../core/undo/Action"; import { Action } from "../../core/undo/Action";
import { QuestEntityModel } from "../model/QuestEntityModel"; import { QuestEntityModel } from "../model/QuestEntityModel";
import { entity_data } from "../../core/data_formats/parsing/quest/entities"; import { entity_data } from "../../core/data_formats/parsing/quest/entities";
import { quest_editor_store } from "../stores/QuestEditorStore";
import { Euler } from "three"; import { Euler } from "three";
import { QuestEditorStore } from "../stores/QuestEditorStore";
export class RotateEntityAction implements Action { export class RotateEntityAction implements Action {
readonly description: string; readonly description: string;
constructor( constructor(
private entity: QuestEntityModel, private readonly quest_editor_store: QuestEditorStore,
private old_rotation: Euler, private readonly entity: QuestEntityModel,
private new_rotation: Euler, private readonly old_rotation: Euler,
private world: boolean, private readonly new_rotation: Euler,
private readonly world: boolean,
) { ) {
this.description = `Rotate ${entity_data(entity.type).name}`; this.description = `Rotate ${entity_data(entity.type).name}`;
} }
undo(): void { undo(): void {
quest_editor_store.set_selected_entity(this.entity); this.quest_editor_store.set_selected_entity(this.entity);
if (this.world) { if (this.world) {
this.entity.set_world_rotation(this.old_rotation); this.entity.set_world_rotation(this.old_rotation);
@ -27,7 +28,7 @@ export class RotateEntityAction implements Action {
} }
redo(): void { redo(): void {
quest_editor_store.set_selected_entity(this.entity); this.quest_editor_store.set_selected_entity(this.entity);
if (this.world) { if (this.world) {
this.entity.set_world_rotation(this.new_rotation); this.entity.set_world_rotation(this.new_rotation);

View File

@ -1,26 +1,27 @@
import { Action } from "../../core/undo/Action"; import { Action } from "../../core/undo/Action";
import { QuestEntityModel } from "../model/QuestEntityModel"; import { QuestEntityModel } from "../model/QuestEntityModel";
import { entity_data } from "../../core/data_formats/parsing/quest/entities"; import { entity_data } from "../../core/data_formats/parsing/quest/entities";
import { quest_editor_store } from "../stores/QuestEditorStore";
import { SectionModel } from "../model/SectionModel"; import { SectionModel } from "../model/SectionModel";
import { Vector3 } from "three"; import { Vector3 } from "three";
import { QuestEditorStore } from "../stores/QuestEditorStore";
export class TranslateEntityAction implements Action { export class TranslateEntityAction implements Action {
readonly description: string; readonly description: string;
constructor( constructor(
private entity: QuestEntityModel, private readonly quest_editor_store: QuestEditorStore,
private old_section: SectionModel | undefined, private readonly entity: QuestEntityModel,
private new_section: SectionModel | undefined, private readonly old_section: SectionModel | undefined,
private old_position: Vector3, private readonly new_section: SectionModel | undefined,
private new_position: Vector3, private readonly old_position: Vector3,
private world: boolean, private readonly new_position: Vector3,
private readonly world: boolean,
) { ) {
this.description = `Move ${entity_data(entity.type).name}`; this.description = `Move ${entity_data(entity.type).name}`;
} }
undo(): void { undo(): void {
quest_editor_store.set_selected_entity(this.entity); this.quest_editor_store.set_selected_entity(this.entity);
if (this.old_section) { if (this.old_section) {
this.entity.set_section(this.old_section); this.entity.set_section(this.old_section);
@ -34,7 +35,7 @@ export class TranslateEntityAction implements Action {
} }
redo(): void { redo(): void {
quest_editor_store.set_selected_entity(this.entity); this.quest_editor_store.set_selected_entity(this.entity);
if (this.new_section) { if (this.new_section) {
this.entity.set_section(this.new_section); this.entity.set_section(this.new_section);

View File

@ -1,9 +1,9 @@
import { ToolBar } from "../../core/gui/ToolBar"; import { ToolBar } from "../../core/gui/ToolBar";
import { CheckBox } from "../../core/gui/CheckBox"; import { CheckBox } from "../../core/gui/CheckBox";
import { asm_editor_store } from "../stores/AsmEditorStore"; import { AsmEditorStore } from "../stores/AsmEditorStore";
export class AsmEditorToolBar extends ToolBar { export class AsmEditorToolBar extends ToolBar {
constructor() { constructor(asm_editor_store: AsmEditorStore) {
const inline_args_mode_checkbox = new CheckBox(true, { const inline_args_mode_checkbox = new CheckBox(true, {
label: "Inline args mode", label: "Inline args mode",
tooltip: asm_editor_store.has_issues.map(has_issues => { tooltip: asm_editor_store.has_issues.map(has_issues => {

View File

@ -1,14 +1,14 @@
import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { el } from "../../core/gui/dom"; import { el } from "../../core/gui/dom";
import { editor, KeyCode, KeyMod, Range } from "monaco-editor"; import { editor, KeyCode, KeyMod, Range } from "monaco-editor";
import { asm_editor_store } from "../stores/AsmEditorStore";
import { AsmEditorToolBar } from "./AsmEditorToolBar"; import { AsmEditorToolBar } from "./AsmEditorToolBar";
import { EditorHistory } from "./EditorHistory"; import { EditorHistory } from "./EditorHistory";
import IStandaloneCodeEditor = editor.IStandaloneCodeEditor;
import "./AsmEditorView.css"; import "./AsmEditorView.css";
import { ListChangeType } from "../../core/observable/property/list/ListProperty"; import { ListChangeType } from "../../core/observable/property/list/ListProperty";
import { quest_editor_store } from "../stores/QuestEditorStore"; import { GuiStore } from "../../core/stores/GuiStore";
import { gui_store } from "../../core/stores/GuiStore"; import { AsmEditorStore } from "../stores/AsmEditorStore";
import { QuestRunner } from "../QuestRunner";
import IStandaloneCodeEditor = editor.IStandaloneCodeEditor;
editor.defineTheme("phantasmal-world", { editor.defineTheme("phantasmal-world", {
base: "vs-dark", base: "vs-dark",
@ -32,7 +32,7 @@ editor.defineTheme("phantasmal-world", {
const DUMMY_MODEL = editor.createModel("", "psoasm"); const DUMMY_MODEL = editor.createModel("", "psoasm");
export class AsmEditorView extends ResizableWidget { 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 editor: IStandaloneCodeEditor;
private readonly history: EditorHistory; private readonly history: EditorHistory;
private breakpoint_decoration_ids: string[] = []; private breakpoint_decoration_ids: string[] = [];
@ -40,9 +40,15 @@ export class AsmEditorView extends ResizableWidget {
readonly element = el.div(); readonly element = el.div();
constructor() { constructor(
gui_store: GuiStore,
quest_runner: QuestRunner,
private readonly asm_editor_store: AsmEditorStore,
) {
super(); super();
this.tool_bar_view = this.disposable(new AsmEditorToolBar(asm_editor_store));
this.element.append(this.tool_bar_view.element); this.element.append(this.tool_bar_view.element);
this.editor = this.disposable( this.editor = this.disposable(
@ -99,7 +105,7 @@ export class AsmEditorView extends ResizableWidget {
this.breakpoint_decoration_ids = []; this.breakpoint_decoration_ids = [];
this.execloc_decoration_id = ""; this.execloc_decoration_id = "";
quest_editor_store.quest_runner.clear_breakpoints(); quest_runner.clear_breakpoints();
}, },
{ call_now: true }, { call_now: true },
), ),
@ -203,7 +209,7 @@ export class AsmEditorView extends ResizableWidget {
if (!pos) { if (!pos) {
return; return;
} }
quest_editor_store.quest_runner.toggle_breakpoint(pos.lineNumber); quest_runner.toggle_breakpoint(pos.lineNumber);
} }
break; break;
default: 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(); this.finalize_construction();
@ -231,6 +237,6 @@ export class AsmEditorView extends ResizableWidget {
super.set_enabled(enabled); super.set_enabled(enabled);
this.tool_bar_view.enabled.val = 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 });
} }
} }

View File

@ -1,7 +1,6 @@
import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { el } from "../../core/gui/dom"; import { el } from "../../core/gui/dom";
import { DisabledView } from "./DisabledView"; import { DisabledView } from "./DisabledView";
import { quest_editor_store } from "../stores/QuestEditorStore";
import { QuestNpcModel } from "../model/QuestNpcModel"; import { QuestNpcModel } from "../model/QuestNpcModel";
import { entity_data } from "../../core/data_formats/parsing/quest/entities"; import { entity_data } from "../../core/data_formats/parsing/quest/entities";
import "./EntityInfoView.css"; import "./EntityInfoView.css";
@ -10,6 +9,7 @@ import { Disposer } from "../../core/observable/Disposer";
import { QuestEntityModel } from "../model/QuestEntityModel"; import { QuestEntityModel } from "../model/QuestEntityModel";
import { Euler, Vector3 } from "three"; import { Euler, Vector3 } from "three";
import { deg_to_rad, rad_to_deg } from "../../core/math"; import { deg_to_rad, rad_to_deg } from "../../core/math";
import { QuestEditorStore } from "../stores/QuestEditorStore";
export class EntityInfoView extends ResizableWidget { export class EntityInfoView extends ResizableWidget {
readonly element = el.div({ class: "quest_editor_EntityInfoView", tab_index: -1 }); 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(); private readonly entity_disposer = new Disposer();
constructor() { constructor(private readonly quest_editor_store: QuestEditorStore) {
super(); super();
const entity = quest_editor_store.selected_entity; const entity = quest_editor_store.selected_entity;
@ -174,7 +174,7 @@ export class EntityInfoView extends ResizableWidget {
), ),
this.pos_x_element.value.observe(({ value }) => this.pos_x_element.value.observe(({ value }) =>
quest_editor_store.translate_entity( this.quest_editor_store.translate_entity(
entity, entity,
entity.section.val, entity.section.val,
entity.section.val, entity.section.val,
@ -185,7 +185,7 @@ export class EntityInfoView extends ResizableWidget {
), ),
this.pos_y_element.value.observe(({ value }) => this.pos_y_element.value.observe(({ value }) =>
quest_editor_store.translate_entity( this.quest_editor_store.translate_entity(
entity, entity,
entity.section.val, entity.section.val,
entity.section.val, entity.section.val,
@ -196,7 +196,7 @@ export class EntityInfoView extends ResizableWidget {
), ),
this.pos_z_element.value.observe(({ value }) => this.pos_z_element.value.observe(({ value }) =>
quest_editor_store.translate_entity( this.quest_editor_store.translate_entity(
entity, entity,
entity.section.val, entity.section.val,
entity.section.val, entity.section.val,
@ -220,7 +220,7 @@ export class EntityInfoView extends ResizableWidget {
), ),
this.rot_x_element.value.observe(({ value }) => this.rot_x_element.value.observe(({ value }) =>
quest_editor_store.rotate_entity( this.quest_editor_store.rotate_entity(
entity, entity,
rot.val, rot.val,
new Euler(deg_to_rad(value), rot.val.y, rot.val.z, "ZXY"), 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 }) => this.rot_y_element.value.observe(({ value }) =>
quest_editor_store.rotate_entity( this.quest_editor_store.rotate_entity(
entity, entity,
rot.val, rot.val,
new Euler(rot.val.x, deg_to_rad(value), rot.val.z, "ZXY"), 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 }) => this.rot_z_element.value.observe(({ value }) =>
quest_editor_store.rotate_entity( this.quest_editor_store.rotate_entity(
entity, entity,
rot.val, rot.val,
new Euler(rot.val.x, rot.val.y, deg_to_rad(value), "ZXY"), new Euler(rot.val.x, rot.val.y, deg_to_rad(value), "ZXY"),

View File

@ -6,14 +6,14 @@ import { entity_dnd_source } from "./entity_dnd";
import { render_entity_to_image } from "../rendering/render_entity_to_image"; import { render_entity_to_image } from "../rendering/render_entity_to_image";
import { WritableListProperty } from "../../core/observable/property/list/WritableListProperty"; import { WritableListProperty } from "../../core/observable/property/list/WritableListProperty";
import { list_property } from "../../core/observable"; import { list_property } from "../../core/observable";
import { quest_editor_store } from "../stores/QuestEditorStore"; import { QuestEditorStore } from "../stores/QuestEditorStore";
export abstract class EntityListView<T extends EntityType> extends ResizableWidget { export abstract class EntityListView<T extends EntityType> extends ResizableWidget {
readonly element: HTMLElement; readonly element: HTMLElement;
protected readonly entities: WritableListProperty<T> = list_property(); protected readonly entities: WritableListProperty<T> = list_property();
protected constructor(class_name: string) { protected constructor(quest_editor_store: QuestEditorStore, class_name: string) {
super(); super();
const list_element = el.div({ class: "quest_editor_EntityListView_entity_list" }); const list_element = el.div({ class: "quest_editor_EntityListView_entity_list" });

View File

@ -1,6 +1,5 @@
import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { el } from "../../core/gui/dom"; import { el } from "../../core/gui/dom";
import { quest_editor_store } from "../stores/QuestEditorStore";
import { QuestEventDagModel } from "../model/QuestEventDagModel"; import { QuestEventDagModel } from "../model/QuestEventDagModel";
import { Disposer } from "../../core/observable/Disposer"; import { Disposer } from "../../core/observable/Disposer";
import { NumberInput } from "../../core/gui/NumberInput"; import { NumberInput } from "../../core/gui/NumberInput";
@ -11,6 +10,7 @@ import {
ListChangeType, ListChangeType,
ListPropertyChangeEvent, ListPropertyChangeEvent,
} from "../../core/observable/property/list/ListProperty"; } from "../../core/observable/property/list/ListProperty";
import { QuestEditorStore } from "../stores/QuestEditorStore";
type DagGuiData = { type DagGuiData = {
dag: QuestEventDagModel; dag: QuestEventDagModel;
@ -29,7 +29,7 @@ export class EventsView extends ResizableWidget {
readonly element = el.div({ class: "quest_editor_EventsView" }); readonly element = el.div({ class: "quest_editor_EventsView" });
constructor() { constructor(private readonly quest_editor_store: QuestEditorStore) {
super(); super();
this.disposables( this.disposables(
@ -69,8 +69,8 @@ export class EventsView extends ResizableWidget {
this.event_dags_observer.dispose(); this.event_dags_observer.dispose();
} }
const quest = quest_editor_store.current_quest.val; const quest = this.quest_editor_store.current_quest.val;
const area = quest_editor_store.current_area.val; const area = this.quest_editor_store.current_area.val;
if (quest && area) { if (quest && area) {
const event_dags = quest.event_dags.filtered(dag => dag.area_id === area.id); 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 => { private update_edges = (): void => {
const SPACING = 8; const SPACING = 8;
let max_depth = 0; let max_depth = 0;

View File

@ -1,11 +1,11 @@
import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { el } from "../../core/gui/dom"; 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 { npc_data, NpcType } from "../../core/data_formats/parsing/quest/npc_types";
import "./NpcCountsView.css"; import "./NpcCountsView.css";
import { DisabledView } from "./DisabledView"; import { DisabledView } from "./DisabledView";
import { property } from "../../core/observable"; import { property } from "../../core/observable";
import { QuestNpcModel } from "../model/QuestNpcModel"; import { QuestNpcModel } from "../model/QuestNpcModel";
import { QuestEditorStore } from "../stores/QuestEditorStore";
export class NpcCountsView extends ResizableWidget { export class NpcCountsView extends ResizableWidget {
readonly element = el.div({ class: "quest_editor_NpcCountsView" }); 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."); private readonly no_quest_view = new DisabledView("No quest loaded.");
constructor() { constructor(quest_editor_store: QuestEditorStore) {
super(); super();
this.element.append(this.table_element, this.no_quest_view.element); this.element.append(this.table_element, this.no_quest_view.element);

View File

@ -1,11 +1,11 @@
import { EntityListView } from "./EntityListView"; import { EntityListView } from "./EntityListView";
import { npc_data, NPC_TYPES, NpcType } from "../../core/data_formats/parsing/quest/npc_types"; 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"; import { Episode } from "../../core/data_formats/parsing/quest/Episode";
export class NpcListView extends EntityListView<NpcType> { export class NpcListView extends EntityListView<NpcType> {
constructor() { constructor(private readonly quest_editor_store: QuestEditorStore) {
super("quest_editor_NpcListView"); super(quest_editor_store, "quest_editor_NpcListView");
this.disposables( this.disposables(
quest_editor_store.current_quest.observe(this.filter_npcs), quest_editor_store.current_quest.observe(this.filter_npcs),
@ -17,8 +17,8 @@ export class NpcListView extends EntityListView<NpcType> {
} }
private filter_npcs = (): void => { private filter_npcs = (): void => {
const quest = quest_editor_store.current_quest.val; const quest = this.quest_editor_store.current_quest.val;
const area = quest_editor_store.current_area.val; const area = this.quest_editor_store.current_area.val;
const episode = quest ? quest.episode : Episode.I; const episode = quest ? quest.episode : Episode.I;
const area_id = area ? area.id : 0; const area_id = area ? area.id : 0;

View File

@ -4,12 +4,12 @@ import {
OBJECT_TYPES, OBJECT_TYPES,
ObjectType, ObjectType,
} from "../../core/data_formats/parsing/quest/object_types"; } 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 { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { QuestEditorStore } from "../stores/QuestEditorStore";
export class ObjectListView extends EntityListView<ObjectType> { export class ObjectListView extends EntityListView<ObjectType> {
constructor() { constructor(private readonly quest_editor_store: QuestEditorStore) {
super("quest_editor_ObjectListView"); super(quest_editor_store, "quest_editor_ObjectListView");
this.disposables( this.disposables(
quest_editor_store.current_quest.observe(this.filter_objects), quest_editor_store.current_quest.observe(this.filter_objects),
@ -21,8 +21,8 @@ export class ObjectListView extends EntityListView<ObjectType> {
} }
private filter_objects = (): void => { private filter_objects = (): void => {
const quest = quest_editor_store.current_quest.val; const quest = this.quest_editor_store.current_quest.val;
const area = quest_editor_store.current_area.val; const area = this.quest_editor_store.current_area.val;
const episode = quest ? quest.episode : Episode.I; const episode = quest ? quest.episode : Episode.I;
const area_id = area ? area.id : 0; const area_id = area ? area.id : 0;

View File

@ -1,18 +1,33 @@
import { QuestRenderer } from "../rendering/QuestRenderer"; import { QuestRenderer } from "../rendering/QuestRenderer";
import { quest_editor_store } from "../stores/QuestEditorStore"; import { QuestEditorStore } from "../stores/QuestEditorStore";
import { QuestEditorModelManager } from "../rendering/QuestEditorModelManager"; import { QuestEditorModelManager } from "../rendering/QuestEditorModelManager";
import { QuestRendererView } from "./QuestRendererView"; import { QuestRendererView } from "./QuestRendererView";
import { QuestEntityControls } from "../rendering/QuestEntityControls"; import { QuestEntityControls } from "../rendering/QuestEntityControls";
import { GuiStore } from "../../core/stores/GuiStore";
export class QuestEditorRendererView extends QuestRendererView { export class QuestEditorRendererView extends QuestRendererView {
private readonly entity_controls: QuestEntityControls; private readonly entity_controls: QuestEntityControls;
constructor() { constructor(gui_store: GuiStore, quest_editor_store: QuestEditorStore) {
super("quest_editor_QuestEditorRendererView", new QuestRenderer(QuestEditorModelManager)); 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.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( this.disposables(
quest_editor_store.selected_entity.observe( quest_editor_store.selected_entity.observe(

View File

@ -1,7 +1,6 @@
import { ToolBar } from "../../core/gui/ToolBar"; import { ToolBar } from "../../core/gui/ToolBar";
import { FileButton } from "../../core/gui/FileButton"; import { FileButton } from "../../core/gui/FileButton";
import { Button } from "../../core/gui/Button"; import { Button } from "../../core/gui/Button";
import { quest_editor_store } from "../stores/QuestEditorStore";
import { undo_manager } from "../../core/undo/UndoManager"; import { undo_manager } from "../../core/undo/UndoManager";
import { Select } from "../../core/gui/Select"; import { Select } from "../../core/gui/Select";
import { list_property, map } from "../../core/observable"; import { list_property, map } from "../../core/observable";
@ -10,10 +9,11 @@ import { Icon } from "../../core/gui/dom";
import { DropDown } from "../../core/gui/DropDown"; import { DropDown } from "../../core/gui/DropDown";
import { Episode } from "../../core/data_formats/parsing/quest/Episode"; import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { area_store } from "../stores/AreaStore"; 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 { export class QuestEditorToolBar extends ToolBar {
constructor() { constructor(gui_store: GuiStore, quest_editor_store: QuestEditorStore) {
const new_quest_button = new DropDown( const new_quest_button = new DropDown(
"New quest", "New quest",
[Episode.I], [Episode.I],

View File

@ -10,41 +10,19 @@ import { NpcCountsView } from "./NpcCountsView";
import { QuestEditorRendererView } from "./QuestEditorRendererView"; import { QuestEditorRendererView } from "./QuestEditorRendererView";
import { AsmEditorView } from "./AsmEditorView"; import { AsmEditorView } from "./AsmEditorView";
import { EntityInfoView } from "./EntityInfoView"; import { EntityInfoView } from "./EntityInfoView";
import { gui_store, GuiTool } from "../../core/stores/GuiStore"; import { GuiStore, GuiTool } from "../../core/stores/GuiStore";
import { quest_editor_store } from "../stores/QuestEditorStore";
import { NpcListView } from "./NpcListView"; import { NpcListView } from "./NpcListView";
import { ObjectListView } from "./ObjectListView"; import { ObjectListView } from "./ObjectListView";
import { EventsView } from "./EventsView"; import { EventsView } from "./EventsView";
import { RegistersView } from "./RegistersView"; import { RegistersView } from "./RegistersView";
import { LogView } from "./LogView"; import { LogView } from "./LogView";
import { QuestRunnerRendererView } from "./QuestRunnerRendererView"; import { QuestRunnerRendererView } from "./QuestRunnerRendererView";
import { QuestEditorStore } from "../stores/QuestEditorStore";
import Logger = require("js-logger"); import Logger = require("js-logger");
import { AsmEditorStore } from "../stores/AsmEditorStore";
const logger = Logger.get("quest_editor/gui/QuestEditorView"); 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<new () => 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 = { const DEFAULT_LAYOUT_CONFIG = {
settings: { settings: {
showPopoutIcon: false, 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 { export class QuestEditorView extends ResizableWidget {
readonly element = el.div({ class: "quest_editor_QuestEditorView" }); 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_element = create_element("div", { class: "quest_editor_gl_container" });
private readonly layout: Promise<GoldenLayout>; private readonly layout: Promise<GoldenLayout>;
@ -173,10 +61,85 @@ export class QuestEditorView extends ResizableWidget {
private readonly sub_views = new Map<string, ResizableWidget>(); private readonly sub_views = new Map<string, ResizableWidget>();
constructor() { constructor(
private readonly gui_store: GuiStore,
quest_editor_store: QuestEditorStore,
asm_editor_store: AsmEditorStore,
) {
super(); 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(); this.layout = this.init_golden_layout();
@ -194,20 +157,20 @@ export class QuestEditorView extends ResizableWidget {
if (quest_editor_store.quest_runner.running.val === running) { if (quest_editor_store.quest_runner.running.val === running) {
const runner_items = layout.root.getItemsById( const runner_items = layout.root.getItemsById(
VIEW_TO_NAME.get(QuestRunnerRendererView)!, this.view_map.get(QuestRunnerRendererView)!.name,
); );
if (running) { if (running) {
if (runner_items.length === 0) { if (runner_items.length === 0) {
const renderer_item = layout.root.getItemsById( const renderer_item = layout.root.getItemsById(
VIEW_TO_NAME.get(QuestEditorRendererView)!, this.view_map.get(QuestEditorRendererView)!.name,
)[0]; )[0];
renderer_item.parent.addChild({ renderer_item.parent.addChild({
id: VIEW_TO_NAME.get(QuestRunnerRendererView), id: this.view_map.get(QuestRunnerRendererView)!.name,
title: "Quest", title: "Quest",
type: "component", type: "component",
componentName: VIEW_TO_NAME.get(QuestRunnerRendererView), componentName: this.view_map.get(QuestRunnerRendererView)!.name,
isClosable: false, isClosable: false,
}); });
} }
@ -234,7 +197,7 @@ export class QuestEditorView extends ResizableWidget {
resize(width: number, height: number): this { resize(width: number, height: number): this {
super.resize(width, height); 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.width = `${width}px`;
this.layout_element.style.height = `${layout_height}px`; this.layout_element.style.height = `${layout_height}px`;
this.layout.then(layout => layout.updateSize(width, layout_height)); this.layout.then(layout => layout.updateSize(width, layout_height));
@ -254,9 +217,11 @@ export class QuestEditorView extends ResizableWidget {
} }
private async init_golden_layout(): Promise<GoldenLayout> { private async init_golden_layout(): Promise<GoldenLayout> {
const default_layout_content = this.get_default_layout_content();
const content = await quest_editor_ui_persister.load_layout_config( const content = await quest_editor_ui_persister.load_layout_config(
VIEW_WHITE_LIST, this.view_white_list,
DEFAULT_LAYOUT_CONTENT, default_layout_content,
); );
try { try {
@ -269,7 +234,7 @@ export class QuestEditorView extends ResizableWidget {
return this.attempt_gl_init({ return this.attempt_gl_init({
...DEFAULT_LAYOUT_CONFIG, ...DEFAULT_LAYOUT_CONFIG,
content: DEFAULT_LAYOUT_CONTENT, content: default_layout_content,
}); });
} }
} }
@ -280,15 +245,16 @@ export class QuestEditorView extends ResizableWidget {
const self = this; const self = this;
try { try {
for (const [view_ctor, name] of VIEW_TO_NAME) { for (const { name, create } of this.view_map.values()) {
// registerComponent expects a regular function and not an arrow function. // registerComponent expects a regular function and not an arrow function. This
// This function will be called with new. // function will be called with new.
layout.registerComponent(name, function(container: Container) { layout.registerComponent(name, function(container: Container) {
const view = new view_ctor(); const view = create();
container.on("close", () => view.dispose()); container.on("close", () => view.dispose());
container.on("resize", () => 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), view.resize(container.width, container.height - 4),
); );
@ -320,4 +286,106 @@ export class QuestEditorView extends ResizableWidget {
throw e; 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,
},
]
: []),
],
},
],
},
];
}
} }

View File

@ -1,6 +1,5 @@
import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { el } from "../../core/gui/dom"; import { el } from "../../core/gui/dom";
import { quest_editor_store } from "../stores/QuestEditorStore";
import { Episode } from "../../core/data_formats/parsing/quest/Episode"; import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { NumberInput } from "../../core/gui/NumberInput"; import { NumberInput } from "../../core/gui/NumberInput";
import { Disposer } from "../../core/observable/Disposer"; import { Disposer } from "../../core/observable/Disposer";
@ -8,6 +7,7 @@ import { TextInput } from "../../core/gui/TextInput";
import { TextArea } from "../../core/gui/TextArea"; import { TextArea } from "../../core/gui/TextArea";
import "./QuestInfoView.css"; import "./QuestInfoView.css";
import { DisabledView } from "./DisabledView"; import { DisabledView } from "./DisabledView";
import { QuestEditorStore } from "../stores/QuestEditorStore";
export class QuestInfoView extends ResizableWidget { export class QuestInfoView extends ResizableWidget {
readonly element = el.div({ class: "quest_editor_QuestInfoView", tab_index: -1 }); 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()); private readonly quest_disposer = this.disposable(new Disposer());
constructor() { constructor(quest_editor_store: QuestEditorStore) {
super(); super();
const quest = quest_editor_store.current_quest; const quest = quest_editor_store.current_quest;

View File

@ -1,9 +1,9 @@
import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { RendererWidget } from "../../core/gui/RendererWidget"; import { RendererWidget } from "../../core/gui/RendererWidget";
import { QuestRenderer } from "../rendering/QuestRenderer"; import { QuestRenderer } from "../rendering/QuestRenderer";
import { gui_store, GuiTool } from "../../core/stores/GuiStore"; import { GuiStore, GuiTool } from "../../core/stores/GuiStore";
import { quest_editor_store } from "../stores/QuestEditorStore";
import { el } from "../../core/gui/dom"; import { el } from "../../core/gui/dom";
import { QuestEditorStore } from "../stores/QuestEditorStore";
export abstract class QuestRendererView extends ResizableWidget { export abstract class QuestRendererView extends ResizableWidget {
private readonly renderer_view: RendererWidget; private readonly renderer_view: RendererWidget;
@ -12,7 +12,12 @@ export abstract class QuestRendererView extends ResizableWidget {
readonly element: HTMLElement; readonly element: HTMLElement;
protected constructor(className: string, renderer: QuestRenderer) { protected constructor(
gui_store: GuiStore,
quest_editor_store: QuestEditorStore,
className: string,
renderer: QuestRenderer,
) {
super(); super();
this.element = el.div({ class: className, tab_index: -1 }); this.element = el.div({ class: className, tab_index: -1 });

View File

@ -1,10 +1,20 @@
import { QuestRenderer } from "../rendering/QuestRenderer"; import { QuestRenderer } from "../rendering/QuestRenderer";
import { QuestRunnerModelManager } from "../rendering/QuestRunnerModelManager"; import { QuestRunnerModelManager } from "../rendering/QuestRunnerModelManager";
import { QuestRendererView } from "./QuestRendererView"; import { QuestRendererView } from "./QuestRendererView";
import { QuestRunner } from "../QuestRunner";
import { GuiStore } from "../../core/stores/GuiStore";
import { QuestEditorStore } from "../stores/QuestEditorStore";
export class QuestRunnerRendererView extends QuestRendererView { export class QuestRunnerRendererView extends QuestRendererView {
constructor() { constructor(gui_store: GuiStore, quest_editor_store: QuestEditorStore) {
super("quest_editor_QuestRunnerRendererView", new QuestRenderer(QuestRunnerModelManager)); 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(); this.renderer.init_camera_controls();

View File

@ -1,5 +1,4 @@
import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { quest_editor_store } from "../stores/QuestEditorStore";
import { el } from "../../core/gui/dom"; import { el } from "../../core/gui/dom";
import { REGISTER_COUNT } from "../scripting/vm/VirtualMachine"; import { REGISTER_COUNT } from "../scripting/vm/VirtualMachine";
import { TextInput } from "../../core/gui/TextInput"; 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 { number_to_hex_string } from "../../core/util";
import "./RegistersView.css"; import "./RegistersView.css";
import { Select } from "../../core/gui/Select"; import { Select } from "../../core/gui/Select";
import { QuestRunner } from "../QuestRunner";
enum RegisterDisplayType { enum RegisterDisplayType {
Signed, Signed,
@ -65,7 +65,7 @@ export class RegistersView extends ResizableWidget {
this.container_element, this.container_element,
); );
constructor() { constructor(private readonly quest_runner: QuestRunner) {
super(); super();
this.type_select.selected.val = RegisterDisplayType.Unsigned; this.type_select.selected.val = RegisterDisplayType.Unsigned;
@ -96,8 +96,7 @@ export class RegistersView extends ResizableWidget {
// predicate that indicates whether to display // predicate that indicates whether to display
// placeholder text or the actual register values // placeholder text or the actual register values
const should_use_placeholders = (): boolean => const should_use_placeholders = (): boolean =>
!quest_editor_store.quest_runner.paused.val || !this.quest_runner.paused.val || !this.quest_runner.running.val;
!quest_editor_store.quest_runner.running.val;
// set initial values // set initial values
this.update(should_use_placeholders(), this.hex_checkbox.checked.val); this.update(should_use_placeholders(), this.hex_checkbox.checked.val);
@ -105,10 +104,10 @@ export class RegistersView extends ResizableWidget {
this.disposables( this.disposables(
// check if values need to be updated // check if values need to be updated
// when QuestRunner execution state changes // 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), 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), this.update(should_use_placeholders(), this.hex_checkbox.checked.val),
), ),
@ -132,23 +131,23 @@ export class RegistersView extends ResizableWidget {
switch (type) { switch (type) {
case RegisterDisplayType.Signed: case RegisterDisplayType.Signed:
getter = quest_editor_store.quest_runner.vm.get_register_signed; getter = this.quest_runner.vm.get_register_signed;
break; break;
case RegisterDisplayType.Unsigned: case RegisterDisplayType.Unsigned:
getter = quest_editor_store.quest_runner.vm.get_register_unsigned; getter = this.quest_runner.vm.get_register_unsigned;
break; break;
case RegisterDisplayType.Word: case RegisterDisplayType.Word:
getter = quest_editor_store.quest_runner.vm.get_register_word; getter = this.quest_runner.vm.get_register_word;
break; break;
case RegisterDisplayType.Byte: case RegisterDisplayType.Byte:
getter = quest_editor_store.quest_runner.vm.get_register_byte; getter = this.quest_runner.vm.get_register_byte;
break; break;
case RegisterDisplayType.Float: case RegisterDisplayType.Float:
getter = quest_editor_store.quest_runner.vm.get_register_float; getter = this.quest_runner.vm.get_register_float;
break; 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 { private update(use_placeholders: boolean, use_hex: boolean): void {
@ -162,7 +161,7 @@ export class RegistersView extends ResizableWidget {
} else if (use_hex) { } else if (use_hex) {
for (let i = 0; i < REGISTER_COUNT; i++) { for (let i = 0; i < REGISTER_COUNT; i++) {
const reg_el = this.register_els[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 }); reg_el.value.set_val(number_to_hex_string(reg_val), { silent: true });
} }

11
src/quest_editor/index.ts Normal file
View File

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

View File

@ -14,7 +14,7 @@ export class QuestEditorUiPersister extends Persister {
); );
async load_layout_config( async load_layout_config(
components: string[], components: readonly string[],
default_config: GoldenLayout.ItemConfigType[], default_config: GoldenLayout.ItemConfigType[],
): Promise<any> { ): Promise<any> {
const config = await this.load<GoldenLayout.ItemConfigType[]>(LAYOUT_CONFIG_KEY); const config = await this.load<GoldenLayout.ItemConfigType[]>(LAYOUT_CONFIG_KEY);
@ -28,7 +28,7 @@ export class QuestEditorUiPersister extends Persister {
private verify_layout_config( private verify_layout_config(
config: GoldenLayout.ItemConfigType[], config: GoldenLayout.ItemConfigType[],
components: string[], components: readonly string[],
): boolean { ): boolean {
const set = new Set(components); const set = new Set(components);

View File

@ -1,28 +1,34 @@
import { QuestRenderer } from "./QuestRenderer"; import { QuestRenderer } from "./QuestRenderer";
import { quest_editor_store } from "../stores/QuestEditorStore";
import { AreaVariantDetails, QuestModelManager } from "./QuestModelManager"; import { AreaVariantDetails, QuestModelManager } from "./QuestModelManager";
import { AreaVariantModel } from "../model/AreaVariantModel"; import { AreaVariantModel } from "../model/AreaVariantModel";
import { QuestNpcModel } from "../model/QuestNpcModel"; import { QuestNpcModel } from "../model/QuestNpcModel";
import { QuestObjectModel } from "../model/QuestObjectModel"; import { QuestObjectModel } from "../model/QuestObjectModel";
import { ListProperty } from "../../core/observable/property/list/ListProperty"; import { ListProperty } from "../../core/observable/property/list/ListProperty";
import { list_property } from "../../core/observable"; 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. * Model loader used while editing a quest.
*/ */
export class QuestEditorModelManager extends QuestModelManager { export class QuestEditorModelManager extends QuestModelManager {
constructor(renderer: QuestRenderer) { constructor(
private readonly current_quest: Property<QuestModel | undefined>,
private readonly current_area: Property<AreaModel | undefined>,
renderer: QuestRenderer,
) {
super(renderer); super(renderer);
this.disposer.add_all( this.disposer.add_all(
quest_editor_store.current_quest.observe(this.area_variant_changed), current_quest.observe(this.area_variant_changed),
quest_editor_store.current_area.observe(this.area_variant_changed), current_area.observe(this.area_variant_changed),
); );
} }
protected get_area_variant_details(): AreaVariantDetails { protected get_area_variant_details(): AreaVariantDetails {
const quest = quest_editor_store.current_quest.val; const quest = this.current_quest.val;
const area = quest_editor_store.current_area.val; const area = this.current_area.val;
let area_variant: AreaVariantModel | undefined; let area_variant: AreaVariantModel | undefined;
let npcs: ListProperty<QuestNpcModel>; let npcs: ListProperty<QuestNpcModel>;

View File

@ -1,7 +1,6 @@
import { QuestEntityModel } from "../model/QuestEntityModel"; import { QuestEntityModel } from "../model/QuestEntityModel";
import { Euler, Intersection, Mesh, Plane, Quaternion, Raycaster, Vector2, Vector3 } from "three"; import { Euler, Intersection, Mesh, Plane, Quaternion, Raycaster, Vector2, Vector3 } from "three";
import { QuestRenderer } from "./QuestRenderer"; import { QuestRenderer } from "./QuestRenderer";
import { quest_editor_store } from "../stores/QuestEditorStore";
import { EntityUserData } from "./conversion/entities"; import { EntityUserData } from "./conversion/entities";
import { QuestNpcModel } from "../model/QuestNpcModel"; import { QuestNpcModel } from "../model/QuestNpcModel";
import { AreaUserData } from "./conversion/areas"; import { AreaUserData } from "./conversion/areas";
@ -18,6 +17,7 @@ import {
import { QuestObjectModel } from "../model/QuestObjectModel"; import { QuestObjectModel } from "../model/QuestObjectModel";
import { AreaModel } from "../model/AreaModel"; import { AreaModel } from "../model/AreaModel";
import { QuestModel } from "../model/QuestModel"; import { QuestModel } from "../model/QuestModel";
import { QuestEditorStore } from "../stores/QuestEditorStore";
const ZERO_VECTOR = Object.freeze(new Vector3(0, 0, 0)); const ZERO_VECTOR = Object.freeze(new Vector3(0, 0, 0));
const UP_VECTOR = Object.freeze(new Vector3(0, 1, 0)); const UP_VECTOR = Object.freeze(new Vector3(0, 1, 0));
@ -42,10 +42,13 @@ export class QuestEntityControls implements Disposable {
set enabled(enabled: boolean) { set enabled(enabled: boolean) {
this._enabled = enabled; this._enabled = enabled;
this.state.cancel(); 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)); this.disposer.add(quest_editor_store.selected_entity.observe(this.selected_entity_changed));
renderer.dom_element.addEventListener("keydown", this.keydown); 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, "dragleave", this.dragleave);
add_entity_dnd_listener(renderer.dom_element, "drop", this.drop); 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 => { dispose = (): void => {
@ -304,16 +307,20 @@ interface State {
} }
class IdleState implements 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 { process_event(evt: Evt): State {
switch (evt.type) { switch (evt.type) {
case EvtType.KeyDown: { case EvtType.KeyDown: {
if (this.enabled) { 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") { 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 (pick) {
if (evt.buttons === 1) { if (evt.buttons === 1) {
quest_editor_store.set_selected_entity(pick.entity); this.quest_editor_store.set_selected_entity(pick.entity);
if (this.enabled) { if (this.enabled) {
return new TranslationState( return new TranslationState(
this.quest_editor_store,
this.renderer, this.renderer,
pick.entity, pick.entity,
pick.drag_adjust, pick.drag_adjust,
@ -336,10 +344,11 @@ class IdleState implements State {
); );
} }
} else if (evt.buttons === 2) { } 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) { if (this.enabled) {
return new RotationState( return new RotationState(
this.quest_editor_store,
this.renderer, this.renderer,
pick.entity, pick.entity,
pick.mesh, pick.mesh,
@ -365,7 +374,7 @@ class IdleState implements State {
!evt.moved_since_last_pointer_down && !evt.moved_since_last_pointer_down &&
!this.pick_entity(evt.pointer_device_position) !this.pick_entity(evt.pointer_device_position)
) { ) {
quest_editor_store.set_selected_entity(undefined); this.quest_editor_store.set_selected_entity(undefined);
} }
return this; return this;
@ -378,12 +387,18 @@ class IdleState implements State {
case EvtType.EntityDragEnter: { case EvtType.EntityDragEnter: {
if (this.enabled) { if (this.enabled) {
const area = quest_editor_store.current_area.val; const area = this.quest_editor_store.current_area.val;
const quest = quest_editor_store.current_quest.val; const quest = this.quest_editor_store.current_quest.val;
if (!area || !quest) return this; 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 { } else {
return this; return this;
} }
@ -440,6 +455,7 @@ class TranslationState implements State {
private cancelled = false; private cancelled = false;
constructor( constructor(
private readonly quest_editor_store: QuestEditorStore,
private readonly renderer: QuestRenderer, private readonly renderer: QuestRenderer,
private readonly entity: QuestEntityModel, private readonly entity: QuestEntityModel,
private readonly drag_adjust: Vector3, private readonly drag_adjust: Vector3,
@ -454,7 +470,7 @@ class TranslationState implements State {
switch (evt.type) { switch (evt.type) {
case EvtType.MouseMove: { case EvtType.MouseMove: {
if (this.cancelled) { 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) { if (evt.moved_since_last_pointer_down) {
@ -475,7 +491,7 @@ class TranslationState implements State {
this.renderer.controls.enabled = true; this.renderer.controls.enabled = true;
if (!this.cancelled && evt.moved_since_last_pointer_down) { if (!this.cancelled && evt.moved_since_last_pointer_down) {
quest_editor_store.translate_entity( this.quest_editor_store.translate_entity(
this.entity, this.entity,
this.initial_section, this.initial_section,
this.entity.section.val, 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: 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; private cancelled = false;
constructor( constructor(
private readonly quest_editor_store: QuestEditorStore,
private readonly renderer: QuestRenderer, private readonly renderer: QuestRenderer,
private readonly entity: QuestEntityModel, private readonly entity: QuestEntityModel,
private readonly mesh: Mesh, private readonly mesh: Mesh,
@ -526,7 +545,7 @@ class RotationState implements State {
switch (evt.type) { switch (evt.type) {
case EvtType.MouseMove: { case EvtType.MouseMove: {
if (this.cancelled) { 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) { if (evt.moved_since_last_pointer_down) {
@ -547,7 +566,7 @@ class RotationState implements State {
this.renderer.controls.enabled = true; this.renderer.controls.enabled = true;
if (!this.cancelled && evt.moved_since_last_pointer_down) { if (!this.cancelled && evt.moved_since_last_pointer_down) {
quest_editor_store.rotate_entity( this.quest_editor_store.rotate_entity(
this.entity, this.entity,
this.initial_rotation, this.initial_rotation,
this.entity.world_rotation.val, 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: 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 readonly drag_adjust = new Vector3(0, 0, 0);
private cancelled = false; 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; this.renderer = renderer;
evt.drag_element.style.display = "none"; evt.drag_element.style.display = "none";
@ -637,7 +664,7 @@ class CreationState implements State {
); );
quest.add_entity(this.entity); 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 { process_event(evt: Evt): State {
@ -650,7 +677,7 @@ class CreationState implements State {
evt.data_transfer.dropEffect = "none"; evt.data_transfer.dropEffect = "none";
} }
return new IdleState(this.renderer, true); return new IdleState(this.quest_editor_store, this.renderer, true);
} }
evt.stop_propagation(); evt.stop_propagation();
@ -676,21 +703,21 @@ class CreationState implements State {
case EvtType.EntityDragLeave: { case EvtType.EntityDragLeave: {
evt.drag_element.style.display = "flex"; 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) { if (quest) {
quest.remove_entity(this.entity); quest.remove_entity(this.entity);
} }
return new IdleState(this.renderer, true); return new IdleState(this.quest_editor_store, this.renderer, true);
} }
case EvtType.EntityDrop: { case EvtType.EntityDrop: {
if (!this.cancelled) { 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: default:
@ -701,7 +728,7 @@ class CreationState implements State {
cancel(): void { cancel(): void {
this.cancelled = true; this.cancelled = true;
const quest = quest_editor_store.current_quest.val; const quest = this.quest_editor_store.current_quest.val;
if (quest) { if (quest) {
quest.remove_entity(this.entity); quest.remove_entity(this.entity);

View File

@ -52,10 +52,10 @@ export class QuestRenderer extends Renderer {
selected_entity: QuestEntityModel | undefined; selected_entity: QuestEntityModel | undefined;
constructor(ModelManager: new (renderer: QuestRenderer) => QuestModelManager) { constructor(create_model_manager: (renderer: QuestRenderer) => QuestModelManager) {
super(); super();
this.disposer.add(new ModelManager(this)); this.disposer.add(create_model_manager(this));
} }
/** /**

View File

@ -1,24 +1,23 @@
import { QuestRenderer } from "./QuestRenderer"; import { QuestRenderer } from "./QuestRenderer";
import { quest_editor_store } from "../stores/QuestEditorStore";
import { AreaVariantDetails, QuestModelManager } from "./QuestModelManager"; import { AreaVariantDetails, QuestModelManager } from "./QuestModelManager";
import { QuestRunner } from "../QuestRunner";
/** /**
* Model loader used while running a quest. * Model loader used while running a quest.
*/ */
export class QuestRunnerModelManager extends QuestModelManager { export class QuestRunnerModelManager extends QuestModelManager {
constructor(renderer: QuestRenderer) { constructor(private readonly quest_runner: QuestRunner, renderer: QuestRenderer) {
super(renderer); super(renderer);
this.disposer.add_all( this.disposer.add_all(
quest_editor_store.quest_runner.game_state.current_area_variant.observe( this.quest_runner.game_state.current_area_variant.observe(this.area_variant_changed, {
this.area_variant_changed, call_now: true,
{ call_now: true }, }),
),
); );
} }
protected get_area_variant_details(): AreaVariantDetails { 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 { return {
episode: game_state.episode, episode: game_state.episode,

View File

@ -3,7 +3,6 @@ import { AssemblyAnalyser } from "../scripting/AssemblyAnalyser";
import { Disposable } from "../../core/observable/Disposable"; import { Disposable } from "../../core/observable/Disposable";
import { Disposer } from "../../core/observable/Disposer"; import { Disposer } from "../../core/observable/Disposer";
import { SimpleUndo } from "../../core/undo/SimpleUndo"; import { SimpleUndo } from "../../core/undo/SimpleUndo";
import { quest_editor_store } from "./QuestEditorStore";
import { ASM_SYNTAX } from "./asm_syntax"; import { ASM_SYNTAX } from "./asm_syntax";
import { AssemblyError, AssemblyWarning } from "../scripting/assembly"; import { AssemblyError, AssemblyWarning } from "../scripting/assembly";
import { Observable } from "../../core/observable/Observable"; import { Observable } from "../../core/observable/Observable";
@ -18,6 +17,7 @@ import SignatureHelpResult = languages.SignatureHelpResult;
import LocationLink = languages.LocationLink; import LocationLink = languages.LocationLink;
import IModelContentChange = editor.IModelContentChange; import IModelContentChange = editor.IModelContentChange;
import { Breakpoint } from "../scripting/vm/Debugger"; import { Breakpoint } from "../scripting/vm/Debugger";
import { QuestEditorStore } from "./QuestEditorStore";
const assembly_analyser = new AssemblyAnalyser(); const assembly_analyser = new AssemblyAnalyser();
@ -104,11 +104,13 @@ export class AsmEditorStore implements Disposable {
readonly has_issues: Property<boolean> = assembly_analyser.issues.map( readonly has_issues: Property<boolean> = assembly_analyser.issues.map(
issues => issues.warnings.length + issues.errors.length > 0, issues => issues.warnings.length + issues.errors.length > 0,
); );
readonly breakpoints: ListProperty<Breakpoint> = quest_editor_store.quest_runner.breakpoints; readonly breakpoints: ListProperty<Breakpoint>;
readonly execution_location: Property<number | undefined> = readonly execution_location: Property<number | undefined>;
quest_editor_store.quest_runner.pause_location;
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( this.disposer.add_all(
quest_editor_store.current_quest.observe(this.quest_changed, { quest_editor_store.current_quest.observe(this.quest_changed, {
call_now: true, call_now: true,
@ -231,7 +233,7 @@ export class AsmEditorStore implements Disposable {
this.undo.reset(); this.undo.reset();
this.model_disposer.dispose_all(); this.model_disposer.dispose_all();
const quest = quest_editor_store.current_quest.val; const quest = this.quest_editor_store.current_quest.val;
if (quest) { if (quest) {
const manual_stack = !this.inline_args_mode.val; 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. // Line numbers can't go lower than 1.
const new_line_num = Math.max(line_num - num_removed_lines, 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); new_breakpoints.push(new_line_num);
} }
} }
@ -281,7 +283,9 @@ export class AsmEditorStore implements Disposable {
// number of removed lines. // number of removed lines.
for (const breakpoint of this.breakpoints.val) { for (const breakpoint of this.breakpoints.val) {
if (breakpoint.line_no > change.range.endLineNumber) { 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); 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 // forwards by the number of added lines
for (const breakpoint of this.breakpoints.val) { for (const breakpoint of this.breakpoints.val) {
if (breakpoint.line_no > change.range.endLineNumber) { 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); new_breakpoints.push(breakpoint.line_no + num_added_lines);
} }
} }
@ -302,10 +308,8 @@ export class AsmEditorStore implements Disposable {
} }
for (const breakpoint of new_breakpoints) { 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();

View File

@ -13,7 +13,7 @@ import { SectionModel } from "../model/SectionModel";
import { QuestEntityModel } from "../model/QuestEntityModel"; import { QuestEntityModel } from "../model/QuestEntityModel";
import { Disposable } from "../../core/observable/Disposable"; import { Disposable } from "../../core/observable/Disposable";
import { Disposer } from "../../core/observable/Disposer"; 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 { UndoStack } from "../../core/undo/UndoStack";
import { TranslateEntityAction } from "../actions/TranslateEntityAction"; import { TranslateEntityAction } from "../actions/TranslateEntityAction";
import { EditShortDescriptionAction } from "../actions/EditShortDescriptionAction"; import { EditShortDescriptionAction } from "../actions/EditShortDescriptionAction";
@ -48,7 +48,7 @@ export class QuestEditorStore implements Disposable {
readonly current_area: Property<AreaModel | undefined> = this._current_area; readonly current_area: Property<AreaModel | undefined> = this._current_area;
readonly selected_entity: Property<QuestEntityModel | undefined> = this._selected_entity; readonly selected_entity: Property<QuestEntityModel | undefined> = this._selected_entity;
constructor() { constructor(gui_store: GuiStore) {
this.disposer.add_all( this.disposer.add_all(
gui_store.tool.observe( gui_store.tool.observe(
({ value: tool }) => { ({ value: tool }) => {
@ -181,6 +181,7 @@ export class QuestEditorStore implements Disposable {
this.undo this.undo
.push( .push(
new TranslateEntityAction( new TranslateEntityAction(
this,
entity, entity,
old_section, old_section,
new_section, new_section,
@ -198,15 +199,17 @@ export class QuestEditorStore implements Disposable {
new_rotation: Euler, new_rotation: Euler,
world: boolean, world: boolean,
): void => { ): 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 => { 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 => { 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<void> { private async set_quest(quest?: QuestModel, filename?: string): Promise<void> {
@ -268,5 +271,3 @@ export class QuestEditorStore implements Disposable {
} }
}; };
} }
export const quest_editor_store = new QuestEditorStore();

View File

@ -2,10 +2,10 @@ import { el, Icon } from "../../core/gui/dom";
import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { FileButton } from "../../core/gui/FileButton"; import { FileButton } from "../../core/gui/FileButton";
import { ToolBar } from "../../core/gui/ToolBar"; import { ToolBar } from "../../core/gui/ToolBar";
import { texture_store } from "../stores/TextureStore";
import { RendererWidget } from "../../core/gui/RendererWidget"; import { RendererWidget } from "../../core/gui/RendererWidget";
import { TextureRenderer } from "../rendering/TextureRenderer"; 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 { export class TextureView extends ResizableWidget {
readonly element = el.div({ class: "viewer_TextureView" }); 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 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(); super();
this.renderer_view = this.disposable(
new RendererWidget(new TextureRenderer(texture_store)),
);
this.element.append(this.tool_bar.element, this.renderer_view.element); this.element.append(this.tool_bar.element, this.renderer_view.element);
this.disposable( this.disposable(

View File

@ -1,23 +1,24 @@
import { TabContainer } from "../../core/gui/TabContainer"; import { TabContainer } from "../../core/gui/TabContainer";
import { Model3DView } from "./model_3d/Model3DView";
import { TextureView } from "./TextureView";
export class ViewerView extends TabContainer { export class ViewerView extends TabContainer {
constructor() { constructor(
create_model_3d_view: () => Promise<Model3DView>,
create_texture_view: () => Promise<TextureView>,
) {
super({ super({
class: "viewer_ViewerView", class: "viewer_ViewerView",
tabs: [ tabs: [
{ {
title: "Models", title: "Models",
key: "model", key: "model",
create_view: async function() { create_view: create_model_3d_view,
return new (await import("./model_3d/Model3DView")).Model3DView();
},
}, },
{ {
title: "Textures", title: "Textures",
key: "texture", key: "texture",
create_view: async function() { create_view: create_texture_view,
return new (await import("./TextureView")).TextureView();
},
}, },
], ],
}); });

View File

@ -3,12 +3,12 @@ import { FileButton } from "../../../core/gui/FileButton";
import { CheckBox } from "../../../core/gui/CheckBox"; import { CheckBox } from "../../../core/gui/CheckBox";
import { NumberInput } from "../../../core/gui/NumberInput"; import { NumberInput } from "../../../core/gui/NumberInput";
import { PSO_FRAME_RATE } from "../../../core/rendering/conversion/ninja_animation"; import { PSO_FRAME_RATE } from "../../../core/rendering/conversion/ninja_animation";
import { model_store } from "../../stores/Model3DStore";
import { Label } from "../../../core/gui/Label"; import { Label } from "../../../core/gui/Label";
import { Icon } from "../../../core/gui/dom"; import { Icon } from "../../../core/gui/dom";
import { Model3DStore } from "../../stores/Model3DStore";
export class Model3DToolBar extends ToolBar { export class Model3DToolBar extends ToolBar {
constructor() { constructor(model_3d_store: Model3DStore) {
const open_file_button = new FileButton("Open file...", { const open_file_button = new FileButton("Open file...", {
icon_left: Icon.File, icon_left: Icon.File,
accept: ".nj, .njm, .xj, .xvm", accept: ".nj, .njm, .xj, .xvm",
@ -24,11 +24,11 @@ export class Model3DToolBar extends ToolBar {
const animation_frame_input = new NumberInput(1, { const animation_frame_input = new NumberInput(1, {
label: "Frame:", label: "Frame:",
min: 1, min: 1,
max: model_store.animation_frame_count, max: model_3d_store.animation_frame_count,
step: 1, step: 1,
}); });
const animation_frame_count_label = new Label( 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({ super({
@ -45,33 +45,35 @@ export class Model3DToolBar extends ToolBar {
// Always-enabled controls. // Always-enabled controls.
this.disposables( this.disposables(
open_file_button.files.observe(({ value: files }) => { 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. // 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( this.disposables(
play_animation_checkbox.enabled.bind_to(enabled), 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 }) => 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.enabled.bind_to(enabled),
animation_frame_rate_input.value.observe(({ value }) => 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.enabled.bind_to(enabled),
animation_frame_input.value.bind_to( 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 }) => 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), animation_frame_count_label.enabled.bind_to(enabled),

View File

@ -1,14 +1,14 @@
import { el } from "../../../core/gui/dom"; import { el } from "../../../core/gui/dom";
import { ResizableWidget } from "../../../core/gui/ResizableWidget"; import { ResizableWidget } from "../../../core/gui/ResizableWidget";
import "./Model3DView.css"; 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 { RendererWidget } from "../../../core/gui/RendererWidget";
import { model_store } from "../../stores/Model3DStore";
import { Model3DRenderer } from "../../rendering/Model3DRenderer"; import { Model3DRenderer } from "../../rendering/Model3DRenderer";
import { Model3DToolBar } from "./Model3DToolBar"; import { Model3DToolBar } from "./Model3DToolBar";
import { Model3DSelectListView } from "./Model3DSelectListView"; import { Model3DSelectListView } from "./Model3DSelectListView";
import { CharacterClassModel } from "../../model/CharacterClassModel"; import { CharacterClassModel } from "../../model/CharacterClassModel";
import { CharacterClassAnimationModel } from "../../model/CharacterClassAnimationModel"; import { CharacterClassAnimationModel } from "../../model/CharacterClassAnimationModel";
import { Model3DStore } from "../../stores/Model3DStore";
const MODEL_LIST_WIDTH = 100; const MODEL_LIST_WIDTH = 100;
const ANIMATION_LIST_WIDTH = 140; const ANIMATION_LIST_WIDTH = 140;
@ -21,25 +21,27 @@ export class Model3DView extends ResizableWidget {
private animation_list_view: Model3DSelectListView<CharacterClassAnimationModel>; private animation_list_view: Model3DSelectListView<CharacterClassAnimationModel>;
private renderer_view: RendererWidget; private renderer_view: RendererWidget;
constructor() { constructor(gui_store: GuiStore, model_3d_store: Model3DStore) {
super(); 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( this.model_list_view = this.disposable(
new Model3DSelectListView( new Model3DSelectListView(
model_store.models, model_3d_store.models,
model_store.current_model, model_3d_store.current_model,
model_store.set_current_model, model_3d_store.set_current_model,
), ),
); );
this.animation_list_view = this.disposable( this.animation_list_view = this.disposable(
new Model3DSelectListView( new Model3DSelectListView(
model_store.animations, model_3d_store.animations,
model_store.current_animation, model_3d_store.current_animation,
model_store.set_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; 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(); this.renderer_view.start_rendering();

18
src/viewer/index.ts Normal file
View File

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

View File

@ -12,7 +12,6 @@ import {
SkinnedMesh, SkinnedMesh,
Vector3, Vector3,
} from "three"; } from "three";
import { model_store } from "../stores/Model3DStore";
import { Disposable } from "../../core/observable/Disposable"; import { Disposable } from "../../core/observable/Disposable";
import { NjMotion } from "../../core/data_formats/parsing/ninja/motion"; import { NjMotion } from "../../core/data_formats/parsing/ninja/motion";
import { xvm_to_textures } from "../../core/rendering/conversion/ninja_textures"; import { xvm_to_textures } from "../../core/rendering/conversion/ninja_textures";
@ -25,6 +24,7 @@ import {
import { Renderer } from "../../core/rendering/Renderer"; import { Renderer } from "../../core/rendering/Renderer";
import { Disposer } from "../../core/observable/Disposer"; import { Disposer } from "../../core/observable/Disposer";
import { ChangeEvent } from "../../core/observable/Observable"; import { ChangeEvent } from "../../core/observable/Observable";
import { Model3DStore } from "../stores/Model3DStore";
export class Model3DRenderer extends Renderer implements Disposable { export class Model3DRenderer extends Renderer implements Disposable {
private readonly disposer = new Disposer(); private readonly disposer = new Disposer();
@ -40,17 +40,17 @@ export class Model3DRenderer extends Renderer implements Disposable {
readonly camera = new PerspectiveCamera(75, 1, 1, 200); readonly camera = new PerspectiveCamera(75, 1, 1, 200);
constructor() { constructor(private readonly model_3d_store: Model3DStore) {
super(); super();
this.disposer.add_all( this.disposer.add_all(
model_store.current_nj_data.observe(this.nj_data_or_xvm_changed), model_3d_store.current_nj_data.observe(this.nj_data_or_xvm_changed),
model_store.current_xvm.observe(this.nj_data_or_xvm_changed), model_3d_store.current_xvm.observe(this.nj_data_or_xvm_changed),
model_store.current_nj_motion.observe(this.nj_motion_changed), model_3d_store.current_nj_motion.observe(this.nj_motion_changed),
model_store.show_skeleton.observe(this.show_skeleton_changed), model_3d_store.show_skeleton.observe(this.show_skeleton_changed),
model_store.animation_playing.observe(this.animation_playing_changed), model_3d_store.animation_playing.observe(this.animation_playing_changed),
model_store.animation_frame_rate.observe(this.animation_frame_rate_changed), model_3d_store.animation_frame_rate.observe(this.animation_frame_rate_changed),
model_store.animation_frame.observe(this.animation_frame_changed), model_3d_store.animation_frame.observe(this.animation_frame_changed),
); );
this.init_camera_controls(); this.init_camera_controls();
@ -95,14 +95,14 @@ export class Model3DRenderer extends Renderer implements Disposable {
this.animation = undefined; 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) { if (nj_data) {
const { nj_object, has_skeleton } = nj_data; const { nj_object, has_skeleton } = nj_data;
let mesh: Mesh; 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 textures = xvm ? xvm_to_textures(xvm) : undefined;
const materials = const materials =
@ -132,7 +132,7 @@ export class Model3DRenderer extends Renderer implements Disposable {
this.scene.add(mesh); this.scene.add(mesh);
this.skeleton_helper = new SkeletonHelper(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.skeleton_helper.material as any).linewidth = 3;
this.scene.add(this.skeleton_helper); this.scene.add(this.skeleton_helper);
@ -150,7 +150,7 @@ export class Model3DRenderer extends Renderer implements Disposable {
mixer = this.animation.mixer; 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; 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<number>): void => { private animation_frame_changed = ({ value: frame }: ChangeEvent<number>): 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) { if (this.animation && nj_motion) {
const frame_count = nj_motion.frame_count; 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) { if (this.animation && !this.animation.action.paused) {
const time = this.animation.action.time; const time = this.animation.action.time;
this.update_animation_time = false; 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; this.update_animation_time = true;
} }
} }

View File

@ -12,7 +12,7 @@ import { Renderer } from "../../core/rendering/Renderer";
import { Disposer } from "../../core/observable/Disposer"; import { Disposer } from "../../core/observable/Disposer";
import { Xvm } from "../../core/data_formats/parsing/ninja/texture"; import { Xvm } from "../../core/data_formats/parsing/ninja/texture";
import { xvm_texture_to_texture } from "../../core/rendering/conversion/ninja_textures"; 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"); import Logger = require("js-logger");
const logger = Logger.get("viewer/rendering/TextureRenderer"); 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); readonly camera = new OrthographicCamera(-400, 400, 300, -300, 1, 10);
constructor() { constructor(texture_store: TextureStore) {
super(); super();
this.disposer.add_all( this.disposer.add_all(

View File

@ -278,5 +278,3 @@ export class Model3DStore implements Disposable {
} }
} }
} }
export const model_store = new Model3DStore();

View File

@ -21,5 +21,3 @@ export class TextureStore {
} }
}; };
} }
export const texture_store = new TextureStore();