about summary refs log blame commit diff stats
path: root/src/subway_map.cpp
blob: 100d3515610f2af819d00e2ebfe87009da8b020d (plain) (tree)
1
2
3
4
5
6
7
8
                       
                        
                       
 

                     



                                    
                                   
 
                                              
 
                                                                    
                                       




                                                                               




                                                                            






                                                                               





                                                                           
           
 
                                    
                                                    
                                                           
                                                        
                                               

                                                                  


                                                                  
 











                                                                   
                                                                 



















                                                                         
                                    










                                                                          
   
           

                                                         
                                                         


                                                             


















                                                                        
                                              
                                         


















                                                                         
                      
   
                             
                                   









































                                                                                


                                                                             
   
 
                      
                                                                             


                                                                              
 
                          
 


                                                                     
 


                                                 
 

                                           




                                                                    
       
                                                             
       
 
                                    
                                                            
 
                            
 
                          
 
                                                   
                                                                       
 
                                                                     
 


                                                           

                                                                             
 










                                                             


                                                                              
 


                                                        




                                                                 
                                            
                                                                
                                            
                



                        
                                          
                            
                                            
                                           
 


                               
 

                         
                  
                                         
 
                                            








                                           
           
 
                                                                 
                                                                 
                                                                
                                                                 
         
       

     


                                                  
                                                             
 
                                          
                                                                                
                         

                                 
   
                                                         
              
   
                                        
   
                                        

               







                                                    
                                           



               


                                                   
 



























                                                                                



                                                         






                                                                             










                                                                                
                          
                                   


                             

                                                             
                                        












                                                





























                                                                      
     
                                                           
 
                                                                               
 

                                                             
                                                                        
                                                 
                                                                     
                                               
     
 
 





                                                                     



























                                                     
                                                         











                                                                


                                                         



































                                                                                
 










                                                            
                                                                             


                                                                               
#include "subway_map.h"

#include <wx/dcbuffer.h>
#include <wx/dcgraph.h>

#include <sstream>

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

  unchecked_eye_ =
      wxBitmap(wxImage(GetAbsolutePath("assets/unchecked.png").c_str(),
                       wxBITMAP_TYPE_PNG)
                   .Scale(32, 32));
  checked_eye_ = wxBitmap(
      wxImage(GetAbsolutePath("assets/checked.png").c_str(), wxBITMAP_TYPE_PNG)
          .Scale(32, 32));

  tree_ = std::make_unique<quadtree::Quadtree<int, GetItemBox>>(
      quadtree::Box<float>{0, 0, static_cast<float>(map_image_.GetWidth()),
                           static_cast<float>(map_image_.GetHeight())});
  for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
    tree_->add(subway_item.id);
  }

  Redraw();

  scroll_timer_ = new wxTimer(this);

  Bind(wxEVT_PAINT, &SubwayMap::OnPaint, this);
  Bind(wxEVT_MOTION, &SubwayMap::OnMouseMove, this);
  Bind(wxEVT_MOUSEWHEEL, &SubwayMap::OnMouseScroll, this);
  Bind(wxEVT_LEAVE_WINDOW, &SubwayMap::OnMouseLeave, this);
  Bind(wxEVT_LEFT_DOWN, &SubwayMap::OnMouseClick, this);
  Bind(wxEVT_TIMER, &SubwayMap::OnTimer, this);

  zoom_slider_ = new wxSlider(this, wxID_ANY, 0, 0, 8, {15, 15});
  zoom_slider_->Bind(wxEVT_SLIDER, &SubwayMap::OnZoomSlide, this);

  help_button_ = new wxButton(this, wxID_ANY, "Help");
  help_button_->Bind(wxEVT_BUTTON, &SubwayMap::OnClickHelp, this);
  SetUpHelpButton();
}

