60 void Draw(
bool* )
override {
64 if (project && project->hack_manifest.loaded()) {
70 ImGui::TextDisabled(
"No Oracle project loaded");
72 "Open a project with a hack manifest to view story events.");
81 if (!graph.loaded()) {
82 ImGui::TextDisabled(
"No story events data available");
87 if (ImGui::Button(
"Reset View")) {
93 ImGui::SliderFloat(
"Zoom", &
zoom_, 0.3f, 2.0f,
"%.1f");
95 ImGui::Text(
"Nodes: %zu Edges: %zu", graph.nodes().size(),
96 graph.edges().size());
99 if (prog_opt.has_value()) {
100 ImGui::TextDisabled(
"Crystals: %d State: %s",
101 prog_opt->GetCrystalCount(),
102 prog_opt->GetGameStateName().c_str());
104 ImGui::TextDisabled(
"No SRAM loaded");
108 if (ImGui::SmallButton(
"Import .srm...")) {
112 if (ImGui::SmallButton(
"Clear SRAM")) {
120 ImGui::TextDisabled(
"SRM: %s", p.filename().string().c_str());
121 if (ImGui::IsItemHovered()) {
127 ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f),
"SRM error: %s",
139 ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
140 ImVec2 canvas_size = ImGui::GetContentRegionAvail();
144 canvas_size.x -= sidebar_width;
146 if (canvas_size.x < 100 || canvas_size.y < 100)
return;
148 ImGui::InvisibleButton(
"story_canvas", canvas_size,
149 ImGuiButtonFlags_MouseButtonLeft |
150 ImGuiButtonFlags_MouseButtonRight);
152 bool is_hovered = ImGui::IsItemHovered();
153 bool is_active = ImGui::IsItemActive();
156 if (is_active && ImGui::IsMouseDragging(ImGuiMouseButton_Right)) {
157 ImVec2 delta = ImGui::GetIO().MouseDelta;
164 float wheel = ImGui::GetIO().MouseWheel;
166 zoom_ *= (wheel > 0) ? 1.1f : 0.9f;
172 ImDrawList* draw_list = ImGui::GetWindowDrawList();
175 draw_list->PushClipRect(canvas_pos,
176 ImVec2(canvas_pos.x + canvas_size.x,
177 canvas_pos.y + canvas_size.y),
181 float cx = canvas_pos.x + canvas_size.x * 0.5f +
scroll_x_;
182 float cy = canvas_pos.y + canvas_size.y * 0.5f +
scroll_y_;
185 for (
const auto& edge : graph.edges()) {
186 const auto* from_node = graph.GetNode(edge.from);
187 const auto* to_node = graph.GetNode(edge.to);
188 if (!from_node || !to_node)
continue;
194 cy + from_node->pos_y *
zoom_);
196 cy + to_node->pos_y *
zoom_);
199 float ctrl_dx = (p2.x - p1.x) * 0.4f;
200 ImVec2 cp1(p1.x + ctrl_dx, p1.y);
201 ImVec2 cp2(p2.x - ctrl_dx, p2.y);
203 draw_list->AddBezierCubic(p1, cp1, cp2, p2, IM_COL32(150, 150, 150, 180),
207 ImVec2 dir(p2.x - cp2.x, p2.y - cp2.y);
208 float len = sqrtf(dir.x * dir.x + dir.y * dir.y);
212 float arrow_size = 8.0f *
zoom_;
213 ImVec2 arrow1(p2.x - dir.x * arrow_size + dir.y * arrow_size * 0.4f,
214 p2.y - dir.y * arrow_size - dir.x * arrow_size * 0.4f);
215 ImVec2 arrow2(p2.x - dir.x * arrow_size - dir.y * arrow_size * 0.4f,
216 p2.y - dir.y * arrow_size + dir.x * arrow_size * 0.4f);
217 draw_list->AddTriangleFilled(p2, arrow1, arrow2,
218 IM_COL32(150, 150, 150, 200));
223 ImVec2 mouse_pos = ImGui::GetIO().MousePos;
225 for (
const auto& node : graph.nodes()) {
235 ImVec2 node_min(nx, ny);
236 ImVec2 node_max(nx + nw, ny + nh);
242 ImU32 border_color = selected ? IM_COL32(255, 255, 100, 255)
243 : (query_match ? IM_COL32(220, 220, 220, 255)
244 : IM_COL32(60, 60, 60, 255));
246 draw_list->AddRectFilled(node_min, node_max, fill_color, 8.0f *
zoom_);
247 draw_list->AddRect(node_min, node_max, border_color, 8.0f *
zoom_,
251 float font_size = 11.0f *
zoom_;
252 if (font_size >= 6.0f) {
254 draw_list->AddText(
nullptr, font_size,
256 IM_COL32(200, 200, 200, 255),
259 std::string display_name = node.name;
260 if (display_name.length() > 25) {
261 display_name = display_name.substr(0, 22) +
"...";
263 draw_list->AddText(
nullptr, font_size,
265 IM_COL32(255, 255, 255, 255),
266 display_name.c_str());
270 if (is_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
271 if (mouse_pos.x >= node_min.x && mouse_pos.x <= node_max.x &&
272 mouse_pos.y >= node_min.y && mouse_pos.y <= node_max.y) {
278 draw_list->PopClipRect();
311 ImGui::BeginChild(
"node_detail", ImVec2(240, 0), ImGuiChildFlags_Borders);
313 ImGui::TextWrapped(
"%s", node->name.c_str());
314 ImGui::TextDisabled(
"%s", node->id.c_str());
317 if (!node->flags.empty()) {
318 ImGui::Text(
"Flags:");
319 for (
const auto& flag : node->flags) {
320 if (!flag.value.empty()) {
321 ImGui::BulletText(
"%s = %s", flag.name.c_str(), flag.value.c_str());
323 ImGui::BulletText(
"%s", flag.name.c_str());
328 if (!node->locations.empty()) {
330 ImGui::Text(
"Locations:");
331 for (
size_t i = 0; i < node->locations.size(); ++i) {
332 const auto& loc = node->locations[i];
333 ImGui::PushID(
static_cast<int>(i));
335 ImGui::BulletText(
"%s", loc.name.c_str());
339 if (ImGui::SmallButton(
"Room")) {
345 if (ImGui::SmallButton(
"Map")) {
350 if (!loc.room_id.empty() || !loc.overworld_id.empty() ||
351 !loc.entrance_id.empty()) {
352 ImGui::TextDisabled(
"room=%s map=%s entrance=%s",
353 loc.room_id.empty() ?
"-" : loc.room_id.c_str(),
354 loc.overworld_id.empty() ?
"-" : loc.overworld_id.c_str(),
355 loc.entrance_id.empty() ?
"-" : loc.entrance_id.c_str());
362 if (!node->text_ids.empty()) {
364 ImGui::Text(
"Text IDs:");
365 for (
size_t i = 0; i < node->text_ids.size(); ++i) {
366 const auto& tid = node->text_ids[i];
367 ImGui::PushID(
static_cast<int>(i));
369 ImGui::BulletText(
"%s", tid.c_str());
371 if (ImGui::SmallButton(
"Open")) {
377 if (ImGui::SmallButton(
"Copy")) {
378 ImGui::SetClipboardText(tid.c_str());
385 if (!node->scripts.empty()) {
387 ImGui::Text(
"Scripts:");
388 for (
size_t i = 0; i < node->scripts.size(); ++i) {
389 const auto& script = node->scripts[i];
390 ImGui::PushID(
static_cast<int>(i));
391 ImGui::BulletText(
"%s", script.c_str());
393 if (ImGui::SmallButton(
"Open")) {
397 if (ImGui::SmallButton(
"Copy")) {
398 ImGui::SetClipboardText(script.c_str());
404 if (!node->notes.empty()) {
406 ImGui::TextWrapped(
"Notes: %s", node->notes.c_str());