Event DAG edges are now shown with lines.

This commit is contained in:
Daan Vanden Bosch 2019-10-31 19:11:14 +01:00
parent 95da6e9e57
commit f1fa19238d
6 changed files with 170 additions and 118 deletions

View File

@ -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%);

View File

@ -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);
}

View File

@ -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];
};

View File

@ -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 : [];

View File

@ -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;
}

View File

@ -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,
});
}