Undo/redo now works again in the quest editor. The NPC counts view is also ported.

This commit is contained in:
Daan Vanden Bosch 2019-08-26 15:42:12 +02:00
parent 17400200a0
commit 03dc60cec9
107 changed files with 1501 additions and 1063 deletions

View File

@ -2,6 +2,7 @@ import { create_element } from "../../core/gui/dom";
import { gui_store, GuiTool } from "../../core/stores/GuiStore"; import { gui_store, GuiTool } from "../../core/stores/GuiStore";
import { LazyView } from "../../core/gui/LazyView"; import { LazyView } from "../../core/gui/LazyView";
import { ResizableView } from "../../core/gui/ResizableView"; import { ResizableView } from "../../core/gui/ResizableView";
import { ChangeEvent } from "../../core/observable/Observable";
const TOOLS: [GuiTool, () => Promise<ResizableView>][] = [ const TOOLS: [GuiTool, () => Promise<ResizableView>][] = [
[GuiTool.Viewer, async () => new (await import("../../viewer/gui/ViewerView")).ViewerView()], [GuiTool.Viewer, async () => new (await import("../../viewer/gui/ViewerView")).ViewerView()],
@ -41,7 +42,7 @@ export class MainContentView extends ResizableView {
return this; return this;
} }
private tool_changed = (new_tool: GuiTool) => { private tool_changed = ({ value: new_tool }: ChangeEvent<GuiTool>) => {
for (const tool of this.tool_views.values()) { for (const tool of this.tool_views.values()) {
tool.visible.val = false; tool.visible.val = false;
} }

View File

@ -28,8 +28,8 @@ export class NavigationView extends View {
this.element.append(button.element); this.element.append(button.element);
} }
this.tool_changed(gui_store.tool.val); this.mark_tool_button(gui_store.tool.val);
this.disposable(gui_store.tool.observe(this.tool_changed)); this.disposable(gui_store.tool.observe(({ value }) => this.mark_tool_button(value)));
} }
private mousedown(e: MouseEvent): void { private mousedown(e: MouseEvent): void {
@ -38,7 +38,7 @@ export class NavigationView extends View {
} }
} }
private tool_changed = (tool: GuiTool) => { private mark_tool_button = (tool: GuiTool) => {
const button = this.buttons.get(tool); const button = this.buttons.get(tool);
if (button) button.checked = true; if (button) button.checked = true;
}; };

View File

@ -1,8 +1,8 @@
import Logger from "js-logger"; import Logger from "js-logger";
import { Endianness } from "../../Endianness"; import { Endianness } from "../../Endianness";
import { ControlFlowGraph } from "../../../../old/quest_editor/scripting/data_flow_analysis/ControlFlowGraph"; import { ControlFlowGraph } from "../../../../quest_editor/scripting/data_flow_analysis/ControlFlowGraph";
import { register_value } from "../../../../old/quest_editor/scripting/data_flow_analysis/register_value"; import { register_value } from "../../../../quest_editor/scripting/data_flow_analysis/register_value";
import { stack_value } from "../../../../old/quest_editor/scripting/data_flow_analysis/stack_value"; import { stack_value } from "../../../../quest_editor/scripting/data_flow_analysis/stack_value";
import { import {
Arg, Arg,
DataSegment, DataSegment,
@ -11,13 +11,13 @@ import {
Segment, Segment,
SegmentType, SegmentType,
StringSegment, StringSegment,
} from "../../../../old/quest_editor/scripting/instructions"; } from "../../../../quest_editor/scripting/instructions";
import { import {
Kind, Kind,
Opcode, Opcode,
OPCODES, OPCODES,
StackInteraction, StackInteraction,
} from "../../../../old/quest_editor/scripting/opcodes"; } from "../../../../quest_editor/scripting/opcodes";
import { ArrayBufferCursor } from "../../cursor/ArrayBufferCursor"; import { ArrayBufferCursor } from "../../cursor/ArrayBufferCursor";
import { Cursor } from "../../cursor/Cursor"; import { Cursor } from "../../cursor/Cursor";
import { ResizableBufferCursor } from "../../cursor/ResizableBufferCursor"; import { ResizableBufferCursor } from "../../cursor/ResizableBufferCursor";

View File

@ -4,8 +4,8 @@ import {
InstructionSegment, InstructionSegment,
Segment, Segment,
SegmentType, SegmentType,
} from "../../../../old/quest_editor/scripting/instructions"; } from "../../../../quest_editor/scripting/instructions";
import { Opcode } from "../../../../old/quest_editor/scripting/opcodes"; import { Opcode } from "../../../../quest_editor/scripting/opcodes";
import { prs_compress } from "../../compression/prs/compress"; import { prs_compress } from "../../compression/prs/compress";
import { prs_decompress } from "../../compression/prs/decompress"; import { prs_decompress } from "../../compression/prs/decompress";
import { ArrayBufferCursor } from "../../cursor/ArrayBufferCursor"; import { ArrayBufferCursor } from "../../cursor/ArrayBufferCursor";

View File