void SubwayMap::OnConnect() {
  networks_.Clear();

  std::map<std::string, std::vector<int>> 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() {
  if (AP_IsPaintingShuffle()) {
    for (const std::string &painting_id : AP_GetCheckedPaintings()) {
      if (!checked_paintings_.count(painting_id)) {
        checked_paintings_.insert(painting_id);

        if (AP_GetPaintingMapping().count(painting_id)) {
          networks_.AddLink(GD_GetSubwayItemForPainting(painting_id),
                            GD_GetSubwayItemForPainting(
                                AP_GetPaintingMapping().at(painting_id)));
        }
      }
    }
  }

  Redraw();
}

void SubwayMap::UpdateSunwarp(SubwaySunwarp from_sunwarp,
                              SubwaySunwarp to_sunwarp) {
  networks_.AddLink(GD_GetSubwayItemForSunwarp(from_sunwarp),
                    GD_GetSubwayItemForSunwarp(to_sunwarp));
}

void SubwayMap::Zoom(bool in) {
  wxPoint focus_point;

  if (mouse_position_) {
    focus_point = *mouse_position_;
  } else {
    focus_point = {GetSize().GetWidth() / 2, GetSize().GetHeight() / 2};
  }

  if (in) {
    if (zoom_ < 3.0) {
      SetZoom(zoom_ + 0.25, focus_point);
    }
  } else {
    if (zoom_ > 1.0) {
      SetZoom(zoom_ - 0.25, focus_point);
    }
  }
}

void SubwayMap::OnPaint(wxPaintEvent &event) {
  if (GetSize() != rendered_.GetSize()) {
    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;
    }

    SetZoomPos({zoom_x_, zoom_y_});

    SetUpHelpButton();
  }

  wxBufferedPaintDC dc(this);
  dc.SetBackground(*wxWHITE_BRUSH);
  dc.Clear();

  {
    wxMemoryDC rendered_dc;
    rendered_dc.SelectObject(rendered_);

    int dst_x;
    int dst_y;
    int dst_w;
    int dst_h;
    int src_x;
    int src_y;
    int src_w;
    int src_h;

    int zoomed_width = render_width_ * zoom_;
    int zoomed_height = render_height_ * zoom_;

    if (zoomed_width <= GetSize().GetWidth()) {
      dst_x = (GetSize().GetWidth() - zoomed_width) / 2;
      dst_w = zoomed_width;
      src_x = 0;
      src_w = map_image_.GetWidth();
    } else {
      dst_x = 0;
      dst_w = GetSize().GetWidth();
      src_x = -zoom_x_ * map_image_.GetWidth() / render_width_ / zoom_;
      src_w =
          GetSize().GetWidth() * map_image_.GetWidth() / render_width_ / zoom_;
    }

    if (zoomed_height <= GetSize().GetHeight()) {
      dst_y = (GetSize().GetHeight() - zoomed_height) / 2;
      dst_h = zoomed_height;
      src_y = 0;
      src_h = map_image_.GetHeight();
    } else {
      dst_y = 0;
      dst_h = GetSize().GetHeight();
      src_y = -zoom_y_ * map_image_.GetWidth() / render_width_ / zoom_;
      src_h =
          GetSize().GetHeight() * map_image_.GetWidth() / render_width_ / zoom_;
    }

    wxGCDC gcdc(dc);
    gcdc.GetGraphicsContext()->SetInterpolationQuality(wxINTERPOLATION_GOOD);
    gcdc.StretchBlit(dst_x, dst_y, dst_w, dst_h, &rendered_dc, src_x, src_y,
                     src_w, src_h);
  }

  if (hovered_item_) {
    // Note that these requirements are duplicated on OnMouseClick so that it
    // knows when an item has a hover effect.
    const SubwayItem &subway_item = GD_GetSubwayItem(*hovered_item_);
    if (subway_item.door && !GetDoorRequirements(*subway_item.door).empty()) {
      const std::map<std::string, bool> &report =
          GetDoorRequirements(*subway_item.door);

      int acc_height = 10;
      int col_width = 0;

      for (const auto &[text, obtained] : report) {
        wxSize item_extent = dc.GetTextExtent(text);
        int item_height = std::max(32, item_extent.GetHeight()) + 10;
        acc_height += item_height;

        if (item_extent.GetWidth() > col_width) {
          col_width = item_extent.GetWidth();
        }
      }

      int item_width = col_width + 10 + 32;
      int full_width = item_width + 20;

      wxPoint popup_pos =
          MapPosToRenderPos({subway_item.x + AREA_ACTUAL_SIZE / 2,
                             subway_item.y + AREA_ACTUAL_SIZE / 2});

      if (popup_pos.x + full_width > GetSize().GetWidth()) {
        popup_pos.x = GetSize().GetWidth() - full_width;
      }
      if (popup_pos.y + acc_height > GetSize().GetHeight()) {
        popup_pos.y = GetSize().GetHeight() - acc_height;
      }

      dc.SetPen(*wxTRANSPARENT_PEN);
      dc.SetBrush(*wxBLACK_BRUSH);
      dc.DrawRectangle(popup_pos, {full_width, acc_height});

      dc.SetFont(GetFont());

      int cur_height = 10;

      for (const auto &[text, obtained] : report) {
        wxBitmap *eye_ptr = obtained ? &checked_eye_ : &unchecked_eye_;

        dc.DrawBitmap(*eye_ptr, popup_pos + wxPoint{10, cur_height});

        dc.SetTextForeground(obtained ? *wxWHITE : *wxRED);
        wxSize item_extent = dc.GetTextExtent(text);
        dc.DrawText(
            text,
            popup_pos +
                wxPoint{10 + 32 + 10,
                        cur_height + (32 - dc.GetFontMetrics().height) / 2});

        cur_height += 10 + 32;
      }
    }

    if (networks_.IsItemInNetwork(*hovered_item_)) {
      dc.SetBrush(*wxTRANSPARENT_BRUSH);

      for (const auto &[item_id1, item_id2] :
           networks_.GetNetworkGraph(*hovered_item_)) {
        const SubwayItem &item1 = GD_GetSubwayItem(item_id1);
        const SubwayItem &item2 = GD_GetSubwayItem(item_id2);

        wxPoint item1_pos = MapPosToRenderPos(
            {item1.x + AREA_ACTUAL_SIZE / 2, item1.y + AREA_ACTUAL_SIZE / 2});
        wxPoint item2_pos = MapPosToRenderPos(
            {item2.x + AREA_ACTUAL_SIZE / 2, item2.y + AREA_ACTUAL_SIZE / 2});

        int left = std::min(item1_pos.x, item2_pos.x);
        int top = std::min(item1_pos.y, item2_pos.y);
        int right = std::max(item1_pos.x, item2_pos.x);
        int bottom = std::max(item1_pos.y, item2_pos.y);

        int halfwidth = right - left;
        int halfheight = bottom - top;

        if (halfwidth < 4 || halfheight < 4) {
          dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4));
          dc.DrawLine(item1_pos, item2_pos);
          dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
          dc.DrawLine(item1_pos, item2_pos);
        } else {
          int ellipse_x;
          int ellipse_y;
          double start;
          double end;

          if (item1_pos.x > item2_pos.x) {
            ellipse_y = top;

            if (item1_pos.y > item2_pos.y) {
              ellipse_x = left - halfwidth;

              start = 0;
              end = 90;
            } else {
              ellipse_x = left;

              start = 90;
              end = 180;
            }
          } else {
            ellipse_y = top - halfheight;

            if (item1_pos.y > item2_pos.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) {
  wxPoint mouse_pos = RenderPosToMapPos(event.GetPosition());

  std::vector<int> hovered = tree_->query(
      {static_cast<float>(mouse_pos.x), static_cast<float>(mouse_pos.y), 2, 2});
  if (!hovered.empty()) {
    actual_hover_= hovered[0];
  } else {
    actual_hover_ = std::nullopt;
  }

  if (!sticky_hover_ && actual_hover_ != hovered_item_) {
    hovered_item_ = actual_hover_;

    Refresh();
  }

  if (scroll_mode_) {
    EvaluateScroll(event.GetPosition());
  }

  mouse_position_ = event.GetPosition();

  event.Skip();
}

void SubwayMap::OnMouseScroll(wxMouseEvent &event) {
  double new_zoom = zoom_;
  if (event.GetWheelRotation() > 0) {
    new_zoom = std::min(3.0, zoom_ + 0.25);
  } else {
    new_zoom = std::max(1.0, zoom_ - 0.25);
  }

  if (zoom_ != new_zoom) {
    SetZoom(new_zoom, event.GetPosition());
  }

  event.Skip();
}

void SubwayMap::OnMouseLeave(wxMouseEvent &event) {
  SetScrollSpeed(0, 0);
  mouse_position_ = std::nullopt;
}

void SubwayMap::OnMouseClick(wxMouseEvent &event) {
  if (sticky_hover_) {
    sticky_hover_ = false;

    if (actual_hover_ != hovered_item_) {
      hovered_item_ = actual_hover_;

      Refresh();
    }
  } else if (hovered_item_) {
    const SubwayItem &subway_item = GD_GetSubwayItem(*hovered_item_);
    if ((subway_item.door && !GetDoorRequirements(*subway_item.door).empty()) ||
        networks_.IsItemInNetwork(*hovered_item_)) {
      sticky_hover_ = true;
    }
  } else if (scroll_mode_) {
    scroll_mode_ = false;

    SetScrollSpeed(0, 0);
  } else if (event.GetPosition().x < GetSize().GetWidth() / 6 ||
      event.GetPosition().x > 5 * GetSize().GetWidth() / 6 ||
      event.GetPosition().y < GetSize().GetHeight() / 6 ||
      event.GetPosition().y > 5 * GetSize().GetHeight() / 6) {
    scroll_mode_ = true;

    EvaluateScroll(event.GetPosition());
  }
}

void SubwayMap::OnTimer(wxTimerEvent &event) {
  SetZoomPos({zoom_x_ + scroll_x_, zoom_y_ + scroll_y_});
  Refresh();
}

void SubwayMap::OnZoomSlide(wxCommandEvent &event) {
  double new_zoom = 1.0 + 0.25 * zoom_slider_->GetValue();

  if (new_zoom != zoom_) {
    SetZoom(new_zoom, {GetSize().GetWidth() / 2, GetSize().GetHeight() / 2});
  }
}

void SubwayMap::OnClickHelp(wxCommandEvent &event) {
  wxMessageBox(
      "Zoom in/out using the mouse wheel, Ctrl +/-, or the slider in the "
      "corner.\nClick on a side of the screen to start panning. It will follow "
      "your mouse. Click again to stop.\nHover over a door to see the "
      "requirements to open it.\nHover over a warp or active painting to see "
      "what it is connected to.\nIn painting shuffle, paintings that have not "
      "yet been checked will not show their connections.\nClick on a door or "
      "warp to make the popup stick until you click again.",
      "Subway Map Help");
}

void SubwayMap::Redraw() {
  rendered_ = wxBitmap(map_image_);

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

    wxPoint real_area_pos = {subway_item.x, subway_item.y};

    int real_area_size =
        (draw_type == ItemDrawType::kOwl ? OWL_ACTUAL_SIZE : AREA_ACTUAL_SIZE);

    if (draw_type == ItemDrawType::kBox) {
      dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1));
      dc.SetBrush(*brush_color);
      dc.DrawRectangle(real_area_pos, {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_pos);
    }
  }
}

void SubwayMap::SetUpHelpButton() {
  help_button_->SetPosition({
      GetSize().GetWidth() - help_button_->GetSize().GetWidth() - 15,
      15,
  });
}

void SubwayMap::EvaluateScroll(wxPoint pos) {
  int scroll_x;
  int scroll_y;
  if (pos.x < GetSize().GetWidth() / 9) {
    scroll_x = 20;
  } else if (pos.x < GetSize().GetWidth() / 6) {
    scroll_x = 5;
  } else if (pos.x > 8 * GetSize().GetWidth() / 9) {
    scroll_x = -20;
  } else if (pos.x > 5 * GetSize().GetWidth() / 6) {
    scroll_x = -5;
  } else {
    scroll_x = 0;
  }
  if (pos.y < GetSize().GetHeight() / 9) {
    scroll_y = 20;
  } else if (pos.y < GetSize().GetHeight() / 6) {
    scroll_y = 5;
  } else if (pos.y > 8 * GetSize().GetHeight() / 9) {
    scroll_y = -20;
  } else if (pos.y > 5 * GetSize().GetHeight() / 6) {
    scroll_y = -5;
  } else {
    scroll_y = 0;
  }

  SetScrollSpeed(scroll_x, scroll_y);
}

wxPoint SubwayMap::MapPosToRenderPos(wxPoint pos) const {
  return {static_cast<int>(pos.x * render_width_ * zoom_ /
                               map_image_.GetSize().GetWidth() +
                           zoom_x_),
          static_cast<int>(pos.y * render_width_ * zoom_ /
                               map_image_.GetSize().GetWidth() +
                           zoom_y_)};
}

wxPoint SubwayMap::MapPosToVirtualPos(wxPoint pos) const {
  return {static_cast<int>(pos.x * render_width_ * zoom_ /
                           map_image_.GetSize().GetWidth()),
          static_cast<int>(pos.y * render_width_ * zoom_ /
                           map_image_.GetSize().GetWidth())};
}

wxPoint SubwayMap::RenderPosToMapPos(wxPoint pos) const {
  return {
      std::clamp(static_cast<int>((pos.x - zoom_x_) * map_image_.GetWidth() /
                                  render_width_ / zoom_),
                 0, map_image_.GetWidth() - 1),
      std::clamp(static_cast<int>((pos.y - zoom_y_) * map_image_.GetWidth() /
                                  render_width_ / zoom_),
                 0, map_image_.GetHeight() - 1)};
}

void SubwayMap::SetZoomPos(wxPoint pos) {
  if (render_width_ * zoom_ <= GetSize().GetWidth()) {
    zoom_x_ = (GetSize().GetWidth() - render_width_ * zoom_) / 2;
  } else {
    zoom_x_ = std::clamp(
        pos.x, GetSize().GetWidth() - static_cast<int>(render_width_ * zoom_),
        0);
  }
  if (render_height_ * zoom_ <= GetSize().GetHeight()) {
    zoom_y_ = (GetSize().GetHeight() - render_height_ * zoom_) / 2;
  } else {
    zoom_y_ = std::clamp(
        pos.y, GetSize().GetHeight() - static_cast<int>(render_height_ * zoom_),
        0);
  }
}

void SubwayMap::SetScrollSpeed(int scroll_x, int scroll_y) {
  bool should_timer = (scroll_x != 0 || scroll_y != 0);
  if (should_timer != scroll_timer_->IsRunning()) {
    if (should_timer) {
      scroll_timer_->Start(1000 / 60);
    } else {
      scroll_timer_->Stop();
    }
  }

  scroll_x_ = scroll_x;
  scroll_y_ = scroll_y;
}

void SubwayMap::SetZoom(double zoom, wxPoint static_point) {
  wxPoint map_pos = RenderPosToMapPos(static_point);
  zoom_ = zoom;

  wxPoint virtual_pos = MapPosToVirtualPos(map_pos);
  SetZoomPos(-(virtual_pos - static_point));

  Refresh();

  zoom_slider_->SetValue((zoom - 1.0) / 0.25);
}

quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int &id) const {
  const SubwayItem &subway_item = GD_GetSubwayItem(id);
  return {static_cast<float>(subway_item.x), static_cast<float>(subway_item.y),
          AREA_ACTUAL_SIZE, AREA_ACTUAL_SIZE};
}