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

View File

@ -28,8 +28,8 @@ export class NavigationView extends View {
this.element.append(button.element);
}
this.tool_changed(gui_store.tool.val);
this.disposable(gui_store.tool.observe(this.tool_changed));
this.mark_tool_button(gui_store.tool.val);
this.disposable(gui_store.tool.observe(({ value }) => this.mark_tool_button(value)));
}
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);
if (button) button.checked = true;
};

View File

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

View File

@ -4,8 +4,8 @@ import {
InstructionSegment,
Segment,
SegmentType,
} from "../../../../old/quest_editor/scripting/instructions";
import { Opcode } from "../../../../old/quest_editor/scripting/opcodes";
} from "../../../../quest_editor/scripting/instructions";
import { Opcode } from "../../../../quest_editor/scripting/opcodes";
import { prs_compress } from "../../compression/prs/compress";
import { prs_decompress } from "../../compression/prs/decompress";
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.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.Nurse2, "Nurse (Ep. II);", "Nurse", "Nurse", 2, false);
define_npc_type_data(NpcType.Nurse2, "Nurse (Ep. II)", "Nurse", "Nurse", 2, false);
//
// Enemy NPCs
@ -450,17 +450,17 @@ define_npc_type_data(NpcType.DarkFalz, "Dark Falz", "Dark Falz", "Dark Falz", 1,
define_npc_type_data(
NpcType.Hildebear2,
"Hildebear (Ep. II);",
"Hildebear (Ep. II)",
"Hildebear",
"Hildelt",
2,
true,
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(
NpcType.RagRappy2,
"Rag Rappy (Ep. II);",
"Rag Rappy (Ep. II)",
"Rag Rappy",
"El Rappy",
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.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.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.PoisonLily2,
"Poison Lily (Ep. II);",
"Poison Lily (Ep. II)",
"Poison Lily",
"Ob Lily",
2,
true,
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(
NpcType.GrassAssassin2,
"Grass Assassin (Ep. II);",
"Grass Assassin (Ep. II)",
"Grass Assassin",
"Crimson Assassin",
2,
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(
NpcType.LaDimenian2,
"La Dimenian (Ep. II);",
"La Dimenian (Ep. II)",
"La Dimenian",
"Merlan",
2,
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(
NpcType.DarkBelra2,
"Dark Belra (Ep. II);",
"Dark Belra (Ep. II)",
"Dark Belra",
"Indi Belra",
2,
@ -515,7 +515,7 @@ define_npc_type_data(NpcType.BarbaRay, "Barba Ray", "Barba Ray", "Barba Ray", 2,
define_npc_type_data(
NpcType.SavageWolf2,
"Savage Wolf (Ep. II);",
"Savage Wolf (Ep. II)",
"Savage Wolf",
"Gulgus",
2,
@ -523,23 +523,23 @@ define_npc_type_data(
);
define_npc_type_data(
NpcType.BarbarousWolf2,
"Barbarous Wolf (Ep. II);",
"Barbarous Wolf (Ep. II)",
"Barbarous Wolf",
"Gulgus-Gue",
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.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.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.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.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.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.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.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.ChaosSorcerer2,
"Chaos Sorcerer (Ep. II);",
"Chaos Sorcerer (Ep. II)",
"Chaos Sorcerer",
"Gran Sorcerer",
2,

View File

@ -15,8 +15,8 @@ export class Button extends Control {
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.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;

View File

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

View File

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

View File

@ -21,12 +21,12 @@ export class Label extends View {
this.element.append(text);
} else {
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.enabled.observe(enabled => {
if (enabled) {
this.enabled.observe(({ value }) => {
if (value) {
this.element.classList.remove("disabled");
} else {
this.element.classList.add("disabled");

View File

@ -15,8 +15,8 @@ export class LazyView extends ResizableView {
this.visible.val = false;
this.disposables(
this.visible.observe(visible => {
if (visible && !this.initialized) {
this.visible.observe(({ value }) => {
if (value && !this.initialized) {
this.initialized = true;
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.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);
}

View File

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

View File

@ -61,5 +61,5 @@ export function bind_hidden(element: HTMLElement, observable: Observable<boolean
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 Logger from "js-logger";
@ -11,13 +11,22 @@ export abstract class AbstractMinimalProperty<T> implements Property<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)) {
this.observers.push(observer);
}
if (options.call_now) {
this.call_observer(observer, this.val);
}
return {
dispose: () => {
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>;
protected emit(): void {
protected emit(old_value: T): void {
for (const observer of this.observers) {
try {
observer(this.val);
} catch (e) {
logger.error("Observer threw error.", e);
}
this.call_observer(observer, old_value);
}
}
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 { Property } from "./Property";
import { PropertyChangeEvent, Property } from "./Property";
import { Disposer } from "./Disposer";
import { AbstractMinimalProperty } from "./AbstractMinimalProperty";
import { FlatMappedProperty } from "./FlatMappedProperty";
@ -15,6 +15,10 @@ export class DependentProperty<T> extends AbstractMinimalProperty<T> implements
private _val?: T;
get val(): T {
return this.get_val();
}
get_val(): T {
if (this.dependency_disposables.length) {
return this._val as T;
} else {
@ -28,7 +32,7 @@ export class DependentProperty<T> extends AbstractMinimalProperty<T> implements
super();
}
observe(observer: (event: T) => void): Disposable {
observe(observer: (event: PropertyChangeEvent<T>) => void): Disposable {
const super_disposable = super.observe(observer);
if (this.dependency_disposables.length === 0) {
@ -37,8 +41,9 @@ export class DependentProperty<T> extends AbstractMinimalProperty<T> implements
this.dependency_disposables.add_all(
...this.dependencies.map(dependency =>
dependency.observe(() => {
const old_value = this._val!;
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();
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 {
/**
* Releases any held resources.
*/
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");
/**
* Container for disposables.
*/
export class Disposer implements Disposable {
private readonly disposables: Disposable[] = [];
private disposed = false;
/**
* The amount of disposables contained in this disposer.
*/
get length(): number {
return this.disposables.length;
}
/**
* Add a single disposable and return the given disposable.
*/
add<T extends Disposable>(disposable: T): T {
this.check_not_disposed();
this.disposables.push(disposable);
return disposable;
}
/**
* Add 0 or more disposables.
*/
add_all(...disposable: Disposable[]): this {
this.check_not_disposed();
this.disposables.push(...disposable);
return this;
}
dispose(): void {
/**
* Disposes all held disposables.
*/
dispose_all(): void {
for (const disposable of this.disposables.splice(0, this.disposables.length)) {
try {
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> {
emit(event: E): void;
export interface Emitter<T> extends Observable<T> {
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 { AbstractMinimalProperty } from "./AbstractMinimalProperty";
import { DependentProperty } from "./DependentProperty";
@ -12,6 +12,10 @@ export class FlatMappedProperty<T, U> extends AbstractMinimalProperty<U> impleme
readonly is_property = true;
get val(): U {
return this.get_val();
}
get_val(): U {
return this.computed_property
? this.computed_property.val
: this.f(this.dependency.val).val;
@ -25,13 +29,14 @@ export class FlatMappedProperty<T, U> extends AbstractMinimalProperty<U> impleme
super();
}
observe(observer: (value: U) => void): Disposable {
observe(observer: (event: PropertyChangeEvent<U>) => void): Disposable {
const super_disposable = super.observe(observer);
if (this.dependency_disposable == undefined) {
this.dependency_disposable = this.dependency.observe(() => {
const old_value = this.val;
this.compute_and_observe();
this.emit();
this.emit(old_value);
});
this.compute_and_observe();
@ -62,9 +67,15 @@ export class FlatMappedProperty<T, U> extends AbstractMinimalProperty<U> impleme
private compute_and_observe(): void {
if (this.computed_disposable) this.computed_disposable.dispose();
this.computed_property = this.f(this.dependency.val);
let old_value = this.computed_property.val;
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";
export interface Observable<E> {
observe(observer: (event: E) => void): Disposable;
export interface ChangeEvent<T> {
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> {
readonly is_property: true;
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>;
flat_map<U>(f: (element: T) => Property<U>): Property<U>;

View File

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

View File

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

View File

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

View File

@ -3,10 +3,10 @@ import { Observable } from "./Observable";
import { Disposable } from "./Disposable";
export interface WritableProperty<T> extends Property<T> {
readonly is_writable_property: true;
val: T;
set_val(value: T, options?: { silent?: boolean }): 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.
*/
bind(observable: Observable<T>): Disposable;
bind_to(observable: Observable<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 {
protected _debug = false;
private _debug = false;
get debug(): boolean {
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 {
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)}`;
});

View File

@ -1,7 +1,5 @@
export class Action {
constructor(
readonly description: string,
readonly undo: () => void,
readonly redo: () => void,
) {}
export interface Action {
readonly description: string;
readonly undo: () => 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.
*/
export class SimpleUndo implements Undo {
private readonly _action: Action;
readonly action: Property<Action>;
private readonly action: Action;
constructor(description: string, undo: () => void, redo: () => void) {
this._action = new Action(description, undo, redo);
this.action = property(this._action);
this.action = { description, undo, redo };
}
make_current(): void {
@ -32,16 +30,16 @@ export class SimpleUndo implements Undo {
readonly can_redo = property(false);
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 =>
can_redo ? this._action : undefined,
can_redo ? this.action : undefined,
);
undo(): boolean {
if (this.can_undo) {
this._action.undo();
this.action.undo();
return true;
} else {
return false;
@ -50,7 +48,7 @@ export class SimpleUndo implements Undo {
redo(): boolean {
if (this.can_redo) {
this._action.redo();
this.action.redo();
return true;
} else {
return false;

View File

@ -1,4 +1,3 @@
import { Action } from "./Action";
import { UndoStack } from "./UndoStack";
test("simple properties and invariants", () => {
@ -7,9 +6,9 @@ test("simple properties and invariants", () => {
expect(stack.can_undo.val).toBe(false);
expect(stack.can_redo.val).toBe(false);
stack.push(new Action("", () => {}, () => {}));
stack.push(new Action("", () => {}, () => {}));
stack.push(new Action("", () => {}, () => {}));
stack.push({ description: "", undo: () => {}, redo: () => {} });
stack.push({ description: "", undo: () => {}, redo: () => {} });
stack.push({ description: "", undo: () => {}, redo: () => {} });
expect(stack.can_undo.val).toBe(true);
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.
let value = 13;
stack.push(new Action("X", () => (value = 3), () => (value = 7)));
stack.push(new Action("Y", () => (value = 7), () => (value = 13)));
stack.push({ description: "X", undo: () => (value = 3), redo: () => (value = 7) });
stack.push({ description: "Y", undo: () => (value = 7), redo: () => (value = 13) });
expect(stack.undo()).toBe(true);
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.
let value = 13;
stack.push(new Action("X", () => (value = 3), () => (value = 7)));
stack.push(new Action("Y", () => (value = 7), () => (value = 13)));
stack.push({ description: "X", undo: () => (value = 3), redo: () => (value = 7) });
stack.push({ description: "Y", undo: () => (value = 7), redo: () => (value = 13) });
stack.undo();
stack.undo();

View File

@ -4,6 +4,9 @@ import { Action } from "./Action";
import { array_property, map, property } from "../observable";
import { NOOP_UNDO } from "./noop_undo";
import { undo_manager } from "./UndoManager";
import Logger = require("js-logger");
const logger = Logger.get("core/undo/UndoStack");
/**
* Full-fledged linear undo/redo implementation.
@ -16,16 +19,6 @@ export class UndoStack implements Undo {
*/
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_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;
});
push_action(description: string, undo: () => void, redo: () => void): void {
this.push(new Action(description, undo, redo));
private undoing_or_redoing = false;
make_current(): void {
undo_manager.current.val = this;
}
push(action: Action): void {
this.stack.splice(this.index.val, this.stack.length.val - this.index.val, action);
this.index.update(i => i + 1);
ensure_not_current(): void {
if (undo_manager.current.val === this) {
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 {
if (this.can_undo) {
this.index.update(i => i - 1);
this.stack.get(this.index.val).undo();
if (this.can_undo.val && !this.undoing_or_redoing) {
try {
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;
} else {
return false;
@ -66,9 +79,17 @@ export class UndoStack implements Undo {
}
redo(): boolean {
if (this.can_redo) {
this.stack.get(this.index.val).redo();
this.index.update(i => i + 1);
if (this.can_redo.val && !this.undoing_or_redoing) {
try {
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;
} else {
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 { Vec3 } from "../../../core/data_formats/vector";
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";
/**
* 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;
@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;
}
@observable.ref section?: Section;
@observable.ref section?: SectionModel;
/**
* Section-relative position
@ -90,13 +90,13 @@ export abstract class ObservableQuestEntity<Type extends EntityType = EntityType
}
@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.section = section;
}
}
export class ObservableQuestObject extends ObservableQuestEntity<ObjectType> {
export class ObservableQuestObject extends QuestEntityModel<ObjectType> {
readonly 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 npc_id: 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 { SimpleUndo, UndoStack } from "../../core/undo";
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 { entity_data } from "../../../core/data_formats/parsing/quest/entities";
import { ObservableQuest } from "../domain/ObservableQuest";
import { ObservableArea } from "../domain/ObservableArea";
import { Section } from "../domain/Section";
import { ObservableQuest } from "../domain/QuestModel";
import { AreaModel } from "../../../quest_editor/model/AreaModel";
import { SectionModel } from "../../../quest_editor/model/SectionModel";
import {
ObservableQuestEntity,
QuestEntityModel,
ObservableQuestNpc,
ObservableQuestObject,
} from "../domain/observable_quest_entities";
@ -29,9 +29,9 @@ class QuestEditorStore {
@observable current_quest_filename?: string;
@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_open: boolean = false;
@ -58,7 +58,7 @@ class QuestEditorStore {
};
@action
set_selected_entity = (entity?: ObservableQuestEntity) => {
set_selected_entity = (entity?: QuestEntityModel) => {
if (entity) {
this.set_current_area_id(entity.area_id);
}
@ -299,7 +299,7 @@ class QuestEditorStore {
@action
push_entity_move_action = (
entity: ObservableQuestEntity,
entity: QuestEntityModel,
old_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);
if (section) {

View File

@ -2,7 +2,7 @@ import { autorun } from "mobx";
import { editor, languages, MarkerSeverity, MarkerTag, Position } from "monaco-editor";
import React, { Component, createRef, ReactNode } from "react";
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 { Action } from "../../core/undo";
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 styles from "./EntityInfoComponent.css";
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
export class EntityInfoComponent extends Component {
@ -57,7 +57,7 @@ export class EntityInfoComponent extends Component {
}
type CoordProps = {
entity: ObservableQuestEntity;
entity: QuestEntityModel;
position_type: "position" | "world_position";
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 { 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 {
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 episode_element: HTMLElement;
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(
new TextArea("", {
max_length: 128,
@ -62,17 +66,28 @@ export class QuesInfoView extends ResizableView {
this.element.append(this.table_element, this.no_quest_element);
this.disposables(
quest.observe(q => {
this.quest_disposer.dispose();
quest.observe(({ value: q }) => {
this.quest_disposer.dispose_all();
this.episode_element.textContent = q ? Episode[q.episode] : "";
if (q) {
this.quest_disposer.add_all(
this.id_input.value.bind_bi(q.id),
this.name_input.value.bind_bi(q.name),
this.short_description_input.value.bind_bi(q.short_description),
this.long_description_input.value.bind_bi(q.long_description),
this.id_input.value.bind_to(q.id),
this.id_input.value.observe(quest_editor_store.push_edit_id_action),
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 { quest_editor_ui_persister } from "../persistence/QuestEditorUiPersister";
import { QuesInfoView } from "./QuesInfoView";
import Logger = require("js-logger");
import "golden-layout/src/css/goldenlayout-base.css";
import "../../core/gui/golden_layout_theme.css";
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");
@ -15,7 +17,7 @@ const logger = Logger.get("quest_editor/gui/QuestEditorView");
const VIEW_TO_NAME = new Map([
[QuesInfoView, "quest_info"],
[NpcCountsView, "npc_counts"],
// [QuestRendererView, "quest_renderer"],
[QuestRendererView, "quest_renderer"],
// [AssemblyEditorView, "assembly_editor"],
// [EntityInfoView, "entity_info"],
// [AddObjectView, "add_object"],
@ -59,24 +61,24 @@ const DEFAULT_LAYOUT_CONTENT: ItemConfigType[] = [
},
],
},
// {
// type: "stack",
// width: 9,
// content: [
// {
// title: "3D View",
// type: "component",
// componentName: Component.QuestRenderer,
// isClosable: false,
// },
// {
// title: "Script",
// type: "component",
// componentName: Component.AssemblyEditor,
// isClosable: false,
// },
// ],
// },
{
type: "stack",
width: 9,
content: [
{
title: "3D View",
type: "component",
componentName: VIEW_TO_NAME.get(QuestRendererView),
isClosable: false,
},
// {
// title: "Script",
// type: "component",
// componentName: Component.AssemblyEditor,
// isClosable: false,
// },
],
},
// {
// title: "Entity",
// type: "component",
@ -150,7 +152,10 @@ export class QuestEditorView extends ResizableView {
const view = new view_ctor();
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);
@ -166,13 +171,13 @@ export class QuestEditorView extends ResizableView {
layout.on("stackCreated", (stack: ContentItem) => {
stack.on("activeContentItemChanged", (item: ContentItem) => {
// if ("component" in item.config) {
// if (item.config.component === CMP_TO_NAME.get(AssemblyEditorComponent)) {
// quest_editor_store.script_undo.make_current();
// } else {
// quest_editor_store.undo.make_current();
// }
// }
if ("componentName" in item.config) {
// if (item.config.componentName === VIEW_TO_NAME.get(AssemblyEditorView)) {
// quest_editor_store.script_undo.make_current();
// } else {
// 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();
this.disposables(
this.open_file_button.files.observe(files => {
this.open_file_button.files.observe(({ value: files }) => {
if (files.length) {
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),
),
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 { Endianness } from "../../../core/data_formats/Endianness";
import { ArrayBufferCursor } from "../../../core/data_formats/cursor/ArrayBufferCursor";
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 { Endianness } from "../../core/data_formats/Endianness";
import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor";
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 { 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 {
area_collision_geometry_to_object_3d,
area_geometry_to_sections_and_object_3d,
} 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<
string,
{ geometry: Promise<Object3D>; sections: Promise<Section[]> }
{ geometry: Promise<Object3D>; sections: Promise<SectionModel[]> }
>();
const collision_geometry_cache = new LoadingCache<string, Promise<Object3D>>();
@ -22,7 +22,7 @@ export async function load_area_sections(
episode: Episode,
area_id: number,
area_variant: number,
): Promise<Section[]> {
): Promise<SectionModel[]> {
return render_geometry_cache.get_or_set(`${episode}-${area_id}-${area_variant}`, () =>
load_area_sections_and_render_geometry(episode, area_id, area_variant),
).sections;
@ -56,7 +56,7 @@ function load_area_sections_and_render_geometry(
episode: Episode,
area_id: 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 =>
area_geometry_to_sections_and_object_3d(
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 { LoadingCache } from "./LoadingCache";
import { Endianness } from "../../../core/data_formats/Endianness";
import { ArrayBufferCursor } from "../../../core/data_formats/cursor/ArrayBufferCursor";
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_xvm } from "../../../core/data_formats/parsing/ninja/texture";
import { xvm_to_textures } from "../../../core/rendering/conversion/ninja_textures";
import { load_array_buffer } from "../../../core/loading";
import { object_data, ObjectType } from "../../../core/data_formats/parsing/quest/object_types";
import { NpcType } from "../../../core/data_formats/parsing/quest/npc_types";
import { Endianness } from "../../core/data_formats/Endianness";
import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor";
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_xvm } from "../../core/data_formats/parsing/ninja/texture";
import { xvm_to_textures } from "../../core/rendering/conversion/ninja_textures";
import { load_array_buffer } from "../../core/loading";
import { object_data, ObjectType } from "../../core/data_formats/parsing/quest/object_types";
import { NpcType } from "../../core/data_formats/parsing/quest/npc_types";
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.
*/
readonly id: number;
readonly name: string;
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)
throw new Error(`Expected id to be a non-negative integer, got ${id}.`);
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 position: Vec3;
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 { 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 { Vec3 } from "../../core/data_formats/vector";
import { QuestRenderer } from "./QuestRenderer";
import { Section } from "../domain/Section";
import { ObservableQuestEntity, ObservableQuestNpc } from "../domain/observable_quest_entities";
import { quest_editor_store } from "../stores/QuestEditorStore";
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);
type Highlighted = {
entity: ObservableQuestEntity;
entity: QuestEntityModel;
mesh: Mesh;
};
@ -23,11 +25,11 @@ type Pick = {
};
type PickResult = Pick & {
entity: ObservableQuestEntity;
entity: QuestEntityModel;
mesh: Mesh;
};
export class QuestEntityControls {
export class QuestEntityControls implements Disposable {
private raycaster = new Raycaster();
private selected?: Highlighted;
private hovered?: Highlighted;
@ -37,29 +39,33 @@ export class QuestEntityControls {
private pick?: Pick;
private last_pointer_position = new Vector2(0, 0);
private moved_since_last_mouse_down = false;
private disposer = new Disposer();
constructor(private renderer: QuestRenderer) {
autorun(() => {
const entity = quest_editor_store.selected_entity;
this.disposer.add(
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) {
this.stop_transforming();
if (entity) {
// Mesh might not be loaded yet.
this.try_highlight_selected();
} else {
this.deselect();
if (entity) {
// Mesh might not be loaded yet.
this.try_highlight(entity);
} else {
this.deselect();
}
}
}
});
}),
);
}
dispose(): void {
this.disposer.dispose();
}
/**
* Highlights the selected entity if its mesh has been loaded.
*/
try_highlight_selected = () => {
const entity = quest_editor_store.selected_entity!;
try_highlight = (entity: QuestEntityModel) => {
const mesh = this.renderer.get_entity_mesh(entity);
if (mesh) {
@ -220,13 +226,15 @@ export class QuestEntityControls {
if (ray.intersectPlane(plane, intersection_point)) {
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_adjust.y -= y_delta;
selection.entity.world_position = new Vec3(
selection.entity.world_position.x,
y,
selection.entity.world_position.z,
selection.entity.set_world_position(
new Vec3(
selection.entity.world_position.val.x,
y,
selection.entity.world_position.val.z,
),
);
}
}
@ -240,14 +248,17 @@ export class QuestEntityControls {
const { intersection, section } = this.pick_terrain(pointer_position, pick);
if (intersection) {
selection.entity.set_world_position_and_section(
selection.entity.set_world_position(
new Vec3(
intersection.point.x,
intersection.point.y + pick.drag_y,
intersection.point.z,
),
section,
);
if (section) {
selection.entity.set_section(section);
}
} else {
// 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);
@ -255,15 +266,17 @@ export class QuestEntityControls {
// ray.origin.add(data.dragAdjust);
const plane = new Plane(
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();
if (ray.intersectPlane(plane, intersection_point)) {
selection.entity.world_position = new Vec3(
intersection_point.x + pick.grab_offset.x,
selection.entity.world_position.y,
intersection_point.z + pick.grab_offset.z,
selection.entity.set_world_position(
new Vec3(
intersection_point.x + pick.grab_offset.x,
selection.entity.world_position.val.y,
intersection_point.z + pick.grab_offset.z,
),
);
}
}
@ -272,10 +285,10 @@ export class QuestEntityControls {
private stop_transforming = () => {
if (this.moved_since_last_mouse_down && this.selected && this.pick) {
const entity = this.selected.entity;
quest_editor_store.push_entity_move_action(
quest_editor_store.push_translate_entity_action(
entity,
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 {
// Find the nearest object and NPC under the pointer.
@ -319,7 +332,7 @@ export class QuestEntityControls {
return {
mesh: intersection.object as Mesh,
entity,
initial_position: entity.world_position,
initial_position: entity.world_position.val,
grab_offset,
drag_adjust,
drag_y,
@ -328,13 +341,14 @@ export class QuestEntityControls {
/**
* @param pointer_pos - pointer coordinates in normalized device space
* @param data - entity picking data
*/
private pick_terrain(
pointer_pos: Vector2,
data: Pick,
): {
intersection?: Intersection;
section?: Section;
section?: SectionModel;
} {
this.raycaster.setFromCamera(pointer_pos, this.renderer.camera);
this.raycaster.ray.origin.add(data.drag_adjust);
@ -360,7 +374,7 @@ export class QuestEntityControls {
}
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 (Array.isArray(mesh.material)) {

View File

@ -1,42 +1,43 @@
import Logger from "js-logger";
import { autorun, IReactionDisposer } from "mobx";
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 {
load_npc_geometry,
load_npc_textures,
load_object_geometry,
load_object_textures,
} from "../loading/entities";
import { create_npc_mesh, create_object_mesh } from "./conversion/entities";
import { QuestRenderer } from "./QuestRenderer";
import { AreaUserData } from "./conversion/areas";
import { ObservableQuest } from "../domain/ObservableQuest";
import { ObservableArea } from "../domain/ObservableArea";
import { ObservableAreaVariant } from "../domain/ObservableAreaVariant";
import { ObservableQuestEntity } from "../domain/observable_quest_entities";
import { load_area_collision_geometry, load_area_render_geometry } from "../loading/areas";
import { QuestEntityModel } from "../model/QuestEntityModel";
import { Disposer } from "../../core/observable/Disposer";
import { Disposable } from "../../core/observable/Disposable";
import { AreaModel } from "../model/AreaModel";
import { AreaVariantModel } from "../model/AreaVariantModel";
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_LOOKAT = new Vector3(0, 0, 0);
const CAMERA_LOOK_AT = new Vector3(0, 0, 0);
const DUMMY_OBJECT = new Object3D();
export class QuestModelManager {
private quest?: ObservableQuest;
private area?: ObservableArea;
private area_variant?: ObservableAreaVariant;
private entity_reaction_disposers: IReactionDisposer[] = [];
export class QuestModelManager implements Disposable {
private quest?: QuestModel;
private area?: AreaModel;
private area_variant?: AreaVariantModel;
private disposer = new Disposer();
constructor(private renderer: QuestRenderer) {}
async load_models(quest?: ObservableQuest, area?: ObservableArea): Promise<void> {
let area_variant: ObservableAreaVariant | undefined;
async load_models(quest?: QuestModel, area?: AreaModel): Promise<void> {
let area_variant: AreaVariantModel | undefined;
if (quest && area) {
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);
}
@ -48,7 +49,7 @@ export class QuestModelManager {
this.area = area;
this.area_variant = area_variant;
this.dispose_entity_reactions();
this.disposer.dispose_all();
if (quest && area) {
try {
@ -76,12 +77,12 @@ export class QuestModelManager {
this.renderer.collision_geometry = collision_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.
this.renderer.reset_entity_models();
for (const npc of quest.npcs) {
for (const npc of quest.npcs.val) {
if (npc.area_id === area.id) {
const npc_geom = await load_npc_geometry(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) {
const object_geom = await load_object_geometry(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(
collision_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.entity_reaction_disposers.push(
autorun(() => {
const { x, y, z } = entity.world_position;
this.disposer.add_all(
entity.world_position.observe(({ value: { x, y, z } }) => {
model.position.set(x, y, z);
const rot = entity.rotation;
model.rotation.set(rot.x, rot.y, rot.z);
this.renderer.schedule_render();
}),
entity.rotation.observe(({ value: { x, y, z } }) => {
model.rotation.set(x, y, z);
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 { QuestEntityModel } from "../model/QuestEntityModel";
import { quest_editor_store } from "../stores/QuestEditorStore";
import { QuestEntityControls } from "./QuestEntityControls";
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 { 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 {
get debug(): boolean {
return this._debug;
return super.debug;
}
set debug(debug: boolean) {
if (this._debug !== debug) {
this._debug = debug;
if (this.debug !== debug) {
super.debug = debug;
this._render_geometry.visible = debug;
this.schedule_render();
}
@ -58,34 +51,33 @@ export class QuestRenderer extends Renderer {
return this._entity_models;
}
private perspective_camera: PerspectiveCamera;
private entity_to_mesh = new Map<ObservableQuestEntity, Mesh>();
private entity_controls: QuestEntityControls;
private readonly disposer = new Disposer();
private readonly perspective_camera: PerspectiveCamera;
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() {
super(new PerspectiveCamera(60, 1, 10, 10000));
this.perspective_camera = this.camera as PerspectiveCamera;
const model_manager = new QuestModelManager(this);
autorun(
() => {
model_manager.load_models(
quest_editor_store.current_quest,
quest_editor_store.current_area,
);
},
{ name: "call load_models" },
this.disposer.add_all(
quest_editor_store.current_quest.observe(this.load_models),
quest_editor_store.current_area.observe(this.load_models),
quest_editor_store.debug.observe(({ value }) => (this.debug = value)),
);
this.entity_controls = new QuestEntityControls(this);
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("mousemove", this.entity_controls.on_mouse_move);
}
dispose(): void {
super.dispose();
this.disposer.dispose();
}
set_size(width: number, height: number): void {
this.perspective_camera.aspect = width / height;
this.perspective_camera.updateProjectionMatrix();
@ -104,12 +96,21 @@ export class QuestRenderer extends Renderer {
this._entity_models.add(model);
this.entity_to_mesh.set(entity, model);
if (entity === quest_editor_store.selected_entity) {
this.entity_controls.try_highlight_selected();
if (entity === quest_editor_store.selected_entity.val) {
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);
}
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 {
Color,
DoubleSide,
Face3,
Geometry,
@ -8,13 +9,12 @@ import {
MeshLambertMaterial,
Object3D,
Vector3,
Color,
} from "three";
import { CollisionObject } from "../../../../core/data_formats/parsing/area_collision_geometry";
import { RenderObject } from "../../../../core/data_formats/parsing/area_geometry";
import { GeometryBuilder } from "../../../../core/rendering/conversion/GeometryBuilder";
import { ninja_object_to_geometry_builder } from "../../../../core/rendering/conversion/ninja_geometry";
import { Section } from "../../domain/Section";
import { CollisionObject } from "../../../core/data_formats/parsing/area_collision_geometry";
import { RenderObject } from "../../../core/data_formats/parsing/area_geometry";
import { GeometryBuilder } from "../../../core/rendering/conversion/GeometryBuilder";
import { ninja_object_to_geometry_builder } from "../../../core/rendering/conversion/ninja_geometry";
import { SectionModel } from "../../model/SectionModel";
const materials = [
// Wall
@ -67,7 +67,7 @@ const wireframe_materials = [
];
export type AreaUserData = {
section?: Section;
section?: SectionModel;
};
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(
object: RenderObject,
): [Section[], Object3D] {
const sections: Section[] = [];
): [SectionModel[], Object3D] {
const sections: SectionModel[] = [];
const group = new Group();
let i = 0;
@ -144,7 +144,7 @@ export function area_geometry_to_sections_and_object_3d(
mesh.updateMatrixWorld();
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);
(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 { create_mesh } from "../../../../core/rendering/conversion/create_mesh";
import { ObjectType } from "../../../../core/data_formats/parsing/quest/object_types";
import { NpcType } from "../../../../core/data_formats/parsing/quest/npc_types";
import {
ObservableQuestEntity,
ObservableQuestNpc,
ObservableQuestObject,
} from "../../domain/observable_quest_entities";
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 { create_mesh } from "../../../core/rendering/conversion/create_mesh";
export enum ColorType {
Normal,
@ -25,11 +23,11 @@ NPC_COLORS[ColorType.Hovered] = 0xff3f5f;
NPC_COLORS[ColorType.Selected] = 0xff0054;
export type EntityUserData = {
entity: ObservableQuestEntity;
entity: QuestEntityModel;
};
export function create_object_mesh(
object: ObservableQuestObject,
object: QuestObjectModel,
geometry: BufferGeometry,
textures: Texture[],
): Mesh {
@ -43,7 +41,7 @@ export function create_object_mesh(
}
export function create_npc_mesh(
npc: ObservableQuestNpc,
npc: QuestNpcModel,
geometry: BufferGeometry,
textures: Texture[],
): Mesh {
@ -51,7 +49,7 @@ export function create_npc_mesh(
}
function create(
entity: ObservableQuestEntity,
entity: QuestEntityModel,
geometry: BufferGeometry,
textures: Texture[],
color: number,
@ -80,9 +78,9 @@ function create(
mesh.name = name;
(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);
const rot = entity.rotation;
const rot = entity.rotation.val;
mesh.rotation.set(rot.x, rot.y, rot.z);
return mesh;

View File

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

View File

@ -24,7 +24,7 @@ import {
} from "./instructions";
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 = {
line_no: number;

View File

@ -10,7 +10,7 @@ import {
import { BasicBlock, ControlFlowGraph } from "./ControlFlowGraph";
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 MAX_REGISTER_VALUE = MAX_SIGNED_DWORD_VALUE;

View File

@ -10,7 +10,7 @@ import { BasicBlock, ControlFlowGraph } from "./ControlFlowGraph";
import { ValueSet } from "./ValueSet";
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 MAX_STACK_VALUE = MAX_SIGNED_DWORD_VALUE;

View File

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

View File

@ -2,7 +2,7 @@ import { Instruction, InstructionSegment, Segment, SegmentType } from "../instru
import { Opcode } from "../opcodes";
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_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 { Episode, EPISODES } from "../../../core/data_formats/parsing/quest/Episode";
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";
import { get_areas_for_episode } from "../../core/data_formats/parsing/quest/areas";
class AreaStore {
private readonly areas: ObservableArea[][] = [];
private readonly areas: AreaModel[][] = [];
constructor() {
for (const episode of EPISODES) {
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) {
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];
};
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);
if (!area) throw new Error(`Area id ${area_id} for episode ${episode} is invalid.`);
return area;
@ -38,7 +38,7 @@ class AreaStore {
episode: Episode,
area_id: number,
variant_id: number,
): ObservableAreaVariant => {
): AreaVariantModel => {
const area = this.get_area(episode, area_id);
const area_variant = area.area_variants[variant_id];
@ -54,7 +54,7 @@ class AreaStore {
episode: Episode,
area_id: number,
variant_id: number,
): Promise<Section[]> => {
): Promise<SectionModel[]> => {
return load_area_sections(episode, area_id, variant_id);
};
}

View File

@ -1,17 +1,33 @@
import { property } from "../../core/observable";
import { ObservableQuest } from "../domain/ObservableQuest";
import { Property } from "../../core/observable/Property";
import { QuestModel } from "../model/QuestModel";
import { Property, PropertyChangeEvent } from "../../core/observable/Property";
import { read_file } from "../../core/read_file";
import { parse_quest } from "../../core/data_formats/parsing/quest";
import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor";
import { Endianness } from "../../core/data_formats/Endianness";
import { SimpleUndo, UndoStack } from "../../old/core/undo";
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");
const logger = Logger.get("quest_editor/gui/QuestEditorStore");
export class QuestEditorStore {
export class QuestEditorStore implements Disposable {
readonly debug: WritableProperty<boolean> = property(false);
readonly undo = new UndoStack();
@ -20,8 +36,54 @@ export class QuestEditorStore {
private readonly _current_quest_filename = property<string | undefined>(undefined);
readonly current_quest_filename: Property<string | undefined> = this._current_quest_filename;
private readonly _current_quest = property<ObservableQuest | undefined>(undefined);
readonly current_quest: Property<ObservableQuest | undefined> = this._current_quest;
private readonly _current_quest = property<QuestModel | undefined>(undefined);
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.
open_file = async (file: File) => {
@ -30,47 +92,47 @@ export class QuestEditorStore {
const quest = parse_quest(new ArrayBufferCursor(buffer, Endianness.Little));
this.set_quest(
quest &&
new ObservableQuest(
new QuestModel(
quest.id,
quest.language,
quest.name,
quest.short_description,
quest.long_description,
quest.episode,
// quest.map_designations,
// quest.objects.map(
// obj =>
// new ObservableQuestObject(
// obj.type,
// obj.id,
// obj.group_id,
// obj.area_id,
// obj.section_id,
// obj.position,
// obj.rotation,
// obj.properties,
// obj.unknown,
// ),
// ),
// quest.npcs.map(
// npc =>
// new ObservableQuestNpc(
// npc.type,
// npc.pso_type_id,
// npc.npc_id,
// npc.script_label,
// npc.roaming,
// npc.area_id,
// npc.section_id,
// npc.position,
// npc.rotation,
// npc.scale,
// npc.unknown,
// ),
// ),
// quest.dat_unknowns,
// quest.object_code,
// quest.shop_items,
quest.map_designations,
quest.objects.map(
obj =>
new QuestObjectModel(
obj.type,
obj.id,
obj.group_id,
obj.area_id,
obj.section_id,
obj.position,
obj.rotation,
obj.properties,
obj.unknown,
),
),
quest.npcs.map(
npc =>
new QuestNpcModel(
npc.type,
npc.pso_type_id,
npc.npc_id,
npc.script_label,
npc.roaming,
npc.area_id,
npc.section_id,
npc.position,
npc.rotation,
npc.scale,
npc.unknown,
),
),
quest.dat_unknowns,
quest.object_code,
quest.shop_items,
),
file.name,
);
@ -79,50 +141,90 @@ export class QuestEditorStore {
}
};
private set_quest(quest?: ObservableQuest, filename?: string): void {
this._current_quest_filename.val = filename;
push_edit_id_action = (event: PropertyChangeEvent<number>) => {
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.script_undo.reset();
// if (quest) {
// this.current_area = area_store.get_area(quest.episode, 0);
// } else {
// this.current_area = undefined;
// }
this._current_area.val = undefined;
this._selected_entity.val = undefined;
this._current_quest_filename.val = filename;
this._current_quest.val = quest;
if (quest) {
this._current_area.val = area_store.get_area(quest.episode, 0);
// Load section data.
// for (const variant of quest.area_variants) {
// const sections = yield area_store.get_area_sections(
// quest.episode,
// variant.area.id,
// variant.id,
// );
// variant.sections.replace(sections);
//
// for (const object of quest.objects.filter(o => o.area_id === variant.area.id)) {
// try {
// this.set_section_on_quest_entity(object, sections);
// } catch (e) {
// logger.error(e);
// }
// }
//
// for (const npc of quest.npcs.filter(npc => npc.area_id === variant.area.id)) {
// try {
// this.set_section_on_quest_entity(npc, sections);
// } catch (e) {
// logger.error(e);
// }
// }
// }
for (const variant of quest.area_variants.val) {
const sections = await area_store.get_area_sections(
quest.episode,
variant.area.id,
variant.id,
);
variant.sections.val.splice(0, Infinity, ...sections);
for (const object of quest.objects.val.filter(o => o.area_id === variant.area.id)) {
try {
this.set_section_on_quest_entity(object, sections);
} catch (e) {
logger.error(e);
}
}
for (const npc of quest.npcs.val.filter(npc => npc.area_id === variant.area.id)) {
try {
this.set_section_on_quest_entity(npc, sections);
} catch (e) {
logger.error(e);
}
}
}
} else {
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();

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

View File

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

View File

@ -1,11 +1,11 @@
import { create_element } from "../../core/gui/dom";
import { ResizableView } from "../../core/gui/ResizableView";
import { ToolBar } from "../../core/gui/ToolBar";
import "./ModelView.css";
import { model_store } from "../stores/ModelStore";
import "./Model3DView.css";
import { model_store } from "../stores/Model3DStore";
import { WritableProperty } from "../../core/observable/WritableProperty";
import { RendererView } from "../../core/gui/RendererView";
import { ModelRenderer } from "../rendering/ModelRenderer";
import { Model3DRenderer } from "../rendering/Model3DRenderer";
import { View } from "../../core/gui/View";
import { FileButton } from "../../core/gui/FileButton";
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";
const MODEL_LIST_WIDTH = 100;
const ANIMATION_LIST_WIDTH = 130;
const ANIMATION_LIST_WIDTH = 140;
export class ModelView extends ResizableView {
readonly element = create_element("div", { class: "viewer_ModelView" });
export class Model3DView extends ResizableView {
readonly element = create_element("div", { class: "viewer_Model3DView" });
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(
new ModelSelectListView(model_store.models, model_store.current_model),
);
private animation_list_view = this.disposable(
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() {
super();
@ -48,7 +48,7 @@ export class ModelView extends ResizableView {
this.renderer_view.start_rendering();
this.disposable(
gui_store.tool.observe(tool => {
gui_store.tool.observe(({ value: tool }) => {
if (tool === GuiTool.Viewer) {
this.renderer_view.start_rendering();
} else {
@ -116,30 +116,30 @@ class ToolBarView extends View {
// Always-enabled controls.
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]);
}),
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.
const enabled = model_store.current_nj_motion.map(njm => njm != undefined);
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),
this.animation_frame_rate_input.enabled.bind(enabled),
model_store.animation_frame_rate.bind(this.animation_frame_rate_input.value),
this.animation_frame_rate_input.enabled.bind_to(enabled),
model_store.animation_frame_rate.bind_to(this.animation_frame_rate_input.value),
this.animation_frame_input.enabled.bind(enabled),
model_store.animation_frame.bind(this.animation_frame_input.value),
this.animation_frame_input.value.bind(
this.animation_frame_input.enabled.bind_to(enabled),
model_store.animation_frame.bind_to(this.animation_frame_input.value),
this.animation_frame_input.value.bind_to(
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(
selected.observe(model => {
selected.observe(({ value: model }) => {
if (this.selected_element) {
this.selected_element.classList.remove("active");
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.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]);
}),
);
@ -30,7 +30,7 @@ export class TextureView extends ResizableView {
this.renderer_view.start_rendering();
this.disposable(
gui_store.tool.observe(tool => {
gui_store.tool.observe(({ value: tool }) => {
if (tool === GuiTool.Viewer) {
this.renderer_view.start_rendering();
} else {

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