#include "subway_map.h" #include #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; } 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::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(); 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> tagged; for (const SubwayItem &subway_item : GD_GetSubwayItems()) { if (AP_HasEarlyColorHallways() && (subway_item.special == "starting_room_paintings" || subway_item.special == "early_color_hallways")) { tagged["early_color_hallways"].push_back(subway_item.id); } 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); } if (!AP_IsPilgrimageEnabled() && (subway_item.special == "sun_painting" || subway_item.special == "sun_painting_exit")) { tagged["sun_painting"].push_back(subway_item.id); } } if (AP_IsSunwarpShuffle()) { for (const auto &[index, mapping] : AP_GetSunwarpMapping()) { std::ostringstream tag; tag << "sunwarp" << mapping.dots; SubwaySunwarp fromWarp; if (index < 6) { fromWarp.dots = index + 1; fromWarp.type = SubwaySunwarpType::kEnter; } else { fromWarp.dots = index - 5; fromWarp.type = SubwaySunwarpType::kExit; } SubwaySunwarp toWarp; if (mapping.exit_index < 6) { toWarp.dots = mapping.exit_index + 1; toWarp.type = SubwaySunwarpType::kEnter; } else { toWarp.dots = mapping.exit_index - 5; toWarp.type = SubwaySunwarpType::kExit; } tagged[tag.str()].push_back(GD_GetSubwayItemForSunwarp(fromWarp)); tagged[tag.str()].push_back(GD_GetSubwayItemForSunwarp(toWarp)); } } 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 &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 hovered = tree_->query( {static_cast(mouse_pos.x), static_cast(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) { bool finished = false; if (actual_hover_) { const SubwayItem &subway_item = GD_GetSubwayItem(*actual_hover_); if ((subway_item.door && !GetDoorRequirements(*subway_item.door).empty()) || networks_.IsItemInNetwork(*hovered_item_)) { if (actual_hover_ != hovered_item_) { hovered_item_ = actual_hover_; if (!hovered_item_) { sticky_hover_ = false; } Refresh(); } else { sticky_hover_ = !sticky_hover_; } finished = true; } } if (!finished) { if (scroll_mode_) { scroll_mode_ = false; SetScrollSpeed(0, 0); SetCursor(wxCURSOR_ARROW); } 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()); SetCursor(wxCURSOR_CROSS); } else { sticky_hover_ = false; } } } 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.\nA green shaded owl " "means that there is a painting entrance there.\nA red shaded owl means " "that there are only painting exits there.\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_); wxGCDC gcdc(dc); for (const SubwayItem &subway_item : GD_GetSubwayItems()) { ItemDrawType draw_type = ItemDrawType::kNone; const wxBrush *brush_color = wxGREY_BRUSH; std::optional shade_color; if (AP_HasEarlyColorHallways() && (subway_item.special == "starting_room_paintings" || subway_item.special == "early_color_hallways")) { draw_type = ItemDrawType::kOwl; if (subway_item.special == "starting_room_paintings") { shade_color = wxColour(0, 255, 0, 128); } else { shade_color = wxColour(255, 0, 0, 128); } } else if (subway_item.special == "sun_painting") { if (!AP_IsPilgrimageEnabled()) { if (IsDoorOpen(*subway_item.door)) { draw_type = ItemDrawType::kOwl; shade_color = wxColour(0, 255, 0, 128); } else { draw_type = ItemDrawType::kBox; 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; bool has_codomain_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 if (AP_IsPaintingMappedTo(painting_id)) { has_codomain_painting = true; } } else { has_unchecked_painting = true; } } if (has_unchecked_painting || has_mapped_painting || has_codomain_painting) { draw_type = ItemDrawType::kOwl; if (has_checked_painting) { if (has_mapped_painting) { shade_color = wxColour(0, 255, 0, 128); } else { shade_color = wxColour(255, 0, 0, 128); } } } } else if (!subway_item.tags.empty()) { draw_type = ItemDrawType::kOwl; } } else if (subway_item.door) { draw_type = ItemDrawType::kBox; if (IsDoorOpen(*subway_item.door)) { brush_color = wxGREEN_BRUSH; } else { brush_color = wxRED_BRUSH; } } 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) { gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1)); gcdc.SetBrush(*brush_color); gcdc.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)); gcdc.DrawBitmap(owl_bitmap, real_area_pos); if (shade_color) { gcdc.SetBrush(wxBrush(*shade_color)); gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size}); } } } } 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(pos.x * render_width_ * zoom_ / map_image_.GetSize().GetWidth() + zoom_x_), static_cast(pos.y * render_width_ * zoom_ / map_image_.GetSize().GetWidth() + zoom_y_)}; } wxPoint SubwayMap::MapPosToVirtualPos(wxPoint pos) const { return {static_cast(pos.x * render_width_ * zoom_ / map_image_.GetSize().GetWidth()), static_cast(pos.y * render_width_ * zoom_ / map_image_.GetSize().GetWidth())}; } wxPoint SubwayMap::RenderPosToMapPos(wxPoint pos) const { return { std::clamp(static_cast((pos.x - zoom_x_) * map_image_.GetWidth() / render_width_ / zoom_), 0, map_image_.GetWidth() - 1), std::clamp(static_cast((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(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(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 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}; } 8800; background-color: #fff0ff } /* Literal.String.Regex */ .highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */ .highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */ .highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */ .highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */ .highlight .vc { color: #336699 } /* Name.Variable.Class */ .highlight .vg { color: #dd7700 } /* Name.Variable.Global */ .highlight .vi { color: #3333bb } /* Name.Variable.Instance */ .highlight .vm { color: #336699 } /* Name.Variable.Magic */ .highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
name: "Tower"
panels {
  name: "RHINO"
  path: "Panels/Floor 2/panel_1"
  clue: "rhino"
  answer: "hornet"
  symbols: SPARKLES
  symbols: BOXES
}
panels {
  name: "FISH"
  path: "Panels/Floor 2/panel_2"
  clue: "fish"
  answer: "fishes"
  symbols: PLANET
}
panels {
  name: "LINEAGE"
  path: "Panels/Floor 2/panel_3"
  clue: "lineage"
  answer: "eaglet"
  symbols: AGE
  symbols: EVAL
}
panels {
  name: "GRUMPY"
  path: "Panels/Floor 2/panel_4"
  clue: "grumpy"
  answer: "crab"
  symbols: SUN
  symbols: SPARKLES
}
panels {
  name: "ZEBRA"
  path: "Panels/Floor 2/panel_5"
  clue: "zebra"
  answer: "tiger"
  symbols: BOXES
}
panels {
  name: "SLID"
  path: "Panels/Floor 2/panel_6"
  clue: "slid"
  answer: "liquid"
  symbols: SPARKLES
  symbols: CROSS
}
panels {
  name: "FUEL"
  path: "Panels/Floor 2/panel_7"
  clue: "fuel"
  answer: "fir"
  symbols: SPARKLES
  symbols: CROSS
}
panels {
  name: "DOUGH"
  path: "Panels/Floor 2/panel_8"
  clue: "dough"
  answer: "sandwich"
  symbols: BOXES
  symbols: CROSS
}
panels {
  name: "LIQUID"
  path: "Panels/Floor 2/panel_9"
  clue: "liquid"
  answer: "plasma"
  symbols: CROSS
}
panels {
  name: "ICE"
  path: "Panels/Floor 2/panel_10"
  clue: "ice"
  answer: "sea"
  symbols: BOXES
  symbols: CROSS
}
panels {
  name: "FLEECE"
  path: "Panels/Floor 2/panel_11"
  clue: "fleece"
  answer: "nephew"
  symbols: ZERO
  symbols: GENDER
}
panels {
  name: "NEED"
  path: "Panels/Floor 2/panel_12"
  clue: "need"
  answer: "aunt"
  symbols: ZERO
  symbols: PYRAMID
}
panels {
  name: "ANNOY (1)"
  path: "Panels/Floor 2/panel_13"
  clue: "annoy"
  answer: "brother"
  symbols: SPARKLES
  symbols: PYRAMID
}
panels {
  name: "ANNOY (2)"
  path: "Panels/Floor 2/panel_14"
  clue: "annoy"
  answer: "father"
  symbols: ZERO
  symbols: PYRAMID
}
panels {
  name: "GIGGLING"
  path: "Panels/Floor 2/panel_15"
  clue: "giggling"
  answer: "daughter"
  symbols: SPARKLES
  symbols: PYRAMID
}
panels {
  name: "MINUSCULE"
  path: "Panels/Floor 3/panel_1"
  clue: "minuscule"
  answer: "tin"
  symbols: SUN
  symbols: SPARKLES
}
panels {
  name: "TYPEWRITER"
  path: "Panels/Floor 3/panel_2"
  clue: "typewriter"
  answer: "keyboards"
  symbols: PLANET
  symbols: AGE
}
panels {
  name: "READJUST"
  path: "Panels/Floor 3/panel_3"
  clue: "readjust"
  answer: "adjusted"
  symbols: SPARKLES
}
panels {
  name: "REINDICT"
  path: "Panels/Floor 3/panel_4"
  clue: "reindict"
  answer: "credit"
  symbols: ANAGRAM
  symbols: EVAL
}
panels {
  name: "COPYRIGHT"
  path: "Panels/Floor 3/panel_5"
  clue: "copyright"
  answer: "trader"
  symbols: SUN
  symbols: SPARKLES
}
panels {
  name: "CONTINENT"
  path: "Panels/Floor 3/panel_6"
  clue: "continent"
  answer: "shop"
  symbols: BOXES
}
panels {
  name: "FOOT"
  path: "Panels/Floor 3/panel_7"
  clue: "foot"
  answer: "house"
  symbols: BOXES
}
panels {
  name: "EYE"
  path: "Panels/Floor 3/panel_8"
  clue: "eye"
  answer: "school"
  symbols: BOXES
  symbols: STARS
}
panels {
  name: "RIVER"
  path: "Panels/Floor 3/panel_9"
  clue: "river"
  answer: "hospital"
  symbols: BOXES
}
panels {
  name: "NUCLEUS"
  path: "Panels/Floor 3/panel_10"
  clue: "nucleus"
  answer: "jail"
  symbols: BOXES
}
panels {
  name: "MIX (1)"
  path: "Panels/Floor 3/panel_11"
  clue: "mix"
  answer: "bar"
  symbols: JOB
  symbols: STARS
}
panels {
  name: "HEAL"
  path: "Panels/Floor 3/panel_12"
  clue: "heal"
  answer: "hospital"
  symbols: JOB
  symbols: STARS
}
panels {
  name: "SCREW"
  path: "Panels/Floor 3/panel_13"
  clue: "screw"
  answer: "toolbox"
  symbols: JOB
  symbols: STARS
}
panels {
  name: "FLY"
  path: "Panels/Floor 3/panel_14"
  clue: "fly"
  answer: "airplane"
  symbols: JOB
  symbols: STARS
}
panels {
  name: "MIX (2)"
  path: "Panels/Floor 3/panel_15"
  clue: "mix"
  answer: "kitchen"
  symbols: JOB
  symbols: STARS
}
panels {
  name: "BARE"
  path: "Panels/Floor 4/panel_1"
  clue: "bare"
  answer: "cub"
  symbols: ZERO
  symbols: AGE
}
panels {
  name: "CANON"
  path: "Panels/Floor 4/panel_2"
  clue: "canon"
  answer: "boom"
  symbols: ZERO
  symbols: SOUND
}
panels {
  name: "DUCTS"
  path: "Panels/Floor 4/panel_3"
  clue: "ducts"
  answer: "quack"
  symbols: ZERO
  symbols: SOUND
}
panels {
  name: "COY"
  path: "Panels/Floor 4/panel_4"
  clue: "coy"
  answer: "fish"
  symbols: ZERO
  symbols: EXAMPLE
}
panels {
  name: "LYNX"
  path: "Panels/Floor 4/panel_5"
  clue: "lynx"
  answer: "slink"
  symbols: ZERO
  symbols: ANAGRAM
}
panels {
  name: "SCENTS (1)"
  path: "Panels/Floor 4/panel_6"
  clue: "scents"
  answer: "dollars"
  symbols: ZERO
  symbols: BOXES
}
panels {
  name: "SCENTS (2)"
  path: "Panels/Floor 4/panel_7"
  clue: "scents"
  answer: "sight"
  symbols: ZERO
  symbols: EXAMPLE
}
panels {
  name: "SEIZE (1)"
  path: "Panels/Floor 4/panel_8"
  clue: "seize"
  answer: "oceans"
  symbols: ZERO
  symbols: PYRAMID
}
panels {
  name: "SEIZE (2)"
  path: "Panels/Floor 4/panel_9"
  clue: "seize"
  answer: "eye"
  symbols: ZERO
  symbols: PLANET
  symbols: JOB
}
panels {
  name: "THROWN"
  path: "Panels/Floor 4/panel_10"
  clue: "thrown"
  answer: "chair"
  symbols: ZERO
  symbols: EXAMPLE
}
panels {
  name: "FUND"
  path: "Panels/Floor 5/panel_1"
  clue: "fund"
  answer: "find"
  symbols: SPARKLES
  symbols: AGE
}
panels {
  name: "ENVELOPE"
  path: "Panels/Floor 5/panel_2"
  clue: "envelope"
  answer: "letters"
  symbols: PLANET
  symbols: STARS
}
panels {
  name: "ROTE"
  path: "Panels/Floor 5/panel_3"
  clue: "rote"
  answer: "write"
  symbols: ZERO
  symbols: AGE
}
panels {
  name: "ENVELOPES"
  path: "Panels/Floor 5/panel_4"
  clue: "envelopes"
  answer: "words"
  symbols: BOXES
  symbols: STARS
}
panels {
  name: "ERECTS"
  path: "Panels/Floor 5/panel_5"
  clue: "erects"
  answer: "hidden"
  symbols: SUN
  symbols: ANAGRAM
}
panels {
  name: "HIDE (1)"
  path: "Panels/Floor 5/panel_6"
  clue: "hide"
  answer: "see"
  symbols: SUN
  symbols: SPARKLES
}
panels {
  name: "HUE"
  path: "Panels/Floor 5/panel_7"
  clue: "hue"
  answer: "colors"
  symbols: SUN
  symbols: PLANET
}
panels {
  name: "CRUST"
  path: "Panels/Floor 5/panel_8"
  clue: "crust"
  answer: "read"
  symbols: SPARKLES
  symbols: BOXES
}
panels {
  name: "HIDE (2)"
  path: "Panels/Floor 5/panel_9"
  clue: "hide"
  answer: "secret"
  symbols: SUN
  symbols: SPARKLES
}
panels {
  name: "BETTER"
  path: "Panels/Floor 5/panel_10"
  clue: "better"
  answer: "letters"
  symbols: ZERO
  symbols: PLANET
}
panels {
  name: "Colors"
  path: "Panels/End/panel_1"
  clue: ""
  answer: "backdoor"
  symbols: QUESTION
  required_door { name: "Colors Panel" }
}
panels {
  name: "Context"
  path: "Panels/End/panel_2"
  clue: ""
  answer: "door"
  required_door { name: "Context Panel" }
}