@ -314,7 +314,7 @@ define_npc_type_data(NpcType.Scientist, "Scientist", "Scientist", "Scientist", u
define_npc_type_data(NpcType.Nurse, "Nurse", "Nurse", "Nurse", undefined, false); define_npc_type_data(NpcType.Nurse, "Nurse", "Nurse", "Nurse", undefined, false);
define_npc_type_data(NpcType.Irene, "Irene", "Irene", "Irene", undefined, false); define_npc_type_data(NpcType.Irene, "Irene", "Irene", "Irene", undefined, false);
define_npc_type_data(NpcType.ItemShop, "Item Shop", "Item Shop", "Item Shop", undefined, false); define_npc_type_data(NpcType.ItemShop, "Item Shop", "Item Shop", "Item Shop", undefined, false);
define_npc_type_data(NpcType.Nurse2, "Nurse (Ep. II);", "Nurse", "Nurse", 2, false); define_npc_type_data(NpcType.Nurse2, "Nurse (Ep. II)", "Nurse", "Nurse", 2, false);
// //
// Enemy NPCs // Enemy NPCs
@ -450,17 +450,17 @@ define_npc_type_data(NpcType.DarkFalz, "Dark Falz", "Dark Falz", "Dark Falz", 1,
define_npc_type_data( define_npc_type_data(
NpcType.Hildebear2, NpcType.Hildebear2,
"Hildebear (Ep. II);", "Hildebear (Ep. II)",
"Hildebear", "Hildebear",
"Hildelt", "Hildelt",
2, 2,
true, true,
NpcType.Hildeblue2, NpcType.Hildeblue2,
); );
define_npc_type_data(NpcType.Hildeblue2, "Hildeblue (Ep. II);", "Hildeblue", "Hildetorr", 2, true); define_npc_type_data(NpcType.Hildeblue2, "Hildeblue (Ep. II)", "Hildeblue", "Hildetorr", 2, true);
define_npc_type_data( define_npc_type_data(
NpcType.RagRappy2, NpcType.RagRappy2,
"Rag Rappy (Ep. II);", "Rag Rappy (Ep. II)",
"Rag Rappy", "Rag Rappy",
"El Rappy", "El Rappy",
2, 2,
@ -471,39 +471,39 @@ define_npc_type_data(NpcType.LoveRappy, "Love Rappy", "Love Rappy", "Love Rappy"
define_npc_type_data(NpcType.StRappy, "St. Rappy", "St. Rappy", "St. Rappy", 2, true); define_npc_type_data(NpcType.StRappy, "St. Rappy", "St. Rappy", "St. Rappy", 2, true);
define_npc_type_data(NpcType.HalloRappy, "Hallo Rappy", "Hallo Rappy", "Hallo Rappy", 2, true); define_npc_type_data(NpcType.HalloRappy, "Hallo Rappy", "Hallo Rappy", "Hallo Rappy", 2, true);
define_npc_type_data(NpcType.EggRappy, "Egg Rappy", "Egg Rappy", "Egg Rappy", 2, true); define_npc_type_data(NpcType.EggRappy, "Egg Rappy", "Egg Rappy", "Egg Rappy", 2, true);
define_npc_type_data(NpcType.Monest2, "Monest (Ep. II);", "Monest", "Mothvist", 2, true); define_npc_type_data(NpcType.Monest2, "Monest (Ep. II)", "Monest", "Mothvist", 2, true);
define_npc_type_data(NpcType.Mothmant2, "Mothmant", "Mothmant", "Mothvert", 2, true); define_npc_type_data(NpcType.Mothmant2, "Mothmant", "Mothmant", "Mothvert", 2, true);
define_npc_type_data( define_npc_type_data(
NpcType.PoisonLily2, NpcType.PoisonLily2,
"Poison Lily (Ep. II);", "Poison Lily (Ep. II)",
"Poison Lily", "Poison Lily",
"Ob Lily", "Ob Lily",
2, 2,
true, true,
NpcType.NarLily2, NpcType.NarLily2,
); );
define_npc_type_data(NpcType.NarLily2, "Nar Lily (Ep. II);", "Nar Lily", "Mil Lily", 2, true); define_npc_type_data(NpcType.NarLily2, "Nar Lily (Ep. II)", "Nar Lily", "Mil Lily", 2, true);
define_npc_type_data( define_npc_type_data(
NpcType.GrassAssassin2, NpcType.GrassAssassin2,
"Grass Assassin (Ep. II);", "Grass Assassin (Ep. II)",
"Grass Assassin", "Grass Assassin",
"Crimson Assassin", "Crimson Assassin",
2, 2,
true, true,
); );
define_npc_type_data(NpcType.Dimenian2, "Dimenian (Ep. II);", "Dimenian", "Arlan", 2, true); define_npc_type_data(NpcType.Dimenian2, "Dimenian (Ep. II)", "Dimenian", "Arlan", 2, true);
define_npc_type_data( define_npc_type_data(
NpcType.LaDimenian2, NpcType.LaDimenian2,
"La Dimenian (Ep. II);", "La Dimenian (Ep. II)",
"La Dimenian", "La Dimenian",
"Merlan", "Merlan",
2, 2,
true, true,
); );
define_npc_type_data(NpcType.SoDimenian2, "So Dimenian (Ep. II);", "So Dimenian", "Del-D", 2, true); define_npc_type_data(NpcType.SoDimenian2, "So Dimenian (Ep. II)", "So Dimenian", "Del-D", 2, true);
define_npc_type_data( define_npc_type_data(
NpcType.DarkBelra2, NpcType.DarkBelra2,
"Dark Belra (Ep. II);", "Dark Belra (Ep. II)",
"Dark Belra", "Dark Belra",
"Indi Belra", "Indi Belra",
2, 2,
@ -515,7 +515,7 @@ define_npc_type_data(NpcType.BarbaRay, "Barba Ray", "Barba Ray", "Barba Ray", 2,
define_npc_type_data( define_npc_type_data(
NpcType.SavageWolf2, NpcType.SavageWolf2,
"Savage Wolf (Ep. II);", "Savage Wolf (Ep. II)",
"Savage Wolf", "Savage Wolf",
"Gulgus", "Gulgus",
2, 2,
@ -523,23 +523,23 @@ define_npc_type_data(
); );
define_npc_type_data( define_npc_type_data(
NpcType.BarbarousWolf2, NpcType.BarbarousWolf2,
"Barbarous Wolf (Ep. II);", "Barbarous Wolf (Ep. II)",
"Barbarous Wolf", "Barbarous Wolf",
"Gulgus-Gue", "Gulgus-Gue",
2, 2,
true, true,
); );
define_npc_type_data(NpcType.PanArms2, "Pan Arms (Ep. II);", "Pan Arms", "Pan Arms", 2, true); define_npc_type_data(NpcType.PanArms2, "Pan Arms (Ep. II)", "Pan Arms", "Pan Arms", 2, true);
define_npc_type_data(NpcType.Migium2, "Migium (Ep. II);", "Migium", "Migium", 2, true); define_npc_type_data(NpcType.Migium2, "Migium (Ep. II)", "Migium", "Migium", 2, true);
define_npc_type_data(NpcType.Hidoom2, "Hidoom (Ep. II);", "Hidoom", "Hidoom", 2, true); define_npc_type_data(NpcType.Hidoom2, "Hidoom (Ep. II)", "Hidoom", "Hidoom", 2, true);
define_npc_type_data(NpcType.Dubchic2, "Dubchic (Ep. II);", "Dubchic", "Dubchich", 2, true); define_npc_type_data(NpcType.Dubchic2, "Dubchic (Ep. II)", "Dubchic", "Dubchich", 2, true);
define_npc_type_data(NpcType.Gilchic2, "Gilchic (Ep. II);", "Gilchic", "Gilchich", 2, true); define_npc_type_data(NpcType.Gilchic2, "Gilchic (Ep. II)", "Gilchic", "Gilchich", 2, true);
define_npc_type_data(NpcType.Garanz2, "Garanz (Ep. II);", "Garanz", "Baranz", 2, true); define_npc_type_data(NpcType.Garanz2, "Garanz (Ep. II)", "Garanz", "Baranz", 2, true);
define_npc_type_data(NpcType.Dubswitch2, "Dubswitch (Ep. II);", "Dubswitch", "Dubswitch", 2, true); define_npc_type_data(NpcType.Dubswitch2, "Dubswitch (Ep. II)", "Dubswitch", "Dubswitch", 2, true);
define_npc_type_data(NpcType.Delsaber2, "Delsaber (Ep. II);", "Delsaber", "Delsaber", 2, true); define_npc_type_data(NpcType.Delsaber2, "Delsaber (Ep. II)", "Delsaber", "Delsaber", 2, true);
define_npc_type_data( define_npc_type_data(
NpcType.ChaosSorcerer2, NpcType.ChaosSorcerer2,
"Chaos Sorcerer (Ep. II);", "Chaos Sorcerer (Ep. II)",
"Chaos Sorcerer", "Chaos Sorcerer",
"Gran Sorcerer", "Gran Sorcerer",
2, 2,

View File

@ -15,8 +15,8 @@ export class Button extends Control {
this.element.append(create_element("span", { class: "core_Button_inner", text })); this.element.append(create_element("span", { class: "core_Button_inner", text }));
this.enabled.observe(enabled => (this.element.disabled = !enabled)); this.disposables(this.enabled.observe(({ value }) => (this.element.disabled = !value)));
this.element.onclick = (e: MouseEvent) => this._click.emit(e); this.element.onclick = (e: MouseEvent) => this._click.emit({ value: e });
} }
} }

View File

@ -17,9 +17,9 @@ export class CheckBox extends LabelledControl {
this.element.onchange = () => (this.checked.val = this.element.checked); this.element.onchange = () => (this.checked.val = this.element.checked);
this.disposables( this.disposables(
this.checked.observe(checked => (this.element.checked = checked)), this.checked.observe(({ value }) => (this.element.checked = value)),
this.enabled.observe(enabled => (this.element.disabled = !enabled)), this.enabled.observe(({ value }) => (this.element.disabled = !value)),
); );
this.checked.val = checked; this.checked.val = checked;

View File

@ -38,14 +38,16 @@ export class FileButton extends Control {
this.input, this.input,
); );
this.enabled.observe(enabled => { this.disposables(
this.input.disabled = !enabled; this.enabled.observe(({ value }) => {
this.input.disabled = !value;
if (enabled) { if (value) {
this.element.classList.remove("disabled"); this.element.classList.remove("disabled");
} else { } else {
this.element.classList.add("disabled"); this.element.classList.add("disabled");
} }
}); }),
);
} }
} }

View File

@ -35,12 +35,12 @@ export abstract class Input<T> extends LabelledControl {
this.element.append(this.input); this.element.append(this.input);
this.disposables( this.disposables(
this.value.observe(value => this.set_input_value(value)), this.value.observe(({ value }) => this.set_input_value(value)),
this.enabled.observe(enabled => { this.enabled.observe(({ value }) => {
this.input.disabled = !enabled; this.input.disabled = !value;
if (enabled) { if (value) {
this.element.classList.remove("disabled"); this.element.classList.remove("disabled");
} else { } else {
this.element.classList.add("disabled"); this.element.classList.add("disabled");
@ -71,7 +71,7 @@ export abstract class Input<T> extends LabelledControl {
if (is_any_property(value)) { if (is_any_property(value)) {
input[attr] = cvt(value.val); input[attr] = cvt(value.val);
this.disposable(value.observe(v => (input[attr] = cvt(v)))); this.disposable(value.observe(({ value }) => (input[attr] = cvt(value))));
} else { } else {
input[attr] = cvt(value); input[attr] = cvt(value);
} }

View File

@ -21,12 +21,12 @@ export class Label extends View {
this.element.append(text); this.element.append(text);
} else { } else {
this.element.append(text.val); this.element.append(text.val);
this.disposable(text.observe(text => (this.element.textContent = text))); this.disposable(text.observe(({ value }) => (this.element.textContent = value)));
} }
this.disposables( this.disposables(
this.enabled.observe(enabled => { this.enabled.observe(({ value }) => {
if (enabled) { if (value) {
this.element.classList.remove("disabled"); this.element.classList.remove("disabled");
} else { } else {
this.element.classList.add("disabled"); this.element.classList.add("disabled");

View File

@ -15,8 +15,8 @@ export class LazyView extends ResizableView {
this.visible.val = false; this.visible.val = false;
this.disposables( this.disposables(
this.visible.observe(visible => { this.visible.observe(({ value }) => {
if (visible && !this.initialized) { if (value && !this.initialized) {
this.initialized = true; this.initialized = true;
this.create_view().then(view => { this.create_view().then(view => {

View File

@ -39,7 +39,7 @@ export class TextArea extends LabelledControl {
this.text_element.onchange = () => (this.value.val = this.text_element.value); this.text_element.onchange = () => (this.value.val = this.text_element.value);
this.disposables(this.value.observe(value => (this.text_element.value = value))); this.disposables(this.value.observe(({ value }) => (this.text_element.value = value)));
this.element.append(this.text_element); this.element.append(this.text_element);
} }

View File

@ -21,7 +21,7 @@ export abstract class View implements Disposable {
private disposer = new Disposer(); private disposer = new Disposer();
constructor() { constructor() {
this.disposables(this.visible.observe(visible => (this.element.hidden = !visible))); this.disposables(this.visible.observe(({ value }) => (this.element.hidden = !value)));
} }
dispose(): void { dispose(): void {

View File

@ -61,5 +61,5 @@ export function bind_hidden(element: HTMLElement, observable: Observable<boolean
element.hidden = observable.val; element.hidden = observable.val;
} }
return observable.observe(v => (element.hidden = v)); return observable.observe(({ value }) => (element.hidden = value));
} }

View File

@ -1,4 +1,4 @@
import { Property } from "./Property"; import { Property, PropertyChangeEvent } from "./Property";
import { Disposable } from "./Disposable"; import { Disposable } from "./Disposable";
import Logger from "js-logger"; import Logger from "js-logger";
@ -11,13 +11,22 @@ export abstract class AbstractMinimalProperty<T> implements Property<T> {
abstract readonly val: T; abstract readonly val: T;
protected readonly observers: ((value: T) => void)[] = []; abstract get_val(): T;
observe(observer: (value: T) => void): Disposable { protected readonly observers: ((change: PropertyChangeEvent<T>) => void)[] = [];
observe(
observer: (change: PropertyChangeEvent<T>) => void,
options: { call_now?: boolean } = {},
): Disposable {
if (!this.observers.includes(observer)) { if (!this.observers.includes(observer)) {
this.observers.push(observer); this.observers.push(observer);
} }
if (options.call_now) {
this.call_observer(observer, this.val);
}
return { return {
dispose: () => { dispose: () => {
const index = this.observers.indexOf(observer); const index = this.observers.indexOf(observer);
@ -33,13 +42,17 @@ export abstract class AbstractMinimalProperty<T> implements Property<T> {
abstract flat_map<U>(f: (element: T) => Property<U>): Property<U>; abstract flat_map<U>(f: (element: T) => Property<U>): Property<U>;
protected emit(): void { protected emit(old_value: T): void {
for (const observer of this.observers) { for (const observer of this.observers) {
try { this.call_observer(observer, old_value);
observer(this.val); }
} catch (e) { }
logger.error("Observer threw error.", e);
} private call_observer(observer: (event: PropertyChangeEvent<T>) => void, old_value: T): void {
try {
observer({ value: this.val, old_value });
} catch (e) {
logger.error("Observer threw error.", e);
} }
} }
} }

View File

@ -1,5 +1,5 @@
import { Disposable } from "./Disposable"; import { Disposable } from "./Disposable";
import { Property } from "./Property"; import { PropertyChangeEvent, Property } from "./Property";
import { Disposer } from "./Disposer"; import { Disposer } from "./Disposer";
import { AbstractMinimalProperty } from "./AbstractMinimalProperty"; import { AbstractMinimalProperty } from "./AbstractMinimalProperty";
import { FlatMappedProperty } from "./FlatMappedProperty"; import { FlatMappedProperty } from "./FlatMappedProperty";
@ -15,6 +15,10 @@ export class DependentProperty<T> extends AbstractMinimalProperty<T> implements
private _val?: T; private _val?: T;
get val(): T { get val(): T {
return this.get_val();
}
get_val(): T {
if (this.dependency_disposables.length) { if (this.dependency_disposables.length) {
return this._val as T; return this._val as T;
} else { } else {
@ -28,7 +32,7 @@ export class DependentProperty<T> extends AbstractMinimalProperty<T> implements
super(); super();
} }
observe(observer: (event: T) => void): Disposable { observe(observer: (event: PropertyChangeEvent<T>) => void): Disposable {
const super_disposable = super.observe(observer); const super_disposable = super.observe(observer);
if (this.dependency_disposables.length === 0) { if (this.dependency_disposables.length === 0) {
@ -37,8 +41,9 @@ export class DependentProperty<T> extends AbstractMinimalProperty<T> implements
this.dependency_disposables.add_all( this.dependency_disposables.add_all(
...this.dependencies.map(dependency => ...this.dependencies.map(dependency =>
dependency.observe(() => { dependency.observe(() => {
const old_value = this._val!;
this._val = this.f(); this._val = this.f();
this.emit(); this.emit(old_value);
}), }),
), ),
); );
@ -49,7 +54,7 @@ export class DependentProperty<T> extends AbstractMinimalProperty<T> implements
super_disposable.dispose(); super_disposable.dispose();
if (this.observers.length === 0) { if (this.observers.length === 0) {
this.dependency_disposables.dispose(); this.dependency_disposables.dispose_all();
} }
}, },
}; };

View File

@ -1,3 +1,10 @@
/**
* Objects implementing this interface should be disposed when they're not used anymore.
* This is to avoid e.g. memory leaks.
*/
export interface Disposable { export interface Disposable {
/**
* Releases any held resources.
*/
dispose(): void; dispose(): void;
} }

View File

@ -0,0 +1,51 @@
import { Disposer } from "./Disposer";
import { Disposable } from "./Disposable";
test("calling add or add_all should increase length correctly", () => {
const disposer = new Disposer();
expect(disposer.length).toBe(0);
disposer.add(dummy());
expect(disposer.length).toBe(1);
disposer.add_all(dummy(), dummy());
expect(disposer.length).toBe(3);
disposer.add(dummy());
expect(disposer.length).toBe(4);
disposer.add_all(dummy(), dummy());
expect(disposer.length).toBe(6);
});
test("length should be 0 after calling dispose", () => {
const disposer = new Disposer();
disposer.add_all(dummy(), dummy(), dummy());
expect(disposer.length).toBe(3);
disposer.dispose();
expect(disposer.length).toBe(0);
});
test("contained disposables should be disposed when calling dispose", () => {
let dispose_calls = 0;
function disposable(): Disposable {
return {
dispose(): void {
dispose_calls++;
},
};
}
const disposer = new Disposer();
disposer.add_all(disposable(), disposable(), disposable());
expect(dispose_calls).toBe(0);
disposer.dispose();
expect(dispose_calls).toBe(3);
});
function dummy(): Disposable {
return { dispose(): void {} };
}

View File

@ -3,24 +3,42 @@ import Logger = require("js-logger");
const logger = Logger.get("core/observable/Disposer"); const logger = Logger.get("core/observable/Disposer");
/**
* Container for disposables.
*/
export class Disposer implements Disposable { export class Disposer implements Disposable {
private readonly disposables: Disposable[] = []; private readonly disposables: Disposable[] = [];
private disposed = false;
/**
* The amount of disposables contained in this disposer.
*/
get length(): number { get length(): number {
return this.disposables.length; return this.disposables.length;
} }
/**
* Add a single disposable and return the given disposable.
*/
add<T extends Disposable>(disposable: T): T { add<T extends Disposable>(disposable: T): T {
this.check_not_disposed();
this.disposables.push(disposable); this.disposables.push(disposable);
return disposable; return disposable;
} }
/**
* Add 0 or more disposables.
*/
add_all(...disposable: Disposable[]): this { add_all(...disposable: Disposable[]): this {
this.check_not_disposed();
this.disposables.push(...disposable); this.disposables.push(...disposable);
return this; return this;
} }
dispose(): void { /**
* Disposes all held disposables.
*/
dispose_all(): void {
for (const disposable of this.disposables.splice(0, this.disposables.length)) { for (const disposable of this.disposables.splice(0, this.disposables.length)) {
try { try {
disposable.dispose(); disposable.dispose();
@ -29,4 +47,18 @@ export class Disposer implements Disposable {
} }
} }
} }
/**
* Disposes all held disposables.
*/
dispose(): void {
this.dispose_all();
this.disposed = true;
}
private check_not_disposed(): void {
if (this.disposed) {
throw new Error("This disposer has been disposed.");
}
}
} }

View File

@ -1,5 +1,5 @@
import { Observable } from "./Observable"; import { ChangeEvent, Observable } from "./Observable";
export interface Emitter<E> extends Observable<E> { export interface Emitter<T> extends Observable<T> {
emit(event: E): void; emit(event: ChangeEvent<T>): void;
} }

View File

@ -1,4 +1,4 @@
import { Property } from "./Property"; import { PropertyChangeEvent, Property } from "./Property";
import { Disposable } from "./Disposable"; import { Disposable } from "./Disposable";
import { AbstractMinimalProperty } from "./AbstractMinimalProperty"; import { AbstractMinimalProperty } from "./AbstractMinimalProperty";
import { DependentProperty } from "./DependentProperty"; import { DependentProperty } from "./DependentProperty";
@ -12,6 +12,10 @@ export class FlatMappedProperty<T, U> extends AbstractMinimalProperty<U> impleme
readonly is_property = true; readonly is_property = true;
get val(): U { get val(): U {
return this.get_val();
}
get_val(): U {
return this.computed_property return this.computed_property
? this.computed_property.val ? this.computed_property.val
: this.f(this.dependency.val).val; : this.f(this.dependency.val).val;
@ -25,13 +29,14 @@ export class FlatMappedProperty<T, U> extends AbstractMinimalProperty<U> impleme
super(); super();
} }
observe(observer: (value: U) => void): Disposable { observe(observer: (event: PropertyChangeEvent<U>) => void): Disposable {
const super_disposable = super.observe(observer); const super_disposable = super.observe(observer);
if (this.dependency_disposable == undefined) { if (this.dependency_disposable == undefined) {
this.dependency_disposable = this.dependency.observe(() => { this.dependency_disposable = this.dependency.observe(() => {
const old_value = this.val;
this.compute_and_observe(); this.compute_and_observe();
this.emit(); this.emit(old_value);
}); });
this.compute_and_observe(); this.compute_and_observe();
@ -62,9 +67,15 @@ export class FlatMappedProperty<T, U> extends AbstractMinimalProperty<U> impleme
private compute_and_observe(): void { private compute_and_observe(): void {
if (this.computed_disposable) this.computed_disposable.dispose(); if (this.computed_disposable) this.computed_disposable.dispose();
this.computed_property = this.f(this.dependency.val); this.computed_property = this.f(this.dependency.val);
let old_value = this.computed_property.val;
this.computed_disposable = this.computed_property.observe(() => { this.computed_disposable = this.computed_property.observe(() => {
this.emit(); const ov = old_value;
old_value = this.val;
this.emit(ov);
}); });
} }
} }

View File

@ -1,5 +1,9 @@
import { Disposable } from "./Disposable"; import { Disposable } from "./Disposable";
export interface Observable<E> { export interface ChangeEvent<T> {
observe(observer: (event: E) => void): Disposable; value: T;
}
export interface Observable<T> {
observe(observer: (event: ChangeEvent<T>) => void): Disposable;
} }

View File

@ -1,10 +1,22 @@
import { Observable } from "./Observable"; import { ChangeEvent, Observable } from "./Observable";
import { Disposable } from "./Disposable";
export interface PropertyChangeEvent<T> extends ChangeEvent<T> {
old_value: T;
}
export interface Property<T> extends Observable<T> { export interface Property<T> extends Observable<T> {
readonly is_property: true; readonly is_property: true;
readonly val: T; readonly val: T;
get_val(): T;
observe(
observer: (event: PropertyChangeEvent<T>) => void,
options?: { call_now?: boolean },
): Disposable;
map<U>(f: (element: T) => U): Property<U>; map<U>(f: (element: T) => U): Property<U>;
flat_map<U>(f: (element: T) => Property<U>): Property<U>; flat_map<U>(f: (element: T) => Property<U>): Property<U>;

View File

@ -1,12 +1,14 @@
import { Disposable } from "./Disposable"; import { Disposable } from "./Disposable";
import Logger from "js-logger"; import Logger from "js-logger";
import { Emitter } from "./Emitter";
import { ChangeEvent } from "./Observable";
const logger = Logger.get("core/observable/SimpleEmitter"); const logger = Logger.get("core/observable/SimpleEmitter");
export class SimpleEmitter<E> { export class SimpleEmitter<T> implements Emitter<T> {
protected readonly observers: ((event: E) => void)[] = []; protected readonly observers: ((event: ChangeEvent<T>) => void)[] = [];
emit(event: E): void { emit(event: ChangeEvent<T>): void {
for (const observer of this.observers) { for (const observer of this.observers) {
try { try {
observer(event); observer(event);
@ -16,7 +18,7 @@ export class SimpleEmitter<E> {
} }
} }
observe(observer: (event: E) => void): Disposable { observe(observer: (event: ChangeEvent<T>) => void): Disposable {
if (!this.observers.includes(observer)) { if (!this.observers.includes(observer)) {
this.observers.push(observer); this.observers.push(observer);
} }

View File

@ -5,20 +5,30 @@ import { is_property } from "./Property";
import { AbstractProperty } from "./AbstractProperty"; import { AbstractProperty } from "./AbstractProperty";
export class SimpleProperty<T> extends AbstractProperty<T> implements WritableProperty<T> { export class SimpleProperty<T> extends AbstractProperty<T> implements WritableProperty<T> {
readonly is_writable_property = true;
constructor(private _val: T) { constructor(private _val: T) {
super(); super();
} }
get val(): T { get val(): T {
return this.get_val();
}
set val(value: T) {
this.set_val(value);
}
get_val(): T {
return this._val; return this._val;
} }
set val(val: T) { set_val(val: T, options: { silent?: boolean } = {}): void {
if (val !== this._val) { if (val !== this._val) {
const old_value = this._val;
this._val = val; this._val = val;
this.emit();
if (!options.silent) {
this.emit(old_value);
}
} }
} }
@ -26,17 +36,17 @@ export class SimpleProperty<T> extends AbstractProperty<T> implements WritablePr
this.val = f(this.val); this.val = f(this.val);
} }
bind(observable: Observable<T>): Disposable { bind_to(observable: Observable<T>): Disposable {
if (is_property(observable)) { if (is_property(observable)) {
this.val = observable.val; this.val = observable.val;
} }
return observable.observe(v => (this.val = v)); return observable.observe(event => (this.val = event.value));
} }
bind_bi(property: WritableProperty<T>): Disposable { bind_bi(property: WritableProperty<T>): Disposable {
const bind_1 = this.bind(property); const bind_1 = this.bind_to(property);
const bind_2 = property.bind(this); const bind_2 = property.bind_to(this);
return { return {
dispose(): void { dispose(): void {
bind_1.dispose(); bind_1.dispose();

View File

@ -10,23 +10,35 @@ export class SimpleWritableArrayProperty<T> extends AbstractProperty<T[]>
implements WritableArrayProperty<T> { implements WritableArrayProperty<T> {
readonly is_property = true; readonly is_property = true;
readonly is_writable_property = true;
private readonly _length = property(0); private readonly _length = property(0);
readonly length = this._length; readonly length = this._length;
private readonly values: T[]; private readonly values: T[];
get val(): T[] { get val(): T[] {
return this.get_val();
}
set val(values: T[]) {
this.set_val(values);
}
get_val(): T[] {
return this.values; return this.values;
} }
set_val(values: T[]): T[] {
const replaced_values = this.values.splice(0, this.values.length, ...values);
this.emit(this.values);
return replaced_values;
}
constructor(...values: T[]) { constructor(...values: T[]) {
super(); super();
this.values = values; this.values = values;
} }
bind(observable: Observable<T[]>): Disposable { bind_to(observable: Observable<T[]>): Disposable {
/* TODO */ throw new Error("not implemented"); /* TODO */ throw new Error("not implemented");
} }
@ -44,12 +56,12 @@ export class SimpleWritableArrayProperty<T> extends AbstractProperty<T[]>
set(index: number, value: T): void { set(index: number, value: T): void {
this.values[index] = value; this.values[index] = value;
this.emit(); this.emit(this.values);
} }
clear(): void { clear(): void {
this.values.splice(0, this.values.length); this.values.splice(0, this.values.length);
this.emit(); this.emit(this.values);
} }
splice(index: number, delete_count?: number): T[]; splice(index: number, delete_count?: number): T[];
@ -63,7 +75,7 @@ export class SimpleWritableArrayProperty<T> extends AbstractProperty<T[]>
ret = this.values.splice(index, delete_count, ...items); ret = this.values.splice(index, delete_count, ...items);
} }
this.emit(); this.emit(this.values);
return ret; return ret;
} }

View File

@ -3,10 +3,10 @@ import { Observable } from "./Observable";
import { Disposable } from "./Disposable"; import { Disposable } from "./Disposable";
export interface WritableProperty<T> extends Property<T> { export interface WritableProperty<T> extends Property<T> {
readonly is_writable_property: true;
val: T; val: T;
set_val(value: T, options?: { silent?: boolean }): void;
update(f: (value: T) => T): void; update(f: (value: T) => T): void;
/** /**
@ -14,13 +14,7 @@ export interface WritableProperty<T> extends Property<T> {
* *
* @param observable the observable who's events will be propagated to this property. * @param observable the observable who's events will be propagated to this property.
*/ */
bind(observable: Observable<T>): Disposable; bind_to(observable: Observable<T>): Disposable;
bind_bi(property: WritableProperty<T>): Disposable; bind_bi(property: WritableProperty<T>): Disposable;
} }
export function is_writable_property<T>(
observable: Observable<T>,
): observable is WritableProperty<T> {
return (observable as any).is_writable_property;
}

View File

@ -24,7 +24,7 @@ CameraControls.install({
}); });
export abstract class Renderer implements Disposable { export abstract class Renderer implements Disposable {
protected _debug = false; private _debug = false;
get debug(): boolean { get debug(): boolean {
return this._debug; return this._debug;

View File

@ -18,7 +18,7 @@ const STRING_TO_GUI_TOOL = new Map([...GUI_TOOL_TO_STRING.entries()].map(([k, v]
class GuiStore implements Disposable { class GuiStore implements Disposable {
readonly tool: WritableProperty<GuiTool> = property(GuiTool.Viewer); readonly tool: WritableProperty<GuiTool> = property(GuiTool.Viewer);
private hash_disposer = this.tool.observe(tool => { private hash_disposer = this.tool.observe(({ value: tool }) => {
window.location.hash = `#/${gui_tool_to_string(tool)}`; window.location.hash = `#/${gui_tool_to_string(tool)}`;
}); });

View File

@ -1,7 +1,5 @@
export class Action { export interface Action {
constructor( readonly description: string;
readonly description: string, readonly undo: () => void;
readonly undo: () => void, readonly redo: () => void;
readonly redo: () => void,
) {}
} }

View File

@ -9,12 +9,10 @@ import { undo_manager } from "./UndoManager";
* Simply contains a single action. `can_undo` and `can_redo` must be managed manually. * Simply contains a single action. `can_undo` and `can_redo` must be managed manually.
*/ */
export class SimpleUndo implements Undo { export class SimpleUndo implements Undo {
private readonly _action: Action; private readonly action: Action;
readonly action: Property<Action>;
constructor(description: string, undo: () => void, redo: () => void) { constructor(description: string, undo: () => void, redo: () => void) {
this._action = new Action(description, undo, redo); this.action = { description, undo, redo };
this.action = property(this._action);
} }
make_current(): void { make_current(): void {
@ -32,16 +30,16 @@ export class SimpleUndo implements Undo {
readonly can_redo = property(false); readonly can_redo = property(false);
readonly first_undo: Property<Action | undefined> = this.can_undo.map(can_undo => readonly first_undo: Property<Action | undefined> = this.can_undo.map(can_undo =>
can_undo ? this._action : undefined, can_undo ? this.action : undefined,
); );
readonly first_redo: Property<Action | undefined> = this.can_redo.map(can_redo => readonly first_redo: Property<Action | undefined> = this.can_redo.map(can_redo =>
can_redo ? this._action : undefined, can_redo ? this.action : undefined,
); );
undo(): boolean { undo(): boolean {
if (this.can_undo) { if (this.can_undo) {
this._action.undo(); this.action.undo();
return true; return true;
} else { } else {
return false; return false;
@ -50,7 +48,7 @@ export class SimpleUndo implements Undo {
redo(): boolean { redo(): boolean {
if (this.can_redo) { if (this.can_redo) {
this._action.redo(); this.action.redo();
return true; return true;
} else { } else {
return false; return false;

View File

@ -1,4 +1,3 @@
import { Action } from "./Action";
import { UndoStack } from "./UndoStack"; import { UndoStack } from "./UndoStack";
test("simple properties and invariants", () => { test("simple properties and invariants", () => {
@ -7,9 +6,9 @@ test("simple properties and invariants", () => {
expect(stack.can_undo.val).toBe(false); expect(stack.can_undo.val).toBe(false);
expect(stack.can_redo.val).toBe(false); expect(stack.can_redo.val).toBe(false);
stack.push(new Action("", () => {}, () => {})); stack.push({ description: "", undo: () => {}, redo: () => {} });
stack.push(new Action("", () => {}, () => {})); stack.push({ description: "", undo: () => {}, redo: () => {} });
stack.push(new Action("", () => {}, () => {})); stack.push({ description: "", undo: () => {}, redo: () => {} });
expect(stack.can_undo.val).toBe(true); expect(stack.can_undo.val).toBe(true);
expect(stack.can_redo.val).toBe(false); expect(stack.can_redo.val).toBe(false);
@ -32,8 +31,8 @@ test("undo", () => {
// Pretend value started and 3 and we've set it to 7 and then 13. // Pretend value started and 3 and we've set it to 7 and then 13.
let value = 13; let value = 13;
stack.push(new Action("X", () => (value = 3), () => (value = 7))); stack.push({ description: "X", undo: () => (value = 3), redo: () => (value = 7) });
stack.push(new Action("Y", () => (value = 7), () => (value = 13))); stack.push({ description: "Y", undo: () => (value = 7), redo: () => (value = 13) });
expect(stack.undo()).toBe(true); expect(stack.undo()).toBe(true);
expect(value).toBe(7); expect(value).toBe(7);
@ -51,8 +50,8 @@ test("redo", () => {
// Pretend value started and 3 and we've set it to 7 and then 13. // Pretend value started and 3 and we've set it to 7 and then 13.
let value = 13; let value = 13;
stack.push(new Action("X", () => (value = 3), () => (value = 7))); stack.push({ description: "X", undo: () => (value = 3), redo: () => (value = 7) });
stack.push(new Action("Y", () => (value = 7), () => (value = 13))); stack.push({ description: "Y", undo: () => (value = 7), redo: () => (value = 13) });
stack.undo(); stack.undo();
stack.undo(); stack.undo();

View File

@ -4,6 +4,9 @@ import { Action } from "./Action";
import { array_property, map, property } from "../observable"; import { array_property, map, property } from "../observable";
import { NOOP_UNDO } from "./noop_undo"; import { NOOP_UNDO } from "./noop_undo";
import { undo_manager } from "./UndoManager"; import { undo_manager } from "./UndoManager";
import Logger = require("js-logger");
const logger = Logger.get("core/undo/UndoStack");
/** /**
* Full-fledged linear undo/redo implementation. * Full-fledged linear undo/redo implementation.
@ -16,16 +19,6 @@ export class UndoStack implements Undo {
*/ */
private readonly index = property(0); private readonly index = property(0);
make_current(): void {
undo_manager.current.val = this;
}
ensure_not_current(): void {
if (undo_manager.current.val === this) {
undo_manager.current.val = NOOP_UNDO;
}
}
readonly can_undo = this.index.map(index => index > 0); readonly can_undo = this.index.map(index => index > 0);
readonly can_redo = map((stack, index) => index < stack.length, this.stack, this.index); readonly can_redo = map((stack, index) => index < stack.length, this.stack, this.index);
@ -38,13 +31,25 @@ export class UndoStack implements Undo {
return can_redo ? this.stack.get(this.index.val) : undefined; return can_redo ? this.stack.get(this.index.val) : undefined;
}); });
push_action(description: string, undo: () => void, redo: () => void): void { private undoing_or_redoing = false;
this.push(new Action(description, undo, redo));
make_current(): void {
undo_manager.current.val = this;
} }
push(action: Action): void { ensure_not_current(): void {
this.stack.splice(this.index.val, this.stack.length.val - this.index.val, action); if (undo_manager.current.val === this) {
this.index.update(i => i + 1); undo_manager.current.val = NOOP_UNDO;
}
}
push(action: Action): Action {
if (!this.undoing_or_redoing) {
this.stack.splice(this.index.val, Infinity, action);
this.index.update(i => i + 1);
}
return action;
} }
/** /**
@ -56,9 +61,17 @@ export class UndoStack implements Undo {
} }
undo(): boolean { undo(): boolean {
if (this.can_undo) { if (this.can_undo.val && !this.undoing_or_redoing) {
this.index.update(i => i - 1); try {
this.stack.get(this.index.val).undo(); this.undoing_or_redoing = true;
this.index.update(i => i - 1);
this.stack.get(this.index.val).undo();
} catch (e) {
logger.warn("Error while undoing action.", e);
} finally {
this.undoing_or_redoing = false;
}
return true; return true;
} else { } else {
return false; return false;
@ -66,9 +79,17 @@ export class UndoStack implements Undo {
} }
redo(): boolean { redo(): boolean {
if (this.can_redo) { if (this.can_redo.val && !this.undoing_or_redoing) {
this.stack.get(this.index.val).redo(); try {
this.index.update(i => i + 1); this.undoing_or_redoing = true;
this.stack.get(this.index.val).redo();
this.index.update(i => i + 1);
} catch (e) {
logger.warn("Error while redoing action.", e);
} finally {
this.undoing_or_redoing = false;
}
return true; return true;
} else { } else {
return false; return false;

View File

@ -1,69 +0,0 @@
import { UndoStack, Action } from "./undo";
test("simple properties and invariants", () => {
const stack = new UndoStack();
expect(stack.can_undo).toBe(false);
expect(stack.can_redo).toBe(false);
stack.push(new Action("", () => {}, () => {}));
stack.push(new Action("", () => {}, () => {}));
stack.push(new Action("", () => {}, () => {}));
expect(stack.can_undo).toBe(true);
expect(stack.can_redo).toBe(false);
stack.undo();
expect(stack.can_undo).toBe(true);
expect(stack.can_redo).toBe(true);
stack.undo();
stack.undo();
expect(stack.can_undo).toBe(false);
expect(stack.can_redo).toBe(true);
});
test("undo", () => {
const stack = new UndoStack();
// Pretend value started and 3 and we've set it to 7 and then 13.
let value = 13;
stack.push(new Action("X", () => (value = 3), () => (value = 7)));
stack.push(new Action("Y", () => (value = 7), () => (value = 13)));
expect(stack.undo()).toBe(true);
expect(value).toBe(7);
expect(stack.undo()).toBe(true);
expect(value).toBe(3);
expect(stack.undo()).toBe(false);
expect(value).toBe(3);
});
test("redo", () => {
const stack = new UndoStack();
// Pretend value started and 3 and we've set it to 7 and then 13.
let value = 13;
stack.push(new Action("X", () => (value = 3), () => (value = 7)));
stack.push(new Action("Y", () => (value = 7), () => (value = 13)));
stack.undo();
stack.undo();
expect(value).toBe(3);
expect(stack.redo()).toBe(true);
expect(value).toBe(7);
expect(stack.redo()).toBe(true);
expect(value).toBe(13);
expect(stack.redo()).toBe(false);
expect(value).toBe(13);
});

View File

@ -1,17 +0,0 @@
import { ObservableArea } from "./ObservableArea";
import { IObservableArray, observable } from "mobx";
import { Section } from "./Section";
export class ObservableAreaVariant {
readonly id: number;
readonly area: ObservableArea;
@observable.shallow readonly sections: IObservableArray<Section> = observable.array();
constructor(id: number, area: ObservableArea) {
if (!Number.isInteger(id) || id < 0)
throw new Error(`Expected id to be a non-negative integer, got ${id}.`);
this.id = id;
this.area = area;
}
}

View File

@ -1,178 +0,0 @@
import { action, computed, observable } from "mobx";
import { check_episode, Episode } from "../../../core/data_formats/parsing/quest/Episode";
import { ObservableAreaVariant } from "./ObservableAreaVariant";
import { area_store } from "../stores/AreaStore";
import { DatUnknown } from "../../../core/data_formats/parsing/quest/dat";
import { Segment } from "../scripting/instructions";
import Logger from "js-logger";
import { ObservableQuestNpc, ObservableQuestObject } from "./observable_quest_entities";
const logger = Logger.get("domain/ObservableQuest");
export class ObservableQuest {
@observable private _id!: number;
get id(): number {
return this._id;
}
@action
set_id(id: number): void {
if (!Number.isInteger(id) || id < 0 || id > 4294967295)
throw new Error("id must be an integer greater than 0 and less than 4294967295.");
this._id = id;
}
@observable private _language!: number;
get language(): number {
return this._language;
}
@action
set_language(language: number): void {
if (!Number.isInteger(language)) throw new Error("language must be an integer.");
this._language = language;
}
@observable private _name!: string;
get name(): string {
return this._name;
}
@action
set_name(name: string): void {
if (name.length > 32) throw new Error("name can't be longer than 32 characters.");
this._name = name;
}
@observable private _short_description!: string;
get short_description(): string {
return this._short_description;
}
@action
set_short_description(short_description: string): void {
if (short_description.length > 128)
throw new Error("short_description can't be longer than 128 characters.");
this._short_description = short_description;
}
@observable private _long_description!: string;
get long_description(): string {
return this._long_description;
}
@action
set_long_description(long_description: string): void {
if (long_description.length > 288)
throw new Error("long_description can't be longer than 288 characters.");
this._long_description = long_description;
}
readonly episode: Episode;
@observable readonly objects: ObservableQuestObject[];
@observable readonly npcs: ObservableQuestNpc[];
/**
* Map of area IDs to entity counts.
*/
@computed get entities_per_area(): Map<number, number> {
const map = new Map<number, number>();
for (const npc of this.npcs) {
map.set(npc.area_id, (map.get(npc.area_id) || 0) + 1);
}
for (const obj of this.objects) {
map.set(obj.area_id, (map.get(obj.area_id) || 0) + 1);
}
return map;
}
@observable.ref private _map_designations!: Map<number, number>;
/**
* Map of area IDs to area variant IDs. One designation per area.
*/
get map_designations(): Map<number, number> {
return this._map_designations;
}
set_map_designations(map_designations: Map<number, number>): void {
this._map_designations = map_designations;
}
/**
* One variant per area.
*/
@computed get area_variants(): ObservableAreaVariant[] {
const variants = new Map<number, ObservableAreaVariant>();
for (const area_id of this.entities_per_area.keys()) {
try {
variants.set(area_id, area_store.get_variant(this.episode, area_id, 0));
} catch (e) {
logger.warn(e);
}
}
for (const [area_id, variant_id] of this._map_designations) {
try {
variants.set(area_id, area_store.get_variant(this.episode, area_id, variant_id));
} catch (e) {
logger.warn(e);
}
}
return [...variants.values()];
}
/**
* (Partial) raw DAT data that can't be parsed yet by Phantasmal.
*/
readonly dat_unknowns: DatUnknown[];
readonly object_code: Segment[];
readonly shop_items: number[];
constructor(
id: number,
language: number,
name: string,
short_description: string,
long_description: string,
episode: Episode,
map_designations: Map<number, number>,
objects: ObservableQuestObject[],
npcs: ObservableQuestNpc[],
dat_unknowns: DatUnknown[],
object_code: Segment[],
shop_items: number[],
) {
check_episode(episode);
if (!map_designations) throw new Error("map_designations is required.");
if (!Array.isArray(objects)) throw new Error("objs is required.");
if (!Array.isArray(npcs)) throw new Error("npcs is required.");
if (!dat_unknowns) throw new Error("dat_unknowns is required.");
if (!object_code) throw new Error("object_code is required.");
if (!shop_items) throw new Error("shop_items is required.");
this.set_id(id);
this.set_language(language);
this.set_name(name);
this.set_short_description(short_description);
this.set_long_description(long_description);
this.episode = episode;
this.set_map_designations(map_designations);
this.objects = objects;
this.npcs = npcs;
this.dat_unknowns = dat_unknowns;
this.object_code = object_code;
this.shop_items = shop_items;
}
}

View File

@ -2,13 +2,13 @@ import { ObjectType } from "../../../core/data_formats/parsing/quest/object_type
import { action, computed, observable } from "mobx"; import { action, computed, observable } from "mobx";
import { Vec3 } from "../../../core/data_formats/vector"; import { Vec3 } from "../../../core/data_formats/vector";
import { EntityType } from "../../../core/data_formats/parsing/quest/entities"; import { EntityType } from "../../../core/data_formats/parsing/quest/entities";
import { Section } from "./Section"; import { SectionModel } from "../../../quest_editor/model/SectionModel";
import { NpcType } from "../../../core/data_formats/parsing/quest/npc_types"; import { NpcType } from "../../../core/data_formats/parsing/quest/npc_types";
/** /**
* Abstract class from which ObservableQuestNpc and ObservableQuestObject derive. * Abstract class from which ObservableQuestNpc and QuestObjectModel derive.
*/ */
export abstract class ObservableQuestEntity<Type extends EntityType = EntityType> { export abstract class QuestEntityModel<Type extends EntityType = EntityType> {
readonly type: Type; readonly type: Type;
@observable area_id: number; @observable area_id: number;
@ -19,7 +19,7 @@ export abstract class ObservableQuestEntity<Type extends EntityType = EntityType
return this.section ? this.section.id : this._section_id; return this.section ? this.section.id : this._section_id;
} }
@observable.ref section?: Section; @observable.ref section?: SectionModel;
/** /**
* Section-relative position * Section-relative position
@ -90,13 +90,13 @@ export abstract class ObservableQuestEntity<Type extends EntityType = EntityType
} }
@action @action
set_world_position_and_section(world_position: Vec3, section?: Section): void { set_world_position_and_section(world_position: Vec3, section?: SectionModel): void {
this.world_position = world_position; this.world_position = world_position;
this.section = section; this.section = section;
} }
} }
export class ObservableQuestObject extends ObservableQuestEntity<ObjectType> { export class ObservableQuestObject extends QuestEntityModel<ObjectType> {
readonly id: number; readonly id: number;
readonly group_id: number; readonly group_id: number;
@ -145,7 +145,7 @@ export class ObservableQuestObject extends ObservableQuestEntity<ObjectType> {
} }
} }
export class ObservableQuestNpc extends ObservableQuestEntity<NpcType> { export class ObservableQuestNpc extends QuestEntityModel<NpcType> {
readonly pso_type_id: number; readonly pso_type_id: number;
readonly npc_id: number; readonly npc_id: number;
readonly script_label: number; readonly script_label: number;

View File

@ -7,14 +7,14 @@ import { Vec3 } from "../../../core/data_formats/vector";
import { read_file } from "../../../core/read_file"; import { read_file } from "../../../core/read_file";
import { SimpleUndo, UndoStack } from "../../core/undo"; import { SimpleUndo, UndoStack } from "../../core/undo";
import { area_store } from "./AreaStore"; import { area_store } from "./AreaStore";
import { create_new_quest } from "./quest_creation"; import { create_new_quest } from "../../../quest_editor/stores/quest_creation";
import { Episode } from "../../../core/data_formats/parsing/quest/Episode"; import { Episode } from "../../../core/data_formats/parsing/quest/Episode";
import { entity_data } from "../../../core/data_formats/parsing/quest/entities"; import { entity_data } from "../../../core/data_formats/parsing/quest/entities";
import { ObservableQuest } from "../domain/ObservableQuest"; import { ObservableQuest } from "../domain/QuestModel";
import { ObservableArea } from "../domain/ObservableArea"; import { AreaModel } from "../../../quest_editor/model/AreaModel";
import { Section } from "../domain/Section"; import { SectionModel } from "../../../quest_editor/model/SectionModel";
import { import {
ObservableQuestEntity, QuestEntityModel,
ObservableQuestNpc, ObservableQuestNpc,
ObservableQuestObject, ObservableQuestObject,
} from "../domain/observable_quest_entities"; } from "../domain/observable_quest_entities";
@ -29,9 +29,9 @@ class QuestEditorStore {
@observable current_quest_filename?: string; @observable current_quest_filename?: string;
@observable current_quest?: ObservableQuest; @observable current_quest?: ObservableQuest;
@observable current_area?: ObservableArea; @observable current_area?: AreaModel;
@observable selected_entity?: ObservableQuestEntity; @observable selected_entity?: QuestEntityModel;
@observable save_dialog_filename?: string; @observable save_dialog_filename?: string;
@observable save_dialog_open: boolean = false; @observable save_dialog_open: boolean = false;
@ -58,7 +58,7 @@ class QuestEditorStore {
}; };
@action @action
set_selected_entity = (entity?: ObservableQuestEntity) => { set_selected_entity = (entity?: QuestEntityModel) => {
if (entity) { if (entity) {
this.set_current_area_id(entity.area_id); this.set_current_area_id(entity.area_id);
} }
@ -299,7 +299,7 @@ class QuestEditorStore {
@action @action
push_entity_move_action = ( push_entity_move_action = (
entity: ObservableQuestEntity, entity: QuestEntityModel,
old_position: Vec3, old_position: Vec3,
new_position: Vec3, new_position: Vec3,
) => { ) => {
@ -368,7 +368,7 @@ class QuestEditorStore {
} }
}); });
private set_section_on_quest_entity = (entity: ObservableQuestEntity, sections: Section[]) => { private set_section_on_quest_entity = (entity: QuestEntityModel, sections: SectionModel[]) => {
const section = sections.find(s => s.id === entity.section_id); const section = sections.find(s => s.id === entity.section_id);
if (section) { if (section) {

View File

@ -2,7 +2,7 @@ import { autorun } from "mobx";
import { editor, languages, MarkerSeverity, MarkerTag, Position } from "monaco-editor"; import { editor, languages, MarkerSeverity, MarkerTag, Position } from "monaco-editor";
import React, { Component, createRef, ReactNode } from "react"; import React, { Component, createRef, ReactNode } from "react";
import { AutoSizer } from "react-virtualized"; import { AutoSizer } from "react-virtualized";
import { AssemblyAnalyser } from "../scripting/AssemblyAnalyser"; import { AssemblyAnalyser } from "../../../quest_editor/scripting/AssemblyAnalyser";
import { quest_editor_store } from "../stores/QuestEditorStore"; import { quest_editor_store } from "../stores/QuestEditorStore";
import { Action } from "../../core/undo"; import { Action } from "../../core/undo";
import styles from "./AssemblyEditorComponent.css"; import styles from "./AssemblyEditorComponent.css";

View File

@ -7,7 +7,7 @@ import { quest_editor_store } from "../stores/QuestEditorStore";
import { DisabledTextComponent } from "../../core/ui/DisabledTextComponent"; import { DisabledTextComponent } from "../../core/ui/DisabledTextComponent";
import styles from "./EntityInfoComponent.css"; import styles from "./EntityInfoComponent.css";
import { entity_data, entity_type_to_string } from "../../../core/data_formats/parsing/quest/entities"; import { entity_data, entity_type_to_string } from "../../../core/data_formats/parsing/quest/entities";
import { ObservableQuestEntity, ObservableQuestNpc } from "../domain/observable_quest_entities"; import { QuestEntityModel, ObservableQuestNpc } from "../domain/observable_quest_entities";
@observer @observer
export class EntityInfoComponent extends Component { export class EntityInfoComponent extends Component {
@ -57,7 +57,7 @@ export class EntityInfoComponent extends Component {
} }
type CoordProps = { type CoordProps = {
entity: ObservableQuestEntity; entity: QuestEntityModel;
position_type: "position" | "world_position"; position_type: "position" | "world_position";
coord: "x" | "y" | "z"; coord: "x" | "y" | "z";
}; };

View File

@ -1,17 +0,0 @@
.main {
height: 100%;
width: 100%;
padding: 2px 10px 10px 10px;
display: flex;
flex-direction: column;
overflow: auto;
}
.main table {
border-collapse: collapse;
width: 100%;
}
.main textarea {
font-family: 'Courier New', Courier, monospace
}

View File

@ -1,126 +0,0 @@
import { observer } from "mobx-react";
import React, { ChangeEvent, Component, ReactNode } from "react";
import { quest_editor_store } from "../stores/QuestEditorStore";
import { DisabledTextComponent } from "../../core/ui/DisabledTextComponent";
import styles from "./QuestInfoComponent.css";
import { Episode } from "../../../core/data_formats/parsing/quest/Episode";
import { NumberInput } from "../../core/ui/NumberInput";
import { TextInput } from "../../core/ui/TextInput";
import { TextArea } from "../../core/ui/TextArea";
@observer
export class QuestInfoComponent extends Component {
render(): ReactNode {
const quest = quest_editor_store.current_quest;
let body: ReactNode;
if (quest) {
const episode =
quest.episode === Episode.IV ? "IV" : quest.episode === Episode.II ? "II" : "I";
body = (
<table>
<tbody>
<tr>
<th>Episode:</th>
<td>{episode}</td>
</tr>
<tr>
<th>ID:</th>
<td>
<NumberInput
value={quest.id}
min={0}
max={4294967295}
on_change={this.id_changed}
/>
</td>
</tr>
<tr>
<th>Name:</th>
<td>
<TextInput
value={quest.name}
max_length={32}
on_change={this.name_changed}
/>
</td>
</tr>
<tr>
<th colSpan={2}>Short description:</th>
</tr>
<tr>
<td colSpan={2}>
<TextArea
value={quest.short_description}
max_length={128}
rows={3}
on_change={this.short_description_changed}
/>
</td>
</tr>
<tr>
<th colSpan={2}>Long description:</th>
</tr>
<tr>
<td colSpan={2}>
<TextArea
value={quest.long_description}
max_length={288}
rows={5}
on_change={this.long_description_changed}
/>
</td>
</tr>
</tbody>
</table>
);
} else {
body = <DisabledTextComponent>No quest loaded.</DisabledTextComponent>;
}
return (
<div className={styles.main} tabIndex={-1}>
{body}
</div>
);
}
private id_changed(value?: number): void {
const quest = quest_editor_store.current_quest;
if (quest && value != undefined) {
quest_editor_store.push_id_edit_action(quest.id, value);
}
}
private name_changed(e: ChangeEvent<HTMLInputElement>): void {
const quest = quest_editor_store.current_quest;
if (quest) {
quest_editor_store.push_name_edit_action(quest.name, e.currentTarget.value);
}
}
private short_description_changed(e: ChangeEvent<HTMLTextAreaElement>): void {
const quest = quest_editor_store.current_quest;
if (quest) {
quest_editor_store.push_short_description_edit_action(
quest.short_description,
e.currentTarget.value,
);
}
}
private long_description_changed(e: ChangeEvent<HTMLTextAreaElement>): void {
const quest = quest_editor_store.current_quest;
if (quest) {
quest_editor_store.push_long_description_edit_action(
quest.long_description,
e.currentTarget.value,
);
}
}
}

View File

@ -1,26 +0,0 @@
import { observer } from "mobx-react";
import React, { Component, ReactNode } from "react";
import { AutoSizer } from "react-virtualized";
import { get_quest_renderer } from "../rendering/QuestRenderer";
import { quest_editor_store } from "../stores/QuestEditorStore";
import { RendererComponent } from "../../core/ui/RendererComponent";
@observer
export class QuestRendererComponent extends Component {
render(): ReactNode {
const debug = quest_editor_store.debug;
return (
<AutoSizer>
{({ width, height }) => (
<RendererComponent
renderer={get_quest_renderer()}
width={width}
height={height}
debug={debug}
/>
)}
</AutoSizer>
);
}
}

View File

@ -0,0 +1,13 @@
import { QuestEditAction } from "./QuestEditAction";
export class EditIdAction extends QuestEditAction<number> {
readonly description = "Edit ID";
undo(): void {
this.quest.set_id(this.old);
}
redo(): void {
this.quest.set_id(this.new);
}
}

View File

@ -0,0 +1,13 @@
import { QuestEditAction } from "./QuestEditAction";
export class EditLongDescriptionAction extends QuestEditAction<string> {
readonly description = "Edit long description";
undo(): void {
this.quest.set_long_description(this.old);
}
redo(): void {
this.quest.set_long_description(this.new);
}
}

View File

@ -0,0 +1,13 @@
import { QuestEditAction } from "./QuestEditAction";
export class EditNameAction extends QuestEditAction<string> {
readonly description = "Edit name";
undo(): void {
this.quest.set_name(this.old);
}
redo(): void {
this.quest.set_name(this.new);
}
}

View File

@ -0,0 +1,13 @@
import { QuestEditAction } from "./QuestEditAction";
export class EditShortDescriptionAction extends QuestEditAction<string> {
readonly description = "Edit short description";
undo(): void {
this.quest.set_short_description(this.old);
}
redo(): void {
this.quest.set_short_description(this.new);
}
}

View File

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

View File

@ -0,0 +1,27 @@
import { Action } from "../../core/undo/Action";
import { QuestEntityModel } from "../model/QuestEntityModel";
import { Vec3 } from "../../core/data_formats/vector";
import { entity_data } from "../../core/data_formats/parsing/quest/entities";
import { quest_editor_store } from "../stores/QuestEditorStore";
export class TranslateEntityAction implements Action {
readonly description: string;
constructor(
private entity: QuestEntityModel,
private old_position: Vec3,
private new_position: Vec3,
) {
this.description = `Move ${entity_data(entity.type).name}`;
}
undo(): void {
this.entity.set_world_position(this.old_position);
quest_editor_store.set_selected_entity(this.entity);
}
redo(): void {
this.entity.set_world_position(this.new_position);
quest_editor_store.set_selected_entity(this.entity);
}
}

View File

@ -1,28 +0,0 @@
import { property } from "../../core/observable";
import { WritableProperty } from "../../core/observable/WritableProperty";
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
export class ObservableQuest {
readonly id: WritableProperty<number>;
readonly language: WritableProperty<number>;
readonly name: WritableProperty<string>;
readonly short_description: WritableProperty<string>;
readonly long_description: WritableProperty<string>;
readonly episode: Episode;
constructor(
id: number,
language: number,
name: string,
short_description: string,
long_description: string,
episode: Episode,
) {
this.id = property(id);
this.language = property(language);
this.name = property(name);
this.short_description = property(short_description);
this.long_description = property(long_description);
this.episode = episode;
}
}

View File

@ -1,9 +0,0 @@
import { EntityType } from "../../core/data_formats/parsing/quest/entities";
export class ObservableQuestEntity<Type extends EntityType = EntityType> {
readonly type: Type;
constructor(type: Type) {
this.type = type;
}
}

View File

@ -1,8 +0,0 @@
import { ObservableQuestEntity } from "./ObservableQuestEntity";
import { NpcType } from "../../core/data_formats/parsing/quest/npc_types";
export class ObservableQuestNpc extends ObservableQuestEntity<NpcType> {
constructor(type: NpcType) {
super(type);
}
}

View File

@ -1,8 +0,0 @@
import { ObservableQuestEntity } from "./ObservableQuestEntity";
import { ObjectType } from "../../core/data_formats/parsing/quest/object_types";
export class ObservableQuestObject extends ObservableQuestEntity<ObjectType> {
constructor(type: ObjectType) {
super(type);
}
}

View File

@ -0,0 +1,27 @@
.quest_editor_NpcCountsView {
user-select: text;
box-sizing: border-box;
padding: 3px;
overflow: auto;
}
.quest_editor_NpcCountsView table {
width: 100%;
}
.quest_editor_NpcCountsView th {
cursor: text;
text-align: left;
}
.quest_editor_NpcCountsView td {
cursor: text;
}
.quest_editor_NpcCountsView_no_quest {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}

View File

@ -1,6 +1,69 @@
import { ResizableView } from "../../core/gui/ResizableView"; import { ResizableView } from "../../core/gui/ResizableView";
import { create_element } from "../../core/gui/dom"; import { el } from "../../core/gui/dom";
import { quest_editor_store } from "../stores/QuestEditorStore";
import { npc_data, NpcType } from "../../core/data_formats/parsing/quest/npc_types";
import { Label } from "../../core/gui/Label";
import { QuestModel } from "../model/QuestModel";
import "./NpcCountsView.css";
export class NpcCountsView extends ResizableView { export class NpcCountsView extends ResizableView {
readonly element = create_element("div"); readonly element = el.div({ class: "quest_editor_NpcCountsView" });
private readonly table_element = el.table();
private readonly no_quest_element = el.div({ class: "quest_editor_NpcCountsView_no_quest" });
private readonly no_quest_label = this.disposable(
new Label("No quest loaded.", { enabled: false }),
);
constructor() {
super();
const quest = quest_editor_store.current_quest;
this.no_quest_element.append(this.no_quest_label.element);
this.bind_hidden(this.no_quest_element, quest.map(q => q != undefined));
this.no_quest_element.append(this.no_quest_label.element);
this.element.append(this.table_element, this.no_quest_element);
this.disposables(
quest.observe(({ value }) => this.update_view(value), {
call_now: true,
}),
);
}
private update_view(quest?: QuestModel): void {
const frag = document.createDocumentFragment();
const npc_counts = new Map<NpcType, number>();
if (quest) {
for (const npc of quest.npcs.val) {
const val = npc_counts.get(npc.type) || 0;
npc_counts.set(npc.type, val + 1);
}
}
const extra_canadines = (npc_counts.get(NpcType.Canane) || 0) * 8;
// Sort by canonical order.
const sorted_npc_counts = [...npc_counts].sort((a, b) => a[0] - b[0]);
for (const [npc_type, count] of sorted_npc_counts) {
const extra = npc_type === NpcType.Canadine ? extra_canadines : 0;
frag.append(
el.tr(
{},
el.th({ text: npc_data(npc_type).name + ":" }),
el.td({ text: String(count + extra) }),
),
);
}
this.table_element.innerHTML = "";
this.table_element.append(frag);
}
} }

View File

@ -15,7 +15,11 @@ export class QuesInfoView extends ResizableView {
private readonly table_element = el.table(); private readonly table_element = el.table();
private readonly episode_element: HTMLElement; private readonly episode_element: HTMLElement;
private readonly id_input = this.disposable(new NumberInput()); private readonly id_input = this.disposable(new NumberInput());
private readonly name_input = this.disposable(new TextInput()); private readonly name_input = this.disposable(
new TextInput("", {
max_length: 32,
}),
);
private readonly short_description_input = this.disposable( private readonly short_description_input = this.disposable(
new TextArea("", { new TextArea("", {
max_length: 128, max_length: 128,
@ -62,17 +66,28 @@ export class QuesInfoView extends ResizableView {
this.element.append(this.table_element, this.no_quest_element); this.element.append(this.table_element, this.no_quest_element);
this.disposables( this.disposables(
quest.observe(q => { quest.observe(({ value: q }) => {
this.quest_disposer.dispose(); this.quest_disposer.dispose_all();
this.episode_element.textContent = q ? Episode[q.episode] : ""; this.episode_element.textContent = q ? Episode[q.episode] : "";
if (q) { if (q) {
this.quest_disposer.add_all( this.quest_disposer.add_all(
this.id_input.value.bind_bi(q.id), this.id_input.value.bind_to(q.id),
this.name_input.value.bind_bi(q.name), this.id_input.value.observe(quest_editor_store.push_edit_id_action),
this.short_description_input.value.bind_bi(q.short_description),
this.long_description_input.value.bind_bi(q.long_description), this.name_input.value.bind_to(q.name),
this.name_input.value.observe(quest_editor_store.push_edit_name_action),
this.short_description_input.value.bind_to(q.short_description),
this.short_description_input.value.observe(
quest_editor_store.push_edit_short_description_action,
),
this.long_description_input.value.bind_to(q.long_description),
this.long_description_input.value.observe(
quest_editor_store.push_edit_long_description_action,
),
); );
} }
}), }),

View File

@ -4,10 +4,12 @@ import { ToolBarView } from "./ToolBarView";
import GoldenLayout, { Container, ContentItem, ItemConfigType } from "golden-layout"; import GoldenLayout, { Container, ContentItem, ItemConfigType } from "golden-layout";
import { quest_editor_ui_persister } from "../persistence/QuestEditorUiPersister"; import { quest_editor_ui_persister } from "../persistence/QuestEditorUiPersister";
import { QuesInfoView } from "./QuesInfoView"; import { QuesInfoView } from "./QuesInfoView";
import Logger = require("js-logger");
import "golden-layout/src/css/goldenlayout-base.css"; import "golden-layout/src/css/goldenlayout-base.css";
import "../../core/gui/golden_layout_theme.css"; import "../../core/gui/golden_layout_theme.css";
import { NpcCountsView } from "./NpcCountsView"; import { NpcCountsView } from "./NpcCountsView";
import { QuestRendererView } from "./QuestRendererView";
import { quest_editor_store } from "../stores/QuestEditorStore";
import Logger = require("js-logger");
const logger = Logger.get("quest_editor/gui/QuestEditorView"); const logger = Logger.get("quest_editor/gui/QuestEditorView");
@ -15,7 +17,7 @@ const logger = Logger.get("quest_editor/gui/QuestEditorView");
const VIEW_TO_NAME = new Map([ const VIEW_TO_NAME = new Map([
[QuesInfoView, "quest_info"], [QuesInfoView, "quest_info"],
[NpcCountsView, "npc_counts"], [NpcCountsView, "npc_counts"],
// [QuestRendererView, "quest_renderer"], [QuestRendererView, "quest_renderer"],
// [AssemblyEditorView, "assembly_editor"], // [AssemblyEditorView, "assembly_editor"],
// [EntityInfoView, "entity_info"], // [EntityInfoView, "entity_info"],
// [AddObjectView, "add_object"], // [AddObjectView, "add_object"],
@ -59,24 +61,24 @@ const DEFAULT_LAYOUT_CONTENT: ItemConfigType[] = [
}, },
], ],
}, },
// { {
// type: "stack", type: "stack",
// width: 9, width: 9,
// content: [ content: [
// { {
// title: "3D View", title: "3D View",
// type: "component", type: "component",
// componentName: Component.QuestRenderer, componentName: VIEW_TO_NAME.get(QuestRendererView),
// isClosable: false, isClosable: false,
// }, },
// { // {
// title: "Script", // title: "Script",
// type: "component", // type: "component",
// componentName: Component.AssemblyEditor, // componentName: Component.AssemblyEditor,
// isClosable: false, // isClosable: false,
// }, // },
// ], ],
// }, },
// { // {
// title: "Entity", // title: "Entity",
// type: "component", // type: "component",
@ -150,7 +152,10 @@ export class QuestEditorView extends ResizableView {
const view = new view_ctor(); const view = new view_ctor();
container.on("close", () => view.dispose()); container.on("close", () => view.dispose());
container.on("resize", () => view.resize(container.width, container.height)); container.on("resize", () =>
// Subtract 4 from height to work around bug in Golden Layout related to headerHeight.
view.resize(container.width, container.height - 4),
);
view.resize(container.width, container.height); view.resize(container.width, container.height);
@ -166,13 +171,13 @@ export class QuestEditorView extends ResizableView {
layout.on("stackCreated", (stack: ContentItem) => { layout.on("stackCreated", (stack: ContentItem) => {
stack.on("activeContentItemChanged", (item: ContentItem) => { stack.on("activeContentItemChanged", (item: ContentItem) => {
// if ("component" in item.config) { if ("componentName" in item.config) {
// if (item.config.component === CMP_TO_NAME.get(AssemblyEditorComponent)) { // if (item.config.componentName === VIEW_TO_NAME.get(AssemblyEditorView)) {
// quest_editor_store.script_undo.make_current(); // quest_editor_store.script_undo.make_current();
// } else { // } else {
// quest_editor_store.undo.make_current(); // quest_editor_store.undo.make_current();
// } // }
// } }
}); });
}); });

View File

@ -0,0 +1,37 @@
import { ResizableView } from "../../core/gui/ResizableView";
import { el } from "../../core/gui/dom";
import { RendererView } from "../../core/gui/RendererView";
import { QuestRenderer } from "../rendering/QuestRenderer";
import { gui_store, GuiTool } from "../../core/stores/GuiStore";
export class QuestRendererView extends ResizableView {
readonly element = el.div({ class: "quest_editor_QuestRendererView" });
private renderer_view = this.disposable(new RendererView(new QuestRenderer()));
constructor() {
super();
this.element.append(this.renderer_view.element);
this.renderer_view.start_rendering();
this.disposables(
gui_store.tool.observe(({ value: tool }) => {
if (tool === GuiTool.QuestEditor) {
this.renderer_view.start_rendering();
} else {
this.renderer_view.stop_rendering();
}
}),
);
}
resize(width: number, height: number): this {
super.resize(width, height);
this.renderer_view.resize(width, height);
return this;
}
}

View File

@ -28,19 +28,21 @@ export class ToolBarView extends View {
super(); super();
this.disposables( this.disposables(
this.open_file_button.files.observe(files => { this.open_file_button.files.observe(({ value: files }) => {
if (files.length) { if (files.length) {
quest_editor_store.open_file(files[0]); quest_editor_store.open_file(files[0]);
} }
}), }),
this.save_as_button.enabled.bind( this.save_as_button.enabled.bind_to(
quest_editor_store.current_quest.map(q => q != undefined), quest_editor_store.current_quest.map(q => q != undefined),
), ),
this.undo_button.enabled.bind(undo_manager.can_undo), this.undo_button.enabled.bind_to(undo_manager.can_undo),
this.undo_button.click.observe(() => undo_manager.undo()),
this.redo_button.enabled.bind(undo_manager.can_redo), this.redo_button.enabled.bind_to(undo_manager.can_redo),
this.redo_button.click.observe(() => undo_manager.redo()),
); );
} }
} }

View File

@ -1,20 +1,20 @@
import { Object3D } from "three"; import { Object3D } from "three";
import { Endianness } from "../../../core/data_formats/Endianness"; import { Endianness } from "../../core/data_formats/Endianness";
import { ArrayBufferCursor } from "../../../core/data_formats/cursor/ArrayBufferCursor"; import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor";
import { parse_area_collision_geometry } from "../../../core/data_formats/parsing/area_collision_geometry"; import { parse_area_collision_geometry } from "../../core/data_formats/parsing/area_collision_geometry";
import { parse_area_geometry } from "../../../core/data_formats/parsing/area_geometry"; import { parse_area_geometry } from "../../core/data_formats/parsing/area_geometry";
import { load_array_buffer } from "../../core/loading";
import { LoadingCache } from "./LoadingCache";
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { SectionModel } from "../model/SectionModel";
import { import {
area_collision_geometry_to_object_3d, area_collision_geometry_to_object_3d,
area_geometry_to_sections_and_object_3d, area_geometry_to_sections_and_object_3d,
} from "../rendering/conversion/areas"; } from "../rendering/conversion/areas";
import { load_array_buffer } from "../../../core/loading";
import { LoadingCache } from "./LoadingCache";
import { Section } from "../domain/Section";
import { Episode } from "../../../core/data_formats/parsing/quest/Episode";
const render_geometry_cache = new LoadingCache< const render_geometry_cache = new LoadingCache<
string, string,
{ geometry: Promise<Object3D>; sections: Promise<Section[]> } { geometry: Promise<Object3D>; sections: Promise<SectionModel[]> }
>(); >();
const collision_geometry_cache = new LoadingCache<string, Promise<Object3D>>(); const collision_geometry_cache = new LoadingCache<string, Promise<Object3D>>();
@ -22,7 +22,7 @@ export async function load_area_sections(
episode: Episode, episode: Episode,
area_id: number, area_id: number,
area_variant: number, area_variant: number,
): Promise<Section[]> { ): Promise<SectionModel[]> {
return render_geometry_cache.get_or_set(`${episode}-${area_id}-${area_variant}`, () => return render_geometry_cache.get_or_set(`${episode}-${area_id}-${area_variant}`, () =>
load_area_sections_and_render_geometry(episode, area_id, area_variant), load_area_sections_and_render_geometry(episode, area_id, area_variant),
).sections; ).sections;
@ -56,7 +56,7 @@ function load_area_sections_and_render_geometry(
episode: Episode, episode: Episode,
area_id: number, area_id: number,
area_variant: number, area_variant: number,
): { geometry: Promise<Object3D>; sections: Promise<Section[]> } { ): { geometry: Promise<Object3D>; sections: Promise<SectionModel[]> } {
const promise = get_area_asset(episode, area_id, area_variant, "render").then(buffer => const promise = get_area_asset(episode, area_id, area_variant, "render").then(buffer =>
area_geometry_to_sections_and_object_3d( area_geometry_to_sections_and_object_3d(
parse_area_geometry(new ArrayBufferCursor(buffer, Endianness.Little)), parse_area_geometry(new ArrayBufferCursor(buffer, Endianness.Little)),

View File

@ -1,15 +1,15 @@
import { Texture, CylinderBufferGeometry, BufferGeometry } from "three"; import { BufferGeometry, CylinderBufferGeometry, Texture } from "three";
import Logger from "js-logger"; import Logger from "js-logger";
import { LoadingCache } from "./LoadingCache"; import { LoadingCache } from "./LoadingCache";
import { Endianness } from "../../../core/data_formats/Endianness"; import { Endianness } from "../../core/data_formats/Endianness";
import { ArrayBufferCursor } from "../../../core/data_formats/cursor/ArrayBufferCursor"; import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor";
import { ninja_object_to_buffer_geometry } from "../../../core/rendering/conversion/ninja_geometry"; import { ninja_object_to_buffer_geometry } from "../../core/rendering/conversion/ninja_geometry";
import { parse_nj, parse_xj } from "../../../core/data_formats/parsing/ninja"; import { parse_nj, parse_xj } from "../../core/data_formats/parsing/ninja";
import { parse_xvm } from "../../../core/data_formats/parsing/ninja/texture"; import { parse_xvm } from "../../core/data_formats/parsing/ninja/texture";
import { xvm_to_textures } from "../../../core/rendering/conversion/ninja_textures"; import { xvm_to_textures } from "../../core/rendering/conversion/ninja_textures";
import { load_array_buffer } from "../../../core/loading"; import { load_array_buffer } from "../../core/loading";
import { object_data, ObjectType } from "../../../core/data_formats/parsing/quest/object_types"; import { object_data, ObjectType } from "../../core/data_formats/parsing/quest/object_types";
import { NpcType } from "../../../core/data_formats/parsing/quest/npc_types"; import { NpcType } from "../../core/data_formats/parsing/quest/npc_types";
const logger = Logger.get("loading/entities"); const logger = Logger.get("loading/entities");

View File

@ -1,15 +1,15 @@
import { ObservableAreaVariant } from "./ObservableAreaVariant"; import { AreaVariantModel } from "./AreaVariantModel";
export class ObservableArea { export class AreaModel {
/** /**
* Matches the PSO ID. * Matches the PSO ID.
*/ */
readonly id: number; readonly id: number;
readonly name: string; readonly name: string;
readonly order: number; readonly order: number;
readonly area_variants: ObservableAreaVariant[]; readonly area_variants: AreaVariantModel[];
constructor(id: number, name: string, order: number, area_variants: ObservableAreaVariant[]) { constructor(id: number, name: string, order: number, area_variants: AreaVariantModel[]) {
if (!Number.isInteger(id) || id < 0) if (!Number.isInteger(id) || id < 0)
throw new Error(`Expected id to be a non-negative integer, got ${id}.`); throw new Error(`Expected id to be a non-negative integer, got ${id}.`);
if (!name) throw new Error("name is required."); if (!name) throw new Error("name is required.");

View File

@ -0,0 +1,22 @@
import { ArrayProperty } from "../../core/observable/ArrayProperty";
import { WritableArrayProperty } from "../../core/observable/WritableArrayProperty";
import { array_property } from "../../core/observable";
import { AreaModel } from "./AreaModel";
import { SectionModel } from "./SectionModel";
export class AreaVariantModel {
readonly id: number;
readonly area: AreaModel;
private readonly _sections: WritableArrayProperty<SectionModel> = array_property();
readonly sections: ArrayProperty<SectionModel> = this._sections;
constructor(id: number, area: AreaModel) {
if (!Number.isInteger(id) || id < 0)
throw new Error(`Expected id to be a non-negative integer, got ${id}.`);
this.id = id;
this.area = area;
}
}

View File

@ -0,0 +1,104 @@
import { EntityType } from "../../core/data_formats/parsing/quest/entities";
import { Vec3 } from "../../core/data_formats/vector";
import { Property } from "../../core/observable/Property";
import { property } from "../../core/observable";
import { WritableProperty } from "../../core/observable/WritableProperty";
import { SectionModel } from "./SectionModel";
export abstract class QuestEntityModel<Type extends EntityType = EntityType> {
readonly type: Type;
readonly area_id: number;
private readonly _section_id: WritableProperty<number>;
readonly section_id: Property<number>;
private readonly _section: WritableProperty<SectionModel | undefined> = property(undefined);
readonly section: Property<SectionModel | undefined> = this._section;
set_section(section: SectionModel): this {
this._section.val = section;
this._section_id.val = section.id;
return this;
}
/**
* Section-relative position
*/
private readonly _position: WritableProperty<Vec3>;
readonly position: Property<Vec3>;
set_position(position: Vec3): void {
this._position.val = position;
}
private readonly _rotation: WritableProperty<Vec3>;
readonly rotation: Property<Vec3>;
set_rotation(rotation: Vec3): void {
this._rotation.val = rotation;
}
/**
* World position
*/
readonly world_position: Property<Vec3>;
set_world_position(pos: Vec3): this {
let { x, y, z } = pos;
const section = this.section.val;
if (section) {
const rel_x = x - section.position.x;
const rel_y = y - section.position.y;
const rel_z = z - section.position.z;
const sin = -section.sin_y_axis_rotation;
const cos = section.cos_y_axis_rotation;
const rot_x = cos * rel_x + sin * rel_z;
const rot_z = -sin * rel_x + cos * rel_z;
x = rot_x;
y = rel_y;
z = rot_z;
}
this._position.val = new Vec3(x, y, z);
return this;
}
protected constructor(
type: Type,
area_id: number,
section_id: number,
position: Vec3,
rotation: Vec3,
) {
this.type = type;
this.area_id = area_id;
this._section_id = property(section_id);
this.section_id = this._section_id;
this._position = property(position);
this.position = this._position;
this._rotation = property(rotation);
this.rotation = this._rotation;
this.world_position = this.position.map(this.position_to_world_position);
}
private position_to_world_position = (position: Vec3): Vec3 => {
const section = this.section.val;
if (section) {
let { x: rel_x, y: rel_y, z: rel_z } = position;
const sin = -section.sin_y_axis_rotation;
const cos = section.cos_y_axis_rotation;
const rot_x = cos * rel_x - sin * rel_z;
const rot_z = sin * rel_x + cos * rel_z;
const x = rot_x + section.position.x;
const y = rel_y + section.position.y;
const z = rot_z + section.position.z;
return new Vec3(x, y, z);
} else {
return position;
}
};
}

View File

@ -0,0 +1,194 @@
import { array_property, map, property } from "../../core/observable";
import { WritableProperty } from "../../core/observable/WritableProperty";
import { check_episode, Episode } from "../../core/data_formats/parsing/quest/Episode";
import { QuestObjectModel } from "./QuestObjectModel";
import { QuestNpcModel } from "./QuestNpcModel";
import { DatUnknown } from "../../core/data_formats/parsing/quest/dat";
import { Segment } from "../scripting/instructions";
import { Property } from "../../core/observable/Property";
import Logger from "js-logger";
import { AreaVariantModel } from "./AreaVariantModel";
import { area_store } from "../stores/AreaStore";
import { ArrayProperty } from "../../core/observable/ArrayProperty";
const logger = Logger.get("quest_editor/model/QuestModel");
export class QuestModel {
private readonly _id: WritableProperty<number> = property(0);
readonly id: Property<number> = this._id;
set_id(id: number): this {
if (id < 0) throw new Error(`id should be greater than or equal to 0, was ${id}.`);
this._id.val = id;
return this;
}
private readonly _language: WritableProperty<number> = property(0);
readonly language: Property<number> = this._language;
set_language(language: number): this {
if (language < 0)
throw new Error(`language should be greater than or equal to 0, was ${language}.`);
this._language.val = language;
return this;
}
private readonly _name: WritableProperty<string> = property("");
readonly name: Property<string> = this._name;
set_name(name: string): this {
if (name.length > 32)
throw new Error(`name can't be longer than 32 characters, got "${name}".`);
this._name.val = name;
return this;
}
readonly _short_description: WritableProperty<string> = property("");
readonly short_description: Property<string> = this._short_description;
set_short_description(short_description: string): this {
if (short_description.length > 128)
throw new Error(
`short_description can't be longer than 128 characters, got "${short_description}".`,
);
this._short_description.val = short_description;
return this;
}
readonly _long_description: WritableProperty<string> = property("");
readonly long_description: Property<string> = this._long_description;
set_long_description(long_description: string): this {
if (long_description.length > 288)
throw new Error(
`long_description can't be longer than 288 characters, got "${long_description}".`,
);
this._long_description.val = long_description;
return this;
}
readonly episode: Episode;
/**
* Map of area IDs to entity counts.
*/
readonly entities_per_area: Property<Map<number, number>>;
/**
* Map of area IDs to area variant IDs. One designation per area.
*/
readonly _map_designations: WritableProperty<Map<number, number>>;
readonly map_designations: Property<Map<number, number>>;
set_map_designations(map_designations: Map<number, number>): this {
this._map_designations.val = map_designations;
return this;
}
/**
* One variant per area.
*/
readonly area_variants: Property<AreaVariantModel[]>;
readonly objects: ArrayProperty<QuestObjectModel>;
readonly npcs: ArrayProperty<QuestNpcModel>;
/**
* (Partial) raw DAT data that can't be parsed yet by Phantasmal.
*/
readonly dat_unknowns: DatUnknown[];
readonly object_code: Segment[];
readonly shop_items: number[];
constructor(
id: number,
language: number,
name: string,
short_description: string,
long_description: string,
episode: Episode,
map_designations: Map<number, number>,
objects: QuestObjectModel[],
npcs: QuestNpcModel[],
dat_unknowns: DatUnknown[],
object_code: Segment[],
shop_items: number[],
) {
check_episode(episode);
if (!map_designations) throw new Error("map_designations is required.");
if (!Array.isArray(objects)) throw new Error("objs is required.");
if (!Array.isArray(npcs)) throw new Error("npcs is required.");
if (!Array.isArray(dat_unknowns)) throw new Error("dat_unknowns is required.");
if (!Array.isArray(object_code)) throw new Error("object_code is required.");
if (!Array.isArray(shop_items)) throw new Error("shop_items is required.");
this.set_id(id);
this.set_language(language);
this.set_name(name);
this.set_short_description(short_description);
this.set_long_description(long_description);
this.episode = episode;
this._map_designations = property(map_designations);
this.map_designations = this._map_designations;
this.objects = array_property(...objects);
this.npcs = array_property(...npcs);
this.dat_unknowns = dat_unknowns;
this.object_code = object_code;
this.shop_items = shop_items;
this.entities_per_area = map(
(npcs, objects) => {
const map = new Map<number, number>();
for (const npc of npcs) {
map.set(npc.area_id, (map.get(npc.area_id) || 0) + 1);
}
for (const obj of objects) {
map.set(obj.area_id, (map.get(obj.area_id) || 0) + 1);
}
return map;
},
this.npcs,
this.objects,
);
this.area_variants = map(
(entities_per_area, map_designations) => {
const variants = new Map<number, AreaVariantModel>();
for (const area_id of entities_per_area.keys()) {
try {
variants.set(area_id, area_store.get_variant(this.episode, area_id, 0));
} catch (e) {
logger.warn(e);
}
}
for (const [area_id, variant_id] of map_designations) {
try {
variants.set(
area_id,
area_store.get_variant(this.episode, area_id, variant_id),
);
} catch (e) {
logger.warn(e);
}
}
return [...variants.values()];
},
this.entities_per_area,
this.map_designations,
);
}
}

View File

@ -0,0 +1,38 @@
import { QuestEntityModel } from "./QuestEntityModel";
import { NpcType } from "../../core/data_formats/parsing/quest/npc_types";
import { Vec3 } from "../../core/data_formats/vector";
export class QuestNpcModel extends QuestEntityModel<NpcType> {
readonly pso_type_id: number;
readonly npc_id: number;
readonly script_label: number;
readonly roaming: number;
readonly scale: Vec3;
/**
* Data of which the purpose hasn't been discovered yet.
*/
readonly unknown: number[][];
constructor(
type: NpcType,
pso_type_id: number,
npc_id: number,
script_label: number,
roaming: number,
area_id: number,
section_id: number,
position: Vec3,
rotation: Vec3,
scale: Vec3,
unknown: number[][],
) {
super(type, area_id, section_id, position, rotation);
this.pso_type_id = pso_type_id;
this.npc_id = npc_id;
this.script_label = script_label;
this.roaming = roaming;
this.unknown = unknown;
this.scale = scale;
}
}

View File

@ -0,0 +1,25 @@
import { QuestEntityModel } from "./QuestEntityModel";
import { ObjectType } from "../../core/data_formats/parsing/quest/object_types";
import { Vec3 } from "../../core/data_formats/vector";
export class QuestObjectModel extends QuestEntityModel<ObjectType> {
readonly id: number;
readonly group_id: number;
constructor(
type: ObjectType,
id: number,
group_id: number,
area_id: number,
section_id: number,
position: Vec3,
rotation: Vec3,
properties: Map<string, number>,
unknown: number[][],
) {
super(type, area_id, section_id, position, rotation);
this.id = id;
this.group_id = group_id;
}
}

View File

@ -1,6 +1,6 @@
import { Vec3 } from "../../../core/data_formats/vector"; import { Vec3 } from "../../core/data_formats/vector";
export class Section { export class SectionModel {
readonly id: number; readonly id: number;
readonly position: Vec3; readonly position: Vec3;
readonly y_axis_rotation: number; readonly y_axis_rotation: number;

View File

@ -1,17 +1,19 @@
import { autorun } from "mobx"; import { QuestEntityModel } from "../model/QuestEntityModel";
import { Intersection, Mesh, MeshLambertMaterial, Plane, Raycaster, Vector2, Vector3 } from "three"; import { Intersection, Mesh, MeshLambertMaterial, Plane, Raycaster, Vector2, Vector3 } from "three";
import { Vec3 } from "../../../core/data_formats/vector"; import { Vec3 } from "../../core/data_formats/vector";
import { quest_editor_store } from "../stores/QuestEditorStore";
import { AreaUserData } from "./conversion/areas";
import { ColorType, EntityUserData, NPC_COLORS, OBJECT_COLORS } from "./conversion/entities";
import { QuestRenderer } from "./QuestRenderer"; import { QuestRenderer } from "./QuestRenderer";
import { Section } from "../domain/Section"; import { quest_editor_store } from "../stores/QuestEditorStore";
import { ObservableQuestEntity, ObservableQuestNpc } from "../domain/observable_quest_entities"; import { ColorType, EntityUserData, NPC_COLORS, OBJECT_COLORS } from "./conversion/entities";
import { QuestNpcModel } from "../model/QuestNpcModel";
import { AreaUserData } from "./conversion/areas";
import { SectionModel } from "../model/SectionModel";
import { Disposable } from "../../core/observable/Disposable";
import { Disposer } from "../../core/observable/Disposer";
const DOWN_VECTOR = new Vector3(0, -1, 0); const DOWN_VECTOR = new Vector3(0, -1, 0);
type Highlighted = { type Highlighted = {
entity: ObservableQuestEntity; entity: QuestEntityModel;
mesh: Mesh; mesh: Mesh;
}; };
@ -23,11 +25,11 @@ type Pick = {
}; };
type PickResult = Pick & { type PickResult = Pick & {
entity: ObservableQuestEntity; entity: QuestEntityModel;
mesh: Mesh; mesh: Mesh;
}; };
export class QuestEntityControls { export class QuestEntityControls implements Disposable {
private raycaster = new Raycaster(); private raycaster = new Raycaster();
private selected?: Highlighted; private selected?: Highlighted;
private hovered?: Highlighted; private hovered?: Highlighted;
@ -37,29 +39,33 @@ export class QuestEntityControls {
private pick?: Pick; private pick?: Pick;
private last_pointer_position = new Vector2(0, 0); private last_pointer_position = new Vector2(0, 0);
private moved_since_last_mouse_down = false; private moved_since_last_mouse_down = false;
private disposer = new Disposer();
constructor(private renderer: QuestRenderer) { constructor(private renderer: QuestRenderer) {
autorun(() => { this.disposer.add(
const entity = quest_editor_store.selected_entity; quest_editor_store.selected_entity.observe(({ value: entity }) => {
if (!this.selected || this.selected.entity !== entity) {
this.stop_transforming();
if (!this.selected || this.selected.entity !== entity) { if (entity) {
this.stop_transforming(); // Mesh might not be loaded yet.
this.try_highlight(entity);
if (entity) { } else {
// Mesh might not be loaded yet. this.deselect();
this.try_highlight_selected(); }
} else {
this.deselect();
} }
} }),
}); );
}
dispose(): void {
this.disposer.dispose();
} }
/** /**
* Highlights the selected entity if its mesh has been loaded. * Highlights the selected entity if its mesh has been loaded.
*/ */
try_highlight_selected = () => { try_highlight = (entity: QuestEntityModel) => {
const entity = quest_editor_store.selected_entity!;
const mesh = this.renderer.get_entity_mesh(entity); const mesh = this.renderer.get_entity_mesh(entity);
if (mesh) { if (mesh) {
@ -220,13 +226,15 @@ export class QuestEntityControls {
if (ray.intersectPlane(plane, intersection_point)) { if (ray.intersectPlane(plane, intersection_point)) {
const y = intersection_point.y + pick.grab_offset.y; const y = intersection_point.y + pick.grab_offset.y;
const y_delta = y - selection.entity.world_position.y; const y_delta = y - selection.entity.world_position.val.y;
pick.drag_y += y_delta; pick.drag_y += y_delta;
pick.drag_adjust.y -= y_delta; pick.drag_adjust.y -= y_delta;
selection.entity.world_position = new Vec3( selection.entity.set_world_position(
selection.entity.world_position.x, new Vec3(
y, selection.entity.world_position.val.x,
selection.entity.world_position.z, y,
selection.entity.world_position.val.z,
),
); );
} }
} }
@ -240,14 +248,17 @@ export class QuestEntityControls {
const { intersection, section } = this.pick_terrain(pointer_position, pick); const { intersection, section } = this.pick_terrain(pointer_position, pick);
if (intersection) { if (intersection) {
selection.entity.set_world_position_and_section( selection.entity.set_world_position(
new Vec3( new Vec3(
intersection.point.x, intersection.point.x,
intersection.point.y + pick.drag_y, intersection.point.y + pick.drag_y,
intersection.point.z, intersection.point.z,
), ),
section,
); );
if (section) {
selection.entity.set_section(section);
}
} else { } else {
// If the cursor is not over any terrain, we translate the entity accross the horizontal plane in which the entity's origin lies. // If the cursor is not over any terrain, we translate the entity accross the horizontal plane in which the entity's origin lies.
this.raycaster.setFromCamera(pointer_position, this.renderer.camera); this.raycaster.setFromCamera(pointer_position, this.renderer.camera);
@ -255,15 +266,17 @@ export class QuestEntityControls {
// ray.origin.add(data.dragAdjust); // ray.origin.add(data.dragAdjust);
const plane = new Plane( const plane = new Plane(
new Vector3(0, 1, 0), new Vector3(0, 1, 0),
-selection.entity.world_position.y + pick.grab_offset.y, -selection.entity.world_position.val.y + pick.grab_offset.y,
); );
const intersection_point = new Vector3(); const intersection_point = new Vector3();
if (ray.intersectPlane(plane, intersection_point)) { if (ray.intersectPlane(plane, intersection_point)) {
selection.entity.world_position = new Vec3( selection.entity.set_world_position(
intersection_point.x + pick.grab_offset.x, new Vec3(
selection.entity.world_position.y, intersection_point.x + pick.grab_offset.x,
intersection_point.z + pick.grab_offset.z, selection.entity.world_position.val.y,
intersection_point.z + pick.grab_offset.z,
),
); );
} }
} }
@ -272,10 +285,10 @@ export class QuestEntityControls {
private stop_transforming = () => { private stop_transforming = () => {
if (this.moved_since_last_mouse_down && this.selected && this.pick) { if (this.moved_since_last_mouse_down && this.selected && this.pick) {
const entity = this.selected.entity; const entity = this.selected.entity;
quest_editor_store.push_entity_move_action( quest_editor_store.push_translate_entity_action(
entity, entity,
this.pick.initial_position, this.pick.initial_position,
entity.world_position, entity.world_position.val,
); );
} }
@ -283,7 +296,7 @@ export class QuestEntityControls {
}; };
/** /**
* @param pointer_position pointer coordinates in normalized device space * @param pointer_position - pointer coordinates in normalized device space
*/ */
private pick_entity(pointer_position: Vector2): PickResult | undefined { private pick_entity(pointer_position: Vector2): PickResult | undefined {
// Find the nearest object and NPC under the pointer. // Find the nearest object and NPC under the pointer.
@ -319,7 +332,7 @@ export class QuestEntityControls {
return { return {
mesh: intersection.object as Mesh, mesh: intersection.object as Mesh,
entity, entity,
initial_position: entity.world_position, initial_position: entity.world_position.val,
grab_offset, grab_offset,
drag_adjust, drag_adjust,
drag_y, drag_y,
@ -328,13 +341,14 @@ export class QuestEntityControls {
/** /**
* @param pointer_pos - pointer coordinates in normalized device space * @param pointer_pos - pointer coordinates in normalized device space
* @param data - entity picking data
*/ */
private pick_terrain( private pick_terrain(
pointer_pos: Vector2, pointer_pos: Vector2,
data: Pick, data: Pick,
): { ): {
intersection?: Intersection; intersection?: Intersection;
section?: Section; section?: SectionModel;
} { } {
this.raycaster.setFromCamera(pointer_pos, this.renderer.camera); this.raycaster.setFromCamera(pointer_pos, this.renderer.camera);
this.raycaster.ray.origin.add(data.drag_adjust); this.raycaster.ray.origin.add(data.drag_adjust);
@ -360,7 +374,7 @@ export class QuestEntityControls {
} }
function set_color({ entity, mesh }: Highlighted, type: ColorType): void { function set_color({ entity, mesh }: Highlighted, type: ColorType): void {
const color = entity instanceof ObservableQuestNpc ? NPC_COLORS[type] : OBJECT_COLORS[type]; const color = entity instanceof QuestNpcModel ? NPC_COLORS[type] : OBJECT_COLORS[type];
if (mesh) { if (mesh) {
if (Array.isArray(mesh.material)) { if (Array.isArray(mesh.material)) {

View File

@ -1,42 +1,43 @@
import Logger from "js-logger"; import Logger from "js-logger";
import { autorun, IReactionDisposer } from "mobx";
import { Intersection, Mesh, Object3D, Raycaster, Vector3 } from "three"; import { Intersection, Mesh, Object3D, Raycaster, Vector3 } from "three";
import { load_area_collision_geometry, load_area_render_geometry } from "../loading/areas"; import { QuestRenderer } from "./QuestRenderer";
import { QuestModel } from "../model/QuestModel";
import { import {
load_npc_geometry, load_npc_geometry,
load_npc_textures, load_npc_textures,
load_object_geometry, load_object_geometry,
load_object_textures, load_object_textures,
} from "../loading/entities"; } from "../loading/entities";
import { create_npc_mesh, create_object_mesh } from "./conversion/entities"; import { load_area_collision_geometry, load_area_render_geometry } from "../loading/areas";
import { QuestRenderer } from "./QuestRenderer"; import { QuestEntityModel } from "../model/QuestEntityModel";
import { AreaUserData } from "./conversion/areas"; import { Disposer } from "../../core/observable/Disposer";
import { ObservableQuest } from "../domain/ObservableQuest"; import { Disposable } from "../../core/observable/Disposable";
import { ObservableArea } from "../domain/ObservableArea"; import { AreaModel } from "../model/AreaModel";
import { ObservableAreaVariant } from "../domain/ObservableAreaVariant"; import { AreaVariantModel } from "../model/AreaVariantModel";
import { ObservableQuestEntity } from "../domain/observable_quest_entities";
import { area_store } from "../stores/AreaStore"; import { area_store } from "../stores/AreaStore";
import { create_npc_mesh, create_object_mesh } from "./conversion/entities";
import { AreaUserData } from "./conversion/areas";
const logger = Logger.get("rendering/QuestModelManager"); const logger = Logger.get("quest_editor/rendering/QuestModelManager");
const CAMERA_POSITION = new Vector3(0, 800, 700); const CAMERA_POSITION = new Vector3(0, 800, 700);
const CAMERA_LOOKAT = new Vector3(0, 0, 0); const CAMERA_LOOK_AT = new Vector3(0, 0, 0);
const DUMMY_OBJECT = new Object3D(); const DUMMY_OBJECT = new Object3D();
export class QuestModelManager { export class QuestModelManager implements Disposable {
private quest?: ObservableQuest; private quest?: QuestModel;
private area?: ObservableArea; private area?: AreaModel;
private area_variant?: ObservableAreaVariant; private area_variant?: AreaVariantModel;
private entity_reaction_disposers: IReactionDisposer[] = []; private disposer = new Disposer();
constructor(private renderer: QuestRenderer) {} constructor(private renderer: QuestRenderer) {}
async load_models(quest?: ObservableQuest, area?: ObservableArea): Promise<void> { async load_models(quest?: QuestModel, area?: AreaModel): Promise<void> {
let area_variant: ObservableAreaVariant | undefined; let area_variant: AreaVariantModel | undefined;
if (quest && area) { if (quest && area) {
area_variant = area_variant =
quest.area_variants.find(v => v.area.id === area.id) || quest.area_variants.val.find(v => v.area.id === area.id) ||
area_store.get_variant(quest.episode, area.id, 0); area_store.get_variant(quest.episode, area.id, 0);
} }
@ -48,7 +49,7 @@ export class QuestModelManager {
this.area = area; this.area = area;
this.area_variant = area_variant; this.area_variant = area_variant;
this.dispose_entity_reactions(); this.disposer.dispose_all();
if (quest && area) { if (quest && area) {
try { try {
@ -76,12 +77,12 @@ export class QuestModelManager {
this.renderer.collision_geometry = collision_geometry; this.renderer.collision_geometry = collision_geometry;
this.renderer.render_geometry = render_geometry; this.renderer.render_geometry = render_geometry;
this.renderer.reset_camera(CAMERA_POSITION, CAMERA_LOOKAT); this.renderer.reset_camera(CAMERA_POSITION, CAMERA_LOOK_AT);
// Load entity models. // Load entity models.
this.renderer.reset_entity_models(); this.renderer.reset_entity_models();
for (const npc of quest.npcs) { for (const npc of quest.npcs.val) {
if (npc.area_id === area.id) { if (npc.area_id === area.id) {
const npc_geom = await load_npc_geometry(npc.type); const npc_geom = await load_npc_geometry(npc.type);
const npc_tex = await load_npc_textures(npc.type); const npc_tex = await load_npc_textures(npc.type);
@ -93,7 +94,7 @@ export class QuestModelManager {
} }
} }
for (const object of quest.objects) { for (const object of quest.objects.val) {
if (object.area_id === area.id) { if (object.area_id === area.id) {
const object_geom = await load_object_geometry(object.type); const object_geom = await load_object_geometry(object.type);
const object_tex = await load_object_textures(object.type); const object_tex = await load_object_textures(object.type);
@ -117,6 +118,10 @@ export class QuestModelManager {
} }
} }
dispose(): void {
this.disposer.dispose();
}
private add_sections_to_collision_geometry( private add_sections_to_collision_geometry(
collision_geom: Object3D, collision_geom: Object3D,
render_geom: Object3D, render_geom: Object3D,
@ -158,23 +163,19 @@ export class QuestModelManager {
} }
} }
private update_entity_geometry(entity: ObservableQuestEntity, model: Mesh): void { private update_entity_geometry(entity: QuestEntityModel, model: Mesh): void {
this.renderer.add_entity_model(model); this.renderer.add_entity_model(model);
this.entity_reaction_disposers.push( this.disposer.add_all(
autorun(() => { entity.world_position.observe(({ value: { x, y, z } }) => {
const { x, y, z } = entity.world_position;
model.position.set(x, y, z); model.position.set(x, y, z);
const rot = entity.rotation; this.renderer.schedule_render();
model.rotation.set(rot.x, rot.y, rot.z); }),
entity.rotation.observe(({ value: { x, y, z } }) => {
model.rotation.set(x, y, z);
this.renderer.schedule_render(); this.renderer.schedule_render();
}), }),
); );
} }
private dispose_entity_reactions(): void {
for (const disposer of this.entity_reaction_disposers) {
disposer();
}
}
} }

View File

@ -1,27 +1,20 @@
import { autorun } from "mobx"; import { Renderer } from "../../core/rendering/Renderer";
import { Group, Mesh, Object3D, PerspectiveCamera } from "three"; import { Group, Mesh, Object3D, PerspectiveCamera } from "three";
import { QuestEntityModel } from "../model/QuestEntityModel";
import { quest_editor_store } from "../stores/QuestEditorStore"; import { quest_editor_store } from "../stores/QuestEditorStore";
import { QuestEntityControls } from "./QuestEntityControls";
import { QuestModelManager } from "./QuestModelManager"; import { QuestModelManager } from "./QuestModelManager";
import { Renderer } from "../../../core/rendering/Renderer"; import { Disposer } from "../../core/observable/Disposer";
import { QuestEntityControls } from "./QuestEntityControls";
import { EntityUserData } from "./conversion/entities"; import { EntityUserData } from "./conversion/entities";
import { ObservableQuestEntity } from "../domain/observable_quest_entities";
let renderer: QuestRenderer | undefined;
export function get_quest_renderer(): QuestRenderer {
if (!renderer) renderer = new QuestRenderer();
return renderer;
}
export class QuestRenderer extends Renderer { export class QuestRenderer extends Renderer {
get debug(): boolean { get debug(): boolean {
return this._debug; return super.debug;
} }
set debug(debug: boolean) { set debug(debug: boolean) {
if (this._debug !== debug) { if (this.debug !== debug) {
this._debug = debug; super.debug = debug;
this._render_geometry.visible = debug; this._render_geometry.visible = debug;
this.schedule_render(); this.schedule_render();
} }
@ -58,34 +51,33 @@ export class QuestRenderer extends Renderer {
return this._entity_models; return this._entity_models;
} }
private perspective_camera: PerspectiveCamera; private readonly disposer = new Disposer();
private entity_to_mesh = new Map<ObservableQuestEntity, Mesh>(); private readonly perspective_camera: PerspectiveCamera;
private entity_controls: QuestEntityControls; private readonly entity_to_mesh = new Map<QuestEntityModel, Mesh>();
private readonly model_manager = this.disposer.add(new QuestModelManager(this));
private readonly entity_controls = this.disposer.add(new QuestEntityControls(this));
constructor() { constructor() {
super(new PerspectiveCamera(60, 1, 10, 10000)); super(new PerspectiveCamera(60, 1, 10, 10000));
this.perspective_camera = this.camera as PerspectiveCamera; this.perspective_camera = this.camera as PerspectiveCamera;
const model_manager = new QuestModelManager(this); this.disposer.add_all(
quest_editor_store.current_quest.observe(this.load_models),
autorun( quest_editor_store.current_area.observe(this.load_models),
() => { quest_editor_store.debug.observe(({ value }) => (this.debug = value)),
model_manager.load_models(
quest_editor_store.current_quest,
quest_editor_store.current_area,
);
},
{ name: "call load_models" },
); );
this.entity_controls = new QuestEntityControls(this);
this.dom_element.addEventListener("mousedown", this.entity_controls.on_mouse_down); this.dom_element.addEventListener("mousedown", this.entity_controls.on_mouse_down);
this.dom_element.addEventListener("mouseup", this.entity_controls.on_mouse_up); this.dom_element.addEventListener("mouseup", this.entity_controls.on_mouse_up);
this.dom_element.addEventListener("mousemove", this.entity_controls.on_mouse_move); this.dom_element.addEventListener("mousemove", this.entity_controls.on_mouse_move);
} }
dispose(): void {
super.dispose();
this.disposer.dispose();
}
set_size(width: number, height: number): void { set_size(width: number, height: number): void {
this.perspective_camera.aspect = width / height; this.perspective_camera.aspect = width / height;
this.perspective_camera.updateProjectionMatrix(); this.perspective_camera.updateProjectionMatrix();
@ -104,12 +96,21 @@ export class QuestRenderer extends Renderer {
this._entity_models.add(model); this._entity_models.add(model);
this.entity_to_mesh.set(entity, model); this.entity_to_mesh.set(entity, model);
if (entity === quest_editor_store.selected_entity) { if (entity === quest_editor_store.selected_entity.val) {
this.entity_controls.try_highlight_selected(); this.entity_controls.try_highlight(entity);
} }
this.schedule_render();
} }
get_entity_mesh(entity: ObservableQuestEntity): Mesh | undefined { get_entity_mesh(entity: QuestEntityModel): Mesh | undefined {
return this.entity_to_mesh.get(entity); return this.entity_to_mesh.get(entity);
} }
private load_models = () => {
this.model_manager.load_models(
quest_editor_store.current_quest.val,
quest_editor_store.current_area.val,
);
};
} }

View File

@ -1,4 +1,5 @@
import { import {
Color,
DoubleSide, DoubleSide,
Face3, Face3,
Geometry, Geometry,
@ -8,13 +9,12 @@ import {
MeshLambertMaterial, MeshLambertMaterial,
Object3D, Object3D,
Vector3, Vector3,
Color,
} from "three"; } from "three";
import { CollisionObject } from "../../../../core/data_formats/parsing/area_collision_geometry"; import { CollisionObject } from "../../../core/data_formats/parsing/area_collision_geometry";
import { RenderObject } from "../../../../core/data_formats/parsing/area_geometry"; import { RenderObject } from "../../../core/data_formats/parsing/area_geometry";
import { GeometryBuilder } from "../../../../core/rendering/conversion/GeometryBuilder"; import { GeometryBuilder } from "../../../core/rendering/conversion/GeometryBuilder";
import { ninja_object_to_geometry_builder } from "../../../../core/rendering/conversion/ninja_geometry"; import { ninja_object_to_geometry_builder } from "../../../core/rendering/conversion/ninja_geometry";
import { Section } from "../../domain/Section"; import { SectionModel } from "../../model/SectionModel";
const materials = [ const materials = [
// Wall // Wall
@ -67,7 +67,7 @@ const wireframe_materials = [
]; ];
export type AreaUserData = { export type AreaUserData = {
section?: Section; section?: SectionModel;
}; };
export function area_collision_geometry_to_object_3d(object: CollisionObject): Object3D { export function area_collision_geometry_to_object_3d(object: CollisionObject): Object3D {
@ -116,8 +116,8 @@ export function area_collision_geometry_to_object_3d(object: CollisionObject): O
export function area_geometry_to_sections_and_object_3d( export function area_geometry_to_sections_and_object_3d(
object: RenderObject, object: RenderObject,
): [Section[], Object3D] { ): [SectionModel[], Object3D] {
const sections: Section[] = []; const sections: SectionModel[] = [];
const group = new Group(); const group = new Group();
let i = 0; let i = 0;
@ -144,7 +144,7 @@ export function area_geometry_to_sections_and_object_3d(
mesh.updateMatrixWorld(); mesh.updateMatrixWorld();
if (section.id >= 0) { if (section.id >= 0) {
const sec = new Section(section.id, section.position, section.rotation.y); const sec = new SectionModel(section.id, section.position, section.rotation.y);
sections.push(sec); sections.push(sec);
(mesh.userData as AreaUserData).section = sec; (mesh.userData as AreaUserData).section = sec;
} }

View File

@ -1,12 +1,10 @@
import { QuestEntityModel } from "../../model/QuestEntityModel";
import { QuestObjectModel } from "../../model/QuestObjectModel";
import { BufferGeometry, DoubleSide, Mesh, MeshLambertMaterial, Texture } from "three"; import { BufferGeometry, DoubleSide, Mesh, MeshLambertMaterial, Texture } from "three";
import { create_mesh } from "../../../../core/rendering/conversion/create_mesh"; import { ObjectType } from "../../../core/data_formats/parsing/quest/object_types";
import { ObjectType } from "../../../../core/data_formats/parsing/quest/object_types"; import { QuestNpcModel } from "../../model/QuestNpcModel";
import { NpcType } from "../../../../core/data_formats/parsing/quest/npc_types"; import { NpcType } from "../../../core/data_formats/parsing/quest/npc_types";
import { import { create_mesh } from "../../../core/rendering/conversion/create_mesh";
ObservableQuestEntity,
ObservableQuestNpc,
ObservableQuestObject,
} from "../../domain/observable_quest_entities";
export enum ColorType { export enum ColorType {
Normal, Normal,
@ -25,11 +23,11 @@ NPC_COLORS[ColorType.Hovered] = 0xff3f5f;
NPC_COLORS[ColorType.Selected] = 0xff0054; NPC_COLORS[ColorType.Selected] = 0xff0054;
export type EntityUserData = { export type EntityUserData = {
entity: ObservableQuestEntity; entity: QuestEntityModel;
}; };
export function create_object_mesh( export function create_object_mesh(
object: ObservableQuestObject, object: QuestObjectModel,
geometry: BufferGeometry, geometry: BufferGeometry,
textures: Texture[], textures: Texture[],
): Mesh { ): Mesh {
@ -43,7 +41,7 @@ export function create_object_mesh(
} }
export function create_npc_mesh( export function create_npc_mesh(
npc: ObservableQuestNpc, npc: QuestNpcModel,
geometry: BufferGeometry, geometry: BufferGeometry,
textures: Texture[], textures: Texture[],
): Mesh { ): Mesh {
@ -51,7 +49,7 @@ export function create_npc_mesh(
} }
function create( function create(
entity: ObservableQuestEntity, entity: QuestEntityModel,
geometry: BufferGeometry, geometry: BufferGeometry,
textures: Texture[], textures: Texture[],
color: number, color: number,
@ -80,9 +78,9 @@ function create(
mesh.name = name; mesh.name = name;
(mesh.userData as EntityUserData).entity = entity; (mesh.userData as EntityUserData).entity = entity;
const { x, y, z } = entity.world_position; const { x, y, z } = entity.world_position.val;
mesh.position.set(x, y, z); mesh.position.set(x, y, z);
const rot = entity.rotation; const rot = entity.rotation.val;
mesh.rotation.set(rot.x, rot.y, rot.z); mesh.rotation.set(rot.x, rot.y, rot.z);
return mesh; return mesh;

View File

@ -1,4 +1,3 @@
import { action, observable } from "mobx";
import { editor, languages } from "monaco-editor"; import { editor, languages } from "monaco-editor";
import AssemblyWorker from "worker-loader!./assembly_worker"; import AssemblyWorker from "worker-loader!./assembly_worker";
import { import {
@ -11,8 +10,11 @@ import {
} from "./assembly_worker_messages"; } from "./assembly_worker_messages";
import { AssemblyError, AssemblyWarning } from "./assembly"; import { AssemblyError, AssemblyWarning } from "./assembly";
import { disassemble } from "./disassembly"; import { disassemble } from "./disassembly";
import { ObservableQuest } from "../domain/ObservableQuest"; import { QuestModel } from "../model/QuestModel";
import { Kind, OPCODES } from "./opcodes"; import { Kind, OPCODES } from "./opcodes";
import { Property } from "../../core/observable/Property";
import { property } from "../../core/observable";
import { WritableProperty } from "../../core/observable/WritableProperty";
import CompletionList = languages.CompletionList; import CompletionList = languages.CompletionList;
import CompletionItemKind = languages.CompletionItemKind; import CompletionItemKind = languages.CompletionItemKind;
import CompletionItem = languages.CompletionItem; import CompletionItem = languages.CompletionItem;
@ -47,11 +49,14 @@ const KEYWORD_SUGGESTIONS = [
] as CompletionItem[]; ] as CompletionItem[];
export class AssemblyAnalyser { export class AssemblyAnalyser {
@observable warnings: AssemblyWarning[] = []; readonly _warnings: WritableProperty<AssemblyWarning[]> = property([]);
@observable errors: AssemblyError[] = []; readonly warnings: Property<AssemblyWarning[]> = this._warnings;
readonly _errors: WritableProperty<AssemblyError[]> = property([]);
readonly errors: Property<AssemblyError[]> = this._errors;
private worker = new AssemblyWorker(); private worker = new AssemblyWorker();
private quest?: ObservableQuest; private quest?: QuestModel;
private promises = new Map< private promises = new Map<
number, number,
{ resolve: (result: any) => void; reject: (error: Error) => void } { resolve: (result: any) => void; reject: (error: Error) => void }
@ -62,7 +67,7 @@ export class AssemblyAnalyser {
this.worker.onmessage = this.process_worker_message; this.worker.onmessage = this.process_worker_message;
} }
disassemble(quest: ObservableQuest): string[] { disassemble(quest: QuestModel): string[] {
this.quest = quest; this.quest = quest;
const assembly = disassemble(quest.object_code); const assembly = disassemble(quest.object_code);
const message: NewAssemblyInput = { type: InputMessageType.NewAssembly, assembly }; const message: NewAssemblyInput = { type: InputMessageType.NewAssembly, assembly };
@ -122,7 +127,6 @@ export class AssemblyAnalyser {
this.worker.terminate(); this.worker.terminate();
} }
@action
private process_worker_message = (e: MessageEvent): void => { private process_worker_message = (e: MessageEvent): void => {
const message: AssemblyWorkerOutput = e.data; const message: AssemblyWorkerOutput = e.data;
@ -135,8 +139,8 @@ export class AssemblyAnalyser {
...message.object_code, ...message.object_code,
); );
this.quest.set_map_designations(message.map_designations); this.quest.set_map_designations(message.map_designations);
this.warnings = message.warnings; this._warnings.val = message.warnings;
this.errors = message.errors; this._errors.val = message.errors;
} }
break; break;
case OutputMessageType.SignatureHelp: case OutputMessageType.SignatureHelp:

View File

@ -24,7 +24,7 @@ import {
} from "./instructions"; } from "./instructions";
import { Kind, Opcode, OPCODES_BY_MNEMONIC, Param, StackInteraction } from "./opcodes"; import { Kind, Opcode, OPCODES_BY_MNEMONIC, Param, StackInteraction } from "./opcodes";
const logger = Logger.get("scripting/assembly"); const logger = Logger.get("quest_editor/scripting/assembly");
export type AssemblyWarning = { export type AssemblyWarning = {
line_no: number; line_no: number;

View File

@ -10,7 +10,7 @@ import {
import { BasicBlock, ControlFlowGraph } from "./ControlFlowGraph"; import { BasicBlock, ControlFlowGraph } from "./ControlFlowGraph";
import { ValueSet } from "./ValueSet"; import { ValueSet } from "./ValueSet";
const logger = Logger.get("scripting/data_flow_analysis/register_value"); const logger = Logger.get("quest_editor/scripting/data_flow_analysis/register_value");
export const MIN_REGISTER_VALUE = MIN_SIGNED_DWORD_VALUE; export const MIN_REGISTER_VALUE = MIN_SIGNED_DWORD_VALUE;
export const MAX_REGISTER_VALUE = MAX_SIGNED_DWORD_VALUE; export const MAX_REGISTER_VALUE = MAX_SIGNED_DWORD_VALUE;

View File

@ -10,7 +10,7 @@ import { BasicBlock, ControlFlowGraph } from "./ControlFlowGraph";
import { ValueSet } from "./ValueSet"; import { ValueSet } from "./ValueSet";
import { register_value } from "./register_value"; import { register_value } from "./register_value";
const logger = Logger.get("scripting/data_flow_analysis/stack_value"); const logger = Logger.get("quest_editor/scripting/data_flow_analysis/stack_value");
export const MIN_STACK_VALUE = MIN_SIGNED_DWORD_VALUE; export const MIN_STACK_VALUE = MIN_SIGNED_DWORD_VALUE;
export const MAX_STACK_VALUE = MAX_SIGNED_DWORD_VALUE; export const MAX_STACK_VALUE = MAX_SIGNED_DWORD_VALUE;

View File

@ -1,9 +1,9 @@
import { readFileSync } from "fs"; import { readFileSync } from "fs";
import { Endianness } from "../../../core/data_formats/Endianness"; import { Endianness } from "../../core/data_formats/Endianness";
import { prs_decompress } from "../../../core/data_formats/compression/prs/decompress"; import { prs_decompress } from "../../core/data_formats/compression/prs/decompress";
import { ArrayBufferCursor } from "../../../core/data_formats/cursor/ArrayBufferCursor"; import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor";
import { BufferCursor } from "../../../core/data_formats/cursor/BufferCursor"; import { BufferCursor } from "../../core/data_formats/cursor/BufferCursor";
import { parse_bin, write_bin } from "../../../core/data_formats/parsing/quest/bin"; import { parse_bin, write_bin } from "../../core/data_formats/parsing/quest/bin";
import { assemble } from "./assembly"; import { assemble } from "./assembly";
import { disassemble } from "./disassembly"; import { disassemble } from "./disassembly";

View File

@ -2,7 +2,7 @@ import { Instruction, InstructionSegment, Segment, SegmentType } from "../instru
import { Opcode } from "../opcodes"; import { Opcode } from "../opcodes";
import Logger from "js-logger"; import Logger from "js-logger";
const logger = Logger.get("scripting/vm"); const logger = Logger.get("quest_editor/scripting/vm");
const REGISTER_COUNT = 256; const REGISTER_COUNT = 256;
const REGISTER_SIZE = 4; const REGISTER_SIZE = 4;

View File

@ -1,21 +1,21 @@
import { AreaModel } from "../model/AreaModel";
import { AreaVariantModel } from "../model/AreaVariantModel";
import { Episode, EPISODES } from "../../core/data_formats/parsing/quest/Episode";
import { SectionModel } from "../model/SectionModel";
import { load_area_sections } from "../loading/areas"; import { load_area_sections } from "../loading/areas";
import { Episode, EPISODES } from "../../../core/data_formats/parsing/quest/Episode"; import { get_areas_for_episode } from "../../core/data_formats/parsing/quest/areas";
import { get_areas_for_episode } from "../../../core/data_formats/parsing/quest/areas";
import { ObservableAreaVariant } from "../domain/ObservableAreaVariant";
import { ObservableArea } from "../domain/ObservableArea";
import { Section } from "../domain/Section";
class AreaStore { class AreaStore {
private readonly areas: ObservableArea[][] = []; private readonly areas: AreaModel[][] = [];
constructor() { constructor() {
for (const episode of EPISODES) { for (const episode of EPISODES) {
this.areas[episode] = get_areas_for_episode(episode).map(area => { this.areas[episode] = get_areas_for_episode(episode).map(area => {
const observable_area = new ObservableArea(area.id, area.name, area.order, []); const observable_area = new AreaModel(area.id, area.name, area.order, []);
for (const variant of area.area_variants) { for (const variant of area.area_variants) {
observable_area.area_variants.push( observable_area.area_variants.push(
new ObservableAreaVariant(variant.id, observable_area), new AreaVariantModel(variant.id, observable_area),
); );
} }
@ -24,11 +24,11 @@ class AreaStore {
} }
} }
get_areas_for_episode = (episode: Episode): ObservableArea[] => { get_areas_for_episode = (episode: Episode): AreaModel[] => {
return this.areas[episode]; return this.areas[episode];
}; };
get_area = (episode: Episode, area_id: number): ObservableArea => { get_area = (episode: Episode, area_id: number): AreaModel => {
const area = this.areas[episode].find(a => a.id === area_id); const area = this.areas[episode].find(a => a.id === area_id);
if (!area) throw new Error(`Area id ${area_id} for episode ${episode} is invalid.`); if (!area) throw new Error(`Area id ${area_id} for episode ${episode} is invalid.`);
return area; return area;
@ -38,7 +38,7 @@ class AreaStore {
episode: Episode, episode: Episode,
area_id: number, area_id: number,
variant_id: number, variant_id: number,
): ObservableAreaVariant => { ): AreaVariantModel => {
const area = this.get_area(episode, area_id); const area = this.get_area(episode, area_id);
const area_variant = area.area_variants[variant_id]; const area_variant = area.area_variants[variant_id];
@ -54,7 +54,7 @@ class AreaStore {
episode: Episode, episode: Episode,
area_id: number, area_id: number,
variant_id: number, variant_id: number,
): Promise<Section[]> => { ): Promise<SectionModel[]> => {
return load_area_sections(episode, area_id, variant_id); return load_area_sections(episode, area_id, variant_id);
}; };
} }

View File

@ -1,17 +1,33 @@
import { property } from "../../core/observable"; import { property } from "../../core/observable";
import { ObservableQuest } from "../domain/ObservableQuest"; import { QuestModel } from "../model/QuestModel";
import { Property } from "../../core/observable/Property"; import { Property, PropertyChangeEvent } from "../../core/observable/Property";
import { read_file } from "../../core/read_file"; import { read_file } from "../../core/read_file";
import { parse_quest } from "../../core/data_formats/parsing/quest"; import { parse_quest } from "../../core/data_formats/parsing/quest";
import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor"; import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor";
import { Endianness } from "../../core/data_formats/Endianness"; import { Endianness } from "../../core/data_formats/Endianness";
import { SimpleUndo, UndoStack } from "../../old/core/undo";
import { WritableProperty } from "../../core/observable/WritableProperty"; import { WritableProperty } from "../../core/observable/WritableProperty";
import { QuestObjectModel } from "../model/QuestObjectModel";
import { QuestNpcModel } from "../model/QuestNpcModel";
import { AreaModel } from "../model/AreaModel";
import { area_store } from "./AreaStore";
import { SectionModel } from "../model/SectionModel";
import { QuestEntityModel } from "../model/QuestEntityModel";
import { Vec3 } from "../../core/data_formats/vector";
import { Disposable } from "../../core/observable/Disposable";
import { Disposer } from "../../core/observable/Disposer";
import { gui_store, GuiTool } from "../../core/stores/GuiStore";
import { UndoStack } from "../../core/undo/UndoStack";
import { SimpleUndo } from "../../core/undo/SimpleUndo";
import { TranslateEntityAction } from "../actions/TranslateEntityAction";
import { EditShortDescriptionAction } from "../actions/EditShortDescriptionAction";
import { EditLongDescriptionAction } from "../actions/EditLongDescriptionAction";
import { EditNameAction } from "../actions/EditNameAction";
import { EditIdAction } from "../actions/EditIdAction";
import Logger = require("js-logger"); import Logger = require("js-logger");
const logger = Logger.get("quest_editor/gui/QuestEditorStore"); const logger = Logger.get("quest_editor/gui/QuestEditorStore");
export class QuestEditorStore { export class QuestEditorStore implements Disposable {
readonly debug: WritableProperty<boolean> = property(false); readonly debug: WritableProperty<boolean> = property(false);
readonly undo = new UndoStack(); readonly undo = new UndoStack();
@ -20,8 +36,54 @@ export class QuestEditorStore {
private readonly _current_quest_filename = property<string | undefined>(undefined); private readonly _current_quest_filename = property<string | undefined>(undefined);
readonly current_quest_filename: Property<string | undefined> = this._current_quest_filename; readonly current_quest_filename: Property<string | undefined> = this._current_quest_filename;
private readonly _current_quest = property<ObservableQuest | undefined>(undefined); private readonly _current_quest = property<QuestModel | undefined>(undefined);
readonly current_quest: Property<ObservableQuest | undefined> = this._current_quest; readonly current_quest: Property<QuestModel | undefined> = this._current_quest;
private readonly _current_area = property<AreaModel | undefined>(undefined);
readonly current_area: Property<AreaModel | undefined> = this._current_area;
private readonly _selected_entity = property<QuestEntityModel | undefined>(undefined);
readonly selected_entity: Property<QuestEntityModel | undefined> = this._selected_entity;
private readonly disposer = new Disposer();
constructor() {
this.disposer.add(
gui_store.tool.observe(
({ value: tool }) => {
if (tool === GuiTool.QuestEditor) {
this.undo.make_current();
}
},
{ call_now: true },
),
);
}
dispose(): void {
this.disposer.dispose();
}
set_current_area_id = (area_id?: number) => {
this._selected_entity.val = undefined;
if (area_id == undefined) {
this._current_area.val = undefined;
} else if (this.current_quest.val) {
this._current_area.val = area_store.get_area(this.current_quest.val.episode, area_id);
}
};
set_selected_entity = (entity?: QuestEntityModel) => {
if (entity && this.current_quest.val) {
this._current_area.val = area_store.get_area(
this.current_quest.val.episode,
entity.area_id,
);
}
this._selected_entity.val = entity;
};
// TODO: notify user of problems. // TODO: notify user of problems.
open_file = async (file: File) => { open_file = async (file: File) => {
@ -30,47 +92,47 @@ export class QuestEditorStore {
const quest = parse_quest(new ArrayBufferCursor(buffer, Endianness.Little)); const quest = parse_quest(new ArrayBufferCursor(buffer, Endianness.Little));
this.set_quest( this.set_quest(
quest && quest &&
new ObservableQuest( new QuestModel(
quest.id, quest.id,
quest.language, quest.language,
quest.name, quest.name,
quest.short_description, quest.short_description,
quest.long_description, quest.long_description,
quest.episode, quest.episode,
// quest.map_designations, quest.map_designations,
// quest.objects.map( quest.objects.map(
// obj => obj =>
// new ObservableQuestObject( new QuestObjectModel(
// obj.type, obj.type,
// obj.id, obj.id,
// obj.group_id, obj.group_id,
// obj.area_id, obj.area_id,
// obj.section_id, obj.section_id,
// obj.position, obj.position,
// obj.rotation, obj.rotation,
// obj.properties, obj.properties,
// obj.unknown, obj.unknown,
// ), ),
// ), ),
// quest.npcs.map( quest.npcs.map(
// npc => npc =>
// new ObservableQuestNpc( new QuestNpcModel(
// npc.type, npc.type,
// npc.pso_type_id, npc.pso_type_id,
// npc.npc_id, npc.npc_id,
// npc.script_label, npc.script_label,
// npc.roaming, npc.roaming,
// npc.area_id, npc.area_id,
// npc.section_id, npc.section_id,
// npc.position, npc.position,
// npc.rotation, npc.rotation,
// npc.scale, npc.scale,
// npc.unknown, npc.unknown,
// ), ),
// ), ),
// quest.dat_unknowns, quest.dat_unknowns,
// quest.object_code, quest.object_code,
// quest.shop_items, quest.shop_items,
), ),
file.name, file.name,
); );
@ -79,50 +141,90 @@ export class QuestEditorStore {
} }
}; };
private set_quest(quest?: ObservableQuest, filename?: string): void { push_edit_id_action = (event: PropertyChangeEvent<number>) => {
this._current_quest_filename.val = filename; if (this.current_quest.val) {
this.undo.push(new EditIdAction(this.current_quest.val, event)).redo();
}
};
push_edit_name_action = (event: PropertyChangeEvent<string>) => {
if (this.current_quest.val) {
this.undo.push(new EditNameAction(this.current_quest.val, event)).redo();
}
};
push_edit_short_description_action = (event: PropertyChangeEvent<string>) => {
if (this.current_quest.val) {
this.undo.push(new EditShortDescriptionAction(this.current_quest.val, event)).redo();
}
};
push_edit_long_description_action = (event: PropertyChangeEvent<string>) => {
if (this.current_quest.val) {
this.undo.push(new EditLongDescriptionAction(this.current_quest.val, event)).redo();
}
};
push_translate_entity_action = (
entity: QuestEntityModel,
old_position: Vec3,
new_position: Vec3,
) => {
this.undo.push(new TranslateEntityAction(entity, old_position, new_position)).redo();
};
private async set_quest(quest?: QuestModel, filename?: string): Promise<void> {
this.undo.reset(); this.undo.reset();
this.script_undo.reset(); this.script_undo.reset();
// if (quest) { this._current_area.val = undefined;
// this.current_area = area_store.get_area(quest.episode, 0); this._selected_entity.val = undefined;
// } else {
// this.current_area = undefined; this._current_quest_filename.val = filename;
// } this._current_quest.val = quest;
if (quest) { if (quest) {
this._current_area.val = area_store.get_area(quest.episode, 0);
// Load section data. // Load section data.
// for (const variant of quest.area_variants) { for (const variant of quest.area_variants.val) {
// const sections = yield area_store.get_area_sections( const sections = await area_store.get_area_sections(
// quest.episode, quest.episode,
// variant.area.id, variant.area.id,
// variant.id, variant.id,
// ); );
// variant.sections.replace(sections); variant.sections.val.splice(0, Infinity, ...sections);
//
// for (const object of quest.objects.filter(o => o.area_id === variant.area.id)) { for (const object of quest.objects.val.filter(o => o.area_id === variant.area.id)) {
// try { try {
// this.set_section_on_quest_entity(object, sections); this.set_section_on_quest_entity(object, sections);
// } catch (e) { } catch (e) {
// logger.error(e); logger.error(e);
// } }
// } }
//
// for (const npc of quest.npcs.filter(npc => npc.area_id === variant.area.id)) { for (const npc of quest.npcs.val.filter(npc => npc.area_id === variant.area.id)) {
// try { try {
// this.set_section_on_quest_entity(npc, sections); this.set_section_on_quest_entity(npc, sections);
// } catch (e) { } catch (e) {
// logger.error(e); logger.error(e);
// } }
// } }
// } }
} else { } else {
logger.error("Couldn't parse quest file."); logger.error("Couldn't parse quest file.");
} }
// this.selected_entity = undefined;
this._current_quest.val = quest;
} }
private set_section_on_quest_entity = (entity: QuestEntityModel, sections: SectionModel[]) => {
const section = sections.find(s => s.id === entity.section_id.val);
if (section) {
entity.set_section(section);
} else {
logger.warn(`Section ${entity.section_id.val} not found.`);
}
};
} }
export const quest_editor_store = new QuestEditorStore(); export const quest_editor_store = new QuestEditorStore();

View File

@ -1,17 +1,18 @@
import { Vec3 } from "../../../core/data_formats/vector"; import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { QuestModel } from "../model/QuestModel";
import { Instruction, SegmentType } from "../scripting/instructions"; import { Instruction, SegmentType } from "../scripting/instructions";
import { ObjectType } from "../../core/data_formats/parsing/quest/object_types";
import { NpcType } from "../../core/data_formats/parsing/quest/npc_types";
import { Vec3 } from "../../core/data_formats/vector";
import { Opcode } from "../scripting/opcodes"; import { Opcode } from "../scripting/opcodes";
import { Episode } from "../../../core/data_formats/parsing/quest/Episode"; import { QuestObjectModel } from "../model/QuestObjectModel";
import { ObjectType } from "../../../core/data_formats/parsing/quest/object_types"; import { QuestNpcModel } from "../model/QuestNpcModel";
import { NpcType } from "../../../core/data_formats/parsing/quest/npc_types";
import { ObservableQuest } from "../domain/ObservableQuest";
import { ObservableQuestNpc, ObservableQuestObject } from "../domain/observable_quest_entities";
export function create_new_quest(episode: Episode): ObservableQuest { export function create_new_quest(episode: Episode): QuestModel {
if (episode === Episode.II) throw new Error("Episode II not yet supported."); if (episode === Episode.II) throw new Error("Episode II not yet supported.");
if (episode === Episode.IV) throw new Error("Episode IV not yet supported."); if (episode === Episode.IV) throw new Error("Episode IV not yet supported.");
return new ObservableQuest( return new QuestModel(
0, 0,
0, 0,
"Untitled", "Untitled",
@ -80,9 +81,9 @@ export function create_new_quest(episode: Episode): ObservableQuest {
); );
} }
function create_default_objects(): ObservableQuestObject[] { function create_default_objects(): QuestObjectModel[] {
return [ return [
new ObservableQuestObject( new QuestObjectModel(
ObjectType.MenuActivation, ObjectType.MenuActivation,
16384, 16384,
0, 0,
@ -101,7 +102,7 @@ function create_default_objects(): ObservableQuestObject[] {
]), ]),
[[2, 0, 0, 0, 0, 0], [0, 0]], [[2, 0, 0, 0, 0, 0], [0, 0]],
), ),
new ObservableQuestObject( new QuestObjectModel(
ObjectType.MenuActivation, ObjectType.MenuActivation,
16385, 16385,
0, 0,
@ -120,7 +121,7 @@ function create_default_objects(): ObservableQuestObject[] {
]), ]),
[[2, 0, 1, 0, 0, 0], [0, 0]], [[2, 0, 1, 0, 0, 0], [0, 0]],
), ),
new ObservableQuestObject( new QuestObjectModel(
ObjectType.MenuActivation, ObjectType.MenuActivation,
16386, 16386,
0, 0,
@ -139,7 +140,7 @@ function create_default_objects(): ObservableQuestObject[] {
]), ]),
[[2, 0, 2, 0, 0, 0], [0, 0]], [[2, 0, 2, 0, 0, 0], [0, 0]],
), ),
new ObservableQuestObject( new QuestObjectModel(
ObjectType.MenuActivation, ObjectType.MenuActivation,
16387, 16387,
0, 0,
@ -158,7 +159,7 @@ function create_default_objects(): ObservableQuestObject[] {
]), ]),
[[2, 0, 3, 0, 0, 0], [0, 0]], [[2, 0, 3, 0, 0, 0], [0, 0]],
), ),
new ObservableQuestObject( new QuestObjectModel(
ObjectType.PlayerSet, ObjectType.PlayerSet,
16388, 16388,
0, 0,
@ -177,7 +178,7 @@ function create_default_objects(): ObservableQuestObject[] {
]), ]),
[[2, 0, 4, 0, 0, 0], [0, 0]], [[2, 0, 4, 0, 0, 0], [0, 0]],
), ),
new ObservableQuestObject( new QuestObjectModel(
ObjectType.PlayerSet, ObjectType.PlayerSet,
16389, 16389,
0, 0,
@ -196,7 +197,7 @@ function create_default_objects(): ObservableQuestObject[] {
]), ]),
[[2, 0, 5, 0, 0, 0], [0, 0]], [[2, 0, 5, 0, 0, 0], [0, 0]],
), ),
new ObservableQuestObject( new QuestObjectModel(
ObjectType.PlayerSet, ObjectType.PlayerSet,
16390, 16390,
0, 0,
@ -215,7 +216,7 @@ function create_default_objects(): ObservableQuestObject[] {
]), ]),
[[2, 0, 6, 0, 0, 0], [0, 0]], [[2, 0, 6, 0, 0, 0], [0, 0]],
), ),
new ObservableQuestObject( new QuestObjectModel(
ObjectType.PlayerSet, ObjectType.PlayerSet,
16391, 16391,
0, 0,
@ -234,7 +235,7 @@ function create_default_objects(): ObservableQuestObject[] {
]), ]),
[[2, 0, 7, 0, 0, 0], [0, 0]], [[2, 0, 7, 0, 0, 0], [0, 0]],
), ),
new ObservableQuestObject( new QuestObjectModel(
ObjectType.MainRagolTeleporter, ObjectType.MainRagolTeleporter,
18264, 18264,
0, 0,
@ -253,7 +254,7 @@ function create_default_objects(): ObservableQuestObject[] {
]), ]),
[[0, 0, 87, 7, 0, 0], [0, 0]], [[0, 0, 87, 7, 0, 0], [0, 0]],
), ),
new ObservableQuestObject( new QuestObjectModel(
ObjectType.PrincipalWarp, ObjectType.PrincipalWarp,
16393, 16393,
0, 0,
@ -272,7 +273,7 @@ function create_default_objects(): ObservableQuestObject[] {
]), ]),
[[2, 0, 9, 0, 0, 0], [0, 0]], [[2, 0, 9, 0, 0, 0], [0, 0]],
), ),
new ObservableQuestObject( new QuestObjectModel(
ObjectType.MenuActivation, ObjectType.MenuActivation,
16394, 16394,
0, 0,
@ -291,7 +292,7 @@ function create_default_objects(): ObservableQuestObject[] {
]), ]),
[[2, 0, 10, 0, 0, 0], [1, 0]], [[2, 0, 10, 0, 0, 0], [1, 0]],
), ),
new ObservableQuestObject( new QuestObjectModel(
ObjectType.MenuActivation, ObjectType.MenuActivation,
16395, 16395,
0, 0,
@ -310,7 +311,7 @@ function create_default_objects(): ObservableQuestObject[] {
]), ]),
[[2, 0, 11, 0, 0, 0], [0, 0]], [[2, 0, 11, 0, 0, 0], [0, 0]],
), ),
new ObservableQuestObject( new QuestObjectModel(
ObjectType.PrincipalWarp, ObjectType.PrincipalWarp,
16396, 16396,
0, 0,
@ -329,7 +330,7 @@ function create_default_objects(): ObservableQuestObject[] {
]), ]),
[[2, 0, 12, 0, 0, 0], [0, 0]], [[2, 0, 12, 0, 0, 0], [0, 0]],
), ),
new ObservableQuestObject( new QuestObjectModel(
ObjectType.TelepipeLocation, ObjectType.TelepipeLocation,
16397, 16397,
0, 0,
@ -348,7 +349,7 @@ function create_default_objects(): ObservableQuestObject[] {
]), ]),
[[2, 0, 13, 0, 0, 0], [0, 0]], [[2, 0, 13, 0, 0, 0], [0, 0]],
), ),
new ObservableQuestObject( new QuestObjectModel(
ObjectType.TelepipeLocation, ObjectType.TelepipeLocation,
16398, 16398,
0, 0,
@ -367,7 +368,7 @@ function create_default_objects(): ObservableQuestObject[] {
]), ]),
[[2, 0, 14, 0, 0, 0], [0, 0]], [[2, 0, 14, 0, 0, 0], [0, 0]],
), ),
new ObservableQuestObject( new QuestObjectModel(
ObjectType.TelepipeLocation, ObjectType.TelepipeLocation,
16399, 16399,
0, 0,
@ -386,7 +387,7 @@ function create_default_objects(): ObservableQuestObject[] {
]), ]),
[[2, 0, 15, 0, 0, 0], [0, 0]], [[2, 0, 15, 0, 0, 0], [0, 0]],
), ),
new ObservableQuestObject( new QuestObjectModel(
ObjectType.TelepipeLocation, ObjectType.TelepipeLocation,
16400, 16400,
0, 0,
@ -405,7 +406,7 @@ function create_default_objects(): ObservableQuestObject[] {
]), ]),
[[2, 0, 16, 0, 0, 0], [0, 0]], [[2, 0, 16, 0, 0, 0], [0, 0]],
), ),
new ObservableQuestObject( new QuestObjectModel(
ObjectType.MedicalCenterDoor, ObjectType.MedicalCenterDoor,
16401, 16401,
0, 0,
@ -424,7 +425,7 @@ function create_default_objects(): ObservableQuestObject[] {
]), ]),
[[2, 0, 17, 0, 0, 0], [0, 0]], [[2, 0, 17, 0, 0, 0], [0, 0]],
), ),
new ObservableQuestObject( new QuestObjectModel(
ObjectType.ShopDoor, ObjectType.ShopDoor,
16402, 16402,
0, 0,
@ -443,7 +444,7 @@ function create_default_objects(): ObservableQuestObject[] {
]), ]),
[[2, 0, 18, 0, 0, 0], [0, 0]], [[2, 0, 18, 0, 0, 0], [0, 0]],
), ),
new ObservableQuestObject( new QuestObjectModel(
ObjectType.MenuActivation, ObjectType.MenuActivation,
16403, 16403,
0, 0,
@ -462,7 +463,7 @@ function create_default_objects(): ObservableQuestObject[] {
]), ]),
[[2, 0, 19, 0, 0, 0], [0, 0]], [[2, 0, 19, 0, 0, 0], [0, 0]],
), ),
new ObservableQuestObject( new QuestObjectModel(
ObjectType.HuntersGuildDoor, ObjectType.HuntersGuildDoor,
16404, 16404,
0, 0,
@ -481,7 +482,7 @@ function create_default_objects(): ObservableQuestObject[] {
]), ]),
[[2, 0, 20, 0, 0, 0], [0, 0]], [[2, 0, 20, 0, 0, 0], [0, 0]],
), ),
new ObservableQuestObject( new QuestObjectModel(
ObjectType.TeleporterDoor, ObjectType.TeleporterDoor,
16405, 16405,
0, 0,
@ -500,7 +501,7 @@ function create_default_objects(): ObservableQuestObject[] {
]), ]),
[[2, 0, 21, 0, 0, 0], [0, 0]], [[2, 0, 21, 0, 0, 0], [0, 0]],
), ),
new ObservableQuestObject( new QuestObjectModel(
ObjectType.PlayerSet, ObjectType.PlayerSet,
16406, 16406,
0, 0,
@ -519,7 +520,7 @@ function create_default_objects(): ObservableQuestObject[] {
]), ]),
[[2, 0, 22, 0, 0, 0], [0, 0]], [[2, 0, 22, 0, 0, 0], [0, 0]],
), ),
new ObservableQuestObject( new QuestObjectModel(
ObjectType.PlayerSet, ObjectType.PlayerSet,
16407, 16407,
0, 0,
@ -538,7 +539,7 @@ function create_default_objects(): ObservableQuestObject[] {
]), ]),
[[2, 0, 23, 0, 0, 0], [0, 0]], [[2, 0, 23, 0, 0, 0], [0, 0]],
), ),
new ObservableQuestObject( new QuestObjectModel(
ObjectType.PlayerSet, ObjectType.PlayerSet,
16408, 16408,
0, 0,
@ -557,7 +558,7 @@ function create_default_objects(): ObservableQuestObject[] {
]), ]),
[[2, 0, 24, 0, 0, 0], [0, 0]], [[2, 0, 24, 0, 0, 0], [0, 0]],
), ),
new ObservableQuestObject( new QuestObjectModel(
ObjectType.PlayerSet, ObjectType.PlayerSet,
16409, 16409,
0, 0,
@ -579,9 +580,9 @@ function create_default_objects(): ObservableQuestObject[] {
]; ];
} }
function create_default_npcs(): ObservableQuestNpc[] { function create_default_npcs(): QuestNpcModel[] {
return [ return [
new ObservableQuestNpc( new QuestNpcModel(
NpcType.GuildLady, NpcType.GuildLady,
29, 29,
1011.0010986328125, 1011.0010986328125,
@ -594,7 +595,7 @@ function create_default_npcs(): ObservableQuestNpc[] {
new Vec3(0, 0, 0), new Vec3(0, 0, 0),
[[0, 0, 7, 86, 0, 0, 0, 0, 23, 87], [0, 0, 0, 0, 0, 0], [128, 238, 223, 176]], [[0, 0, 7, 86, 0, 0, 0, 0, 23, 87], [0, 0, 0, 0, 0, 0], [128, 238, 223, 176]],
), ),
new ObservableQuestNpc( new QuestNpcModel(
NpcType.FemaleFat, NpcType.FemaleFat,
4, 4,
1016.0010986328125, 1016.0010986328125,
@ -607,7 +608,7 @@ function create_default_npcs(): ObservableQuestNpc[] {
new Vec3(24.000009536743164, 0, 0), new Vec3(24.000009536743164, 0, 0),
[[0, 0, 7, 88, 0, 0, 0, 0, 23, 89], [0, 0, 0, 0, 0, 0], [128, 238, 232, 48]], [[0, 0, 7, 88, 0, 0, 0, 0, 23, 89], [0, 0, 0, 0, 0, 0], [128, 238, 232, 48]],
), ),
new ObservableQuestNpc( new QuestNpcModel(
NpcType.MaleDwarf, NpcType.MaleDwarf,
10, 10,
1015.0010986328125, 1015.0010986328125,
@ -620,7 +621,7 @@ function create_default_npcs(): ObservableQuestNpc[] {
new Vec3(30.000009536743164, 0, 0), new Vec3(30.000009536743164, 0, 0),
[[0, 0, 7, 89, 0, 0, 0, 0, 23, 90], [0, 0, 0, 0, 0, 0], [128, 238, 236, 176]], [[0, 0, 7, 89, 0, 0, 0, 0, 23, 90], [0, 0, 0, 0, 0, 0], [128, 238, 236, 176]],
), ),
new ObservableQuestNpc( new QuestNpcModel(
NpcType.RedSoldier, NpcType.RedSoldier,
26, 26,
1020.0010986328125, 1020.0010986328125,
@ -633,7 +634,7 @@ function create_default_npcs(): ObservableQuestNpc[] {
new Vec3(0, 0, 0), new Vec3(0, 0, 0),
[[0, 0, 7, 90, 0, 0, 0, 0, 23, 91], [0, 0, 0, 0, 0, 0], [128, 238, 241, 48]], [[0, 0, 7, 90, 0, 0, 0, 0, 23, 91], [0, 0, 0, 0, 0, 0], [128, 238, 241, 48]],
), ),
new ObservableQuestNpc( new QuestNpcModel(
NpcType.BlueSoldier, NpcType.BlueSoldier,
25, 25,
1019.0010986328125, 1019.0010986328125,
@ -646,7 +647,7 @@ function create_default_npcs(): ObservableQuestNpc[] {
new Vec3(0, 0, 0), new Vec3(0, 0, 0),
[[0, 0, 7, 91, 0, 0, 0, 0, 23, 92], [0, 0, 0, 0, 0, 0], [128, 238, 245, 176]], [[0, 0, 7, 91, 0, 0, 0, 0, 23, 92], [0, 0, 0, 0, 0, 0], [128, 238, 245, 176]],
), ),
new ObservableQuestNpc( new QuestNpcModel(
NpcType.FemaleMacho, NpcType.FemaleMacho,
5, 5,
1014.0010986328125, 1014.0010986328125,
@ -659,7 +660,7 @@ function create_default_npcs(): ObservableQuestNpc[] {
new Vec3(26.000009536743164, 0, 0), new Vec3(26.000009536743164, 0, 0),
[[0, 0, 7, 92, 0, 0, 0, 0, 23, 93], [0, 0, 0, 0, 0, 0], [128, 238, 250, 48]], [[0, 0, 7, 92, 0, 0, 0, 0, 23, 93], [0, 0, 0, 0, 0, 0], [128, 238, 250, 48]],
), ),
new ObservableQuestNpc( new QuestNpcModel(
NpcType.Scientist, NpcType.Scientist,
30, 30,
1013.0010986328125, 1013.0010986328125,
@ -672,7 +673,7 @@ function create_default_npcs(): ObservableQuestNpc[] {
new Vec3(30.000009536743164, 0, 0), new Vec3(30.000009536743164, 0, 0),
[[0, 0, 7, 93, 0, 0, 0, 0, 23, 94], [0, 0, 0, 0, 0, 0], [128, 238, 254, 176]], [[0, 0, 7, 93, 0, 0, 0, 0, 23, 94], [0, 0, 0, 0, 0, 0], [128, 238, 254, 176]],
), ),
new ObservableQuestNpc( new QuestNpcModel(
NpcType.MaleOld, NpcType.MaleOld,
13, 13,
1012.0010986328125, 1012.0010986328125,
@ -685,7 +686,7 @@ function create_default_npcs(): ObservableQuestNpc[] {
new Vec3(30.000011444091797, 0, 0), new Vec3(30.000011444091797, 0, 0),
[[0, 0, 7, 94, 0, 0, 0, 0, 23, 95], [0, 0, 0, 0, 0, 0], [128, 239, 3, 48]], [[0, 0, 7, 94, 0, 0, 0, 0, 23, 95], [0, 0, 0, 0, 0, 0], [128, 239, 3, 48]],
), ),
new ObservableQuestNpc( new QuestNpcModel(
NpcType.GuildLady, NpcType.GuildLady,
29, 29,
1010.0010986328125, 1010.0010986328125,
@ -698,7 +699,7 @@ function create_default_npcs(): ObservableQuestNpc[] {
new Vec3(0, 0, 0), new Vec3(0, 0, 0),
[[0, 0, 7, 95, 0, 0, 0, 0, 23, 106], [0, 0, 0, 0, 0, 0], [128, 239, 100, 192]], [[0, 0, 7, 95, 0, 0, 0, 0, 23, 106], [0, 0, 0, 0, 0, 0], [128, 239, 100, 192]],
), ),
new ObservableQuestNpc( new QuestNpcModel(
NpcType.Tekker, NpcType.Tekker,
28, 28,
1009, 1009,
@ -711,7 +712,7 @@ function create_default_npcs(): ObservableQuestNpc[] {
new Vec3(0, 0, 0), new Vec3(0, 0, 0),
[[0, 0, 7, 97, 0, 0, 0, 0, 23, 98], [0, 0, 0, 0, 0, 0], [128, 239, 16, 176]], [[0, 0, 7, 97, 0, 0, 0, 0, 23, 98], [0, 0, 0, 0, 0, 0], [128, 239, 16, 176]],
), ),
new ObservableQuestNpc( new QuestNpcModel(
NpcType.MaleMacho, NpcType.MaleMacho,
12, 12,
1006, 1006,
@ -724,7 +725,7 @@ function create_default_npcs(): ObservableQuestNpc[] {
new Vec3(0, 0, 0), new Vec3(0, 0, 0),
[[0, 0, 7, 98, 0, 0, 0, 0, 23, 99], [0, 0, 0, 0, 0, 0], [128, 239, 21, 48]], [[0, 0, 7, 98, 0, 0, 0, 0, 23, 99], [0, 0, 0, 0, 0, 0], [128, 239, 21, 48]],
), ),
new ObservableQuestNpc( new QuestNpcModel(
NpcType.FemaleMacho, NpcType.FemaleMacho,
5, 5,
1008, 1008,
@ -737,7 +738,7 @@ function create_default_npcs(): ObservableQuestNpc[] {
new Vec3(0, 0, 0), new Vec3(0, 0, 0),
[[0, 0, 7, 99, 0, 0, 0, 0, 23, 100], [0, 0, 0, 0, 0, 0], [128, 239, 25, 176]], [[0, 0, 7, 99, 0, 0, 0, 0, 23, 100], [0, 0, 0, 0, 0, 0], [128, 239, 25, 176]],
), ),
new ObservableQuestNpc( new QuestNpcModel(
NpcType.MaleFat, NpcType.MaleFat,
11, 11,
1007.0010986328125, 1007.0010986328125,
@ -750,7 +751,7 @@ function create_default_npcs(): ObservableQuestNpc[] {
new Vec3(0, 0, 0), new Vec3(0, 0, 0),
[[0, 0, 7, 100, 0, 0, 0, 0, 23, 101], [0, 0, 0, 0, 0, 0], [128, 239, 30, 48]], [[0, 0, 7, 100, 0, 0, 0, 0, 23, 101], [0, 0, 0, 0, 0, 0], [128, 239, 30, 48]],
), ),
new ObservableQuestNpc( new QuestNpcModel(
NpcType.FemaleTall, NpcType.FemaleTall,
7, 7,
1021.0010986328125, 1021.0010986328125,
@ -763,7 +764,7 @@ function create_default_npcs(): ObservableQuestNpc[] {
new Vec3(22.000009536743164, 0, 0), new Vec3(22.000009536743164, 0, 0),
[[0, 0, 7, 101, 0, 0, 0, 0, 23, 102], [0, 0, 0, 0, 0, 0], [128, 239, 34, 176]], [[0, 0, 7, 101, 0, 0, 0, 0, 23, 102], [0, 0, 0, 0, 0, 0], [128, 239, 34, 176]],
), ),
new ObservableQuestNpc( new QuestNpcModel(
NpcType.Nurse, NpcType.Nurse,
31, 31,
1017, 1017,
@ -776,7 +777,7 @@ function create_default_npcs(): ObservableQuestNpc[] {
new Vec3(0, 0, 0), new Vec3(0, 0, 0),
[[0, 0, 7, 102, 0, 0, 0, 0, 23, 103], [0, 0, 0, 0, 0, 0], [128, 239, 39, 48]], [[0, 0, 7, 102, 0, 0, 0, 0, 23, 103], [0, 0, 0, 0, 0, 0], [128, 239, 39, 48]],
), ),
new ObservableQuestNpc( new QuestNpcModel(
NpcType.Nurse, NpcType.Nurse,
31, 31,
1018.0010986328125, 1018.0010986328125,

View File

@ -1,4 +1,4 @@
.viewer_ModelView_container { .viewer_Model3DView_container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
} }

View File

@ -1,11 +1,11 @@
import { create_element } from "../../core/gui/dom"; import { create_element } from "../../core/gui/dom";
import { ResizableView } from "../../core/gui/ResizableView"; import { ResizableView } from "../../core/gui/ResizableView";
import { ToolBar } from "../../core/gui/ToolBar"; import { ToolBar } from "../../core/gui/ToolBar";
import "./ModelView.css"; import "./Model3DView.css";
import { model_store } from "../stores/ModelStore"; import { model_store } from "../stores/Model3DStore";
import { WritableProperty } from "../../core/observable/WritableProperty"; import { WritableProperty } from "../../core/observable/WritableProperty";
import { RendererView } from "../../core/gui/RendererView"; import { RendererView } from "../../core/gui/RendererView";
import { ModelRenderer } from "../rendering/ModelRenderer"; import { Model3DRenderer } from "../rendering/Model3DRenderer";
import { View } from "../../core/gui/View"; import { View } from "../../core/gui/View";
import { FileButton } from "../../core/gui/FileButton"; import { FileButton } from "../../core/gui/FileButton";
import { CheckBox } from "../../core/gui/CheckBox"; import { CheckBox } from "../../core/gui/CheckBox";
@ -15,20 +15,20 @@ import { gui_store, GuiTool } from "../../core/stores/GuiStore";
import { PSO_FRAME_RATE } from "../../core/rendering/conversion/ninja_animation"; import { PSO_FRAME_RATE } from "../../core/rendering/conversion/ninja_animation";
const MODEL_LIST_WIDTH = 100; const MODEL_LIST_WIDTH = 100;
const ANIMATION_LIST_WIDTH = 130; const ANIMATION_LIST_WIDTH = 140;
export class ModelView extends ResizableView { export class Model3DView extends ResizableView {
readonly element = create_element("div", { class: "viewer_ModelView" }); readonly element = create_element("div", { class: "viewer_Model3DView" });
private tool_bar_view = this.disposable(new ToolBarView()); private tool_bar_view = this.disposable(new ToolBarView());
private container_element = create_element("div", { class: "viewer_ModelView_container" }); private container_element = create_element("div", { class: "viewer_Model3DView_container" });
private model_list_view = this.disposable( private model_list_view = this.disposable(
new ModelSelectListView(model_store.models, model_store.current_model), new ModelSelectListView(model_store.models, model_store.current_model),
); );
private animation_list_view = this.disposable( private animation_list_view = this.disposable(
new ModelSelectListView(model_store.animations, model_store.current_animation), new ModelSelectListView(model_store.animations, model_store.current_animation),
); );
private renderer_view = this.disposable(new RendererView(new ModelRenderer())); private renderer_view = this.disposable(new RendererView(new Model3DRenderer()));
constructor() { constructor() {
super(); super();
@ -48,7 +48,7 @@ export class ModelView extends ResizableView {
this.renderer_view.start_rendering(); this.renderer_view.start_rendering();
this.disposable( this.disposable(
gui_store.tool.observe(tool => { gui_store.tool.observe(({ value: tool }) => {
if (tool === GuiTool.Viewer) { if (tool === GuiTool.Viewer) {
this.renderer_view.start_rendering(); this.renderer_view.start_rendering();
} else { } else {
@ -116,30 +116,30 @@ class ToolBarView extends View {
// Always-enabled controls. // Always-enabled controls.
this.disposables( this.disposables(
this.open_file_button.files.observe(files => { this.open_file_button.files.observe(({ value: files }) => {
if (files.length) model_store.load_file(files[0]); if (files.length) model_store.load_file(files[0]);
}), }),
model_store.show_skeleton.bind(this.skeleton_checkbox.checked), model_store.show_skeleton.bind_to(this.skeleton_checkbox.checked),
); );
// Controls that are only enabled when an animation is selected. // Controls that are only enabled when an animation is selected.
const enabled = model_store.current_nj_motion.map(njm => njm != undefined); const enabled = model_store.current_nj_motion.map(njm => njm != undefined);
this.disposables( this.disposables(
this.play_animation_checkbox.enabled.bind(enabled), this.play_animation_checkbox.enabled.bind_to(enabled),
model_store.animation_playing.bind_bi(this.play_animation_checkbox.checked), model_store.animation_playing.bind_bi(this.play_animation_checkbox.checked),
this.animation_frame_rate_input.enabled.bind(enabled), this.animation_frame_rate_input.enabled.bind_to(enabled),
model_store.animation_frame_rate.bind(this.animation_frame_rate_input.value), model_store.animation_frame_rate.bind_to(this.animation_frame_rate_input.value),
this.animation_frame_input.enabled.bind(enabled), this.animation_frame_input.enabled.bind_to(enabled),
model_store.animation_frame.bind(this.animation_frame_input.value), model_store.animation_frame.bind_to(this.animation_frame_input.value),
this.animation_frame_input.value.bind( this.animation_frame_input.value.bind_to(
model_store.animation_frame.map(v => Math.round(v)), model_store.animation_frame.map(v => Math.round(v)),
), ),
this.animation_frame_count_label.enabled.bind(enabled), this.animation_frame_count_label.enabled.bind_to(enabled),
); );
} }
} }
@ -172,7 +172,7 @@ class ModelSelectListView<T extends { name: string }> extends ResizableView {
}); });
this.disposable( this.disposable(
selected.observe(model => { selected.observe(({ value: model }) => {
if (this.selected_element) { if (this.selected_element) {
this.selected_element.classList.remove("active"); this.selected_element.classList.remove("active");
this.selected_element = undefined; this.selected_element = undefined;

View File

@ -22,7 +22,7 @@ export class TextureView extends ResizableView {
this.element.append(this.tool_bar.element, this.renderer_view.element); this.element.append(this.tool_bar.element, this.renderer_view.element);
this.disposable( this.disposable(
this.open_file_button.files.observe(files => { this.open_file_button.files.observe(({ value: files }) => {
if (files.length) texture_store.load_file(files[0]); if (files.length) texture_store.load_file(files[0]);
}), }),
); );
@ -30,7 +30,7 @@ export class TextureView extends ResizableView {
this.renderer_view.start_rendering(); this.renderer_view.start_rendering();
this.disposable( this.disposable(
gui_store.tool.observe(tool => { gui_store.tool.observe(({ value: tool }) => {
if (tool === GuiTool.Viewer) { if (tool === GuiTool.Viewer) {
this.renderer_view.start_rendering(); this.renderer_view.start_rendering();
} else { } else {

Some files were not shown because too many files have changed in this diff Show More