diff options
Diffstat (limited to 'src/subway_map.cpp')
| -rw-r--r-- | src/subway_map.cpp | 728 |
1 files changed, 728 insertions, 0 deletions
| diff --git a/src/subway_map.cpp b/src/subway_map.cpp new file mode 100644 index 0000000..408c4f0 --- /dev/null +++ b/src/subway_map.cpp | |||
| @@ -0,0 +1,728 @@ | |||
| 1 | #include "subway_map.h" | ||
| 2 | |||
| 3 | #include <wx/dcbuffer.h> | ||
| 4 | #include <wx/dcgraph.h> | ||
| 5 | |||
| 6 | #include <sstream> | ||
| 7 | |||
| 8 | #include "ap_state.h" | ||
| 9 | #include "game_data.h" | ||
| 10 | #include "global.h" | ||
| 11 | #include "tracker_state.h" | ||
| 12 | |||
| 13 | constexpr int AREA_ACTUAL_SIZE = 21; | ||
| 14 | constexpr int OWL_ACTUAL_SIZE = 32; | ||
| 15 | |||
| 16 | enum class ItemDrawType { kNone, kBox, kOwl }; | ||
| 17 | |||
| 18 | SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) { | ||
| 19 | SetBackgroundStyle(wxBG_STYLE_PAINT); | ||
| 20 | |||
| 21 | map_image_ = | ||
| 22 | wxImage(GetAbsolutePath("assets/subway.png").c_str(), wxBITMAP_TYPE_PNG); | ||
| 23 | if (!map_image_.IsOk()) { | ||
| 24 | return; | ||
| 25 | } | ||
| 26 | |||
| 27 | owl_image_ = | ||
| 28 | wxImage(GetAbsolutePath("assets/owl.png").c_str(), wxBITMAP_TYPE_PNG); | ||
| 29 | if (!owl_image_.IsOk()) { | ||
| 30 | return; | ||
| 31 | } | ||
| 32 | |||
| 33 | unchecked_eye_ = | ||
| 34 | wxBitmap(wxImage(GetAbsolutePath("assets/unchecked.png").c_str(), | ||
| 35 | wxBITMAP_TYPE_PNG) | ||
| 36 | .Scale(32, 32)); | ||
| 37 | checked_eye_ = wxBitmap( | ||
| 38 | wxImage(GetAbsolutePath("assets/checked.png").c_str(), wxBITMAP_TYPE_PNG) | ||
| 39 | .Scale(32, 32)); | ||
| 40 | |||
| 41 | tree_ = std::make_unique<quadtree::Quadtree<int, GetItemBox>>( | ||
| 42 | quadtree::Box<float>{0, 0, static_cast<float>(map_image_.GetWidth()), | ||
| 43 | static_cast<float>(map_image_.GetHeight())}); | ||
| 44 | for (const SubwayItem &subway_item : GD_GetSubwayItems()) { | ||
| 45 | tree_->add(subway_item.id); | ||
| 46 | } | ||
| 47 | |||
| 48 | Redraw(); | ||
| 49 | |||
| 50 | scroll_timer_ = new wxTimer(this); | ||
| 51 | |||
| 52 | Bind(wxEVT_PAINT, &SubwayMap::OnPaint, this); | ||
| 53 | Bind(wxEVT_MOTION, &SubwayMap::OnMouseMove, this); | ||
| 54 | Bind(wxEVT_MOUSEWHEEL, &SubwayMap::OnMouseScroll, this); | ||
| 55 | Bind(wxEVT_LEAVE_WINDOW, &SubwayMap::OnMouseLeave, this); | ||
| 56 | Bind(wxEVT_LEFT_DOWN, &SubwayMap::OnMouseClick, this); | ||
| 57 | Bind(wxEVT_TIMER, &SubwayMap::OnTimer, this); | ||
| 58 | |||
| 59 | zoom_slider_ = new wxSlider(this, wxID_ANY, 0, 0, 8, {15, 15}); | ||
| 60 | zoom_slider_->Bind(wxEVT_SLIDER, &SubwayMap::OnZoomSlide, this); | ||
| 61 | |||
| 62 | help_button_ = new wxButton(this, wxID_ANY, "Help"); | ||
| 63 | help_button_->Bind(wxEVT_BUTTON, &SubwayMap::OnClickHelp, this); | ||
| 64 | SetUpHelpButton(); | ||
| 65 | } | ||
| 66 | |||
| 67 | void SubwayMap::OnConnect() { | ||
| 68 | networks_.Clear(); | ||
| 69 | |||
| 70 | std::map<std::string, std::vector<int>> tagged; | ||
| 71 | for (const SubwayItem &subway_item : GD_GetSubwayItems()) { | ||
| 72 | if (AP_HasEarlyColorHallways() && | ||
| 73 | (subway_item.special == "starting_room_paintings" || | ||
| 74 | subway_item.special == "early_color_hallways")) { | ||
| 75 | tagged["early_color_hallways"].push_back(subway_item.id); | ||
| 76 | } | ||
| 77 | |||
| 78 | if (AP_IsPaintingShuffle() && !subway_item.paintings.empty()) { | ||
| 79 | continue; | ||
| 80 | } | ||
| 81 | |||
| 82 | for (const std::string &tag : subway_item.tags) { | ||
| 83 | tagged[tag].push_back(subway_item.id); | ||
| 84 | } | ||
| 85 | |||
| 86 | if (!AP_IsSunwarpShuffle() && subway_item.sunwarp && | ||
| 87 | subway_item.sunwarp->type != SubwaySunwarpType::kFinal) { | ||
| 88 | std::ostringstream tag; | ||
| 89 | tag << "sunwarp" << subway_item.sunwarp->dots; | ||
| 90 | |||
| 91 | tagged[tag.str()].push_back(subway_item.id); | ||
| 92 | } | ||
| 93 | |||
| 94 | if (!AP_IsPilgrimageEnabled() && | ||
| 95 | (subway_item.special == "sun_painting" || | ||
| 96 | subway_item.special == "sun_painting_exit")) { | ||
| 97 | tagged["sun_painting"].push_back(subway_item.id); | ||
| 98 | } | ||
| 99 | } | ||
| 100 | |||
| 101 | if (AP_IsSunwarpShuffle()) { | ||
| 102 | for (const auto &[index, mapping] : AP_GetSunwarpMapping()) { | ||
| 103 | std::ostringstream tag; | ||
| 104 | tag << "sunwarp" << mapping.dots; | ||
| 105 | |||
| 106 | SubwaySunwarp fromWarp; | ||
| 107 | if (index < 6) { | ||
| 108 | fromWarp.dots = index + 1; | ||
| 109 | fromWarp.type = SubwaySunwarpType::kEnter; | ||
| 110 | } else { | ||
| 111 | fromWarp.dots = index - 5; | ||
| 112 | fromWarp.type = SubwaySunwarpType::kExit; | ||
| 113 | } | ||
| 114 | |||
| 115 | SubwaySunwarp toWarp; | ||
| 116 | if (mapping.exit_index < 6) { | ||
| 117 | toWarp.dots = mapping.exit_index + 1; | ||
| 118 | toWarp.type = SubwaySunwarpType::kEnter; | ||
| 119 | } else { | ||
| 120 | toWarp.dots = mapping.exit_index - 5; | ||
| 121 | toWarp.type = SubwaySunwarpType::kExit; | ||
| 122 | } | ||
| 123 | |||
| 124 | tagged[tag.str()].push_back(GD_GetSubwayItemForSunwarp(fromWarp)); | ||
| 125 | tagged[tag.str()].push_back(GD_GetSubwayItemForSunwarp(toWarp)); | ||
| 126 | } | ||
| 127 | } | ||
| 128 | |||
| 129 | for (const auto &[tag, items] : tagged) { | ||
| 130 | // Pairwise connect all items with the same tag. | ||
| 131 | for (auto tag_it1 = items.begin(); std::next(tag_it1) != items.end(); | ||
| 132 | tag_it1++) { | ||
| 133 | for (auto tag_it2 = std::next(tag_it1); tag_it2 != items.end(); | ||
| 134 | tag_it2++) { | ||
| 135 | networks_.AddLink(*tag_it1, *tag_it2); | ||
| 136 | } | ||
| 137 | } | ||
| 138 | } | ||
| 139 | |||
| 140 | checked_paintings_.clear(); | ||
| 141 | } | ||
| 142 | |||
| 143 | void SubwayMap::UpdateIndicators() { | ||
| 144 | if (AP_IsPaintingShuffle()) { | ||
| 145 | for (const std::string &painting_id : AP_GetCheckedPaintings()) { | ||
| 146 | if (!checked_paintings_.count(painting_id)) { | ||
| 147 | checked_paintings_.insert(painting_id); | ||
| 148 | |||
| 149 | if (AP_GetPaintingMapping().count(painting_id)) { | ||
| 150 | networks_.AddLink(GD_GetSubwayItemForPainting(painting_id), | ||
| 151 | GD_GetSubwayItemForPainting( | ||
| 152 | AP_GetPaintingMapping().at(painting_id))); | ||
| 153 | } | ||
| 154 | } | ||
| 155 | } | ||
| 156 | } | ||
| 157 | |||
| 158 | Redraw(); | ||
| 159 | } | ||
| 160 | |||
| 161 | void SubwayMap::UpdateSunwarp(SubwaySunwarp from_sunwarp, | ||
| 162 | SubwaySunwarp to_sunwarp) { | ||
| 163 | networks_.AddLink(GD_GetSubwayItemForSunwarp(from_sunwarp), | ||
| 164 | GD_GetSubwayItemForSunwarp(to_sunwarp)); | ||
| 165 | } | ||
| 166 | |||
| 167 | void SubwayMap::Zoom(bool in) { | ||
| 168 | wxPoint focus_point; | ||
| 169 | |||
| 170 | if (mouse_position_) { | ||
| 171 | focus_point = *mouse_position_; | ||
| 172 | } else { | ||
| 173 | focus_point = {GetSize().GetWidth() / 2, GetSize().GetHeight() / 2}; | ||
| 174 | } | ||
| 175 | |||
| 176 | if (in) { | ||
| 177 | if (zoom_ < 3.0) { | ||
| 178 | SetZoom(zoom_ + 0.25, focus_point); | ||
| 179 | } | ||
| 180 | } else { | ||
| 181 | if (zoom_ > 1.0) { | ||
| 182 | SetZoom(zoom_ - 0.25, focus_point); | ||
| 183 | } | ||
| 184 | } | ||
| 185 | } | ||
| 186 | |||
| 187 | void SubwayMap::OnPaint(wxPaintEvent &event) { | ||
| 188 | if (GetSize() != rendered_.GetSize()) { | ||
| 189 | wxSize panel_size = GetSize(); | ||
| 190 | wxSize image_size = map_image_.GetSize(); | ||
| 191 | |||
| 192 | render_x_ = 0; | ||
| 193 | render_y_ = 0; | ||
| 194 | render_width_ = panel_size.GetWidth(); | ||
| 195 | render_height_ = panel_size.GetHeight(); | ||
| 196 | |||
| 197 | if (image_size.GetWidth() * panel_size.GetHeight() > | ||
| 198 | panel_size.GetWidth() * image_size.GetHeight()) { | ||
| 199 | render_height_ = (panel_size.GetWidth() * image_size.GetHeight()) / | ||
| 200 | image_size.GetWidth(); | ||
| 201 | render_y_ = (panel_size.GetHeight() - render_height_) / 2; | ||
| 202 | } else { | ||
| 203 | render_width_ = (image_size.GetWidth() * panel_size.GetHeight()) / | ||
| 204 | image_size.GetHeight(); | ||
| 205 | render_x_ = (panel_size.GetWidth() - render_width_) / 2; | ||
| 206 | } | ||
| 207 | |||
| 208 | SetZoomPos({zoom_x_, zoom_y_}); | ||
| 209 | |||
| 210 | SetUpHelpButton(); | ||
| 211 | } | ||
| 212 | |||
| 213 | wxBufferedPaintDC dc(this); | ||
| 214 | dc.SetBackground(*wxWHITE_BRUSH); | ||
| 215 | dc.Clear(); | ||
| 216 | |||
| 217 | { | ||
| 218 | wxMemoryDC rendered_dc; | ||
| 219 | rendered_dc.SelectObject(rendered_); | ||
| 220 | |||
| 221 | int dst_x; | ||
| 222 | int dst_y; | ||
| 223 | int dst_w; | ||
| 224 | int dst_h; | ||
| 225 | int src_x; | ||
| 226 | int src_y; | ||
| 227 | int src_w; | ||
| 228 | int src_h; | ||
| 229 | |||
| 230 | int zoomed_width = render_width_ * zoom_; | ||
| 231 | int zoomed_height = render_height_ * zoom_; | ||
| 232 | |||
| 233 | if (zoomed_width <= GetSize().GetWidth()) { | ||
| 234 | dst_x = (GetSize().GetWidth() - zoomed_width) / 2; | ||
| 235 | dst_w = zoomed_width; | ||
| 236 | src_x = 0; | ||
| 237 | src_w = map_image_.GetWidth(); | ||
| 238 | } else { | ||
| 239 | dst_x = 0; | ||
| 240 | dst_w = GetSize().GetWidth(); | ||
| 241 | src_x = -zoom_x_ * map_image_.GetWidth() / render_width_ / zoom_; | ||
| 242 | src_w = | ||
| 243 | GetSize().GetWidth() * map_image_.GetWidth() / render_width_ / zoom_; | ||
| 244 | } | ||
| 245 | |||
| 246 | if (zoomed_height <= GetSize().GetHeight()) { | ||
| 247 | dst_y = (GetSize().GetHeight() - zoomed_height) / 2; | ||
| 248 | dst_h = zoomed_height; | ||
| 249 | src_y = 0; | ||
| 250 | src_h = map_image_.GetHeight(); | ||
| 251 | } else { | ||
| 252 | dst_y = 0; | ||
| 253 | dst_h = GetSize().GetHeight(); | ||
| 254 | src_y = -zoom_y_ * map_image_.GetWidth() / render_width_ / zoom_; | ||
| 255 | src_h = | ||
| 256 | GetSize().GetHeight() * map_image_.GetWidth() / render_width_ / zoom_; | ||
| 257 | } | ||
| 258 | |||
| 259 | wxGCDC gcdc(dc); | ||
| 260 | gcdc.GetGraphicsContext()->SetInterpolationQuality(wxINTERPOLATION_GOOD); | ||
| 261 | gcdc.StretchBlit(dst_x, dst_y, dst_w, dst_h, &rendered_dc, src_x, src_y, | ||
| 262 | src_w, src_h); | ||
| 263 | } | ||
| 264 | |||
| 265 | if (hovered_item_) { | ||
| 266 | // Note that these requirements are duplicated on OnMouseClick so that it | ||
| 267 | // knows when an item has a hover effect. | ||
| 268 | const SubwayItem &subway_item = GD_GetSubwayItem(*hovered_item_); | ||
| 269 | if (subway_item.door && !GetDoorRequirements(*subway_item.door).empty()) { | ||
| 270 | const std::map<std::string, bool> &report = | ||
| 271 | GetDoorRequirements(*subway_item.door); | ||
| 272 | |||
| 273 | int acc_height = 10; | ||
| 274 | int col_width = 0; | ||
| 275 | |||
| 276 | for (const auto &[text, obtained] : report) { | ||
| 277 | wxSize item_extent = dc.GetTextExtent(text); | ||
| 278 | int item_height = std::max(32, item_extent.GetHeight()) + 10; | ||
| 279 | acc_height += item_height; | ||
| 280 | |||
| 281 | if (item_extent.GetWidth() > col_width) { | ||
| 282 | col_width = item_extent.GetWidth(); | ||
| 283 | } | ||
| 284 | } | ||
| 285 | |||
| 286 | int item_width = col_width + 10 + 32; | ||
| 287 | int full_width = item_width + 20; | ||
| 288 | |||
| 289 | wxPoint popup_pos = | ||
| 290 | MapPosToRenderPos({subway_item.x + AREA_ACTUAL_SIZE / 2, | ||
| 291 | subway_item.y + AREA_ACTUAL_SIZE / 2}); | ||
| 292 | |||
| 293 | if (popup_pos.x + full_width > GetSize().GetWidth()) { | ||
| 294 | popup_pos.x = GetSize().GetWidth() - full_width; | ||
| 295 | } | ||
| 296 | if (popup_pos.y + acc_height > GetSize().GetHeight()) { | ||
| 297 | popup_pos.y = GetSize().GetHeight() - acc_height; | ||
| 298 | } | ||
| 299 | |||
| 300 | dc.SetPen(*wxTRANSPARENT_PEN); | ||
| 301 | dc.SetBrush(*wxBLACK_BRUSH); | ||
| 302 | dc.DrawRectangle(popup_pos, {full_width, acc_height}); | ||
| 303 | |||
| 304 | dc.SetFont(GetFont()); | ||
| 305 | |||
| 306 | int cur_height = 10; | ||
| 307 | |||
| 308 | for (const auto &[text, obtained] : report) { | ||
| 309 | wxBitmap *eye_ptr = obtained ? &checked_eye_ : &unchecked_eye_; | ||
| 310 | |||
| 311 | dc.DrawBitmap(*eye_ptr, popup_pos + wxPoint{10, cur_height}); | ||
| 312 | |||
| 313 | dc.SetTextForeground(obtained ? *wxWHITE : *wxRED); | ||
| 314 | wxSize item_extent = dc.GetTextExtent(text); | ||
| 315 | dc.DrawText( | ||
| 316 | text, | ||
| 317 | popup_pos + | ||
| 318 | wxPoint{10 + 32 + 10, | ||
| 319 | cur_height + (32 - dc.GetFontMetrics().height) / 2}); | ||
| 320 | |||
| 321 | cur_height += 10 + 32; | ||
| 322 | } | ||
| 323 | } | ||
| 324 | |||
| 325 | if (networks_.IsItemInNetwork(*hovered_item_)) { | ||
| 326 | dc.SetBrush(*wxTRANSPARENT_BRUSH); | ||
| 327 | |||
| 328 | for (const auto &[item_id1, item_id2] : | ||
| 329 | networks_.GetNetworkGraph(*hovered_item_)) { | ||
| 330 | const SubwayItem &item1 = GD_GetSubwayItem(item_id1); | ||
| 331 | const SubwayItem &item2 = GD_GetSubwayItem(item_id2); | ||
| 332 | |||
| 333 | wxPoint item1_pos = MapPosToRenderPos( | ||
| 334 | {item1.x + AREA_ACTUAL_SIZE / 2, item1.y + AREA_ACTUAL_SIZE / 2}); | ||
| 335 | wxPoint item2_pos = MapPosToRenderPos( | ||
| 336 | {item2.x + AREA_ACTUAL_SIZE / 2, item2.y + AREA_ACTUAL_SIZE / 2}); | ||
| 337 | |||
| 338 | int left = std::min(item1_pos.x, item2_pos.x); | ||
| 339 | int top = std::min(item1_pos.y, item2_pos.y); | ||
| 340 | int right = std::max(item1_pos.x, item2_pos.x); | ||
| 341 | int bottom = std::max(item1_pos.y, item2_pos.y); | ||
| 342 | |||
| 343 | int halfwidth = right - left; | ||
| 344 | int halfheight = bottom - top; | ||
| 345 | |||
| 346 | if (halfwidth < 4 || halfheight < 4) { | ||
| 347 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4)); | ||
| 348 | dc.DrawLine(item1_pos, item2_pos); | ||
| 349 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2)); | ||
| 350 | dc.DrawLine(item1_pos, item2_pos); | ||
| 351 | } else { | ||
| 352 | int ellipse_x; | ||
| 353 | int ellipse_y; | ||
| 354 | double start; | ||
| 355 | double end; | ||
| 356 | |||
| 357 | if (item1_pos.x > item2_pos.x) { | ||
| 358 | ellipse_y = top; | ||
| 359 | |||
| 360 | if (item1_pos.y > item2_pos.y) { | ||
| 361 | ellipse_x = left - halfwidth; | ||
| 362 | |||
| 363 | start = 0; | ||
| 364 | end = 90; | ||
| 365 | } else { | ||
| 366 | ellipse_x = left; | ||
| 367 | |||
| 368 | start = 90; | ||
| 369 | end = 180; | ||
| 370 | } | ||
| 371 | } else { | ||
| 372 | ellipse_y = top - halfheight; | ||
| 373 | |||
| 374 | if (item1_pos.y > item2_pos.y) { | ||
| 375 | ellipse_x = left - halfwidth; | ||
| 376 | |||
| 377 | start = 270; | ||
| 378 | end = 360; | ||
| 379 | } else { | ||
| 380 | ellipse_x = left; | ||
| 381 | |||
| 382 | start = 180; | ||
| 383 | end = 270; | ||
| 384 | } | ||
| 385 | } | ||
| 386 | |||
| 387 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4)); | ||
| 388 | dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2, | ||
| 389 | halfheight * 2, start, end); | ||
| 390 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2)); | ||
| 391 | dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2, | ||
| 392 | halfheight * 2, start, end); | ||
| 393 | } | ||
| 394 | } | ||
| 395 | } | ||
| 396 | } | ||
| 397 | |||
| 398 | event.Skip(); | ||
| 399 | } | ||
| 400 | |||
| 401 | void SubwayMap::OnMouseMove(wxMouseEvent &event) { | ||
| 402 | wxPoint mouse_pos = RenderPosToMapPos(event.GetPosition()); | ||
| 403 | |||
| 404 | std::vector<int> hovered = tree_->query( | ||
| 405 | {static_cast<float>(mouse_pos.x), static_cast<float>(mouse_pos.y), 2, 2}); | ||
| 406 | if (!hovered.empty()) { | ||
| 407 | actual_hover_= hovered[0]; | ||
| 408 | } else { | ||
| 409 | actual_hover_ = std::nullopt; | ||
| 410 | } | ||
| 411 | |||
| 412 | if (!sticky_hover_ && actual_hover_ != hovered_item_) { | ||
| 413 | hovered_item_ = actual_hover_; | ||
| 414 | |||
| 415 | Refresh(); | ||
| 416 | } | ||
| 417 | |||
| 418 | if (scroll_mode_) { | ||
| 419 | EvaluateScroll(event.GetPosition()); | ||
| 420 | } | ||
| 421 | |||
| 422 | mouse_position_ = event.GetPosition(); | ||
| 423 | |||
| 424 | event.Skip(); | ||
| 425 | } | ||
| 426 | |||
| 427 | void SubwayMap::OnMouseScroll(wxMouseEvent &event) { | ||
| 428 | double new_zoom = zoom_; | ||
| 429 | if (event.GetWheelRotation() > 0) { | ||
| 430 | new_zoom = std::min(3.0, zoom_ + 0.25); | ||
| 431 | } else { | ||
| 432 | new_zoom = std::max(1.0, zoom_ - 0.25); | ||
| 433 | } | ||
| 434 | |||
| 435 | if (zoom_ != new_zoom) { | ||
| 436 | SetZoom(new_zoom, event.GetPosition()); | ||
| 437 | } | ||
| 438 | |||
| 439 | event.Skip(); | ||
| 440 | } | ||
| 441 | |||
| 442 | void SubwayMap::OnMouseLeave(wxMouseEvent &event) { | ||
| 443 | SetScrollSpeed(0, 0); | ||
| 444 | mouse_position_ = std::nullopt; | ||
| 445 | } | ||
| 446 | |||
| 447 | void SubwayMap::OnMouseClick(wxMouseEvent &event) { | ||
| 448 | bool finished = false; | ||
| 449 | |||
| 450 | if (actual_hover_) { | ||
| 451 | const SubwayItem &subway_item = GD_GetSubwayItem(*actual_hover_); | ||
| 452 | if ((subway_item.door && !GetDoorRequirements(*subway_item.door).empty()) || | ||
| 453 | networks_.IsItemInNetwork(*hovered_item_)) { | ||
| 454 | if (actual_hover_ != hovered_item_) { | ||
| 455 | hovered_item_ = actual_hover_; | ||
| 456 | |||
| 457 | if (!hovered_item_) { | ||
| 458 | sticky_hover_ = false; | ||
| 459 | } | ||
| 460 | |||
| 461 | Refresh(); | ||
| 462 | } else { | ||
| 463 | sticky_hover_ = !sticky_hover_; | ||
| 464 | } | ||
| 465 | |||
| 466 | finished = true; | ||
| 467 | } | ||
| 468 | } | ||
| 469 | |||
| 470 | if (!finished) { | ||
| 471 | if (scroll_mode_) { | ||
| 472 | scroll_mode_ = false; | ||
| 473 | |||
| 474 | SetScrollSpeed(0, 0); | ||
| 475 | |||
| 476 | SetCursor(wxCURSOR_ARROW); | ||
| 477 | } else if (event.GetPosition().x < GetSize().GetWidth() / 6 || | ||
| 478 | event.GetPosition().x > 5 * GetSize().GetWidth() / 6 || | ||
| 479 | event.GetPosition().y < GetSize().GetHeight() / 6 || | ||
| 480 | event.GetPosition().y > 5 * GetSize().GetHeight() / 6) { | ||
| 481 | scroll_mode_ = true; | ||
| 482 | |||
| 483 | EvaluateScroll(event.GetPosition()); | ||
| 484 | |||
| 485 | SetCursor(wxCURSOR_CROSS); | ||
| 486 | } else { | ||
| 487 | sticky_hover_ = false; | ||
| 488 | } | ||
| 489 | } | ||
| 490 | } | ||
| 491 | |||
| 492 | void SubwayMap::OnTimer(wxTimerEvent &event) { | ||
| 493 | SetZoomPos({zoom_x_ + scroll_x_, zoom_y_ + scroll_y_}); | ||
| 494 | Refresh(); | ||
| 495 | } | ||
| 496 | |||
| 497 | void SubwayMap::OnZoomSlide(wxCommandEvent &event) { | ||
| 498 | double new_zoom = 1.0 + 0.25 * zoom_slider_->GetValue(); | ||
| 499 | |||
| 500 | if (new_zoom != zoom_) { | ||
| 501 | SetZoom(new_zoom, {GetSize().GetWidth() / 2, GetSize().GetHeight() / 2}); | ||
| 502 | } | ||
| 503 | } | ||
| 504 | |||
| 505 | void SubwayMap::OnClickHelp(wxCommandEvent &event) { | ||
| 506 | wxMessageBox( | ||
| 507 | "Zoom in/out using the mouse wheel, Ctrl +/-, or the slider in the " | ||
| 508 | "corner.\nClick on a side of the screen to start panning. It will follow " | ||
| 509 | "your mouse. Click again to stop.\nHover over a door to see the " | ||
| 510 | "requirements to open it.\nHover over a warp or active painting to see " | ||
| 511 | "what it is connected to.\nIn painting shuffle, paintings that have not " | ||
| 512 | "yet been checked will not show their connections.\nA green shaded owl " | ||
| 513 | "means that there is a painting entrance there.\nA red shaded owl means " | ||
| 514 | "that there are only painting exits there.\nClick on a door or " | ||
| 515 | "warp to make the popup stick until you click again.", | ||
| 516 | "Subway Map Help"); | ||
| 517 | } | ||
| 518 | |||
| 519 | void SubwayMap::Redraw() { | ||
| 520 | rendered_ = wxBitmap(map_image_); | ||
| 521 | |||
| 522 | wxMemoryDC dc; | ||
| 523 | dc.SelectObject(rendered_); | ||
| 524 | |||
| 525 | wxGCDC gcdc(dc); | ||
| 526 | |||
| 527 | for (const SubwayItem &subway_item : GD_GetSubwayItems()) { | ||
| 528 | ItemDrawType draw_type = ItemDrawType::kNone; | ||
| 529 | const wxBrush *brush_color = wxGREY_BRUSH; | ||
| 530 | std::optional<wxColour> shade_color; | ||
| 531 | |||
| 532 | if (AP_HasEarlyColorHallways() && | ||
| 533 | (subway_item.special == "starting_room_paintings" || | ||
| 534 | subway_item.special == "early_color_hallways")) { | ||
| 535 | draw_type = ItemDrawType::kOwl; | ||
| 536 | |||
| 537 | if (subway_item.special == "starting_room_paintings") { | ||
| 538 | shade_color = wxColour(0, 255, 0, 128); | ||
| 539 | } else { | ||
| 540 | shade_color = wxColour(255, 0, 0, 128); | ||
| 541 | } | ||
| 542 | } else if (subway_item.special == "sun_painting") { | ||
| 543 | if (!AP_IsPilgrimageEnabled()) { | ||
| 544 | if (IsDoorOpen(*subway_item.door)) { | ||
| 545 | draw_type = ItemDrawType::kOwl; | ||
| 546 | shade_color = wxColour(0, 255, 0, 128); | ||
| 547 | } else { | ||
| 548 | draw_type = ItemDrawType::kBox; | ||
| 549 | brush_color = wxRED_BRUSH; | ||
| 550 | } | ||
| 551 | } | ||
| 552 | } else if (!subway_item.paintings.empty()) { | ||
| 553 | if (AP_IsPaintingShuffle()) { | ||
| 554 | bool has_checked_painting = false; | ||
| 555 | bool has_unchecked_painting = false; | ||
| 556 | bool has_mapped_painting = false; | ||
| 557 | bool has_codomain_painting = false; | ||
| 558 | |||
| 559 | for (const std::string &painting_id : subway_item.paintings) { | ||
| 560 | if (checked_paintings_.count(painting_id)) { | ||
| 561 | has_checked_painting = true; | ||
| 562 | |||
| 563 | if (AP_GetPaintingMapping().count(painting_id)) { | ||
| 564 | has_mapped_painting = true; | ||
| 565 | } else if (AP_IsPaintingMappedTo(painting_id)) { | ||
| 566 | has_codomain_painting = true; | ||
| 567 | } | ||
| 568 | } else { | ||
| 569 | has_unchecked_painting = true; | ||
| 570 | } | ||
| 571 | } | ||
| 572 | |||
| 573 | if (has_unchecked_painting || has_mapped_painting || has_codomain_painting) { | ||
| 574 | draw_type = ItemDrawType::kOwl; | ||
| 575 | |||
| 576 | if (has_checked_painting) { | ||
| 577 | if (has_mapped_painting) { | ||
| 578 | shade_color = wxColour(0, 255, 0, 128); | ||
| 579 | } else { | ||
| 580 | shade_color = wxColour(255, 0, 0, 128); | ||
| 581 | } | ||
| 582 | } | ||
| 583 | } | ||
| 584 | } else if (!subway_item.tags.empty()) { | ||
| 585 | draw_type = ItemDrawType::kOwl; | ||
| 586 | } | ||
| 587 | } else if (subway_item.door) { | ||
| 588 | draw_type = ItemDrawType::kBox; | ||
| 589 | |||
| 590 | if (IsDoorOpen(*subway_item.door)) { | ||
| 591 | brush_color = wxGREEN_BRUSH; | ||
| 592 | } else { | ||
| 593 | brush_color = wxRED_BRUSH; | ||
| 594 | } | ||
| 595 | } | ||
| 596 | |||
| 597 | wxPoint real_area_pos = {subway_item.x, subway_item.y}; | ||
| 598 | |||
| 599 | int real_area_size = | ||
| 600 | (draw_type == ItemDrawType::kOwl ? OWL_ACTUAL_SIZE : AREA_ACTUAL_SIZE); | ||
| 601 | |||
| 602 | if (draw_type == ItemDrawType::kBox) { | ||
| 603 | gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1)); | ||
| 604 | gcdc.SetBrush(*brush_color); | ||
| 605 | gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size}); | ||
| 606 | } else if (draw_type == ItemDrawType::kOwl) { | ||
| 607 | wxBitmap owl_bitmap = wxBitmap(owl_image_.Scale( | ||
| 608 | real_area_size, real_area_size, wxIMAGE_QUALITY_BILINEAR)); | ||
| 609 | gcdc.DrawBitmap(owl_bitmap, real_area_pos); | ||
| 610 | |||
| 611 | if (shade_color) { | ||
| 612 | gcdc.SetBrush(wxBrush(*shade_color)); | ||
| 613 | gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size}); | ||
| 614 | } | ||
| 615 | } | ||
| 616 | } | ||
| 617 | } | ||
| 618 | |||
| 619 | void SubwayMap::SetUpHelpButton() { | ||
| 620 | help_button_->SetPosition({ | ||
| 621 | GetSize().GetWidth() - help_button_->GetSize().GetWidth() - 15, | ||
| 622 | 15, | ||
| 623 | }); | ||
| 624 | } | ||
| 625 | |||
| 626 | void SubwayMap::EvaluateScroll(wxPoint pos) { | ||
| 627 | int scroll_x; | ||
| 628 | int scroll_y; | ||
| 629 | if (pos.x < GetSize().GetWidth() / 9) { | ||
| 630 | scroll_x = 20; | ||
| 631 | } else if (pos.x < GetSize().GetWidth() / 6) { | ||
| 632 | scroll_x = 5; | ||
| 633 | } else if (pos.x > 8 * GetSize().GetWidth() / 9) { | ||
| 634 | scroll_x = -20; | ||
| 635 | } else if (pos.x > 5 * GetSize().GetWidth() / 6) { | ||
| 636 | scroll_x = -5; | ||
| 637 | } else { | ||
| 638 | scroll_x = 0; | ||
| 639 | } | ||
| 640 | if (pos.y < GetSize().GetHeight() / 9) { | ||
| 641 | scroll_y = 20; | ||
| 642 | } else if (pos.y < GetSize().GetHeight() / 6) { | ||
| 643 | scroll_y = 5; | ||
| 644 | } else if (pos.y > 8 * GetSize().GetHeight() / 9) { | ||
| 645 | scroll_y = -20; | ||
| 646 | } else if (pos.y > 5 * GetSize().GetHeight() / 6) { | ||
| 647 | scroll_y = -5; | ||
| 648 | } else { | ||
| 649 | scroll_y = 0; | ||
| 650 | } | ||
| 651 | |||
| 652 | SetScrollSpeed(scroll_x, scroll_y); | ||
| 653 | } | ||
| 654 | |||
| 655 | wxPoint SubwayMap::MapPosToRenderPos(wxPoint pos) const { | ||
| 656 | return {static_cast<int>(pos.x * render_width_ * zoom_ / | ||
| 657 | map_image_.GetSize().GetWidth() + | ||
| 658 | zoom_x_), | ||
| 659 | static_cast<int>(pos.y * render_width_ * zoom_ / | ||
| 660 | map_image_.GetSize().GetWidth() + | ||
| 661 | zoom_y_)}; | ||
| 662 | } | ||
| 663 | |||
| 664 | wxPoint SubwayMap::MapPosToVirtualPos(wxPoint pos) const { | ||
| 665 | return {static_cast<int>(pos.x * render_width_ * zoom_ / | ||
| 666 | map_image_.GetSize().GetWidth()), | ||
| 667 | static_cast<int>(pos.y * render_width_ * zoom_ / | ||
| 668 | map_image_.GetSize().GetWidth())}; | ||
| 669 | } | ||
| 670 | |||
| 671 | wxPoint SubwayMap::RenderPosToMapPos(wxPoint pos) const { | ||
| 672 | return { | ||
| 673 | std::clamp(static_cast<int>((pos.x - zoom_x_) * map_image_.GetWidth() / | ||
| 674 | render_width_ / zoom_), | ||
| 675 | 0, map_image_.GetWidth() - 1), | ||
| 676 | std::clamp(static_cast<int>((pos.y - zoom_y_) * map_image_.GetWidth() / | ||
| 677 | render_width_ / zoom_), | ||
| 678 | 0, map_image_.GetHeight() - 1)}; | ||
| 679 | } | ||
| 680 | |||
| 681 | void SubwayMap::SetZoomPos(wxPoint pos) { | ||
| 682 | if (render_width_ * zoom_ <= GetSize().GetWidth()) { | ||
| 683 | zoom_x_ = (GetSize().GetWidth() - render_width_ * zoom_) / 2; | ||
| 684 | } else { | ||
| 685 | zoom_x_ = std::clamp( | ||
| 686 | pos.x, GetSize().GetWidth() - static_cast<int>(render_width_ * zoom_), | ||
| 687 | 0); | ||
| 688 | } | ||
| 689 | if (render_height_ * zoom_ <= GetSize().GetHeight()) { | ||
| 690 | zoom_y_ = (GetSize().GetHeight() - render_height_ * zoom_) / 2; | ||
| 691 | } else { | ||
| 692 | zoom_y_ = std::clamp( | ||
| 693 | pos.y, GetSize().GetHeight() - static_cast<int>(render_height_ * zoom_), | ||
| 694 | 0); | ||
| 695 | } | ||
| 696 | } | ||
| 697 | |||
| 698 | void SubwayMap::SetScrollSpeed(int scroll_x, int scroll_y) { | ||
| 699 | bool should_timer = (scroll_x != 0 || scroll_y != 0); | ||
| 700 | if (should_timer != scroll_timer_->IsRunning()) { | ||
| 701 | if (should_timer) { | ||
| 702 | scroll_timer_->Start(1000 / 60); | ||
| 703 | } else { | ||
| 704 | scroll_timer_->Stop(); | ||
| 705 | } | ||
| 706 | } | ||
| 707 | |||
| 708 | scroll_x_ = scroll_x; | ||
| 709 | scroll_y_ = scroll_y; | ||
| 710 | } | ||
| 711 | |||
| 712 | void SubwayMap::SetZoom(double zoom, wxPoint static_point) { | ||
| 713 | wxPoint map_pos = RenderPosToMapPos(static_point); | ||
| 714 | zoom_ = zoom; | ||
| 715 | |||
| 716 | wxPoint virtual_pos = MapPosToVirtualPos(map_pos); | ||
| 717 | SetZoomPos(-(virtual_pos - static_point)); | ||
| 718 | |||
| 719 | Refresh(); | ||
| 720 | |||
| 721 | zoom_slider_->SetValue((zoom - 1.0) / 0.25); | ||
| 722 | } | ||
| 723 | |||
| 724 | quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int &id) const { | ||
| 725 | const SubwayItem &subway_item = GD_GetSubwayItem(id); | ||
| 726 | return {static_cast<float>(subway_item.x), static_cast<float>(subway_item.y), | ||
| 727 | AREA_ACTUAL_SIZE, AREA_ACTUAL_SIZE}; | ||
| 728 | } | ||
