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