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

                     



                                    
                                   
 
                                              
 
                                                                    
                                       




                                                                               




                                                                            






                                                                               





                                                                           
           
 
                                    
                                                    

                                                           
 











                                                                   
                                                                 



















                                                                         
                                    










                                                                          
   
           

                                                         
                                                         


                                                             
                                              
                                         
   
                             

                                             
 



                                                                              
 
                          
 


                                                                     
 


                                                 
 

                                           




                                                                    
       
                                                             
       
 
                                    
                                                            
 
                            
 
                          
 
                                                   
                                                                       
 
                                                                     
 


                                                           

                                                                             
 










                                                             


                                                                              
 


                                                        




                                                                 
                                            
                                                                
                                            
                



                        
                                          
                            
                                            
                                           
 


                               
 

                         
                  
                                         
 
                                            








                                           
           
 
                                                                 
                                                                 
                                                                
                                                                 
         
       

     


                                                  
                                                             
 
                                          
                                                                                




                                          
                                     
              
   

























                                                                     

               



























                                                                           
                          
                                
                                           







                                                                       
                                           

                                                                      
                                           

                                                            


                                                                                


                             

                                                             
                                        












                                                





























                                                                      
     
                                                                               
 
                        
                               




                                                                                

                                                             
                                                                        
                                                 
                                                                     
                                               
     
 
 
                                                         











                                                                


                                                         



































                                                                                
 
                                                                             


                                                                               
#include "subway_map.h"

#include <wx/dcbuffer.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_TIMER, &SubwayMap::OnTimer, this);
}

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

  wxBufferedPaintDC dc(this);
  dc.SetBackground(*wxWHITE_BRUSH);
  dc.Clear();
  dc.DrawBitmap(rendered_, zoom_x_, zoom_y_);

  if (hovered_item_) {
    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 ? &unchecked_eye_ : &checked_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});
  std::optional<int> new_hovered_item;
  if (!hovered.empty()) {
    new_hovered_item = hovered[0];
  }

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

    Refresh();
  }

  int scroll_x;
  int scroll_y;
  if (event.GetPosition().x < GetSize().GetWidth() / 9) {
    scroll_x = 20;
  } else if (event.GetPosition().x < GetSize().GetWidth() / 6) {
    scroll_x = 5;
  } else if (event.GetPosition().x > 8 * GetSize().GetWidth() / 9) {
    scroll_x = -20;
  } else if (event.GetPosition().x > 5 * GetSize().GetWidth() / 6) {
    scroll_x = -5;
  } else {
    scroll_x = 0;
  }
  if (event.GetPosition().y < GetSize().GetHeight() / 9) {
    scroll_y = 20;
  } else if (event.GetPosition().y < GetSize().GetHeight() / 6) {
    scroll_y = 5;
  } else if (event.GetPosition().y > 8 * GetSize().GetHeight() / 9) {
    scroll_y = -20;
  } else if (event.GetPosition().y > 5 * GetSize().GetHeight() / 6) {
    scroll_y = -5;
  } else {
    scroll_y = 0;
  }

  SetScrollSpeed(scroll_x, scroll_y);

  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) {
    wxPoint map_pos = RenderPosToMapPos(event.GetPosition());
    zoom_ = new_zoom;

    wxPoint virtual_pos = MapPosToVirtualPos(map_pos);
    SetZoomPos(-(virtual_pos - event.GetPosition()));

    Redraw();
    Refresh();
  }

  event.Skip();
}

void SubwayMap::OnMouseLeave(wxMouseEvent &event) { SetScrollSpeed(0, 0); }

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

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_ * zoom_, render_height_ * zoom_, wxIMAGE_QUALITY_BILINEAR));

  SetZoomPos({zoom_x_, zoom_y_});

  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 = MapPosToVirtualPos({subway_item.x, subway_item.y});

    int real_area_size =
        render_width_ * zoom_ *
        (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_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);
    }
  }
}

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

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