Peoplemon  0.1.0
Peoplemon 3 game source documentation
ConversationTree.cpp
Go to the documentation of this file.
2 
3 #include <Core/Properties.hpp>
5 #include <cmath>
6 #include <queue>
7 
8 namespace editor
9 {
10 namespace component
11 {
12 namespace
13 {
14 constexpr float FlashPeriod = 0.7f;
15 
16 std::string nodeToString(unsigned int i, const core::file::Conversation::Node& node) {
17  std::stringstream ss;
18  const std::string n = "#" + std::to_string(i);
19  const std::string t = core::file::Conversation::Node::typeToString(node.getType());
20  const unsigned int sc = t.size() > n.size() ? (t.size() - n.size()) / 2 : 0;
21  for (unsigned int i = 0; i < sc; ++i) { ss << " "; }
22  ss << n << "\n" << t;
23  return ss.str();
24 }
25 
26 } // namespace
27 
28 using namespace bl::gui;
29 
31  return Ptr(new ConversationTree(ccb));
32 }
33 
34 ConversationTree::ConversationTree(const ClickCb& ccb)
35 : clickCb(ccb)
36 , selected(0)
37 , flashTime(0.f)
38 , treeVersion(0)
39 , camCenter()
40 , camZoom(1.f)
41 , highlightSelected(true) {
42  setTooltip(
43  "Click and drag to move. Scroll to zoom. Click node to edit. Right click to center view.");
44  getSignal(Event::Dragged)
45  .willAlwaysCall(std::bind(&ConversationTree::onDrag, this, std::placeholders::_1));
46  getSignal(Event::LeftClicked)
47  .willAlwaysCall(std::bind(&ConversationTree::onClick, this, std::placeholders::_1));
48  getSignal(Event::RightClicked).willAlwaysCall(std::bind(&ConversationTree::centerView, this));
49 }
50 
51 bool ConversationTree::handleScroll(const bl::gui::Event& e) {
52  if (getAcquisition().contains(e.mousePosition())) {
53  camZoom /= 1.f + e.scrollDelta() * -0.1f;
54  if (getComponent()) { getComponent()->onElementUpdated(); }
55  return true;
56  }
57  return false;
58 }
59 
60 void ConversationTree::onClick(const bl::gui::Event& e) {
61  const sf::Vector2f mpos = transformToTreeCoord(e.mousePosition());
62  for (unsigned int i = 0; i < renderNodes.size(); ++i) {
63  const Node& n = renderNodes[i];
64  const float dx = n.center.x - mpos.x;
65  const float dy = n.center.y - mpos.y;
66  const float dsqrd = dx * dx + dy * dy;
67  if (dsqrd <= NodeRadiusSquared) {
68  setSelected(i);
69  clickCb(n.index);
70  break;
71  }
72  }
73 }
74 
75 void ConversationTree::setSelected(unsigned int i) {
76  selected = i;
77  camCenter = renderNodes[i].center;
78  flashTime = 0.f;
79  highlightSelected = true;
80  if (getComponent()) { getComponent()->onElementUpdated(); }
81 }
82 
83 void ConversationTree::onDrag(const bl::gui::Event& e) {
84  const sf::Vector2f dragStart = transformToTreeCoord(e.dragStart());
85  const sf::Vector2f mpos = transformToTreeCoord(e.mousePosition());
86  const sf::Vector2f diff(dragStart - mpos);
87  camCenter += glm::vec2(diff.x, diff.y);
88  if (getComponent()) { getComponent()->onElementUpdated(); }
89 }
90 
91 sf::FloatRect ConversationTree::getVirtualBounds() const {
92  sf::FloatRect bounds;
93  for (const auto& node : renderNodes) {
94  bounds.left = std::min(bounds.left, node.center.x - NodeRadius);
95  bounds.top = std::min(bounds.top, node.center.y - NodeRadius);
96  bounds.width = std::max(bounds.width, node.center.x + NodeRadius);
97  bounds.height = std::max(bounds.height, node.center.y + NodeRadius);
98  }
99  return bounds;
100 }
101 
102 void ConversationTree::centerView() {
103  const sf::FloatRect bounds = getVirtualBounds();
104  camCenter = glm::vec2((bounds.left + bounds.width) * 0.5f, (bounds.top + bounds.height) * 0.5f);
105  if (getComponent()) { getComponent()->onElementUpdated(); }
106 }
107 
108 sf::Vector2f ConversationTree::transformToTreeCoord(const sf::Vector2f& p) const {
109  sf::Transform transform;
110  transform.translate(camCenter.x, camCenter.y);
111  transform.scale(1.f / camZoom, 1.f / camZoom);
112  transform.translate(-sf::Vector2f(getAcquisition().width, getAcquisition().height) * 0.5f);
113  return transform.transformPoint(p - getPosition());
114 }
115 
116 void ConversationTree::update(const std::vector<core::file::Conversation::Node>& nodes) {
117  // Clear existing render data
118  renderNodes.clear();
119  // vertexBuffer.resize(0);
120  if (nodes.empty()) return;
121 
122  // Lots of data structures
123  std::queue<unsigned int> toVisit;
124  std::vector<bool> visited(nodes.size(), false);
125  std::vector<unsigned int> levelCounts(nodes.size(), 0);
126  std::vector<unsigned int> nodeLevels(nodes.size(), 0);
127  std::vector<bool> terminators(nodes.size(), false);
128 
129  // Initial setup
130  edges.clear();
131  edges.reserve(nodes.size() * 2);
132  toVisit.push(0);
133  visited[0] = true;
134  levelCounts[0] = 1;
135 
136  // BFS through tree to populate data structures
137  std::vector<unsigned int> next;
138  next.reserve(8);
139  while (!toVisit.empty()) {
140  const unsigned i = toVisit.front();
141  const unsigned int dist = nodeLevels[i];
142  const unsigned int newDist = dist + 1;
143  const core::file::Conversation::Node& node = nodes[i];
144  toVisit.pop();
145 
147 
148  for (unsigned int jump : next) {
149  if (jump >= nodes.size()) {
150  terminators[i] = true;
151  continue;
152  }
153  edges.emplace_back(i, jump);
154  if (visited[jump]) continue;
155 
156  visited[jump] = true;
157  levelCounts[newDist] += 1;
158  nodeLevels[jump] = newDist;
159  toVisit.push(jump);
160  }
161  }
162 
163  // Allocate space for render data
164  renderNodes.resize(nodes.size());
165 
166  // Get ready to position nodes
167  std::vector<sf::Vector2f> nodePositions(nodes.size());
168  std::vector<float> levelLefts(levelCounts.size());
169  for (unsigned int i = 0; i < levelCounts.size(); ++i) {
170  const float width = static_cast<float>(levelCounts[i]) * Spacing;
171  levelLefts[i] = -width * 0.5f;
172  }
173  const unsigned int maxLevelCount =
174  *std::max_element(std::begin(levelCounts), std::end(levelCounts));
175  const float maxwidth = static_cast<float>(maxLevelCount) * Spacing;
176  const float maxLeft = -maxwidth * 0.5f;
177 
178  // Position nodes
179  sf::Vector2f unreachablePos(maxLeft, -Spacing);
180  for (unsigned int i = 0; i < nodes.size(); ++i) {
181  if (visited[i]) {
182  nodePositions[i].x = levelLefts[nodeLevels[i]];
183  nodePositions[i].y = static_cast<float>(nodeLevels[i]) * Spacing;
184  levelLefts[nodeLevels[i]] += Spacing;
185  }
186  else {
187  nodePositions[i] = unreachablePos;
188  unreachablePos.x += NodePadding + NodeRadius * 2.f;
189  }
190 
191  renderNodes[i].setup(nodePositions[i], nodes[i], i, terminators[i]);
192  }
193 
194  // Reset bounds
195  centerView();
196 
197  // Update render component
198  ++treeVersion;
199  if (getComponent()) { getComponent()->onElementUpdated(); }
200 }
201 
202 void ConversationTree::update(float dt) {
203  bl::gui::Element::update(dt);
204 
205  if (selected < renderNodes.size()) {
206  std::optional<sf::Color> color;
207  if (flashTime > 0.f) {
208  flashTime += dt;
209  if (flashTime > FlashPeriod) {
210  flashTime = -flashTime + FlashPeriod;
211  highlightSelected = false;
212  if (getComponent()) { getComponent()->onElementUpdated(); }
213  }
214  }
215  else {
216  flashTime -= dt;
217  if (flashTime < -FlashPeriod) {
218  flashTime = -flashTime - FlashPeriod;
219  highlightSelected = true;
220  if (getComponent()) { getComponent()->onElementUpdated(); }
221  }
222  }
223  }
224 }
225 
226 sf::Vector2f ConversationTree::minimumRequisition() const { return {500.f, 500.f}; }
227 
228 bl::gui::rdr::Component* ConversationTree::doPrepareRender(bl::gui::rdr::Renderer& renderer) {
229  return renderer.createComponent<ConversationTree>(*this);
230 }
231 
232 void ConversationTree::Node::setup(const sf::Vector2f& pos,
233  const core::file::Conversation::Node& src, unsigned int i,
234  bool t) {
235  center = {pos.x, pos.y};
236  terminator = t;
237  index = i;
238  label = nodeToString(i, src);
239 }
240 
241 } // namespace component
242 } // namespace editor
All classes and functionality used for implementing the game editor.
Definition: Tile.hpp:11
static void getNextJumps(const Node &node, std::vector< unsigned int > &jumps)
Populates the jumps vector with the indices reachable from the given node.
Building block of conversations. A conversation is a tree of nodes and each node is an action that ha...
static std::string typeToString(Type type)
Converts a node type to a human readable string.
Type getType() const
Returns the type of this node.
Special component that renders a clickable tree view of a conversation.
void setSelected(unsigned int i)
Set the selected node index.
static constexpr float NodeRadiusSquared
std::function< void(unsigned int)> ClickCb
Called when a node is clicked.
void update(const std::vector< core::file::Conversation::Node > &nodes)
Updates the rendered tree with the new nodes.
static Ptr create(const ClickCb &clickCb)
Construct a new Conversation Tree component.
std::shared_ptr< ConversationTree > Ptr
void setup(const sf::Vector2f &pos, const core::file::Conversation::Node &src, unsigned int i, bool terminator)