phantasmal-world/src/quest_editor/controllers/QuestEditorToolBarController.ts

218 lines
7.7 KiB
TypeScript
Raw Normal View History

import { GuiStore, GuiTool } from "../../core/stores/GuiStore";
import { AreaStore } from "../stores/AreaStore";
import { QuestEditorStore } from "../stores/QuestEditorStore";
import { AreaModel } from "../model/AreaModel";
import { list_property, map } from "../../core/observable";
import { Property } from "../../core/observable/property/Property";
import { undo_manager } from "../../core/undo/UndoManager";
import { Controller } from "../../core/controllers/Controller";
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { create_new_quest } from "../stores/quest_creation";
import { read_file } from "../../core/read_file";
import { parse_quest, write_quest_qst } from "../../core/data_formats/parsing/quest";
import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor";
import { Endianness } from "../../core/data_formats/Endianness";
import { convert_quest_from_model, convert_quest_to_model } from "../stores/model_conversion";
import { create_element } from "../../core/gui/dom";
import { LogManager } from "../../core/Logger";
const logger = LogManager.get("quest_editor/controllers/QuestEditorToolBarController");
export type AreaAndLabel = { readonly area: AreaModel; readonly label: string };
export class QuestEditorToolBarController extends Controller {
private quest_filename?: string;
readonly vm_feature_active: boolean;
readonly areas: Property<readonly AreaAndLabel[]>;
readonly current_area: Property<AreaAndLabel>;
readonly can_save: Property<boolean>;
readonly can_undo: Property<boolean>;
readonly can_redo: Property<boolean>;
readonly can_select_area: Property<boolean>;
readonly can_debug: Property<boolean>;
readonly can_step: Property<boolean>;
readonly can_stop: Property<boolean>;
constructor(
gui_store: GuiStore,
private readonly area_store: AreaStore,
private readonly quest_editor_store: QuestEditorStore,
) {
super();
this.vm_feature_active = gui_store.feature_active("vm");
// Ensure the areas list is updated when entities are added or removed (the count in the
// label should update).
this.areas = quest_editor_store.current_quest.flat_map(quest => {
if (quest) {
return quest?.entities_per_area.flat_map(entities_per_area => {
return list_property<AreaAndLabel>(
undefined,
...area_store.get_areas_for_episode(quest.episode).map(area => {
const entity_count = entities_per_area.get(area.id);
return {
area,
label: area.name + (entity_count ? ` (${entity_count})` : ""),
};
}),
);
});
} else {
return list_property<AreaAndLabel>();
}
});
this.current_area = map(
(areas, area) => areas.find(al => al.area == area)!,
this.areas,
quest_editor_store.current_area,
);
const quest_loaded = quest_editor_store.current_quest.map(q => q != undefined);
this.can_save = quest_loaded;
this.can_select_area = quest_loaded;
this.can_debug = quest_loaded;
this.can_undo = map(
(c, r) => c && !r,
undo_manager.can_undo,
quest_editor_store.quest_runner.running,
);
this.can_redo = map(
(c, r) => c && !r,
undo_manager.can_redo,
quest_editor_store.quest_runner.running,
);
this.can_step = quest_editor_store.quest_runner.paused;
this.can_stop = quest_editor_store.quest_runner.running;
this.disposables(
quest_editor_store.current_quest.observe(() => {
this.quest_filename = undefined;
}),
gui_store.on_global_keydown(GuiTool.QuestEditor, "Ctrl-O", () => {
const input: HTMLInputElement = create_element("input");
input.type = "file";
input.onchange = () => {
if (input.files && input.files.length) {
this.open_file(input.files[0]);
}
};
input.click();
}),
gui_store.on_global_keydown(GuiTool.QuestEditor, "Ctrl-Shift-S", this.save_as),
gui_store.on_global_keydown(GuiTool.QuestEditor, "Ctrl-Z", () => {
undo_manager.undo();
}),
gui_store.on_global_keydown(GuiTool.QuestEditor, "Ctrl-Shift-Z", () => {
undo_manager.redo();
}),
gui_store.on_global_keydown(GuiTool.QuestEditor, "F5", this.debug),
gui_store.on_global_keydown(GuiTool.QuestEditor, "Shift-F5", this.stop),
gui_store.on_global_keydown(GuiTool.QuestEditor, "F6", this.resume),
gui_store.on_global_keydown(GuiTool.QuestEditor, "F8", this.step_over),
gui_store.on_global_keydown(GuiTool.QuestEditor, "F7", this.step_in),
gui_store.on_global_keydown(GuiTool.QuestEditor, "Shift-F8", this.step_out),
);
}
create_new_quest = async (episode: Episode): Promise<void> =>
this.quest_editor_store.set_quest(create_new_quest(this.area_store, episode));
// TODO: notify user of problems.
open_file = async (file: File): Promise<void> => {
try {
const buffer = await read_file(file);
const quest = parse_quest(new ArrayBufferCursor(buffer, Endianness.Little));
if (!quest) {
logger.error("Couldn't parse quest file.");
}
await this.quest_editor_store.set_quest(
quest && convert_quest_to_model(this.area_store, quest),
);
this.quest_filename = file.name;
} catch (e) {
logger.error("Couldn't read file.", e);
}
};
set_area = ({ area }: AreaAndLabel): void => {
this.quest_editor_store.set_current_area(area);
};
save_as = (): void => {
const quest = this.quest_editor_store.current_quest.val;
if (!quest) return;
let default_file_name = this.quest_filename;
if (default_file_name) {
const ext_start = default_file_name.lastIndexOf(".");
if (ext_start !== -1) default_file_name = default_file_name.slice(0, ext_start);
}
let file_name = prompt("File name:", default_file_name);
if (!file_name) return;
const buffer = write_quest_qst(convert_quest_from_model(quest), file_name);
if (!file_name.endsWith(".qst")) {
file_name += ".qst";
}
const a = document.createElement("a");
a.href = URL.createObjectURL(new Blob([buffer], { type: "application/octet-stream" }));
a.download = file_name;
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(a.href);
document.body.removeChild(a);
};
debug = (): void => {
const quest = this.quest_editor_store.current_quest.val;
if (quest) {
this.quest_editor_store.quest_runner.run(quest);
}
};
resume = (): void => {
this.quest_editor_store.quest_runner.resume();
};
step_over = (): void => {
this.quest_editor_store.quest_runner.step_over();
};
step_in = (): void => {
this.quest_editor_store.quest_runner.step_into();
};
step_out = (): void => {
this.quest_editor_store.quest_runner.step_out();
};
stop = (): void => {
this.quest_editor_store.quest_runner.stop();
};
}