#include "subway_map.h" #include #include #include "ap_state.h" #include "game_data.h" #include "global.h" #include "tracker_state.h" constexpr int AREA_ACTUAL_SIZE = 21; constexpr int OWL_ACTUAL_SIZE = 32; enum class ItemDrawType { kNone, kBox, kOwl }; SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) { SetBackgroundStyle(wxBG_STYLE_PAINT); map_image_ = wxImage(GetAbsolutePath("assets/subway.png").c_str(), wxBITMAP_TYPE_PNG); if (!map_image_.IsOk()) { return; } owl_image_ = wxImage(GetAbsolutePath("assets/owl.png").c_str(), wxBITMAP_TYPE_PNG); if (!owl_image_.IsOk()) { return; } tree_ = std::make_unique>( quadtree::Box{0, 0, static_cast(map_image_.GetWidth()), static_cast(map_image_.GetHeight())}); for (const SubwayItem &subway_item : GD_GetSubwayItems()) { tree_->add(subway_item.id); } Redraw(); Bind(wxEVT_PAINT, &SubwayMap::OnPaint, this); Bind(wxEVT_MOTION, &SubwayMap::OnMouseMove, this); } void SubwayMap::OnConnect() { networks_.Clear(); std::map> tagged; for (const SubwayItem &subway_item : GD_GetSubwayItems()) { if (AP_IsPaintingShuffle() && !subway_item.paintings.empty()) { continue; } for (const std::string &tag : subway_item.tags) { tagged[tag].push_back(subway_item.id); } if (!AP_IsSunwarpShuffle() && subway_item.sunwarp && subway_item.sunwarp->type != SubwaySunwarpType::kFinal) { std::ostringstream tag; tag << "sunwarp" << subway_item.sunwarp->dots; tagged[tag.str()].push_back(subway_item.id); } } for (const auto &[tag, items] : tagged) { // Pairwise connect all items with the same tag. for (auto tag_it1 = items.begin(); std::next(tag_it1) != items.end(); tag_it1++) { for (auto tag_it2 = std::next(tag_it1); tag_it2 != items.end(); tag_it2++) { networks_.AddLink(*tag_it1, *tag_it2); } } } checked_paintings_.clear(); } void SubwayMap::UpdateIndicators() { Redraw(); } void SubwayMap::UpdatePainting(std::string from_painting_id, std::optional to_painting_id) { checked_paintings_.insert(from_painting_id); if (to_painting_id) { networks_.AddLink(GD_GetSubwayItemForPainting(from_painting_id), GD_GetSubwayItemForPainting(*to_painting_id)); } } void SubwayMap::UpdateSunwarp(SubwaySunwarp from_sunwarp, SubwaySunwarp to_sunwarp) { networks_.AddLink(GD_GetSubwayItemForSunwarp(from_sunwarp), GD_GetSubwayItemForSunwarp(to_sunwarp)); } void SubwayMap::OnPaint(wxPaintEvent &event) { if (GetSize() != rendered_.GetSize()) { Redraw(); } wxBufferedPaintDC dc(this); dc.DrawBitmap(rendered_, 0, 0); if (hovered_item_ && networks_.IsItemInNetwork(*hovered_item_)) { dc.SetBrush(*wxTRANSPARENT_BRUSH); int network_id = networks_.GetNetworkWithItem(*hovered_item_); for (const auto &[item_id1, item_id2] : networks_.GetNetworkGraph(network_id)) { const SubwayItem &item1 = GD_GetSubwayItem(item_id1); const SubwayItem &item2 = GD_GetSubwayItem(item_id2); int item1_x = (item1.x + AREA_ACTUAL_SIZE / 2) * render_width_ / map_image_.GetWidth() + render_x_; int item1_y = (item1.y + AREA_ACTUAL_SIZE / 2) * render_width_ / map_image_.GetWidth() + render_y_; int item2_x = (item2.x + AREA_ACTUAL_SIZE / 2) * render_width_ / map_image_.GetWidth() + render_x_; int item2_y = (item2.y + AREA_ACTUAL_SIZE / 2) * render_width_ / map_image_.GetWidth() + render_y_; int left = std::min(item1_x, item2_x); int top = std::min(item1_y, item2_y); int right = std::max(item1_x, item2_x); int bottom = std::max(item1_y, item2_y); int halfwidth = right - left; int halfheight = bottom - top; int ellipse_x; int ellipse_y; double start; double end; if (item1_x > item2_x) { ellipse_y = top; if (item1_y > item2_y) { ellipse_x = left - halfwidth; start = 0; end = 90; } else { ellipse_x = left; start = 90; end = 180; } } else { ellipse_y = top - halfheight; if (item1_y > item2_y) { ellipse_x = left - halfwidth; start = 270; end = 360; } else { ellipse_x = left; start = 180; end = 270; } } dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4)); dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2, halfheight * 2, start, end); dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2)); dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2, halfheight * 2, start, end); } } event.Skip(); } void SubwayMap::OnMouseMove(wxMouseEvent &event) { int mouse_x = std::clamp( (event.GetX() - render_x_) * map_image_.GetWidth() / render_width_, 0, map_image_.GetWidth() - 1); int mouse_y = std::clamp( (event.GetY() - render_y_) * map_image_.GetWidth() / render_width_, 0, map_image_.GetHeight() - 1); std::vector hovered = tree_->query( {static_cast(mouse_x), static_cast(mouse_y), 2, 2}); std::optional new_hovered_item; if (!hovered.empty()) { new_hovered_item = hovered[0]; } if (new_hovered_item != hovered_item_) { hovered_item_ = new_hovered_item; Refresh(); } event.Skip(); } void SubwayMap::Redraw() { wxSize panel_size = GetSize(); wxSize image_size = map_image_.GetSize(); render_x_ = 0; render_y_ = 0; render_width_ = panel_size.GetWidth(); render_height_ = panel_size.GetHeight(); if (image_size.GetWidth() * panel_size.GetHeight() > panel_size.GetWidth() * image_size.GetHeight()) { render_height_ = (panel_size.GetWidth() * image_size.GetHeight()) / image_size.GetWidth(); render_y_ = (panel_size.GetHeight() - render_height_) / 2; } else { render_width_ = (image_size.GetWidth() * panel_size.GetHeight()) / image_size.GetHeight(); render_x_ = (panel_size.GetWidth() - render_width_) / 2; } rendered_ = wxBitmap( map_image_ .Scale(render_width_, render_height_, wxIMAGE_QUALITY_BILINEAR) .Size(panel_size, {render_x_, render_y_}, 255, 255, 255)); wxMemoryDC dc; dc.SelectObject(rendered_); for (const SubwayItem &subway_item : GD_GetSubwayItems()) { ItemDrawType draw_type = ItemDrawType::kNone; const wxBrush *brush_color = wxGREY_BRUSH; std::optional shade_color; if (subway_item.door) { draw_type = ItemDrawType::kBox; if (IsDoorOpen(*subway_item.door)) { if (!subway_item.paintings.empty()) { draw_type = ItemDrawType::kOwl; } else { brush_color = wxGREEN_BRUSH; } } else { brush_color = wxRED_BRUSH; } } else if (!subway_item.paintings.empty()) { if (AP_IsPaintingShuffle()) { bool has_checked_painting = false; bool has_unchecked_painting = false; bool has_mapped_painting = false; for (const std::string &painting_id : subway_item.paintings) { if (checked_paintings_.count(painting_id)) { has_checked_painting = true; if (AP_GetPaintingMapping().count(painting_id)) { has_mapped_painting = true; } } else { has_unchecked_painting = true; } } if (has_unchecked_painting || has_mapped_painting) { draw_type = ItemDrawType::kOwl; if (has_unchecked_painting) { if (has_checked_painting) { shade_color = wxColour(255, 255, 0, 100); } else { shade_color = wxColour(100, 100, 100, 100); } } } } else if (!subway_item.tags.empty()) { draw_type = ItemDrawType::kOwl; } } int real_area_x = render_x_ + subway_item.x * render_width_ / image_size.GetWidth(); int real_area_y = render_y_ + subway_item.y * render_width_ / image_size.GetWidth(); int real_area_size = render_width_ * (draw_type == ItemDrawType::kOwl ? OWL_ACTUAL_SIZE : AREA_ACTUAL_SIZE) / image_size.GetWidth(); if (real_area_size == 0) { real_area_size = 1; } if (draw_type == ItemDrawType::kBox) { dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1)); dc.SetBrush(*brush_color); dc.DrawRectangle({real_area_x, real_area_y}, {real_area_size, real_area_size}); } else if (draw_type == ItemDrawType::kOwl) { wxBitmap owl_bitmap = wxBitmap( owl_image_.Scale(real_area_size, real_area_size, wxIMAGE_QUALITY_BILINEAR)); dc.DrawBitmap(owl_bitmap, {real_area_x, real_area_y}); } } } quadtree::Box SubwayMap::GetItemBox::operator()(const int& id) const { const SubwayItem &subway_item = GD_GetSubwayItem(id); return {static_cast(subway_item.x), static_cast(subway_item.y), AREA_ACTUAL_SIZE, AREA_ACTUAL_SIZE}; }