1 ///////////////////////////////////////////////////////////////////////////////
2 //            Copyright (C) 2012-2018 by Bertram (Valyria Tear)
3 //                         All Rights Reserved
4 //
5 // This code is licensed under the GNU GPL version 2. It is free software
6 // and you may modify it and/or redistribute it under the terms of this license.
7 // See https://www.gnu.org/copyleft/gpl.html for details.
8 ///////////////////////////////////////////////////////////////////////////////
9 
10 #include "modes/menu/menu_windows/menu_skillgraph_window.h"
11 
12 #include "modes/menu/menu_mode.h"
13 
14 #include "engine/audio/audio.h"
15 #include "engine/input.h"
16 #include "engine/system.h"
17 
18 #include "common/global/skill_graph/skill_graph.h"
19 #include "common/global/actors/global_character.h"
20 
21 #include <limits>
22 
23 using namespace vt_menu::private_menu;
24 using namespace vt_utils;
25 using namespace vt_audio;
26 using namespace vt_video;
27 using namespace vt_gui;
28 using namespace vt_global;
29 using namespace vt_input;
30 using namespace vt_system;
31 using namespace vt_common;
32 
33 namespace vt_menu
34 {
35 
36 namespace private_menu
37 {
38 
39 //! \brief Area where on can draw the skill tree nodes
40 const float SKILL_GRAPH_AREA_WIDTH = 815.0f;
41 const float SKILL_GRAPH_AREA_HEIGHT = 415.0f;
42 const float WINDOW_BORDER_WIDTH = 18.0f;
43 const float NODES_DISPLAY_MARGIN = 100.0f;
44 //! \brief Skill node colors
45 const vt_video::Color grayed_path = vt_video::Color(0.4f, 0.4f, 0.4f, 0.2f);
46 const vt_video::Color node_blue = vt_video::Color(0.0f, 0.0f, 0.8f, 0.7f);
47 
48 //! \brief Top left bottom menu position
49 const float BOTTOM_MENU_X_POS = 90.0f;
50 const float BOTTOM_MENU_Y_POS = 565.0f;
51 
SkillGraphWindow()52 SkillGraphWindow::SkillGraphWindow() :
53     _skillgraph_state(SKILLGRAPH_STATE_NONE),
54     _selected_character(nullptr), // Invalid character
55     _current_offset(-1.0f, -1.0f), // Invalid view
56     _view_position(0.0f, 0.0f),
57     _selected_node_id(std::numeric_limits<uint32_t>::max()), // Invalid index
58     _character_node_id(std::numeric_limits<uint32_t>::max()), // Invalid index
59     _active(false)
60 {
61     _location_pointer.SetStatic(true);
62     if(!_location_pointer.Load("data/gui/menus/hand_down.png"))
63         PRINT_ERROR << "Could not load pointer image!" << std::endl;
64 
65     _bottom_info.SetPosition(BOTTOM_MENU_X_POS, BOTTOM_MENU_Y_POS);
66 
67     _InitCharSelect();
68 
69     // We set them here so that they are re-translated when changing the language.
70     _select_character_text.SetText(UTranslate("Choose a character."),
71                                    TextStyle("text20"));
72 }
73 
SetActive(bool is_active_state)74 void SkillGraphWindow::SetActive(bool is_active_state)
75 {
76     _active = is_active_state;
77 
78     // Activate window and first option box...or deactivate both
79     if(_active) {
80         _char_select.SetCursorState(VIDEO_CURSOR_STATE_VISIBLE);
81         _skillgraph_state = SKILLGRAPH_STATE_CHAR;
82     } else {
83         _char_select.SetCursorState(VIDEO_CURSOR_STATE_HIDDEN);
84         _skillgraph_state = SKILLGRAPH_STATE_NONE;
85         return;
86     }
87 }
88 
Update()89 void SkillGraphWindow::Update()
90 {
91     if (!_active)
92         return;
93 
94     switch (_skillgraph_state) {
95     // Do nothing in default state
96     default:
97     case SKILLGRAPH_STATE_NONE:
98         return;
99         break;
100     case SKILLGRAPH_STATE_CHAR:
101         _UpdateSkillCharacterSelectState();
102         break;
103     case SKILLGRAPH_STATE_LIST:
104         _UpdateSkillGraphListState();
105         break;
106     }
107 }
108 
Draw()109 void SkillGraphWindow::Draw()
110 {
111     // Background window
112     MenuWindow::Draw();
113 
114     switch (_skillgraph_state) {
115     // Do nothing in default state
116     default:
117     case SKILLGRAPH_STATE_NONE:
118         return;
119         break;
120     case SKILLGRAPH_STATE_CHAR:
121         _DrawCharacterState();
122         break;
123     case SKILLGRAPH_STATE_LIST:
124         _DrawSkillGraphState();
125         break;
126     }
127 }
128 
DrawBottomWindow()129 void SkillGraphWindow::DrawBottomWindow()
130 {
131     switch (_skillgraph_state) {
132     // Do nothing in default state
133     default:
134     case SKILLGRAPH_STATE_NONE:
135         return;
136         break;
137     case SKILLGRAPH_STATE_CHAR:
138         VideoManager->Move(BOTTOM_MENU_X_POS, BOTTOM_MENU_Y_POS);
139         _select_character_text.Draw();
140         break;
141     case SKILLGRAPH_STATE_LIST:
142         _bottom_info.Draw();
143         break;
144     }
145 }
146 
SetCharacter()147 bool SkillGraphWindow::SetCharacter()
148 {
149     _selected_character =
150         GlobalManager->GetCharacterHandler().GetActiveParty().GetCharacterAtIndex(_char_select.GetSelection());
151     if (!_selected_character) {
152         _selected_node_id = 0;
153         return false;
154     }
155 
156     // Set base data for memoization
157     _character_icon = _selected_character->GetStaminaIcon();
158 
159     // Set the selection node to where the character was last located.
160     _selected_node_id = _selected_character->GetSkillNodeLocation();
161     _character_node_id = _selected_node_id;
162 
163     return true;
164 }
165 
_InitCharSelect()166 void SkillGraphWindow::_InitCharSelect()
167 {
168     // Character selection set up
169     std::vector<ustring> options;
170     uint32_t size = GlobalManager->GetCharacterHandler().GetActiveParty().GetPartySize();
171 
172     _char_select.SetPosition(72.0f, 109.0f);
173     _char_select.SetDimensions(360.0f, 432.0f, 1, 4, 1, 4);
174     _char_select.SetCursorOffset(-50.0f, -6.0f);
175     _char_select.SetTextStyle(TextStyle("text20"));
176     _char_select.SetHorizontalWrapMode(VIDEO_WRAP_MODE_STRAIGHT);
177     _char_select.SetVerticalWrapMode(VIDEO_WRAP_MODE_STRAIGHT);
178     _char_select.SetOptionAlignment(VIDEO_X_LEFT, VIDEO_Y_CENTER);
179 
180     // Use blank strings....won't be seen anyway
181     for(uint32_t i = 0; i < size; i++) {
182         options.push_back(MakeUnicodeString(" "));
183     }
184 
185     // Set options, selection and cursor state
186     _char_select.SetOptions(options);
187     _char_select.SetSelection(0);
188     _char_select.SetCursorState(VIDEO_CURSOR_STATE_HIDDEN);
189 }
190 
_UpdateSkillCharacterSelectState()191 void SkillGraphWindow::_UpdateSkillCharacterSelectState()
192 {
193     _char_select.Update();
194 
195     if(InputManager->CancelPress()) {
196         SetActive(false);
197         return;
198     }
199     if (InputManager->UpPress()) {
200         _char_select.InputUp();
201     }
202     else if (InputManager->DownPress()) {
203         _char_select.InputDown();
204     }
205     else if (InputManager->ConfirmPress()) {
206         _char_select.InputConfirm();
207         _char_select.SetCursorState(VIDEO_CURSOR_STATE_HIDDEN);
208 
209         // If the character is unset, set the default node
210         if(!SetCharacter()) {
211             return;
212         }
213         _skillgraph_state = SKILLGRAPH_STATE_LIST;
214 
215         // Set view on node
216         _ResetSkillGraphView();
217     }
218 }
219 
_UpdateSkillGraphListState()220 void SkillGraphWindow::_UpdateSkillGraphListState()
221 {
222     if(InputManager->CancelPress()) {
223         _skillgraph_state = SKILLGRAPH_STATE_CHAR;
224         _char_select.SetCursorState(VIDEO_CURSOR_STATE_VISIBLE);
225         return;
226     }
227 
228     _UpdateSkillGraphView();
229 
230     _HandleNodeTransaction();
231 
232     // Only update animation when necessary
233     if (!_Navigate())
234         return;
235 
236     SkillGraph& skill_graph = vt_global::GlobalManager->GetSkillGraph();
237     SkillNode* current_skill_node = skill_graph.GetSkillNode(_selected_node_id);
238 
239     // Update bottom windows info
240     if (current_skill_node) {
241         _bottom_info.SetNode(*current_skill_node,
242                              _selected_character->GetUnspentExperiencePoints(),
243                              _selected_character->IsSkillNodeObtained(_selected_node_id));
244     }
245 }
246 
_DrawCharacterState()247 void SkillGraphWindow::_DrawCharacterState()
248 {
249     _char_select.Draw();
250 }
251 
_DrawSkillGraphState()252 void SkillGraphWindow::_DrawSkillGraphState()
253 {
254     // Scissor the view to cut the layout properly
255     float left = GetXPosition() + WINDOW_BORDER_WIDTH;
256     float top = GetYPosition() + WINDOW_BORDER_WIDTH;
257     float width = SKILL_GRAPH_AREA_WIDTH;
258     float height = SKILL_GRAPH_AREA_HEIGHT;
259 
260     VideoManager->PushScissoredRect(left,
261                                     top,
262                                     width,
263                                     height);
264 
265     // Debug draw limits
266 //    VideoManager->DrawRectangleOutline(left,
267 //                                       left + SKILL_GRAPH_AREA_WIDTH,
268 //                                       top + SKILL_GRAPH_AREA_HEIGHT,
269 //                                       top,
270 //                                       2, Color::white);
271 
272     // Draw the visible lines
273     for (Line2D node_line : _displayed_node_links) {
274         vt_video::VideoManager->DrawLine(node_line.begin.x,
275                                          node_line.begin.y, 7,
276                                          node_line.end.x,
277                                          node_line.end.y, 7,
278                                          grayed_path);
279     }
280 
281     // Color links between obtained nodes
282     for (Line2D node_line : _colored_displayed_node_links) {
283         vt_video::VideoManager->DrawLine(node_line.begin.x,
284                                          node_line.begin.y, 10,
285                                          node_line.end.x,
286                                          node_line.end.y, 10,
287                                          node_blue);
288     }
289 
290     Position2D pointer_location(-1.0f, -1.0f);
291 
292     // Draw the visible skill nodes
293     for (SkillNode* skill_node : _displayed_skill_nodes) {
294         VideoManager->Move(_view_position.x, _view_position.y);
295         VideoManager->MoveRelative(skill_node->GetXPosition(),
296                                    skill_node->GetYPosition());
297         // Center the image
298         vt_video::AnimatedImage& image = skill_node->GetIconImage();
299         image.SetWidthKeepRatio(36.0f);
300         VideoManager->MoveRelative(-image.GetWidth() / 2.0f,
301                                    -image.GetHeight() / 2.0f);
302         image.Draw();
303 
304         // Setup the marker location to be on the currently selected node
305         if (_selected_node_id == skill_node->GetId()) {
306             pointer_location.x = _view_position.x + skill_node->GetXPosition()
307                 - _location_pointer.GetWidth() / 3.0f;
308             pointer_location.y = _view_position.y + skill_node->GetYPosition()
309                 - image.GetHeight() - _location_pointer.GetHeight();
310         }
311 
312         // Draw the character portrait if the character is on its latest learned skill.
313         if (_character_node_id == skill_node->GetId()) {
314             VideoManager->Move(_view_position.x, _view_position.y);
315             VideoManager->MoveRelative(skill_node->GetXPosition(),
316                                        skill_node->GetYPosition());
317             // Center the image
318             VideoManager->MoveRelative(-_character_icon.GetWidth() / 2.0f,
319                                        -_character_icon.GetHeight() / 2.0f);
320             _character_icon.Draw();
321         }
322     }
323 
324     // Draw the location pointer if the node was found
325     if (pointer_location.x > 0.0f || pointer_location.y > 0.0f) {
326         VideoManager->Move(pointer_location.x, pointer_location.y);
327         _location_pointer.Draw();
328     }
329 
330     VideoManager->PopScissoredRect();
331 }
332 
_ResetSkillGraphView()333 void SkillGraphWindow::_ResetSkillGraphView()
334 {
335     // Set current offset based on the currently selected node
336     SkillGraph& skill_graph = vt_global::GlobalManager->GetSkillGraph();
337     SkillNode* current_skill_node = skill_graph.GetSkillNode(_selected_node_id);
338 
339     // If the node is invalid, try the default one.
340     if (current_skill_node == nullptr) {
341         current_skill_node = skill_graph.GetSkillNode(0);
342         _selected_node_id = 0;
343     }
344 
345     // If the default one fails, set an empty view
346     if (current_skill_node == nullptr) {
347         _current_offset.x = -1.0f;
348         _current_offset.y = -1.0f;
349         _selected_node_id = std::numeric_limits<uint32_t>::max();
350         PRINT_WARNING << "Empty Skill Graph View" << std::endl;
351         return;
352     }
353 
354     _UpdateSkillGraphView(false);
355 }
356 
_UpdateSkillGraphView(bool scroll,bool force)357 void SkillGraphWindow::_UpdateSkillGraphView(bool scroll, bool force)
358 {
359     // Check to prevent invalid updates
360     if (_selected_node_id == std::numeric_limits<uint32_t>::max())
361         return;
362 
363     SkillGraph& skill_graph = vt_global::GlobalManager->GetSkillGraph();
364     SkillNode* current_skill_node = skill_graph.GetSkillNode(_selected_node_id);
365 
366     _current_offset = current_skill_node->GetPosition();
367 
368     // Get the current view offset
369     Position2D target_position = GetPosition();
370     target_position.x = target_position.x
371                        + (SKILL_GRAPH_AREA_WIDTH / 2.0f)
372                        + WINDOW_BORDER_WIDTH
373                        - _current_offset.x;
374     target_position.y = target_position.y
375                        + (SKILL_GRAPH_AREA_HEIGHT / 2.0f)
376                        + WINDOW_BORDER_WIDTH
377                        - _current_offset.y;
378 
379     // Don't update the view if it is already centered
380     if (_view_position == target_position && !force) {
381         return;
382     }
383 
384     Vector2D target_distance(target_position.x - _view_position.x,
385                              target_position.y - _view_position.y);
386 
387     if (!scroll) {
388         // Make it instant
389         _view_position = target_position;
390         // Reset the distance in that case to avoid a faulty view.
391         target_distance = Position2D(0.0f, 0.0f);
392     }
393     else {
394         // Smooth the view move
395         const float max_speed = 30.0f;
396         const float min_speed = 100.0f;
397         const float pixel_move_x = (min_speed - std::abs(target_distance.x)) / 10.0f < max_speed / 10.0f ?
398                                    max_speed / 10.0f : (min_speed - std::abs(target_distance.x)) / 10.0f;
399         const float pixel_move_y = (min_speed - std::abs(target_distance.y)) / 10.0f < max_speed / 10.0f ?
400                                    max_speed / 10.0f : (min_speed - std::abs(target_distance.y)) / 10.0f;
401         const float update_time = static_cast<float>(vt_system::SystemManager->GetUpdateTime());
402         const Position2D update_move(update_time / pixel_move_x,
403                                      update_time / pixel_move_y);
404 
405         // Make the view scroll
406         if (_view_position.x < target_position.x) {
407             _view_position.x += update_move.x;
408             if (_view_position.x > target_position.x)
409                 _view_position.x = target_position.x;
410         }
411         else if (_view_position.x > target_position.x) {
412             _view_position.x -= update_move.x;
413             if (_view_position.x < target_position.x)
414                 _view_position.x = target_position.x;
415         }
416 
417         if (_view_position.y < target_position.y) {
418             _view_position.y += update_move.y;
419             if (_view_position.y > target_position.y)
420                 _view_position.y = target_position.y;
421         }
422         else if (_view_position.y > target_position.y) {
423             _view_position.y -= update_move.y;
424             if (_view_position.y < target_position.y)
425                 _view_position.y = target_position.y;
426         }
427     }
428 
429     // Update the skill node displayed list
430     const float area_width = SKILL_GRAPH_AREA_WIDTH / 2.0f + NODES_DISPLAY_MARGIN;
431     const float area_height = SKILL_GRAPH_AREA_HEIGHT / 2.0f + NODES_DISPLAY_MARGIN;
432     const Position2D min_view(_current_offset.x - area_width + target_distance.x,
433                               _current_offset.y - area_height + target_distance.y);
434     const Position2D max_view(_current_offset.x + area_width + target_distance.x,
435                               _current_offset.y + area_height + target_distance.y);
436     Rectangle2D nodes_rect(min_view.x, max_view.x,
437                            min_view.y, max_view.y);
438 
439     // Do not reload visible nodes more than necessary
440     static uint32_t update_timer = 0;
441     update_timer += vt_system::SystemManager->GetUpdateTime();
442     if (_view_position == target_position || update_timer >= 200) {
443         update_timer = 0;
444         // Based on current offset, reload visible nodes
445         _displayed_skill_nodes.clear();
446         auto skill_nodes = skill_graph.GetSkillNodes();
447         for (SkillNode* skill_node : skill_nodes) {
448             if (!nodes_rect.Contains(skill_node->GetPosition())) {
449                 continue;
450             }
451             _displayed_skill_nodes.push_back(skill_node);
452         }
453     }
454 
455     // Prepare lines coordinates for draw time
456     _displayed_node_links.clear();
457     _colored_displayed_node_links.clear();
458     // default ones (white, grayed out)
459     for (SkillNode* skill_node : _displayed_skill_nodes) {
460         auto node_links = skill_node->GetChildrenNodeLinks();
461         // Don't load anything if there are no links
462         if (node_links.empty())
463             continue;
464 
465         // Load line start
466         Line2D node_line;
467         node_line.begin.x = skill_node->GetXPosition() + _view_position.x;
468         node_line.begin.y = skill_node->GetYPosition() + _view_position.y;
469 
470         // For each link, add a line end
471         for (uint32_t link_id : node_links) {
472             SkillNode* linked_node = skill_graph.GetSkillNode(link_id);
473             if (!linked_node)
474                 continue;
475 
476             // Don't draw the line if it goes over the edge.
477             Position2D node_pos = linked_node->GetPosition();
478 
479             node_line.end.x = node_pos.x + _view_position.x;
480             node_line.end.y = node_pos.y + _view_position.y;
481 
482             // Prepare the line to be colored if both nodes were acquired by the character
483             if (_selected_character->IsSkillNodeObtained(skill_node->GetId())
484                         && _selected_character->IsSkillNodeObtained(link_id)) {
485                 _colored_displayed_node_links.push_back(node_line);
486             }
487 
488             _displayed_node_links.push_back(node_line);
489         }
490     }
491 }
492 
493 //! \brief Returns whether the node link is within the given links
isNodeWithin(const std::vector<uint32_t> & node_links,uint32_t node_id)494 static bool isNodeWithin(const std::vector<uint32_t>& node_links, uint32_t node_id)
495 {
496     for (auto node_link : node_links) {
497         if (node_link == node_id)
498             return true;
499     }
500     return false;
501 }
502 
_Navigate()503 bool SkillGraphWindow::_Navigate()
504 {
505     if (!InputManager->ArrowPress())
506         return false;
507 
508     SkillGraph& skill_graph = vt_global::GlobalManager->GetSkillGraph();
509     SkillNode* current_skill_node = skill_graph.GetSkillNode(_selected_node_id);
510     const Position2D current_pos = current_skill_node->GetPosition();
511     // Get every node links
512     auto node_links = current_skill_node->GetChildrenNodeLinks();
513     const auto parent_node_links = current_skill_node->GetParentNodeLinks();
514     node_links.insert(node_links.end(), parent_node_links.begin(), parent_node_links.end());
515 
516     SkillNode* selected_node = nullptr;
517     for (SkillNode* target_node : _displayed_skill_nodes) {
518         // Don't check against self
519         if (target_node == current_skill_node)
520             continue;
521 
522         // Check whether the node is within current links
523         if (!isNodeWithin(node_links, target_node->GetId()))
524             continue;
525 
526         // We use tan search to help with navigation target angles
527         // Basically this permits to split the direction every 45°
528         // Get the tangent of the 2 points
529         // and check whether the angle correspond to the direction
530         Position2D target_pos = target_node->GetPosition();
531         Position2D tan_pos = current_pos;
532         tan_pos.x = target_pos.x;
533 
534         // Tan X = O / A
535         // Actually we have Tan^2 but the result angle is the same.
536         // We use the default of undefined (angle of 90°)
537         float target_tangent = 90; // Actually, any value above ~25 will do
538         if (current_pos.GetDistance2(tan_pos) != 0.0f)
539             target_tangent = tan_pos.GetDistance2(target_pos) / current_pos.GetDistance2(tan_pos);
540 
541         if (InputManager->LeftPress()) {
542             if (target_pos.x >= current_pos.x)
543                 continue;
544             if (InputManager->UpPress()) {
545                 if (target_pos.y >= current_pos.y)
546                     continue;
547             }
548             else if (InputManager->DownPress()) {
549                 if (target_pos.y <= current_pos.y)
550                     continue;
551             }
552             else { // Left press alone
553                 if (target_tangent > 1)
554                     continue;
555             }
556         }
557         else if (InputManager->RightPress()) {
558             if (target_pos.x <= current_pos.x)
559                 continue;
560             if (InputManager->UpPress()) {
561                 if (target_pos.y >= current_pos.y)
562                     continue;
563             }
564             else if (InputManager->DownPress()) {
565                 if (target_pos.y <= current_pos.y)
566                     continue;
567             }
568             else { // Right press alone
569                 if (target_tangent > 1)
570                     continue;
571             }
572         }
573         else if (InputManager->UpPress()) { // Up press alone
574             if (target_pos.y >= current_pos.y)
575                 continue;
576             if (target_tangent < 1)
577                 continue;
578         }
579         else if (InputManager->DownPress()) { // Down press alone
580             if (target_pos.y <= current_pos.y)
581                 continue;
582             if (target_tangent < 1)
583                 continue;
584         }
585 
586         selected_node = target_node;
587         break;
588     }
589 
590     // Select the new closest node
591     if (selected_node)
592         _selected_node_id = selected_node->GetId();
593 
594     return (selected_node != nullptr);
595 }
596 
_HandleNodeTransaction()597 void SkillGraphWindow::_HandleNodeTransaction()
598 {
599     // Only attempt to buy when the player confirms on the node
600     if (!InputManager->ConfirmPress())
601         return;
602 
603     if (!_selected_character)
604         return;
605 
606     SkillGraph& skill_graph = vt_global::GlobalManager->GetSkillGraph();
607     SkillNode* current_skill_node = skill_graph.GetSkillNode(_selected_node_id);
608 
609     vt_global::GlobalMedia& media = vt_global::GlobalManager->Media();
610     InventoryHandler& inventory_handler = vt_global::GlobalManager->GetInventoryHandler();
611 
612     // Check whether there is enough XP to buy the node
613     if (_selected_character->GetUnspentExperiencePoints() < current_skill_node->GetExperiencePointsNeeded()) {
614         media.PlaySound("bump");
615         return;
616     }
617 
618     // Check whether the needed items are available
619     for (auto item : current_skill_node->GetItemsNeeded()) {
620         auto gbl_obj = inventory_handler.GetGlobalObject(item.first);
621         if (gbl_obj == nullptr) {
622             // item not found
623             media.PlaySound("bump");
624             return;
625         }
626         else if (gbl_obj->GetCount() < item.second) {
627             // Not enough items needed
628             media.PlaySound("bump");
629             return;
630         }
631     }
632 
633     // Cannot obtain a node where the character is
634     uint32_t char_node_id = _selected_character->GetSkillNodeLocation();
635     if (char_node_id == current_skill_node->GetId()) {
636         media.PlaySound("bump");
637         return;
638     }
639 
640     // Check whether at least one of the neighbor nodes in the list is obtained.
641     const std::vector<uint32_t> obtained_nodes = _selected_character->GetObtainedSkillNodes();
642     // If the node was already obtained, we can't buy it again
643     bool neighbor_obtained = false;
644     for (uint32_t obtained_node_id : obtained_nodes) {
645         // Check the node has not already been obtained
646         if (current_skill_node->GetId() == obtained_node_id) {
647             media.PlaySound("bump");
648             return;
649         }
650 
651         // Check the parent or child node of this one has been obtained
652         for (uint32_t child_node_id : current_skill_node->GetChildrenNodeLinks()) {
653             if (child_node_id == obtained_node_id) {
654                 neighbor_obtained = true;
655                 break;
656             }
657         }
658 
659         for (uint32_t parent_node_id : current_skill_node->GetParentNodeLinks()) {
660             if (parent_node_id == obtained_node_id) {
661                 neighbor_obtained = true;
662                 break;
663             }
664         }
665 
666         // The node can be obtained, so let's skip the rest of the loop
667         if (neighbor_obtained)
668             break;
669     }
670 
671     // No obtained neighbor found, we can't get this node
672     if (!neighbor_obtained) {
673         media.PlaySound("bump");
674         return;
675     }
676 
677     // Obtain Node
678     _selected_character->AddObtainedSkillNode(current_skill_node->GetId());
679     media.PlaySound("confirm");
680 
681     // Refresh skill graph view
682     _character_node_id = _selected_character->GetSkillNodeLocation();
683     _UpdateSkillGraphView(true, true);
684 
685     // Refresh info
686     _bottom_info.SetNode(*current_skill_node,
687                          _selected_character->GetUnspentExperiencePoints(),
688                          _selected_character->IsSkillNodeObtained(current_skill_node->GetId()));
689 
690     // Refresh character info
691     MenuMode::CurrentInstance()->ReloadCharacterWindows();
692 }
693 
694 } // namespace private_menu
695 
696 } // namespace vt_menu
697