mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-05 15:28:29 +08:00
Event DAG edges are now shown with lines.
This commit is contained in:
parent
95da6e9e57
commit
f1fa19238d
@ -5,7 +5,8 @@
|
||||
--text-color: hsl(0, 0%, 80%);
|
||||
--text-color-disabled: hsl(0, 0%, 55%);
|
||||
--font-family: Verdana, Geneva, sans-serif;
|
||||
--border: solid 1px hsl(0, 0%, 25%);
|
||||
--border-color: hsl(0, 0%, 25%);
|
||||
--border: solid 1px var(--border-color);
|
||||
|
||||
/* Scrollbars */
|
||||
|
||||
@ -17,7 +18,7 @@
|
||||
--control-bg-color: hsl(0, 0%, 20%);
|
||||
--control-bg-color-hover: hsl(0, 0%, 25%);
|
||||
--control-text-color: hsl(0, 0%, 80%);
|
||||
--control-text-color-hover: hsl(0, 0%, 90%);
|
||||
--control-text-color-hover: hsl(0, 0%, 90%);
|
||||
--control-border: solid 1px hsl(0, 0%, 10%);
|
||||
|
||||
--control-inner-border: solid 1px hsl(0, 0%, 35%);
|
||||
|
@ -9,7 +9,14 @@
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.quest_editor_EventsView_chain {
|
||||
.quest_editor_EventsView_dag {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.quest_editor_EventsView_event {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@ -19,22 +26,14 @@
|
||||
background-color: hsl(0, 0%, 17%);
|
||||
}
|
||||
|
||||
.quest_editor_EventsView_chain_arrow {
|
||||
font-size: 18px; /* For icon */
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.quest_editor_EventsView_event:last-of-type .quest_editor_EventsView_chain_arrow {
|
||||
color: var(--text-color-disabled);
|
||||
}
|
||||
|
||||
.quest_editor_EventsView_event {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.quest_editor_EventsView_event th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.quest_editor_EventsView_edge {
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
border-left: solid 2px var(--border-color);
|
||||
border-top: solid 2px var(--border-color);
|
||||
border-bottom: solid 2px var(--border-color);
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { ResizableWidget } from "../../core/gui/ResizableWidget";
|
||||
import { bind_children_to, el, icon, Icon } from "../../core/gui/dom";
|
||||
import { bind_children_to, el } from "../../core/gui/dom";
|
||||
import { quest_editor_store } from "../stores/QuestEditorStore";
|
||||
import { QuestEventDagModel } from "../model/QuestEventDagModel";
|
||||
import { Disposer } from "../../core/observable/Disposer";
|
||||
import { NumberInput } from "../../core/gui/NumberInput";
|
||||
import "./EventsView.css";
|
||||
import { Button } from "../../core/gui/Button";
|
||||
import { Disposable } from "../../core/observable/Disposable";
|
||||
import { defer } from "lodash";
|
||||
|
||||
export class EventsView extends ResizableWidget {
|
||||
private readonly quest_disposer = this.disposable(new Disposer());
|
||||
@ -34,43 +34,108 @@ export class EventsView extends ResizableWidget {
|
||||
this.quest_disposer.add(
|
||||
bind_children_to(
|
||||
this.element,
|
||||
quest.event_dags.filtered(dag => dag.root_events.get(0).area_id === area.id),
|
||||
this.create_chain_element,
|
||||
quest.event_dags.filtered(dag => dag.area_id === area.id),
|
||||
this.create_dag_element,
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private create_chain_element = (dag: QuestEventDagModel): [HTMLElement, Disposable] => {
|
||||
private create_dag_element = (dag: QuestEventDagModel): [HTMLElement, Disposable] => {
|
||||
const disposer = new Disposer();
|
||||
const element = el.div(
|
||||
{ class: "quest_editor_EventsView_chain" },
|
||||
...dag.events.map(event =>
|
||||
el.div(
|
||||
{ class: "quest_editor_EventsView_event" },
|
||||
el.table(
|
||||
el.tr(el.th({ text: "ID:" }), el.td({ text: event.id.toString() })),
|
||||
el.tr(
|
||||
el.th({ text: "Section:" }),
|
||||
el.td(
|
||||
disposer.add(new NumberInput(event.section_id, { enabled: false }))
|
||||
.element,
|
||||
),
|
||||
),
|
||||
el.tr(el.th({ text: "Wave:" }), el.td({ text: event.wave.toString() })),
|
||||
el.tr(
|
||||
el.th({ text: "Delay:" }),
|
||||
el.td(
|
||||
disposer.add(new NumberInput(event.delay, { enabled: false }))
|
||||
.element,
|
||||
),
|
||||
const element = el.div({ class: "quest_editor_EventsView_dag" });
|
||||
const event_elements = new Map<number, { element: HTMLDivElement; position: number }>();
|
||||
|
||||
// Render events.
|
||||
dag.events.forEach((event, i) => {
|
||||
const event_element = el.div(
|
||||
{ class: "quest_editor_EventsView_event" },
|
||||
el.table(
|
||||
el.tr(el.th({ text: "ID:" }), el.td({ text: event.id.toString() })),
|
||||
el.tr(
|
||||
el.th({ text: "Section:" }),
|
||||
el.td(
|
||||
disposer.add(new NumberInput(event.section_id, { enabled: false }))
|
||||
.element,
|
||||
),
|
||||
),
|
||||
el.tr(el.th({ text: "Wave:" }), el.td({ text: event.wave.toString() })),
|
||||
el.tr(
|
||||
el.th({ text: "Delay:" }),
|
||||
el.td(
|
||||
disposer.add(new NumberInput(event.delay, { enabled: false })).element,
|
||||
),
|
||||
),
|
||||
el.div({ class: "quest_editor_EventsView_chain_arrow" }, icon(Icon.ArrowDown)),
|
||||
),
|
||||
),
|
||||
disposer.add(new Button("Add event", { icon_left: Icon.Plus, enabled: false })).element,
|
||||
);
|
||||
);
|
||||
|
||||
element.append(event_element);
|
||||
event_elements.set(event.id, { element: event_element, position: i });
|
||||
});
|
||||
|
||||
// Render edges.
|
||||
defer(() => {
|
||||
const SPACING = 8;
|
||||
const used_depths: boolean[][] = Array(dag.events.length - 1);
|
||||
|
||||
for (let i = 0; i < used_depths.length; i++) {
|
||||
used_depths[i] = [];
|
||||
}
|
||||
|
||||
let max_depth = 0;
|
||||
|
||||
for (const event of dag.events) {
|
||||
const { element: event_element, position } = event_elements.get(event.id)!;
|
||||
|
||||
const y_offset = event_element.offsetTop + event_element.offsetHeight;
|
||||
|
||||
for (const child of dag.get_children(event)) {
|
||||
const { element: child_element, position: child_position } = event_elements.get(
|
||||
child.id,
|
||||
)!;
|
||||
const child_y_offset = child_element.offsetTop;
|
||||
|
||||
const edge_element = el.div({ class: "quest_editor_EventsView_edge" });
|
||||
|
||||
const top = Math.min(y_offset, child_y_offset) - 20;
|
||||
const height = Math.max(y_offset, child_y_offset) - top + 20;
|
||||
|
||||
let depth = 1;
|
||||
const low_pos = Math.min(position, child_position);
|
||||
const high_pos = Math.max(position, child_position);
|
||||
|
||||
outer: while (true) {
|
||||
for (let i = low_pos; i < high_pos; i++) {
|
||||
if (used_depths[i][depth]) {
|
||||
depth++;
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
console.log(`${event.id} -> ${child.id}`, low_pos, high_pos, `depth: ${depth}`);
|
||||
|
||||
for (let i = low_pos; i < high_pos; i++) {
|
||||
used_depths[i][depth] = true;
|
||||
}
|
||||
|
||||
max_depth = Math.max(depth, max_depth);
|
||||
|
||||
const width = SPACING * depth;
|
||||
|
||||
edge_element.style.left = `${4 - width}px`;
|
||||
edge_element.style.top = `${top}px`;
|
||||
edge_element.style.width = `${width}px`;
|
||||
edge_element.style.height = `${height}px`;
|
||||
|
||||
element.append(edge_element);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(used_depths);
|
||||
element.style.marginLeft = `${SPACING * max_depth}px`;
|
||||
});
|
||||
|
||||
return [element, disposer];
|
||||
};
|
||||
|
@ -15,6 +15,8 @@ export class QuestEventDagModel {
|
||||
private readonly _root_events: WritableListProperty<QuestEventModel>;
|
||||
private meta: Map<QuestEventModel, QuestEventDagModelMeta>;
|
||||
|
||||
readonly area_id: number;
|
||||
|
||||
readonly events: QuestEventModel[];
|
||||
|
||||
/**
|
||||
@ -23,21 +25,23 @@ export class QuestEventDagModel {
|
||||
readonly root_events: ListProperty<QuestEventModel>;
|
||||
|
||||
constructor(
|
||||
area_id: number,
|
||||
events: QuestEventModel[],
|
||||
root_events: QuestEventModel[],
|
||||
meta: Map<QuestEventModel, QuestEventDagModelMeta>,
|
||||
) {
|
||||
if (!Number.isInteger(area_id)) throw new Error("area_id should be an integer.");
|
||||
if (!Array.isArray(events)) throw new Error("events should be an array.");
|
||||
if (!Array.isArray(root_events)) throw new Error("root_events should be an array.");
|
||||
if (!meta) throw new Error("meta is required.");
|
||||
|
||||
this.area_id = area_id;
|
||||
this.events = events;
|
||||
this._root_events = list_property(undefined, ...root_events);
|
||||
this.root_events = this._root_events;
|
||||
this.meta = meta;
|
||||
}
|
||||
|
||||
get_parents(event: QuestEventModel): readonly QuestEventModel[] {
|
||||
const meta = this.meta.get(event);
|
||||
return meta ? meta.parents : [];
|
||||
}
|
||||
|
||||
get_children(event: QuestEventModel): readonly QuestEventModel[] {
|
||||
const meta = this.meta.get(event);
|
||||
return meta ? meta.children : [];
|
||||
|
@ -11,29 +11,19 @@ export class QuestEventModel {
|
||||
readonly wave: number;
|
||||
readonly delay: number;
|
||||
readonly actions: ListProperty<QuestEventActionModel> = this._actions;
|
||||
readonly area_id: number;
|
||||
readonly unknown: number;
|
||||
|
||||
constructor(
|
||||
id: number,
|
||||
section_id: number,
|
||||
wave: number,
|
||||
delay: number,
|
||||
area_id: number,
|
||||
unknown: number,
|
||||
) {
|
||||
constructor(id: number, section_id: number, wave: number, delay: number, unknown: number) {
|
||||
if (!Number.isInteger(id)) throw new Error("id should be an integer.");
|
||||
if (!Number.isInteger(section_id)) throw new Error("section_id should be an integer.");
|
||||
if (!Number.isInteger(wave)) throw new Error("wave should be an integer.");
|
||||
if (!Number.isInteger(delay)) throw new Error("delay should be an integer.");
|
||||
if (!Number.isInteger(area_id)) throw new Error("area_id should be an integer.");
|
||||
if (!Number.isInteger(unknown)) throw new Error("unknown should be an integer.");
|
||||
|
||||
this.id = id;
|
||||
this.section_id = section_id;
|
||||
this.wave = wave;
|
||||
this.delay = delay;
|
||||
this.area_id = area_id;
|
||||
this.unknown = unknown;
|
||||
}
|
||||
|
||||
|
@ -70,47 +70,42 @@ export function convert_quest_to_model(quest: Quest): QuestModel {
|
||||
|
||||
function build_event_dags(dat_events: readonly DatEvent[]): QuestEventDagModel[] {
|
||||
// Build up a temporary data structure with partial data.
|
||||
// Maps id, section id and area id to data.
|
||||
const data_map = new Map<
|
||||
string,
|
||||
// Maps event id and area id to data.
|
||||
const data_map = new Map<string,
|
||||
{
|
||||
event?: QuestEventModel;
|
||||
area_id: number;
|
||||
parents: QuestEventModel[];
|
||||
child_ids: number[];
|
||||
}
|
||||
>();
|
||||
}>();
|
||||
|
||||
for (const event of dat_events) {
|
||||
const key = `${event.id}-${event.section_id}-${event.area_id}`;
|
||||
const key = `${event.id}-${event.area_id}`;
|
||||
let data = data_map.get(key);
|
||||
|
||||
let event_model: QuestEventModel;
|
||||
|
||||
if (data && data.event) {
|
||||
event_model = data.event;
|
||||
logger.warn(
|
||||
`Ignored duplicate event #${data.event.id} for section ${data.event.section_id} of area ${data.event.area_id}.`,
|
||||
);
|
||||
} else {
|
||||
event_model = new QuestEventModel(
|
||||
event.id,
|
||||
event.section_id,
|
||||
event.wave,
|
||||
event.delay,
|
||||
event.area_id,
|
||||
event.unknown,
|
||||
);
|
||||
logger.warn(`Ignored duplicate event #${event.id} for area ${event.area_id}.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
data.event = event_model;
|
||||
} else {
|
||||
data = {
|
||||
event: event_model,
|
||||
parents: [],
|
||||
child_ids: [],
|
||||
};
|
||||
data_map.set(key, data);
|
||||
}
|
||||
const event_model = new QuestEventModel(
|
||||
event.id,
|
||||
event.section_id,
|
||||
event.wave,
|
||||
event.delay,
|
||||
event.unknown,
|
||||
);
|
||||
|
||||
if (data) {
|
||||
data.event = event_model;
|
||||
} else {
|
||||
data = {
|
||||
event: event_model,
|
||||
area_id: event.area_id,
|
||||
parents: [],
|
||||
child_ids: [],
|
||||
};
|
||||
data_map.set(key, data);
|
||||
}
|
||||
|
||||
for (const action of event.actions) {
|
||||
@ -126,22 +121,22 @@ function build_event_dags(dat_events: readonly DatEvent[]): QuestEventDagModel[]
|
||||
case DatEventActionType.Lock:
|
||||
event_model.add_action(new QuestEventActionLockModel(action.door_id));
|
||||
break;
|
||||
case DatEventActionType.TriggerEvent:
|
||||
{
|
||||
data.child_ids.push(action.event_id);
|
||||
case DatEventActionType.TriggerEvent: {
|
||||
data.child_ids.push(action.event_id);
|
||||
|
||||
const child_key = `${action.event_id}-${event.section_id}-${event.area_id}`;
|
||||
const child_data = data_map.get(child_key);
|
||||
const child_key = `${action.event_id}-${event.area_id}`;
|
||||
const child_data = data_map.get(child_key);
|
||||
|
||||
if (child_data) {
|
||||
child_data.parents.push(event_model);
|
||||
} else {
|
||||
data_map.set(child_key, {
|
||||
parents: [event_model],
|
||||
child_ids: [],
|
||||
});
|
||||
}
|
||||
if (child_data) {
|
||||
child_data.parents.push(event_model);
|
||||
} else {
|
||||
data_map.set(child_key, {
|
||||
area_id: event.area_id,
|
||||
parents: [event_model],
|
||||
child_ids: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
logger.warn(`Unknown event action type: ${(action as any).type}.`);
|
||||
@ -153,16 +148,14 @@ function build_event_dags(dat_events: readonly DatEvent[]): QuestEventDagModel[]
|
||||
// Convert temporary structure to complete data structure used to build DAGs. Events that call
|
||||
// nonexistent events are filtered out. This final structure is completely sound.
|
||||
const event_to_full_data = new Map<QuestEventModel, QuestEventDagModelMeta>();
|
||||
const root_events: QuestEventModel[] = [];
|
||||
const root_events: { event: QuestEventModel; area_id: number }[] = [];
|
||||
|
||||
for (const data of data_map.values()) {
|
||||
if (data.event) {
|
||||
const children: QuestEventModel[] = [];
|
||||
|
||||
for (const child_id of data.child_ids) {
|
||||
const child = data_map.get(
|
||||
`${child_id}-${data.event.section_id}-${data.event.area_id}`,
|
||||
)!;
|
||||
const child = data_map.get(`${child_id}-${data.area_id}`)!;
|
||||
|
||||
if (child.event) {
|
||||
children.push(child.event);
|
||||
@ -177,7 +170,7 @@ function build_event_dags(dat_events: readonly DatEvent[]): QuestEventDagModel[]
|
||||
});
|
||||
|
||||
if (data.parents.length === 0) {
|
||||
root_events.push(data.event);
|
||||
root_events.push({ event: data.event, area_id: data.area_id });
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -186,7 +179,7 @@ function build_event_dags(dat_events: readonly DatEvent[]): QuestEventDagModel[]
|
||||
const event_dags: QuestEventDagModel[] = [];
|
||||
|
||||
while (root_events.length) {
|
||||
const event = root_events.shift()!;
|
||||
const { event, area_id } = root_events.shift()!;
|
||||
|
||||
const dag_events: QuestEventModel[] = [];
|
||||
const dag_root_events: QuestEventModel[] = [];
|
||||
@ -203,14 +196,14 @@ function build_event_dags(dat_events: readonly DatEvent[]): QuestEventDagModel[]
|
||||
);
|
||||
|
||||
for (const event of dag_root_events) {
|
||||
const i = root_events.indexOf(event);
|
||||
const i = root_events.findIndex(r => r.event === event);
|
||||
|
||||
if (i !== -1) {
|
||||
root_events.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
event_dags.push(new QuestEventDagModel(dag_events, dag_root_events, dag_meta));
|
||||
event_dags.push(new QuestEventDagModel(area_id, dag_events, dag_root_events, dag_meta));
|
||||
}
|
||||
|
||||
return event_dags;
|
||||
@ -328,7 +321,7 @@ function convert_quest_events_from_model(event_dags: readonly QuestEventDagModel
|
||||
wave: event.wave,
|
||||
delay: event.delay,
|
||||
actions,
|
||||
area_id: event.area_id,
|
||||
area_id: event_dag.area_id,
|
||||
unknown: event.unknown,
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user