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