mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
Ported new quest button to new GUI system.
This commit is contained in:
parent
31c51ca83d
commit
24f0cdb461
@ -55,7 +55,8 @@
|
||||
.core_Button_left,
|
||||
.core_Button_right {
|
||||
display: inline-flex;
|
||||
align-content: stretch;
|
||||
align-content: center;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.core_Button_left {
|
||||
|
@ -1,3 +1,3 @@
|
||||
import { Widget } from "./Widget";
|
||||
|
||||
export abstract class Control<E extends HTMLElement> extends Widget<E> {}
|
||||
export abstract class Control<E extends HTMLElement = HTMLElement> extends Widget<E> {}
|
||||
|
9
src/core/gui/DropDownButton.css
Normal file
9
src/core/gui/DropDownButton.css
Normal file
@ -0,0 +1,9 @@
|
||||
.core_DropDownButton {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.core_DropDownButton .core_Menu {
|
||||
top: 25px;
|
||||
left: 0;
|
||||
min-width: 100%;
|
||||
}
|
75
src/core/gui/DropDownButton.ts
Normal file
75
src/core/gui/DropDownButton.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { disposable_listener, el, Icon } from "./dom";
|
||||
import "./DropDownButton.css";
|
||||
import { Property } from "../observable/property/Property";
|
||||
import { Button, ButtonOptions } from "./Button";
|
||||
import { Menu } from "./Menu";
|
||||
import { Control } from "./Control";
|
||||
import { Observable } from "../observable/Observable";
|
||||
import { Emitter } from "../observable/Emitter";
|
||||
import { emitter } from "../observable";
|
||||
|
||||
export type DropDownButtonOptions = ButtonOptions;
|
||||
|
||||
export class DropDownButton<T> extends Control {
|
||||
readonly chosen: Observable<T>;
|
||||
|
||||
private readonly button: Button;
|
||||
private readonly menu: Menu<T>;
|
||||
private readonly _chosen: Emitter<T>;
|
||||
private just_opened: boolean;
|
||||
|
||||
constructor(
|
||||
text: string,
|
||||
items: T[] | Property<T[]>,
|
||||
to_label: (element: T) => string,
|
||||
options?: DropDownButtonOptions,
|
||||
) {
|
||||
const button = new Button(text, {
|
||||
icon_left: options && options.icon_left,
|
||||
icon_right: Icon.TriangleDown,
|
||||
});
|
||||
const menu = new Menu<T>(items, to_label);
|
||||
|
||||
super(el.div({ class: "core_DropDownButton" }, button.element, menu.element), options);
|
||||
|
||||
this.button = this.disposable(button);
|
||||
this.menu = this.disposable(menu);
|
||||
|
||||
this._chosen = emitter();
|
||||
this.chosen = this._chosen;
|
||||
|
||||
this.just_opened = false;
|
||||
|
||||
this.disposables(
|
||||
disposable_listener(button.element, "mousedown", e => this.button_mousedown(e)),
|
||||
|
||||
button.mouseup.observe(() => this.button_mouseup()),
|
||||
|
||||
this.menu.selected.observe(({ value }) => {
|
||||
if (value) {
|
||||
this._chosen.emit({ value });
|
||||
this.menu.selected.val = undefined;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
protected set_enabled(enabled: boolean): void {
|
||||
super.set_enabled(enabled);
|
||||
this.button.enabled.val = enabled;
|
||||
}
|
||||
|
||||
private button_mousedown(e: Event): void {
|
||||
e.stopPropagation();
|
||||
this.just_opened = !this.menu.visible.val;
|
||||
this.menu.visible.val = true;
|
||||
}
|
||||
|
||||
private button_mouseup(): void {
|
||||
if (!this.just_opened) {
|
||||
this.menu.visible.val = false;
|
||||
}
|
||||
|
||||
this.just_opened = false;
|
||||
}
|
||||
}
|
20
src/core/gui/Menu.css
Normal file
20
src/core/gui/Menu.css
Normal file
@ -0,0 +1,20 @@
|
||||
.core_Menu {
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
border: var(--control-border);
|
||||
}
|
||||
|
||||
.core_Menu .core_Menu_inner {
|
||||
background-color: var(--control-bg-color);
|
||||
border: var(--control-inner-border);
|
||||
}
|
||||
|
||||
.core_Menu .core_Menu_inner > * {
|
||||
padding: 5px 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.core_Menu .core_Menu_inner > *:hover {
|
||||
background-color: var(--control-bg-color-hover);
|
||||
}
|
70
src/core/gui/Menu.ts
Normal file
70
src/core/gui/Menu.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { disposable_listener, el } from "./dom";
|
||||
import { Widget } from "./Widget";
|
||||
import { Property } from "../observable/property/Property";
|
||||
import { property } from "../observable";
|
||||
import { WritableProperty } from "../observable/property/WritableProperty";
|
||||
import { WidgetProperty } from "../observable/property/WidgetProperty";
|
||||
import "./Menu.css";
|
||||
|
||||
export class Menu<T> extends Widget {
|
||||
readonly selected: WritableProperty<T | undefined>;
|
||||
|
||||
private readonly to_label: (element: T) => string;
|
||||
private readonly items: Property<T[]>;
|
||||
private readonly _selected: WidgetProperty<T | undefined>;
|
||||
|
||||
constructor(items: T[] | Property<T[]>, to_label: (element: T) => string) {
|
||||
super(el.div({ class: "core_Menu" }));
|
||||
|
||||
this.element.hidden = true;
|
||||
this.element.onmouseup = (e: Event) => this.mouseup(e);
|
||||
|
||||
const inner_element = el.div({ class: "core_Menu_inner" });
|
||||
this.element.append(inner_element);
|
||||
|
||||
this.to_label = to_label;
|
||||
this.items = Array.isArray(items) ? property(items) : items;
|
||||
|
||||
this._selected = new WidgetProperty<T | undefined>(this, undefined, this.set_selected);
|
||||
this.selected = this._selected;
|
||||
|
||||
this.disposables(
|
||||
this.items.observe(
|
||||
({ value: items }) => {
|
||||
inner_element.innerHTML = "";
|
||||
inner_element.append(
|
||||
...items.map((item, index) =>
|
||||
el.div({ text: to_label(item), data: { index: index.toString() } }),
|
||||
),
|
||||
);
|
||||
},
|
||||
{ call_now: true },
|
||||
),
|
||||
|
||||
disposable_listener(document, "mousedown", (e: Event) => this.document_mousedown(e)),
|
||||
);
|
||||
}
|
||||
|
||||
protected set_selected(): void {
|
||||
// Noop
|
||||
}
|
||||
|
||||
private mouseup(e: Event): void {
|
||||
if (!(e.target instanceof HTMLElement)) return;
|
||||
|
||||
const index_str = e.target.dataset.index;
|
||||
if (index_str == undefined) return;
|
||||
|
||||
const element = this.items.val[parseInt(index_str, 10)];
|
||||
if (!element) return;
|
||||
|
||||
this.selected.set_val(element, { silent: false });
|
||||
this.visible.val = false;
|
||||
}
|
||||
|
||||
private document_mousedown(e: Event): void {
|
||||
if (this.visible.val && !this.element.contains(e.target as Node)) {
|
||||
this.visible.val = false;
|
||||
}
|
||||
}
|
||||
}
|
@ -8,26 +8,8 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.core_Select .core_Select_elements {
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
.core_Select .core_Menu {
|
||||
top: 25px;
|
||||
left: 0;
|
||||
min-width: 100%;
|
||||
border: var(--control-border);
|
||||
}
|
||||
|
||||
.core_Select .core_Select_elements_inner {
|
||||
background-color: var(--control-bg-color);
|
||||
border: var(--control-inner-border);
|
||||
}
|
||||
|
||||
.core_Select .core_Select_elements_inner > * {
|
||||
padding: 5px 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.core_Select .core_Select_elements_inner > *:hover {
|
||||
background-color: var(--control-bg-color-hover);
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { LabelledControl, LabelledControlOptions, LabelPosition } from "./LabelledControl";
|
||||
import { disposable_listener, el, Icon } from "./dom";
|
||||
import "./Select.css";
|
||||
import "./Button.css";
|
||||
import { is_any_property, Property } from "../observable/property/Property";
|
||||
import { Button } from "./Button";
|
||||
import { WritableProperty } from "../observable/property/WritableProperty";
|
||||
import { WidgetProperty } from "../observable/property/WidgetProperty";
|
||||
import { Menu } from "./Menu";
|
||||
|
||||
export type SelectOptions<T> = LabelledControlOptions & {
|
||||
selected: T | Property<T>;
|
||||
@ -18,38 +18,27 @@ export class Select<T> extends LabelledControl {
|
||||
|
||||
private readonly to_label: (element: T) => string;
|
||||
private readonly button: Button;
|
||||
private readonly element_container: HTMLElement;
|
||||
private readonly elements: Property<T[]>;
|
||||
private readonly menu: Menu<T>;
|
||||
private readonly _selected: WidgetProperty<T | undefined>;
|
||||
private just_opened: boolean;
|
||||
|
||||
constructor(
|
||||
elements: Property<T[]>,
|
||||
items: T[] | Property<T[]>,
|
||||
to_label: (element: T) => string,
|
||||
options?: SelectOptions<T>,
|
||||
) {
|
||||
const button = new Button("", {
|
||||
icon_right: Icon.TriangleDown,
|
||||
});
|
||||
const menu = new Menu<T>(items, to_label);
|
||||
|
||||
const element_container = el.div({ class: "core_Select_elements" });
|
||||
|
||||
super(el.div({ class: "core_Select" }, button.element, element_container), options);
|
||||
|
||||
this.element_container = element_container;
|
||||
this.element_container.hidden = true;
|
||||
this.element_container.onmouseup = (e: Event) => this.element_container_mouseup(e);
|
||||
|
||||
const element_container_inner = el.div({ class: "core_Select_elements_inner" });
|
||||
element_container.append(element_container_inner);
|
||||
super(el.div({ class: "core_Select" }, button.element, menu.element), options);
|
||||
|
||||
this.preferred_label_position = "left";
|
||||
|
||||
this.to_label = to_label;
|
||||
|
||||
this.button = this.disposable(button);
|
||||
|
||||
this.elements = elements;
|
||||
this.menu = this.disposable(menu);
|
||||
|
||||
this._selected = new WidgetProperty<T | undefined>(this, undefined, this.set_selected);
|
||||
this.selected = this._selected;
|
||||
@ -57,23 +46,13 @@ export class Select<T> extends LabelledControl {
|
||||
this.just_opened = false;
|
||||
|
||||
this.disposables(
|
||||
elements.observe(
|
||||
({ value: opts }) => {
|
||||
element_container_inner.innerHTML = "";
|
||||
element_container_inner.append(
|
||||
...opts.map((opt, index) =>
|
||||
el.div({ text: to_label(opt), data: { index: index.toString() } }),
|
||||
),
|
||||
);
|
||||
},
|
||||
{ call_now: true },
|
||||
),
|
||||
|
||||
button.mousedown.observe(() => this.button_mousedown()),
|
||||
disposable_listener(button.element, "mousedown", e => this.button_mousedown(e)),
|
||||
|
||||
button.mouseup.observe(() => this.button_mouseup()),
|
||||
|
||||
disposable_listener(document, "mousedown", (e: Event) => this.document_mousedown(e)),
|
||||
this.menu.selected.observe(({ value }) =>
|
||||
this._selected.set_val(value, { silent: false }),
|
||||
),
|
||||
);
|
||||
|
||||
if (options) {
|
||||
@ -92,45 +71,20 @@ export class Select<T> extends LabelledControl {
|
||||
|
||||
protected set_selected(selected?: T): void {
|
||||
this.button.text.val = selected ? this.to_label(selected) : "";
|
||||
this.menu.selected.val = selected;
|
||||
}
|
||||
|
||||
private button_mousedown(): void {
|
||||
this.just_opened = this.element_container.hidden;
|
||||
this.show_menu();
|
||||
private button_mousedown(e: Event): void {
|
||||
e.stopPropagation();
|
||||
this.just_opened = !this.menu.visible.val;
|
||||
this.menu.visible.val = true;
|
||||
}
|
||||
|
||||
private button_mouseup(): void {
|
||||
if (!this.just_opened) {
|
||||
this.hide_menu();
|
||||
this.menu.visible.val = false;
|
||||
}
|
||||
|
||||
this.just_opened = false;
|
||||
}
|
||||
|
||||
private element_container_mouseup(e: Event): void {
|
||||
if (!(e.target instanceof HTMLElement)) return;
|
||||
|
||||
const index_str = e.target.dataset.index;
|
||||
if (index_str == undefined) return;
|
||||
|
||||
const element = this.elements.val[parseInt(index_str, 10)];
|
||||
if (!element) return;
|
||||
|
||||
this.selected.set_val(element, { silent: false });
|
||||
this.hide_menu();
|
||||
}
|
||||
|
||||
private document_mousedown(e: Event): void {
|
||||
if (!this.element.contains(e.target as Node)) {
|
||||
this.hide_menu();
|
||||
}
|
||||
}
|
||||
|
||||
private show_menu(): void {
|
||||
this.element_container.hidden = false;
|
||||
}
|
||||
|
||||
private hide_menu(): void {
|
||||
this.element_container.hidden = true;
|
||||
}
|
||||
}
|
||||
|
@ -89,6 +89,7 @@ export function bind_hidden(element: HTMLElement, observable: Observable<boolean
|
||||
|
||||
export enum Icon {
|
||||
File,
|
||||
NewFile,
|
||||
Save,
|
||||
TriangleDown,
|
||||
Undo,
|
||||
@ -102,6 +103,9 @@ export function icon(icon: Icon): HTMLElement {
|
||||
case Icon.File:
|
||||
icon_str = "fa-file";
|
||||
break;
|
||||
case Icon.NewFile:
|
||||
icon_str = "fa-file-medical";
|
||||
break;
|
||||
case Icon.Save:
|
||||
icon_str = "fa-save";
|
||||
break;
|
||||
@ -123,12 +127,13 @@ export function disposable_listener(
|
||||
element: DocumentAndElementEventHandlers,
|
||||
event: string,
|
||||
listener: EventListenerOrEventListenerObject,
|
||||
options?: AddEventListenerOptions,
|
||||
): Disposable {
|
||||
element.addEventListener(event, listener);
|
||||
|
||||
return {
|
||||
dispose(): void {
|
||||
element.removeEventListener(event, listener);
|
||||
element.removeEventListener(event, listener, options);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -1,37 +1,8 @@
|
||||
import Logger from "js-logger";
|
||||
import { action, flow, observable } from "mobx";
|
||||
import { Endianness } from "../../../core/data_formats/Endianness";
|
||||
import { ArrayBufferCursor } from "../../../core/data_formats/cursor/ArrayBufferCursor";
|
||||
import { parse_quest, write_quest_qst } from "../../../core/data_formats/parsing/quest";
|
||||
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_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/QuestModel";
|
||||
import { AreaModel } from "../../../quest_editor/model/AreaModel";
|
||||
import { SectionModel } from "../../../quest_editor/model/SectionModel";
|
||||
import {
|
||||
QuestEntityModel,
|
||||
ObservableQuestNpc,
|
||||
ObservableQuestObject,
|
||||
} from "../domain/observable_quest_entities";
|
||||
|
||||
const logger = Logger.get("stores/QuestEditorStore");
|
||||
import { action, observable } from "mobx";
|
||||
import { write_quest_qst } from "../../../core/data_formats/parsing/quest";
|
||||
|
||||
class QuestEditorStore {
|
||||
@observable debug = false;
|
||||
|
||||
readonly undo = new UndoStack();
|
||||
readonly script_undo = new SimpleUndo("Text edits", () => {}, () => {});
|
||||
|
||||
@observable current_quest_filename?: string;
|
||||
@observable current_quest?: ObservableQuest;
|
||||
@observable current_area?: AreaModel;
|
||||
|
||||
@observable selected_entity?: QuestEntityModel;
|
||||
|
||||
@observable save_dialog_filename?: string;
|
||||
@observable save_dialog_open: boolean = false;
|
||||
@ -52,92 +23,6 @@ class QuestEditorStore {
|
||||
// application_store.on_global_keyup("quest_editor", "Ctrl-Alt-D", this.toggle_debug);
|
||||
}
|
||||
|
||||
@action
|
||||
toggle_debug = () => {
|
||||
this.debug = !this.debug;
|
||||
};
|
||||
|
||||
@action
|
||||
set_selected_entity = (entity?: QuestEntityModel) => {
|
||||
if (entity) {
|
||||
this.set_current_area_id(entity.area_id);
|
||||
}
|
||||
|
||||
this.selected_entity = entity;
|
||||
};
|
||||
|
||||
@action
|
||||
set_current_area_id = (area_id?: number) => {
|
||||
this.selected_entity = undefined;
|
||||
|
||||
if (area_id == undefined) {
|
||||
this.current_area = undefined;
|
||||
} else if (this.current_quest) {
|
||||
this.current_area = area_store.get_area(this.current_quest.episode, area_id);
|
||||
}
|
||||
};
|
||||
|
||||
@action
|
||||
new_quest = (episode: Episode) => {
|
||||
this.set_quest(create_new_quest(episode));
|
||||
};
|
||||
|
||||
// TODO: notify user of problems.
|
||||
open_file = flow(function* open_file(this: QuestEditorStore, filename: string, file: File) {
|
||||
try {
|
||||
const buffer = yield read_file(file);
|
||||
const quest = parse_quest(new ArrayBufferCursor(buffer, Endianness.Little));
|
||||
this.set_quest(
|
||||
quest &&
|
||||
new ObservableQuest(
|
||||
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,
|
||||
),
|
||||
filename,
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error("Couldn't read file.", e);
|
||||
}
|
||||
});
|
||||
|
||||
@action
|
||||
open_save_dialog = () => {
|
||||
this.save_dialog_filename = this.current_quest_filename
|
||||
@ -218,165 +103,4 @@ class QuestEditorStore {
|
||||
|
||||
this.save_dialog_open = false;
|
||||
};
|
||||
|
||||
@action
|
||||
push_id_edit_action = (old_id: number, new_id: number) => {
|
||||
const quest = quest_editor_store.current_quest;
|
||||
if (quest) quest.set_id(new_id);
|
||||
|
||||
this.undo.push_action(
|
||||
`Edit ID`,
|
||||
() => {
|
||||
const quest = quest_editor_store.current_quest;
|
||||
if (quest) quest.set_id(old_id);
|
||||
},
|
||||
() => {
|
||||
const quest = quest_editor_store.current_quest;
|
||||
if (quest) quest.set_id(new_id);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@action
|
||||
push_name_edit_action = (old_name: string, new_name: string) => {
|
||||
const quest = quest_editor_store.current_quest;
|
||||
if (quest) quest.set_name(new_name);
|
||||
|
||||
this.undo.push_action(
|
||||
`Edit name`,
|
||||
() => {
|
||||
const quest = quest_editor_store.current_quest;
|
||||
if (quest) quest.set_name(old_name);
|
||||
},
|
||||
() => {
|
||||
const quest = quest_editor_store.current_quest;
|
||||
if (quest) quest.set_name(new_name);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@action
|
||||
push_short_description_edit_action = (
|
||||
old_short_description: string,
|
||||
new_short_description: string,
|
||||
) => {
|
||||
const quest = quest_editor_store.current_quest;
|
||||
if (quest) quest.set_short_description(new_short_description);
|
||||
|
||||
this.undo.push_action(
|
||||
`Edit short description`,
|
||||
() => {
|
||||
const quest = quest_editor_store.current_quest;
|
||||
if (quest) quest.set_short_description(old_short_description);
|
||||
},
|
||||
() => {
|
||||
const quest = quest_editor_store.current_quest;
|
||||
if (quest) quest.set_short_description(new_short_description);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@action
|
||||
push_long_description_edit_action = (
|
||||
old_long_description: string,
|
||||
new_long_description: string,
|
||||
) => {
|
||||
const quest = quest_editor_store.current_quest;
|
||||
if (quest) quest.set_long_description(new_long_description);
|
||||
|
||||
this.undo.push_action(
|
||||
`Edit long description`,
|
||||
() => {
|
||||
const quest = quest_editor_store.current_quest;
|
||||
if (quest) quest.set_long_description(old_long_description);
|
||||
},
|
||||
() => {
|
||||
const quest = quest_editor_store.current_quest;
|
||||
if (quest) quest.set_long_description(new_long_description);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@action
|
||||
push_entity_move_action = (
|
||||
entity: QuestEntityModel,
|
||||
old_position: Vec3,
|
||||
new_position: Vec3,
|
||||
) => {
|
||||
this.undo.push_action(
|
||||
`Move ${entity_data(entity.type).name}`,
|
||||
() => {
|
||||
entity.world_position = old_position;
|
||||
quest_editor_store.set_selected_entity(entity);
|
||||
},
|
||||
() => {
|
||||
entity.world_position = new_position;
|
||||
quest_editor_store.set_selected_entity(entity);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@action
|
||||
private set_quest = flow(function* set_quest(
|
||||
this: QuestEditorStore,
|
||||
quest?: ObservableQuest,
|
||||
filename?: string,
|
||||
) {
|
||||
this.current_quest_filename = filename;
|
||||
|
||||
if (quest !== this.current_quest) {
|
||||
this.undo.reset();
|
||||
this.script_undo.reset();
|
||||
this.selected_entity = undefined;
|
||||
this.current_quest = quest;
|
||||
|
||||
if (quest) {
|
||||
this.current_area = area_store.get_area(quest.episode, 0);
|
||||
} else {
|
||||
this.current_area = undefined;
|
||||
}
|
||||
|
||||
if (quest) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.error("Couldn't parse quest file.");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
private set_section_on_quest_entity = (entity: QuestEntityModel, sections: SectionModel[]) => {
|
||||
const section = sections.find(s => s.id === entity.section_id);
|
||||
|
||||
if (section) {
|
||||
entity.section = section;
|
||||
} else {
|
||||
logger.warn(`Section ${entity.section_id} not found.`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const quest_editor_store = new QuestEditorStore();
|
||||
|
@ -1,29 +0,0 @@
|
||||
.main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 2px 10px 10px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table th:not([colspan]) {
|
||||
width: 65px;
|
||||
}
|
||||
|
||||
.coord_label {
|
||||
padding: 0px 5px 0px 10px;
|
||||
}
|
||||
|
||||
.coord {
|
||||
width: 100px !important;
|
||||
}
|
||||
|
||||
.coord input {
|
||||
text-align: right;
|
||||
padding-right: 24px !important;
|
||||
}
|
@ -1,152 +0,0 @@
|
||||
import { InputNumber } from "antd";
|
||||
import { autorun, IReactionDisposer } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import React, { Component, PureComponent, ReactNode } from "react";
|
||||
import { Vec3 } from "../../../core/data_formats/vector";
|
||||
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 { QuestEntityModel, ObservableQuestNpc } from "../domain/observable_quest_entities";
|
||||
|
||||
@observer
|
||||
export class EntityInfoComponent extends Component {
|
||||
render(): ReactNode {
|
||||
const entity = quest_editor_store.selected_entity;
|
||||
let body: ReactNode;
|
||||
|
||||
if (entity) {
|
||||
const section_id = entity.section ? entity.section.id : entity.section_id;
|
||||
|
||||
body = (
|
||||
<table className={styles.table}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{entity instanceof ObservableQuestNpc ? "NPC" : "Object"}:</th>
|
||||
<td>{entity_data(entity.type).name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Section:</th>
|
||||
<td>{section_id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colSpan={2}>Section position:</th>
|
||||
</tr>
|
||||
<CoordRow entity={entity} position_type="position" coord="x" />
|
||||
<CoordRow entity={entity} position_type="position" coord="y" />
|
||||
<CoordRow entity={entity} position_type="position" coord="z" />
|
||||
<tr>
|
||||
<th colSpan={2}>World position:</th>
|
||||
</tr>
|
||||
<CoordRow entity={entity} position_type="world_position" coord="x" />
|
||||
<CoordRow entity={entity} position_type="world_position" coord="y" />
|
||||
<CoordRow entity={entity} position_type="world_position" coord="z" />
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
} else {
|
||||
body = <DisabledTextComponent>No entity selected.</DisabledTextComponent>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.main} tabIndex={-1}>
|
||||
{body}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type CoordProps = {
|
||||
entity: QuestEntityModel;
|
||||
position_type: "position" | "world_position";
|
||||
coord: "x" | "y" | "z";
|
||||
};
|
||||
|
||||
class CoordRow extends PureComponent<CoordProps> {
|
||||
render(): ReactNode {
|
||||
return (
|
||||
<tr>
|
||||
<th className={styles.coord_label}>{this.props.coord.toUpperCase()}:</th>
|
||||
<td>
|
||||
<CoordInput {...this.props} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CoordInput extends Component<CoordProps, { value: number; initial_position: Vec3 }> {
|
||||
private disposer?: IReactionDisposer;
|
||||
|
||||
state = { value: 0, initial_position: new Vec3(0, 0, 0) };
|
||||
|
||||
componentDidMount(): void {
|
||||
this.start_observing();
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
if (this.disposer) this.disposer();
|
||||
}
|
||||
|
||||
componentDidUpdate(prev_props: CoordProps): void {
|
||||
if (this.props.entity !== prev_props.entity) {
|
||||
this.start_observing();
|
||||
}
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
return (
|
||||
<InputNumber
|
||||
value={this.state.value}
|
||||
size="small"
|
||||
precision={3}
|
||||
className={styles.coord}
|
||||
onFocus={this.focus}
|
||||
onBlur={this.blur}
|
||||
onChange={this.changed}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private start_observing(): void {
|
||||
if (this.disposer) this.disposer();
|
||||
|
||||
this.disposer = autorun(
|
||||
() => {
|
||||
this.setState({
|
||||
value: this.props.entity[this.props.position_type][this.props.coord],
|
||||
});
|
||||
},
|
||||
{
|
||||
name: `${entity_type_to_string(this.props.entity.type)}.${
|
||||
this.props.position_type
|
||||
}.${this.props.coord} changed`,
|
||||
delay: 50,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private focus = () => {
|
||||
this.setState({ initial_position: this.props.entity.world_position });
|
||||
};
|
||||
|
||||
private blur = () => {
|
||||
if (!this.state.initial_position.equals(this.props.entity.world_position)) {
|
||||
quest_editor_store.push_entity_move_action(
|
||||
this.props.entity,
|
||||
this.state.initial_position,
|
||||
this.props.entity.world_position,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private changed = (value?: number) => {
|
||||
if (value != null) {
|
||||
const entity = this.props.entity;
|
||||
const pos_type = this.props.position_type;
|
||||
const pos = entity[pos_type].clone();
|
||||
pos[this.props.coord] = value;
|
||||
entity[pos_type] = pos;
|
||||
}
|
||||
};
|
||||
}
|
@ -7,9 +7,19 @@ import { Select } from "../../core/gui/Select";
|
||||
import { array_property } from "../../core/observable";
|
||||
import { AreaModel } from "../model/AreaModel";
|
||||
import { Icon } from "../../core/gui/dom";
|
||||
import { DropDownButton } from "../../core/gui/DropDownButton";
|
||||
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
|
||||
|
||||
export class QuestEditorToolBar extends ToolBar {
|
||||
constructor() {
|
||||
const new_quest_button = new DropDownButton(
|
||||
"New quest",
|
||||
[Episode.I],
|
||||
episode => `Episode ${Episode[episode]}`,
|
||||
{
|
||||
icon_left: Icon.NewFile,
|
||||
},
|
||||
);
|
||||
const open_file_button = new FileButton("Open file...", {
|
||||
icon_left: Icon.File,
|
||||
accept: ".qst",
|
||||
@ -41,12 +51,23 @@ export class QuestEditorToolBar extends ToolBar {
|
||||
);
|
||||
|
||||
super({
|
||||
children: [open_file_button, save_as_button, undo_button, redo_button, area_select],
|
||||
children: [
|
||||
new_quest_button,
|
||||
open_file_button,
|
||||
save_as_button,
|
||||
undo_button,
|
||||
redo_button,
|
||||
area_select,
|
||||
],
|
||||
});
|
||||
|
||||
const quest_loaded = quest_editor_store.current_quest.map(q => q != undefined);
|
||||
|
||||
this.disposables(
|
||||
new_quest_button.chosen.observe(({ value: episode }) =>
|
||||
quest_editor_store.new_quest(episode),
|
||||
),
|
||||
|
||||
open_file_button.files.observe(({ value: files }) => {
|
||||
if (files.length) {
|
||||
quest_editor_store.open_file(files[0]);
|
||||
|
@ -10,6 +10,7 @@ import { AssemblyError, AssemblyWarning } from "../scripting/assembly";
|
||||
import { Observable } from "../../core/observable/Observable";
|
||||
import { emitter, property } from "../../core/observable";
|
||||
import { WritableProperty } from "../../core/observable/property/WritableProperty";
|
||||
import { Property } from "../../core/observable/property/Property";
|
||||
import SignatureHelp = languages.SignatureHelp;
|
||||
import ITextModel = editor.ITextModel;
|
||||
import CompletionList = languages.CompletionList;
|
||||
@ -59,15 +60,9 @@ languages.setLanguageConfiguration("psoasm", {
|
||||
});
|
||||
|
||||
export class AsmEditorStore implements Disposable {
|
||||
private readonly _model: WritableProperty<ITextModel | undefined> = property(undefined);
|
||||
readonly model = this._model;
|
||||
|
||||
private readonly _did_undo = emitter<string>();
|
||||
readonly did_undo: Observable<string> = this._did_undo;
|
||||
|
||||
private readonly _did_redo = emitter<string>();
|
||||
readonly did_redo: Observable<string> = this._did_redo;
|
||||
|
||||
readonly model: Property<ITextModel | undefined>;
|
||||
readonly did_undo: Observable<string>;
|
||||
readonly did_redo: Observable<string>;
|
||||
readonly undo = new SimpleUndo(
|
||||
"Text edits",
|
||||
() => this._did_undo.emit({ value: "asm undo" }),
|
||||
@ -76,8 +71,15 @@ export class AsmEditorStore implements Disposable {
|
||||
|
||||
private readonly disposer = new Disposer();
|
||||
private readonly model_disposer = this.disposer.add(new Disposer());
|
||||
private readonly _model: WritableProperty<ITextModel | undefined> = property(undefined);
|
||||
private readonly _did_undo = emitter<string>();
|
||||
private readonly _did_redo = emitter<string>();
|
||||
|
||||
constructor() {
|
||||
this.model = this._model;
|
||||
this.did_undo = this._did_undo;
|
||||
this.did_redo = this._did_redo;
|
||||
|
||||
this.disposer.add_all(
|
||||
quest_editor_store.current_quest.observe(({ value }) => this.quest_changed(value), {
|
||||
call_now: true,
|
||||
|
@ -22,30 +22,32 @@ import { EditShortDescriptionAction } from "../actions/EditShortDescriptionActio
|
||||
import { EditLongDescriptionAction } from "../actions/EditLongDescriptionAction";
|
||||
import { EditNameAction } from "../actions/EditNameAction";
|
||||
import { EditIdAction } from "../actions/EditIdAction";
|
||||
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
|
||||
import { create_new_quest } from "./quest_creation";
|
||||
import Logger = require("js-logger");
|
||||
|
||||
const logger = Logger.get("quest_editor/gui/QuestEditorStore");
|
||||
|
||||
export class QuestEditorStore implements Disposable {
|
||||
readonly debug: WritableProperty<boolean> = property(false);
|
||||
|
||||
readonly undo = new UndoStack();
|
||||
|
||||
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<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;
|
||||
readonly current_quest_filename: Property<string | undefined>;
|
||||
readonly current_quest: Property<QuestModel | undefined>;
|
||||
readonly current_area: Property<AreaModel | undefined>;
|
||||
readonly selected_entity: Property<QuestEntityModel | undefined>;
|
||||
|
||||
private readonly disposer = new Disposer();
|
||||
private readonly _current_quest_filename = property<string | undefined>(undefined);
|
||||
private readonly _current_quest = property<QuestModel | undefined>(undefined);
|
||||
private readonly _current_area = property<AreaModel | undefined>(undefined);
|
||||
private readonly _selected_entity = property<QuestEntityModel | undefined>(undefined);
|
||||
|
||||
constructor() {
|
||||
this.current_quest_filename = this._current_quest_filename;
|
||||
this.current_quest = this._current_quest;
|
||||
this.current_area = this._current_area;
|
||||
this.selected_entity = this._selected_entity;
|
||||
|
||||
this.disposer.add(
|
||||
gui_store.tool.observe(
|
||||
({ value: tool }) => {
|
||||
@ -62,14 +64,6 @@ export class QuestEditorStore implements Disposable {
|
||||
this.disposer.dispose();
|
||||
}
|
||||
|
||||
set_current_area_id = (area_id?: number) => {
|
||||
if (area_id == undefined) {
|
||||
this.set_current_area(undefined);
|
||||
} else if (this.current_quest.val) {
|
||||
this.set_current_area(area_store.get_area(this.current_quest.val.episode, area_id));
|
||||
}
|
||||
};
|
||||
|
||||
set_current_area = (area?: AreaModel) => {
|
||||
this._selected_entity.val = undefined;
|
||||
|
||||
@ -87,6 +81,10 @@ export class QuestEditorStore implements Disposable {
|
||||
this._selected_entity.val = entity;
|
||||
};
|
||||
|
||||
new_quest = (episode: Episode) => {
|
||||
this.set_quest(create_new_quest(episode));
|
||||
};
|
||||
|
||||
// TODO: notify user of problems.
|
||||
open_file = async (file: File) => {
|
||||
try {
|
||||
|
Loading…
Reference in New Issue
Block a user