about summary refs log tree commit diff stats
path: root/src/subway_map.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/subway_map.cpp')
-rw-r--r--src/subway_map.cpp842
1 files changed, 677 insertions, 165 deletions
diff --git a/src/subway_map.cpp b/src/subway_map.cpp index 6070fd5..55ac411 100644 --- a/src/subway_map.cpp +++ b/src/subway_map.cpp
@@ -1,22 +1,36 @@
1#include "subway_map.h" 1#include "subway_map.h"
2 2
3#include <fmt/core.h>
3#include <wx/dcbuffer.h> 4#include <wx/dcbuffer.h>
5#include <wx/dcgraph.h>
4 6
5#include <sstream> 7#include <sstream>
6 8
7#include "ap_state.h" 9#include "ap_state.h"
8#include "game_data.h" 10#include "game_data.h"
9#include "global.h" 11#include "global.h"
12#include "report_popup.h"
10#include "tracker_state.h" 13#include "tracker_state.h"
11 14
12constexpr int AREA_ACTUAL_SIZE = 21; 15constexpr int AREA_ACTUAL_SIZE = 21;
13constexpr int OWL_ACTUAL_SIZE = 32; 16constexpr int OWL_ACTUAL_SIZE = 32;
17constexpr int PAINTING_RADIUS = 9; // the actual circles on the map are radius 11
18constexpr int PAINTING_EXIT_RADIUS = 6;
14 19
15enum class ItemDrawType { 20enum class ItemDrawType { kNone, kBox, kOwl, kOwlExit };
16 kNone, 21
17 kBox, 22namespace {
18 kOwl 23
19}; 24wxPoint GetSubwayItemMapCenter(const SubwayItem &subway_item) {
25 if (subway_item.painting) {
26 return {subway_item.x, subway_item.y};
27 } else {
28 return {subway_item.x + AREA_ACTUAL_SIZE / 2,
29 subway_item.y + AREA_ACTUAL_SIZE / 2};
30 }
31}
32
33} // namespace
20 34
21SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) { 35SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
22 SetBackgroundStyle(wxBG_STYLE_PAINT); 36 SetBackgroundStyle(wxBG_STYLE_PAINT);
@@ -42,28 +56,109 @@ SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
42 56
43 Redraw(); 57 Redraw();
44 58
59 scroll_timer_ = new wxTimer(this);
60
45 Bind(wxEVT_PAINT, &SubwayMap::OnPaint, this); 61 Bind(wxEVT_PAINT, &SubwayMap::OnPaint, this);
46 Bind(wxEVT_MOTION, &SubwayMap::OnMouseMove, this); 62 Bind(wxEVT_MOTION, &SubwayMap::OnMouseMove, this);
63 Bind(wxEVT_MOUSEWHEEL, &SubwayMap::OnMouseScroll, this);
64 Bind(wxEVT_LEAVE_WINDOW, &SubwayMap::OnMouseLeave, this);
65 Bind(wxEVT_LEFT_DOWN, &SubwayMap::OnMouseClick, this);
66 Bind(wxEVT_TIMER, &SubwayMap::OnTimer, this);
67
68 zoom_slider_ = new wxSlider(this, wxID_ANY, 0, 0, 8, FromDIP(wxPoint{15, 15}));
69 zoom_slider_->Bind(wxEVT_SLIDER, &SubwayMap::OnZoomSlide, this);
70
71 help_button_ = new wxButton(this, wxID_ANY, "Help");
72 help_button_->Bind(wxEVT_BUTTON, &SubwayMap::OnClickHelp, this);
73 SetUpHelpButton();
74
75 report_popup_ = new ReportPopup(this);
47} 76}
48 77
49void SubwayMap::OnConnect() { 78void SubwayMap::OnConnect() {
50 networks_.Clear(); 79 networks_.Clear();
51 80
52 std::map<std::string, std::vector<int>> tagged; 81 std::map<std::string, std::vector<int>> tagged;
82 std::map<std::string, std::vector<int>> entrances;
83 std::map<std::string, std::vector<int>> exits;
53 for (const SubwayItem &subway_item : GD_GetSubwayItems()) { 84 for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
54 if (AP_IsPaintingShuffle() && !subway_item.paintings.empty()) { 85 if (AP_HasEarlyColorHallways() &&
86 subway_item.special == "early_color_hallways") {
87 entrances["early_ch"].push_back(subway_item.id);
88 }
89
90 if (AP_IsPaintingShuffle() && subway_item.painting) {
55 continue; 91 continue;
56 } 92 }
57 93
58 for (const std::string &tag : subway_item.tags) { 94 for (const std::string &tag : subway_item.tags) {
59 tagged[tag].push_back(subway_item.id); 95 tagged[tag].push_back(subway_item.id);
60 } 96 }
97 for (const std::string &tag : subway_item.entrances) {
98 entrances[tag].push_back(subway_item.id);
99 }
100 for (const std::string &tag : subway_item.exits) {
101 exits[tag].push_back(subway_item.id);
102 }
61 103
62 if (!AP_IsSunwarpShuffle() && subway_item.sunwarp && subway_item.sunwarp->type != SubwaySunwarpType::kFinal) { 104 if (!AP_IsSunwarpShuffle() && subway_item.sunwarp) {
63 std::ostringstream tag; 105 std::string tag = fmt::format("sunwarp{}", subway_item.sunwarp->dots);
64 tag << "sunwarp" << subway_item.sunwarp->dots; 106 switch (subway_item.sunwarp->type) {
107 case SubwaySunwarpType::kEnter:
108 entrances[tag].push_back(subway_item.id);
109 break;
110 case SubwaySunwarpType::kExit:
111 exits[tag].push_back(subway_item.id);
112 break;
113 default:
114 break;
115 }
116 }
65 117
66 tagged[tag.str()].push_back(subway_item.id); 118 if (!AP_IsPilgrimageEnabled()) {
119 if (subway_item.special == "sun_painting") {
120 entrances["sun_painting"].push_back(subway_item.id);
121 } else if (subway_item.special == "sun_painting_exit") {
122 exits["sun_painting"].push_back(subway_item.id);
123 }
124 }
125 }
126
127 if (AP_IsSunwarpShuffle()) {
128 sunwarp_mapping_ = AP_GetSunwarpMapping();
129
130 SubwaySunwarp final_sunwarp{.dots = 6, .type = SubwaySunwarpType::kFinal};
131 int final_sunwarp_item = GD_GetSubwayItemForSunwarp(final_sunwarp);
132
133 for (const auto &[index, mapping] : sunwarp_mapping_) {
134 std::string tag = fmt::format("sunwarp{}", mapping.dots);
135
136 SubwaySunwarp fromWarp;
137 if (index < 6) {
138 fromWarp.dots = index + 1;
139 fromWarp.type = SubwaySunwarpType::kEnter;
140 } else {
141 fromWarp.dots = index - 5;
142 fromWarp.type = SubwaySunwarpType::kExit;
143 }
144
145 SubwaySunwarp toWarp;
146 if (mapping.exit_index < 6) {
147 toWarp.dots = mapping.exit_index + 1;
148 toWarp.type = SubwaySunwarpType::kEnter;
149 } else {
150 toWarp.dots = mapping.exit_index - 5;
151 toWarp.type = SubwaySunwarpType::kExit;
152 }
153
154 entrances[tag].push_back(GD_GetSubwayItemForSunwarp(fromWarp));
155 exits[tag].push_back(GD_GetSubwayItemForSunwarp(toWarp));
156
157 networks_.AddLinkToNetwork(
158 final_sunwarp_item, GD_GetSubwayItemForSunwarp(fromWarp),
159 mapping.dots == 6 ? final_sunwarp_item
160 : GD_GetSubwayItemForSunwarp(toWarp),
161 false);
67 } 162 }
68 } 163 }
69 164
@@ -73,115 +168,243 @@ void SubwayMap::OnConnect() {
73 tag_it1++) { 168 tag_it1++) {
74 for (auto tag_it2 = std::next(tag_it1); tag_it2 != items.end(); 169 for (auto tag_it2 = std::next(tag_it1); tag_it2 != items.end();
75 tag_it2++) { 170 tag_it2++) {
76 networks_.AddLink(*tag_it1, *tag_it2); 171 // two links because tags are bi-directional
172 networks_.AddLink(*tag_it1, *tag_it2, true);
173 }
174 }
175 }
176
177 for (const auto &[tag, items] : entrances) {
178 if (!exits.contains(tag)) continue;
179 for (auto exit : exits[tag]) {
180 for (auto entrance : items) {
181 networks_.AddLink(entrance, exit, false);
77 } 182 }
78 } 183 }
79 } 184 }
80 185
81 checked_paintings_.clear(); 186 checked_paintings_.clear();
187
188 UpdateIndicators();
82} 189}
83 190
84void SubwayMap::UpdateIndicators() { 191void SubwayMap::UpdateIndicators() {
192 if (AP_IsSunwarpShuffle()) {
193 sunwarp_mapping_ = AP_GetSunwarpMapping();
194 }
195
85 if (AP_IsPaintingShuffle()) { 196 if (AP_IsPaintingShuffle()) {
86 for (const std::string &painting_id : AP_GetCheckedPaintings()) { 197 std::map<std::string, std::string> painting_mapping =
198 AP_GetPaintingMapping();
199 std::set<std::string> remote_checked_paintings = AP_GetCheckedPaintings();
200
201 for (const std::string &painting_id : remote_checked_paintings) {
87 if (!checked_paintings_.count(painting_id)) { 202 if (!checked_paintings_.count(painting_id)) {
88 checked_paintings_.insert(painting_id); 203 checked_paintings_.insert(painting_id);
89 204
90 if (AP_GetPaintingMapping().count(painting_id)) { 205 if (painting_mapping.count(painting_id)) {
91 networks_.AddLink(GD_GetSubwayItemForPainting(painting_id), 206 std::optional<int> from_id = GD_GetSubwayItemForPainting(painting_id);
92 GD_GetSubwayItemForPainting( 207 std::optional<int> to_id = GD_GetSubwayItemForPainting(painting_mapping.at(painting_id));
93 AP_GetPaintingMapping().at(painting_id))); 208
209 if (from_id && to_id) {
210 networks_.AddLink(*from_id, *to_id, false);
211 }
94 } 212 }
95 } 213 }
96 } 214 }
97 } 215 }
98 216
217 report_popup_->UpdateIndicators();
218
99 Redraw(); 219 Redraw();
100} 220}
101 221
102void SubwayMap::UpdateSunwarp(SubwaySunwarp from_sunwarp, 222void SubwayMap::UpdateSunwarp(SubwaySunwarp from_sunwarp,
103 SubwaySunwarp to_sunwarp) { 223 SubwaySunwarp to_sunwarp) {
104 networks_.AddLink(GD_GetSubwayItemForSunwarp(from_sunwarp), 224 networks_.AddLink(GD_GetSubwayItemForSunwarp(from_sunwarp),
105 GD_GetSubwayItemForSunwarp(to_sunwarp)); 225 GD_GetSubwayItemForSunwarp(to_sunwarp), false);
226}
227
228void SubwayMap::Zoom(bool in) {
229 wxPoint focus_point;
230
231 if (mouse_position_) {
232 focus_point = *mouse_position_;
233 } else {
234 focus_point = {GetSize().GetWidth() / 2, GetSize().GetHeight() / 2};
235 }
236
237 if (in) {
238 if (zoom_ < 3.0) {
239 SetZoom(zoom_ + 0.25, focus_point);
240 }
241 } else {
242 if (zoom_ > 1.0) {
243 SetZoom(zoom_ - 0.25, focus_point);
244 }
245 }
106} 246}
107 247
108void SubwayMap::OnPaint(wxPaintEvent &event) { 248void SubwayMap::OnPaint(wxPaintEvent &event) {
109 if (GetSize() != rendered_.GetSize()) { 249 if (GetSize() != rendered_.GetSize()) {
110 Redraw(); 250 wxSize panel_size = GetSize();
111 } 251 wxSize image_size = map_image_.GetSize();
252
253 render_x_ = 0;
254 render_y_ = 0;
255 render_width_ = panel_size.GetWidth();
256 render_height_ = panel_size.GetHeight();
257
258 if (image_size.GetWidth() * panel_size.GetHeight() >
259 panel_size.GetWidth() * image_size.GetHeight()) {
260 render_height_ = (panel_size.GetWidth() * image_size.GetHeight()) /
261 image_size.GetWidth();
262 render_y_ = (panel_size.GetHeight() - render_height_) / 2;
263 } else {
264 render_width_ = (image_size.GetWidth() * panel_size.GetHeight()) /
265 image_size.GetHeight();
266 render_x_ = (panel_size.GetWidth() - render_width_) / 2;
267 }
112 268
113 wxBufferedPaintDC dc(this); 269 SetZoomPos({zoom_x_, zoom_y_});
114 dc.DrawBitmap(rendered_, 0, 0);
115 270
116 if (hovered_item_ && networks_.IsItemInNetwork(*hovered_item_)) { 271 SetUpHelpButton();
117 dc.SetBrush(*wxTRANSPARENT_BRUSH);
118 272
119 for (const auto &[item_id1, item_id2] : 273 zoom_slider_->SetSize(FromDIP(15), FromDIP(15), wxDefaultCoord,
120 networks_.GetNetworkGraph(*hovered_item_)) { 274 wxDefaultCoord, wxSIZE_AUTO);
121 const SubwayItem &item1 = GD_GetSubwayItem(item_id1); 275 }
122 const SubwayItem &item2 = GD_GetSubwayItem(item_id2);
123 276
124 int item1_x = (item1.x + AREA_ACTUAL_SIZE / 2) * render_width_ / map_image_.GetWidth() + render_x_; 277 wxBufferedPaintDC dc(this);
125 int item1_y = (item1.y + AREA_ACTUAL_SIZE / 2) * render_width_ / map_image_.GetWidth() + render_y_; 278 dc.SetBackground(*wxWHITE_BRUSH);
279 dc.Clear();
280
281 {
282 wxMemoryDC rendered_dc;
283 rendered_dc.SelectObject(rendered_);
284
285 int dst_x;
286 int dst_y;
287 int dst_w;
288 int dst_h;
289 int src_x;
290 int src_y;
291 int src_w;
292 int src_h;
293
294 int zoomed_width = render_width_ * zoom_;
295 int zoomed_height = render_height_ * zoom_;
296
297 if (zoomed_width <= GetSize().GetWidth()) {
298 dst_x = (GetSize().GetWidth() - zoomed_width) / 2;
299 dst_w = zoomed_width;
300 src_x = 0;
301 src_w = map_image_.GetWidth();
302 } else {
303 dst_x = 0;
304 dst_w = GetSize().GetWidth();
305 src_x = -zoom_x_ * map_image_.GetWidth() / render_width_ / zoom_;
306 src_w =
307 GetSize().GetWidth() * map_image_.GetWidth() / render_width_ / zoom_;
308 }
126 309
127 int item2_x = (item2.x + AREA_ACTUAL_SIZE / 2) * render_width_ / map_image_.GetWidth() + render_x_; 310 if (zoomed_height <= GetSize().GetHeight()) {
128 int item2_y = (item2.y + AREA_ACTUAL_SIZE / 2) * render_width_ / map_image_.GetWidth() + render_y_; 311 dst_y = (GetSize().GetHeight() - zoomed_height) / 2;
312 dst_h = zoomed_height;
313 src_y = 0;
314 src_h = map_image_.GetHeight();
315 } else {
316 dst_y = 0;
317 dst_h = GetSize().GetHeight();
318 src_y = -zoom_y_ * map_image_.GetWidth() / render_width_ / zoom_;
319 src_h =
320 GetSize().GetHeight() * map_image_.GetWidth() / render_width_ / zoom_;
321 }
129 322
130 int left = std::min(item1_x, item2_x); 323 wxGCDC gcdc(dc);
131 int top = std::min(item1_y, item2_y); 324 gcdc.GetGraphicsContext()->SetInterpolationQuality(wxINTERPOLATION_GOOD);
132 int right = std::max(item1_x, item2_x); 325 gcdc.StretchBlit(dst_x, dst_y, dst_w, dst_h, &rendered_dc, src_x, src_y,
133 int bottom = std::max(item1_y, item2_y); 326 src_w, src_h);
327 }
134 328
135 int halfwidth = right - left; 329 if (hovered_item_) {
136 int halfheight = bottom - top; 330 if (networks_.IsItemInNetwork(*hovered_item_)) {
331 dc.SetBrush(*wxTRANSPARENT_BRUSH);
332
333 for (const auto node : networks_.GetNetworkGraph(*hovered_item_)) {
334 const SubwayItem &item1 = GD_GetSubwayItem(node.entry);
335 const SubwayItem &item2 = GD_GetSubwayItem(node.exit);
336
337 wxPoint item1_pos = MapPosToRenderPos(GetSubwayItemMapCenter(item1));
338 wxPoint item2_pos = MapPosToRenderPos(GetSubwayItemMapCenter(item2));
339
340 int left = std::min(item1_pos.x, item2_pos.x);
341 int top = std::min(item1_pos.y, item2_pos.y);
342 int right = std::max(item1_pos.x, item2_pos.x);
343 int bottom = std::max(item1_pos.y, item2_pos.y);
344
345 int halfwidth = right - left;
346 int halfheight = bottom - top;
347
348 if (halfwidth < 4 || halfheight < 4) {
349 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4));
350 dc.DrawLine(item1_pos, item2_pos);
351 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
352 dc.DrawLine(item1_pos, item2_pos);
353 if (!node.two_way) {
354 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 2));
355 dc.SetBrush(*wxCYAN_BRUSH);
356 dc.DrawCircle(item2_pos, 4);
357 dc.SetBrush(*wxTRANSPARENT_BRUSH);
358 }
359 } else {
360 int ellipse_x;
361 int ellipse_y;
362 double start;
363 double end;
137 364
138 if (halfwidth < 4 || halfheight < 4) { 365 if (item1_pos.x > item2_pos.x) {
139 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4)); 366 ellipse_y = top;
140 dc.DrawLine(item1_x, item1_y, item2_x, item2_y);
141 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
142 dc.DrawLine(item1_x, item1_y, item2_x, item2_y);
143 } else {
144 int ellipse_x;
145 int ellipse_y;
146 double start;
147 double end;
148 367
149 if (item1_x > item2_x) { 368 if (item1_pos.y > item2_pos.y) {
150 ellipse_y = top; 369 ellipse_x = left - halfwidth;
151 370
152 if (item1_y > item2_y) { 371 start = 0;
153 ellipse_x = left - halfwidth; 372 end = 90;
373 } else {
374 ellipse_x = left;
154 375
155 start = 0; 376 start = 90;
156 end = 90; 377 end = 180;
378 }
157 } else { 379 } else {
158 ellipse_x = left; 380 ellipse_y = top - halfheight;
159 381
160 start = 90; 382 if (item1_pos.y > item2_pos.y) {
161 end = 180; 383 ellipse_x = left - halfwidth;
162 }
163 } else {
164 ellipse_y = top - halfheight;
165 384
166 if (item1_y > item2_y) { 385 start = 270;
167 ellipse_x = left - halfwidth; 386 end = 360;
387 } else {
388 ellipse_x = left;
168 389
169 start = 270; 390 start = 180;
170 end = 360; 391 end = 270;
171 } else { 392 }
172 ellipse_x = left; 393 }
173 394
174 start = 180; 395 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4));
175 end = 270; 396 dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2,
397 halfheight * 2, start, end);
398 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
399 dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2,
400 halfheight * 2, start, end);
401 if (!node.two_way) {
402 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 2));
403 dc.SetBrush(*wxCYAN_BRUSH);
404 dc.DrawCircle(item2_pos, 4);
405 dc.SetBrush(*wxTRANSPARENT_BRUSH);
176 } 406 }
177 } 407 }
178
179 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4));
180 dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2, halfheight * 2,
181 start, end);
182 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
183 dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2, halfheight * 2,
184 start, end);
185 } 408 }
186 } 409 }
187 } 410 }
@@ -190,137 +413,426 @@ void SubwayMap::OnPaint(wxPaintEvent &event) {
190} 413}
191 414
192void SubwayMap::OnMouseMove(wxMouseEvent &event) { 415void SubwayMap::OnMouseMove(wxMouseEvent &event) {
193 int mouse_x = std::clamp( 416 wxPoint mouse_pos = RenderPosToMapPos(event.GetPosition());
194 (event.GetX() - render_x_) * map_image_.GetWidth() / render_width_,
195 0, map_image_.GetWidth() - 1);
196 int mouse_y = std::clamp(
197 (event.GetY() - render_y_) * map_image_.GetWidth() / render_width_,
198 0, map_image_.GetHeight() - 1);
199 417
200 std::vector<int> hovered = tree_->query( 418 std::vector<int> hovered = tree_->query(
201 {static_cast<float>(mouse_x), static_cast<float>(mouse_y), 2, 2}); 419 {static_cast<float>(mouse_pos.x), static_cast<float>(mouse_pos.y), 2, 2});
202 std::optional<int> new_hovered_item;
203 if (!hovered.empty()) { 420 if (!hovered.empty()) {
204 new_hovered_item = hovered[0]; 421 actual_hover_ = hovered[0];
422 } else {
423 actual_hover_ = std::nullopt;
205 } 424 }
206 425
207 if (new_hovered_item != hovered_item_) { 426 if (!sticky_hover_ && actual_hover_ != hovered_item_) {
208 hovered_item_ = new_hovered_item; 427 EvaluateHover();
428 }
209 429
210 Refresh(); 430 if (scroll_mode_) {
431 EvaluateScroll(event.GetPosition());
211 } 432 }
212 433
434 mouse_position_ = event.GetPosition();
435
213 event.Skip(); 436 event.Skip();
214} 437}
215 438
216void SubwayMap::Redraw() { 439void SubwayMap::OnMouseScroll(wxMouseEvent &event) {
217 wxSize panel_size = GetSize(); 440 double new_zoom = zoom_;
218 wxSize image_size = map_image_.GetSize(); 441 if (event.GetWheelRotation() > 0) {
219 442 new_zoom = std::min(3.0, zoom_ + 0.25);
220 render_x_ = 0;
221 render_y_ = 0;
222 render_width_ = panel_size.GetWidth();
223 render_height_ = panel_size.GetHeight();
224
225 if (image_size.GetWidth() * panel_size.GetHeight() >
226 panel_size.GetWidth() * image_size.GetHeight()) {
227 render_height_ = (panel_size.GetWidth() * image_size.GetHeight()) /
228 image_size.GetWidth();
229 render_y_ = (panel_size.GetHeight() - render_height_) / 2;
230 } else { 443 } else {
231 render_width_ = (image_size.GetWidth() * panel_size.GetHeight()) / 444 new_zoom = std::max(1.0, zoom_ - 0.25);
232 image_size.GetHeight(); 445 }
233 render_x_ = (panel_size.GetWidth() - render_width_) / 2; 446
447 if (zoom_ != new_zoom) {
448 SetZoom(new_zoom, event.GetPosition());
449 }
450
451 event.Skip();
452}
453
454void SubwayMap::OnMouseLeave(wxMouseEvent &event) {
455 SetScrollSpeed(0, 0);
456 mouse_position_ = std::nullopt;
457}
458
459void SubwayMap::OnMouseClick(wxMouseEvent &event) {
460 bool finished = false;
461
462 if (actual_hover_) {
463 const SubwayItem &subway_item = GD_GetSubwayItem(*actual_hover_);
464 std::optional<int> subway_door = GetRealSubwayDoor(subway_item);
465
466 if ((subway_door && !GetDoorRequirements(*subway_door).empty()) ||
467 networks_.IsItemInNetwork(*hovered_item_)) {
468 if (actual_hover_ != hovered_item_) {
469 EvaluateHover();
470
471 if (!hovered_item_) {
472 sticky_hover_ = false;
473 }
474 } else {
475 sticky_hover_ = !sticky_hover_;
476 }
477
478 finished = true;
479 }
480 }
481
482 if (!finished) {
483 if (scroll_mode_) {
484 scroll_mode_ = false;
485
486 SetScrollSpeed(0, 0);
487
488 SetCursor(wxCURSOR_ARROW);
489 } else if (event.GetPosition().x < GetSize().GetWidth() / 6 ||
490 event.GetPosition().x > 5 * GetSize().GetWidth() / 6 ||
491 event.GetPosition().y < GetSize().GetHeight() / 6 ||
492 event.GetPosition().y > 5 * GetSize().GetHeight() / 6) {
493 scroll_mode_ = true;
494
495 EvaluateScroll(event.GetPosition());
496
497 SetCursor(wxCURSOR_CROSS);
498 } else {
499 sticky_hover_ = false;
500 }
501 }
502}
503
504void SubwayMap::OnTimer(wxTimerEvent &event) {
505 SetZoomPos({zoom_x_ + scroll_x_, zoom_y_ + scroll_y_});
506 Refresh();
507}
508
509void SubwayMap::OnZoomSlide(wxCommandEvent &event) {
510 double new_zoom = 1.0 + 0.25 * zoom_slider_->GetValue();
511
512 if (new_zoom != zoom_) {
513 SetZoom(new_zoom, {GetSize().GetWidth() / 2, GetSize().GetHeight() / 2});
234 } 514 }
515}
235 516
236 rendered_ = wxBitmap( 517void SubwayMap::OnClickHelp(wxCommandEvent &event) {
237 map_image_ 518 wxMessageBox(
238 .Scale(render_width_, render_height_, wxIMAGE_QUALITY_BILINEAR) 519 "Zoom in/out using the mouse wheel, Ctrl +/-, or the slider in the "
239 .Size(panel_size, {render_x_, render_y_}, 255, 255, 255)); 520 "corner.\nClick on a side of the screen to start panning. It will follow "
521 "your mouse. Click again to stop.\nHover over a door to see the "
522 "requirements to open it.\nHover over a warp or active painting to see "
523 "what it is connected to.\nFor one-way connections, there will be a "
524 "circle at the exit.\nCircles represent paintings.\nA red circle means "
525 "that the painting is locked by a door.\nA blue circle means painting "
526 "shuffle is enabled and the painting has not been checked yet.\nA black "
527 "circle means the painting is not a warp.\nA green circle means that the "
528 "painting is a warp.\nPainting exits will be indicated with an X.\nClick "
529 "on a door or warp to make the popup stick until you click again.",
530 "Subway Map Help");
531}
532
533void SubwayMap::Redraw() {
534 rendered_ = wxBitmap(map_image_);
240 535
241 wxMemoryDC dc; 536 wxMemoryDC dc;
242 dc.SelectObject(rendered_); 537 dc.SelectObject(rendered_);
243 538
539 wxGCDC gcdc(dc);
540
541 std::map<std::string, std::string> painting_mapping = AP_GetPaintingMapping();
542
244 for (const SubwayItem &subway_item : GD_GetSubwayItems()) { 543 for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
245 ItemDrawType draw_type = ItemDrawType::kNone; 544 ItemDrawType draw_type = ItemDrawType::kNone;
246 const wxBrush *brush_color = wxGREY_BRUSH; 545 const wxBrush *brush_color = wxGREY_BRUSH;
247 std::optional<wxColour> shade_color; 546 std::optional<int> subway_door = GetRealSubwayDoor(subway_item);
547
548 if (AP_HasEarlyColorHallways() &&
549 subway_item.special == "early_color_hallways") {
550 draw_type = ItemDrawType::kOwl;
551 brush_color = wxGREEN_BRUSH;
552 } else if (subway_item.special == "starting_room_overhead") {
553 // Do not draw.
554 } else if (AP_IsColorShuffle() && subway_item.special &&
555 subway_item.special->starts_with("color_")) {
556 std::string color_name = subway_item.special->substr(6);
557 LingoColor lingo_color = GetLingoColorForString(color_name);
558 int color_item_id = GD_GetItemIdForColor(lingo_color);
248 559
249 if (subway_item.door) {
250 draw_type = ItemDrawType::kBox; 560 draw_type = ItemDrawType::kBox;
251 561 if (AP_HasItemSafe(color_item_id)) {
252 if (IsDoorOpen(*subway_item.door)) { 562 brush_color = wxGREEN_BRUSH;
253 if (!subway_item.paintings.empty()) { 563 } else {
254 draw_type = ItemDrawType::kOwl; 564 brush_color = wxRED_BRUSH;
255 } else { 565 }
566 } else if (subway_item.special == "sun_painting") {
567 if (!AP_IsPilgrimageEnabled()) {
568 draw_type = ItemDrawType::kOwl;
569 if (IsDoorOpen(*subway_item.door)) {
256 brush_color = wxGREEN_BRUSH; 570 brush_color = wxGREEN_BRUSH;
571 } else {
572 brush_color = wxRED_BRUSH;
257 } 573 }
574 }
575 } else if (subway_item.sunwarp &&
576 subway_item.sunwarp->type == SubwaySunwarpType::kFinal &&
577 AP_IsPilgrimageEnabled()) {
578 draw_type = ItemDrawType::kBox;
579
580 if (IsPilgrimageDoable()) {
581 brush_color = wxGREEN_BRUSH;
258 } else { 582 } else {
259 brush_color = wxRED_BRUSH; 583 brush_color = wxRED_BRUSH;
260 } 584 }
261 } else if (!subway_item.paintings.empty()) { 585 } else if (subway_item.painting) {
262 if (AP_IsPaintingShuffle()) { 586 if (subway_door && !IsDoorOpen(*subway_door)) {
263 bool has_checked_painting = false; 587 draw_type = ItemDrawType::kOwl;
264 bool has_unchecked_painting = false; 588 brush_color = wxRED_BRUSH;
265 bool has_mapped_painting = false; 589 } else if (AP_IsPaintingShuffle()) {
266 590 if (!checked_paintings_.count(*subway_item.painting)) {
267 for (const std::string &painting_id : subway_item.paintings) { 591 draw_type = ItemDrawType::kOwl;
268 if (checked_paintings_.count(painting_id)) { 592 brush_color = wxBLUE_BRUSH;
269 has_checked_painting = true; 593 } else if (painting_mapping.count(*subway_item.painting)) {
270 594 draw_type = ItemDrawType::kOwl;
271 if (AP_GetPaintingMapping().count(painting_id)) { 595 brush_color = wxGREEN_BRUSH;
272 has_mapped_painting = true; 596 } else if (AP_IsPaintingMappedTo(*subway_item.painting)) {
273 } 597 draw_type = ItemDrawType::kOwlExit;
274 } else { 598 brush_color = wxGREEN_BRUSH;
275 has_unchecked_painting = true;
276 }
277 } 599 }
278 600 } else if (subway_item.HasWarps()) {
279 if (has_unchecked_painting || has_mapped_painting) { 601 brush_color = wxGREEN_BRUSH;
602 if (!subway_item.exits.empty()) {
603 draw_type = ItemDrawType::kOwlExit;
604 } else {
280 draw_type = ItemDrawType::kOwl; 605 draw_type = ItemDrawType::kOwl;
281
282 if (has_unchecked_painting) {
283 if (has_checked_painting) {
284 shade_color = wxColour(255, 255, 0, 100);
285 } else {
286 shade_color = wxColour(100, 100, 100, 100);
287 }
288 }
289 } 606 }
290 } else if (!subway_item.tags.empty()) { 607 }
291 draw_type = ItemDrawType::kOwl; 608 } else if (subway_door) {
609 draw_type = ItemDrawType::kBox;
610
611 if (IsDoorOpen(*subway_door)) {
612 brush_color = wxGREEN_BRUSH;
613 } else {
614 brush_color = wxRED_BRUSH;
292 } 615 }
293 } 616 }
294 617
295 int real_area_x = 618 wxPoint real_area_pos = {subway_item.x, subway_item.y};
296 render_x_ + subway_item.x * render_width_ / image_size.GetWidth();
297 int real_area_y =
298 render_y_ + subway_item.y * render_width_ / image_size.GetWidth();
299 619
300 int real_area_size = 620 int real_area_size =
301 render_width_ * 621 (draw_type == ItemDrawType::kOwl ? OWL_ACTUAL_SIZE : AREA_ACTUAL_SIZE);
302 (draw_type == ItemDrawType::kOwl ? OWL_ACTUAL_SIZE : AREA_ACTUAL_SIZE) /
303 image_size.GetWidth();
304 if (real_area_size == 0) {
305 real_area_size = 1;
306 }
307 622
308 if (draw_type == ItemDrawType::kBox) { 623 if (draw_type == ItemDrawType::kBox) {
309 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1)); 624 gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1));
310 dc.SetBrush(*brush_color); 625 gcdc.SetBrush(*brush_color);
311 dc.DrawRectangle({real_area_x, real_area_y}, 626
312 {real_area_size, real_area_size}); 627 if (subway_item.tilted) {
313 } else if (draw_type == ItemDrawType::kOwl) { 628 constexpr int AREA_TILTED_SIDE =
314 wxBitmap owl_bitmap = wxBitmap( 629 static_cast<int>(AREA_ACTUAL_SIZE / 1.41421356237);
315 owl_image_.Scale(real_area_size, real_area_size, 630 const wxPoint poly_points[] = {{AREA_TILTED_SIDE, 0},
316 wxIMAGE_QUALITY_BILINEAR)); 631 {2 * AREA_TILTED_SIDE, AREA_TILTED_SIDE},
317 dc.DrawBitmap(owl_bitmap, {real_area_x, real_area_y}); 632 {AREA_TILTED_SIDE, 2 * AREA_TILTED_SIDE},
633 {0, AREA_TILTED_SIDE}};
634 gcdc.DrawPolygon(4, poly_points, subway_item.x, subway_item.y);
635 } else {
636 gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size});
637 }
638 } else if (draw_type == ItemDrawType::kOwl || draw_type == ItemDrawType::kOwlExit) {
639 gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1));
640 gcdc.SetBrush(*brush_color);
641 gcdc.DrawCircle(real_area_pos, PAINTING_RADIUS);
642
643 if (draw_type == ItemDrawType::kOwlExit) {
644 gcdc.DrawLine(subway_item.x - PAINTING_EXIT_RADIUS,
645 subway_item.y - PAINTING_EXIT_RADIUS,
646 subway_item.x + PAINTING_EXIT_RADIUS,
647 subway_item.y + PAINTING_EXIT_RADIUS);
648 gcdc.DrawLine(subway_item.x + PAINTING_EXIT_RADIUS,
649 subway_item.y - PAINTING_EXIT_RADIUS,
650 subway_item.x - PAINTING_EXIT_RADIUS,
651 subway_item.y + PAINTING_EXIT_RADIUS);
652 }
318 } 653 }
319 } 654 }
320} 655}
321 656
322quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int& id) const { 657void SubwayMap::SetUpHelpButton() {
658 help_button_->SetSize(wxDefaultCoord, wxDefaultCoord, wxDefaultCoord,
659 wxDefaultCoord, wxSIZE_AUTO);
660 help_button_->SetPosition({
661 GetSize().GetWidth() - help_button_->GetSize().GetWidth() - 15,
662 15,
663 });
664}
665
666void SubwayMap::EvaluateScroll(wxPoint pos) {
667 int scroll_x;
668 int scroll_y;
669 if (pos.x < GetSize().GetWidth() / 9) {
670 scroll_x = 20;
671 } else if (pos.x < GetSize().GetWidth() / 6) {
672 scroll_x = 5;
673 } else if (pos.x > 8 * GetSize().GetWidth() / 9) {
674 scroll_x = -20;
675 } else if (pos.x > 5 * GetSize().GetWidth() / 6) {
676 scroll_x = -5;
677 } else {
678 scroll_x = 0;
679 }
680 if (pos.y < GetSize().GetHeight() / 9) {
681 scroll_y = 20;
682 } else if (pos.y < GetSize().GetHeight() / 6) {
683 scroll_y = 5;
684 } else if (pos.y > 8 * GetSize().GetHeight() / 9) {
685 scroll_y = -20;
686 } else if (pos.y > 5 * GetSize().GetHeight() / 6) {
687 scroll_y = -5;
688 } else {
689 scroll_y = 0;
690 }
691
692 SetScrollSpeed(scroll_x, scroll_y);
693}
694
695void SubwayMap::EvaluateHover() {
696 hovered_item_ = actual_hover_;
697
698 if (hovered_item_) {
699 // Note that these requirements are duplicated on OnMouseClick so that it
700 // knows when an item has a hover effect.
701 const SubwayItem &subway_item = GD_GetSubwayItem(*hovered_item_);
702 std::optional<int> subway_door = GetRealSubwayDoor(subway_item);
703
704 if (subway_door && !GetDoorRequirements(*subway_door).empty()) {
705 report_popup_->SetDoorId(*subway_door);
706
707 wxPoint popupPos =
708 MapPosToRenderPos({subway_item.x + AREA_ACTUAL_SIZE / 2,
709 subway_item.y + AREA_ACTUAL_SIZE / 2});
710
711 report_popup_->SetClientSize(
712 report_popup_->GetVirtualSize().GetWidth(),
713 std::min(GetSize().GetHeight(),
714 report_popup_->GetVirtualSize().GetHeight()));
715
716 if (popupPos.x + report_popup_->GetSize().GetWidth() >
717 GetSize().GetWidth()) {
718 popupPos.x = GetSize().GetWidth() - report_popup_->GetSize().GetWidth();
719 }
720 if (popupPos.y + report_popup_->GetSize().GetHeight() >
721 GetSize().GetHeight()) {
722 popupPos.y =
723 GetSize().GetHeight() - report_popup_->GetSize().GetHeight();
724 }
725 report_popup_->SetPosition(popupPos);
726
727 report_popup_->Show();
728 } else {
729 report_popup_->Reset();
730 report_popup_->Hide();
731 }
732 } else {
733 report_popup_->Reset();
734 report_popup_->Hide();
735 }
736
737 Refresh();
738}
739
740wxPoint SubwayMap::MapPosToRenderPos(wxPoint pos) const {
741 return {static_cast<int>(pos.x * render_width_ * zoom_ /
742 map_image_.GetSize().GetWidth() +
743 zoom_x_),
744 static_cast<int>(pos.y * render_width_ * zoom_ /
745 map_image_.GetSize().GetWidth() +
746 zoom_y_)};
747}
748
749wxPoint SubwayMap::MapPosToVirtualPos(wxPoint pos) const {
750 return {static_cast<int>(pos.x * render_width_ * zoom_ /
751 map_image_.GetSize().GetWidth()),
752 static_cast<int>(pos.y * render_width_ * zoom_ /
753 map_image_.GetSize().GetWidth())};
754}
755
756wxPoint SubwayMap::RenderPosToMapPos(wxPoint pos) const {
757 return {
758 std::clamp(static_cast<int>((pos.x - zoom_x_) * map_image_.GetWidth() /
759 render_width_ / zoom_),
760 0, map_image_.GetWidth() - 1),
761 std::clamp(static_cast<int>((pos.y - zoom_y_) * map_image_.GetWidth() /
762 render_width_ / zoom_),
763 0, map_image_.GetHeight() - 1)};
764}
765
766void SubwayMap::SetZoomPos(wxPoint pos) {
767 if (render_width_ * zoom_ <= GetSize().GetWidth()) {
768 zoom_x_ = (GetSize().GetWidth() - render_width_ * zoom_) / 2;
769 } else {
770 zoom_x_ = std::clamp(
771 pos.x, GetSize().GetWidth() - static_cast<int>(render_width_ * zoom_),
772 0);
773 }
774 if (render_height_ * zoom_ <= GetSize().GetHeight()) {
775 zoom_y_ = (GetSize().GetHeight() - render_height_ * zoom_) / 2;
776 } else {
777 zoom_y_ = std::clamp(
778 pos.y, GetSize().GetHeight() - static_cast<int>(render_height_ * zoom_),
779 0);
780 }
781}
782
783void SubwayMap::SetScrollSpeed(int scroll_x, int scroll_y) {
784 bool should_timer = (scroll_x != 0 || scroll_y != 0);
785 if (should_timer != scroll_timer_->IsRunning()) {
786 if (should_timer) {
787 scroll_timer_->Start(1000 / 60);
788 } else {
789 scroll_timer_->Stop();
790 }
791 }
792
793 scroll_x_ = scroll_x;
794 scroll_y_ = scroll_y;
795}
796
797void SubwayMap::SetZoom(double zoom, wxPoint static_point) {
798 wxPoint map_pos = RenderPosToMapPos(static_point);
799 zoom_ = zoom;
800
801 wxPoint virtual_pos = MapPosToVirtualPos(map_pos);
802 SetZoomPos(-(virtual_pos - static_point));
803
804 Refresh();
805
806 zoom_slider_->SetValue((zoom - 1.0) / 0.25);
807}
808
809std::optional<int> SubwayMap::GetRealSubwayDoor(const SubwayItem subway_item) {
810 if (AP_IsSunwarpShuffle() && subway_item.sunwarp &&
811 subway_item.sunwarp->type != SubwaySunwarpType::kFinal) {
812 int sunwarp_index = subway_item.sunwarp->dots - 1;
813 if (subway_item.sunwarp->type == SubwaySunwarpType::kExit) {
814 sunwarp_index += 6;
815 }
816
817 for (const auto &[start_index, mapping] : sunwarp_mapping_) {
818 if (start_index == sunwarp_index || mapping.exit_index == sunwarp_index) {
819 return GD_GetSunwarpDoors().at(mapping.dots - 1);
820 }
821 }
822 }
823
824 return subway_item.door;
825}
826
827quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int &id) const {
323 const SubwayItem &subway_item = GD_GetSubwayItem(id); 828 const SubwayItem &subway_item = GD_GetSubwayItem(id);
324 return {static_cast<float>(subway_item.x), static_cast<float>(subway_item.y), 829 if (subway_item.painting) {
325 AREA_ACTUAL_SIZE, AREA_ACTUAL_SIZE}; 830 return {static_cast<float>(subway_item.x) - PAINTING_RADIUS,
831 static_cast<float>(subway_item.y) - PAINTING_RADIUS,
832 PAINTING_RADIUS * 2, PAINTING_RADIUS * 2};
833 } else {
834 return {static_cast<float>(subway_item.x),
835 static_cast<float>(subway_item.y), AREA_ACTUAL_SIZE,
836 AREA_ACTUAL_SIZE};
837 }
326} 838}