Most dependencies are now injected to improve testability.

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

View File

@ -2,20 +2,28 @@ import { NavigationView } from "./NavigationView";
import { MainContentView } from "./MainContentView";
import { 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();

View File

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

View File

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

View File

@ -17,7 +17,7 @@ const GUI_TOOL_TO_STRING = new Map([
]);
const STRING_TO_GUI_TOOL = new Map([...GUI_TOOL_TO_STRING.entries()].map(([k, v]) => [v, k]));
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);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,29 @@
import { HuntOptimizerView } from "./gui/HuntOptimizerView";
import { ServerMap } from "../core/stores/ServerMap";
import { HuntMethodStore, load_hunt_method_stores } from "./stores/HuntMethodStore";
import { GuiStore } from "../core/stores/GuiStore";
import { HuntOptimizerStore, load_hunt_optimizer_stores } from "./stores/HuntOptimizerStore";
import { ItemTypeStore } from "../core/stores/ItemTypeStore";
import { HuntMethodPersister } from "./persistence/HuntMethodPersister";
import { HuntOptimizerPersister } from "./persistence/HuntOptimizerPersister";
import { ItemDropStore } from "./stores/ItemDropStore";
export function initialize_hunt_optimizer(
gui_store: GuiStore,
item_type_stores: ServerMap<ItemTypeStore>,
item_drop_stores: ServerMap<ItemDropStore>,
): HuntOptimizerView {
const hunt_method_stores: ServerMap<HuntMethodStore> = load_hunt_method_stores(
gui_store,
new HuntMethodPersister(),
);
const hunt_optimizer_stores: ServerMap<HuntOptimizerStore> = load_hunt_optimizer_stores(
gui_store,
new HuntOptimizerPersister(item_type_stores),
item_type_stores,
item_drop_stores,
hunt_method_stores,
);
return new HuntOptimizerView(hunt_optimizer_stores, hunt_method_stores);
}

View File

@ -5,7 +5,7 @@ import { Duration } from "luxon";
const METHOD_USER_TIMES_KEY = "HuntMethodStore.methodUserTimes";
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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 => {

View File

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

View File

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

View File

@ -6,14 +6,14 @@ import { entity_dnd_source } from "./entity_dnd";
import { render_entity_to_image } from "../rendering/render_entity_to_image";
import { 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" });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
},
]
: []),
],
},
],
},
];
}
}

View File

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

View File

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

View File

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

View File

@ -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
View File

@ -0,0 +1,11 @@
import { QuestEditorView } from "./gui/QuestEditorView";
import { GuiStore } from "../core/stores/GuiStore";
import { QuestEditorStore } from "./stores/QuestEditorStore";
import { AsmEditorStore } from "./stores/AsmEditorStore";
export function initialize_quest_editor(gui_store: GuiStore): QuestEditorView {
const quest_editor_store = new QuestEditorStore(gui_store);
const asm_editor_store = new AsmEditorStore(quest_editor_store);
return new QuestEditorView(gui_store, quest_editor_store, asm_editor_store);
}

View File

@ -14,7 +14,7 @@ export class QuestEditorUiPersister extends Persister {
);
async load_layout_config(
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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View File

@ -0,0 +1,18 @@
import { ViewerView } from "./gui/ViewerView";
import { GuiStore } from "../core/stores/GuiStore";
export function initialize_viewer(gui_store: GuiStore): ViewerView {
return new ViewerView(
async () => {
const { Model3DStore } = await import("./stores/Model3DStore");
const { Model3DView } = await import("./gui/model_3d/Model3DView");
return new Model3DView(gui_store, new Model3DStore());
},
async () => {
const { TextureStore } = await import("./stores/TextureStore");
const { TextureView } = await import("./gui/TextureView");
return new TextureView(gui_store, new TextureStore());
},
);
}

View File

@ -12,7 +12,6 @@ import {
SkinnedMesh,
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;
}
}

View File

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

View File

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

View File

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