Peoplemon  0.1.0
Peoplemon 3 game source documentation
ConversationNode.cpp
Go to the documentation of this file.
2 
3 namespace editor
4 {
5 namespace component
6 {
7 namespace
8 {
9 constexpr unsigned int ConversationEnd = 99999999;
10 constexpr unsigned int MaxLen = 24;
11 
12 std::string nodeToString(unsigned int i, const core::file::Conversation::Node& node) {
14 
15  std::stringstream ss;
16  ss << "#" << i << ":" << core::file::Conversation::Node::typeToString(node.getType());
17  if (node.getType() == T::Talk || node.getType() == T::Prompt) {
18  ss << ":"
19  << (node.message().size() > MaxLen ? node.message().substr(0, MaxLen - 3) + "..." :
20  node.message());
21  }
22  return ss.str();
23 }
24 
25 } // namespace
26 
28 using namespace bl::gui;
29 
30 ConversationNode::ConversationNode(const FocusCb& fcb, const NotifyCb& ecb, const NotifyCb& odcb,
31  const NotifyCb& rgtcb, const CreateNode& cncb,
32  const SelectCb& selectCb, Box::Ptr& container,
33  const std::vector<core::file::Conversation::Node>* nodes)
34 : focusCb(fcb)
35 , onEdit(ecb)
36 , onDelete(odcb)
37 , regenTree(rgtcb)
38 , createNodeCb(cncb)
39 , onJump(selectCb)
40 , allNodes(nodes)
41 , passNext(
42  "Pass:", regenTree, createNodeCb, std::bind(&ConversationNode::syncJumps, this),
43  [this](unsigned int nn) {
44  current.nextOnPass() = nn;
45  onEdit();
46  regenTree();
47  },
48  onJump, nodes)
49 , failNext(
50  "Fail:", regenTree, createNodeCb, std::bind(&ConversationNode::syncJumps, this),
51  [this](unsigned int nn) {
52  current.nextOnReject() = nn;
53  onEdit();
54  regenTree();
55  },
56  onJump, nodes)
57 , nextNode(
58  "Next:", regenTree, createNodeCb, std::bind(&ConversationNode::syncJumps, this),
59  [this](unsigned int nn) {
60  current.next() = nn;
61  onEdit();
62  regenTree();
63  },
64  onJump, nodes)
65 , scriptSelector(
66  [this](const std::string& s) {
67  current.script() = s;
68  scriptLabel->setText(s);
69  onEdit();
70  },
71  [this]() { focusCb(true); }) {
72  nodeTitle = Label::create("");
73  nodeTitle->setCharacterSize(32);
74  nodeTitle->setColor(sf::Color::Cyan, sf::Color::Transparent);
75  container->pack(nodeTitle, true, false);
76 
77  promptRow = Box::create(LinePacker::create(LinePacker::Horizontal, 6.f));
78  promptEntry = TextEntry::create(3, true);
79  promptEntry->getSignal(Event::ValueChanged).willAlwaysCall([this](const Event&, Element*) {
80  current.message() = promptEntry->getInput();
81  onEdit();
82  });
83  promptRow->pack(Label::create("Say:"), false, true);
84  promptRow->pack(promptEntry, true, true);
85 
86  Box::Ptr row =
87  Box::create(LinePacker::create(LinePacker::Horizontal, 4.f, LinePacker::Uniform));
88  Box::Ptr subrow = Box::create(LinePacker::create(LinePacker::Horizontal, 4.f));
89  subrow->pack(Label::create("Node Type:"), false, true);
90  typeEntry = ComboBox::create();
91  for (unsigned int i = 0; i < 10; ++i) {
92  const auto t = static_cast<Conversation::Node::Type>(i);
93  typeEntry->addOption(core::file::Conversation::Node::typeToString(t));
94  }
95  typeEntry->getSignal(Event::ValueChanged)
96  .willAlwaysCall(std::bind(&ConversationNode::onTypeChange, this));
97  subrow->pack(typeEntry, false, true);
98  Button::Ptr but = Button::create("Delete Node");
99  but->getSignal(Event::LeftClicked).willAlwaysCall([this](const Event&, Element*) {
100  onDelete();
101  });
102  but->setColor(sf::Color::Red, sf::Color::Black);
103  row->pack(subrow, false, true);
104  row->pack(but, false, true);
105  container->pack(row, true, false);
106 
107  choiceArea = Box::create(LinePacker::create(LinePacker::Vertical, 4.f));
108  choiceScroll = ScrollArea::create(LinePacker::create(LinePacker::Vertical, 4.f));
109  choiceScroll->setColor(sf::Color::Transparent, sf::Color::Black);
110  choiceScroll->setOutlineThickness(2.f);
111  choiceScroll->setMaxSize({1000.f, 400.f});
112  choiceScroll->includeScrollbarsInRequisition(true);
113  but = Button::create("Add Choice");
114  but->getSignal(Event::LeftClicked).willAlwaysCall([this](const Event&, Element*) {
115  current.choices().emplace_back("", ConversationEnd);
116  auto it = choices.emplace(choices.end(),
117  choices.size(),
118  "",
119  ConversationEnd,
120  std::bind(&ConversationNode::onChoiceChange,
121  this,
122  std::placeholders::_1,
123  std::placeholders::_2,
124  std::placeholders::_3),
125  std::bind(&ConversationNode::onChoiceDelete,
126  this,
127  std::placeholders::_1,
128  std::placeholders::_2),
129  regenTree,
130  createNodeCb,
131  std::bind(&ConversationNode::syncJumps, this),
132  onJump,
133  allNodes);
134  it->setIterator(it);
135  choiceScroll->pack(it->content(), true, false);
136  onEdit();
137  regenTree();
138  });
139  choiceArea->pack(choiceScroll, true, true);
140  choiceArea->pack(but, false, false);
141 
142  itemRow = Box::create(LinePacker::create(LinePacker::Vertical, 4.f));
143  itemSelector =
144  component::ItemSelector::create(std::bind(&ConversationNode::onItemChange, this));
145  row = Box::create(LinePacker::create(LinePacker::Horizontal, 2.f));
146  row->pack(Label::create("Item:"), false, true);
147  row->pack(itemSelector, false, true);
148  itemBeforeCheck = CheckButton::create("Prompt Before");
149  itemBeforeCheck->getSignal(Event::ValueChanged)
150  .willAlwaysCall(std::bind(&ConversationNode::onItemChange, this));
151  itemAfterCheck = CheckButton::create("Display Result");
152  itemAfterCheck->getSignal(Event::ValueChanged)
153  .willAlwaysCall(std::bind(&ConversationNode::onItemChange, this));
154  itemRow->pack(row, true, false);
155  itemRow->pack(itemBeforeCheck);
156  itemRow->pack(itemAfterCheck);
157 
158  moneyRow = Box::create(LinePacker::create(LinePacker::Horizontal, 4.f));
159  moneyEntry = TextEntry::create();
160  moneyEntry->setMode(TextEntry::Mode::Integer);
161  moneyEntry->getSignal(Event::ValueChanged)
162  .willAlwaysCall(std::bind(&ConversationNode::onMoneyChange, this));
163  moneyRow->pack(Label::create("Money Amount:"), false, true);
164  moneyRow->pack(moneyEntry, true, true);
165 
166  flagRow = Box::create(LinePacker::create(LinePacker::Horizontal, 4.f));
167  flagEntry = TextEntry::create();
168  flagEntry->getSignal(Event::ValueChanged).willAlwaysCall([this](const Event&, Element*) {
169  current.saveFlag() = flagEntry->getInput();
170  onEdit();
171  });
172  flagRow->pack(Label::create("Flag Name:"), false, true);
173  flagRow->pack(flagEntry, true, true);
174 
175  editArea = Box::create(LinePacker::create(LinePacker::Vertical, 4.f));
176  container->pack(editArea, true, true);
177 
178  scriptRow = Box::create(LinePacker::create(LinePacker::Vertical, 4.f));
179  ScrollArea::Ptr scroll = ScrollArea::create(LinePacker::create());
180  scroll->setMaxSize({450.f, 550.f});
181  scroll->setColor(sf::Color(30, 180, 240, 130), sf::Color::Blue);
182  scriptLabel = Label::create("<no script selected>");
183  scriptLabel->setColor(sf::Color(30, 180, 60), sf::Color::Transparent);
184  scroll->pack(scriptLabel);
185  scriptRow->pack(Label::create("Script:"));
186  scriptRow->pack(scroll, true, false);
187  but = Button::create("Edit Script");
188  but->getSignal(Event::LeftClicked).willAlwaysCall([this](const Event&, Element*) {
189  focusCb(false);
190  scriptSelector.open(parent, current.script());
191  });
192  scriptRow->pack(but);
193 }
194 
195 void ConversationNode::setParent(GUI* p) { parent = p; }
196 
197 void ConversationNode::update(unsigned int i, const Conversation::Node& node) {
198  current = node;
199  index = i;
200 
201  nodeTitle->setText(nodeToString(index, node));
202  editArea->clearChildren(true);
203  nextNode.sync();
204  passNext.sync();
205  failNext.sync();
206  typeEntry->setSelectedOption(static_cast<int>(node.getType()), false);
207  nextNode.setSelected(node.next());
208  passNext.setSelected(node.nextOnPass());
209  failNext.setSelected(node.nextOnReject());
210 
211  switch (node.getType()) {
212  case Conversation::Node::Type::Talk:
213  promptEntry->setInput(node.message());
214  editArea->pack(promptRow, true, false);
215  editArea->pack(nextNode.content(), true, false);
216  break;
217 
218  case Conversation::Node::Type::Prompt:
219  promptEntry->setInput(node.message());
220  editArea->pack(promptRow, true, false);
221  choiceScroll->clearChildren(true);
222  choices.clear();
223  for (unsigned int i = 0; i < node.choices().size(); ++i) {
224  const auto& c = node.choices()[i];
225  auto it = choices.emplace(choices.end(),
226  i,
227  c.first,
228  c.second,
229  std::bind(&ConversationNode::onChoiceChange,
230  this,
231  std::placeholders::_1,
232  std::placeholders::_2,
233  std::placeholders::_3),
234  std::bind(&ConversationNode::onChoiceDelete,
235  this,
236  std::placeholders::_1,
237  std::placeholders::_2),
238  regenTree,
239  createNodeCb,
240  std::bind(&ConversationNode::syncJumps, this),
241  onJump,
242  allNodes);
243  it->setIterator(it);
244  choiceScroll->pack(choices.back().content(), true, false);
245  }
246  editArea->pack(choiceArea, true, true);
247  break;
248 
250  editArea->pack(itemRow, true, false);
251  itemSelector->setItem(node.item().id);
252  editArea->pack(nextNode.content(), true, false);
253  itemBeforeCheck->setValue(node.item().beforePrompt);
254  itemAfterCheck->setValue(node.item().afterPrompt);
255  break;
256 
258  editArea->pack(itemRow, true, false);
259  itemSelector->setItem(node.item().id);
260  itemBeforeCheck->setValue(node.item().beforePrompt);
261  itemAfterCheck->setValue(node.item().afterPrompt);
262  editArea->pack(passNext.content(), true, false);
263  editArea->pack(failNext.content(), true, false);
264  break;
265 
267  editArea->pack(moneyRow, true, false);
268  moneyEntry->setInput(std::to_string(node.money()));
269  editArea->pack(nextNode.content(), true, false);
270  break;
271 
273  editArea->pack(moneyRow, true, false);
274  moneyEntry->setInput(std::to_string(node.money()));
275  editArea->pack(passNext.content(), true, false);
276  editArea->pack(failNext.content(), true, false);
277  break;
278 
280  editArea->pack(flagRow, true, false);
281  flagEntry->setInput(node.saveFlag());
282  editArea->pack(nextNode.content(), true, false);
283  break;
284 
286  editArea->pack(flagRow, true, false);
287  flagEntry->setInput(node.saveFlag());
288  editArea->pack(passNext.content(), true, false);
289  editArea->pack(failNext.content(), true, false);
290  break;
291 
293  editArea->pack(passNext.content(), true, false);
294  editArea->pack(failNext.content(), true, false);
295  break;
296 
298  editArea->pack(scriptRow, true, false);
299  scriptLabel->setText(node.script().empty() ? "<no script selected>" : node.script());
300  editArea->pack(nextNode.content(), true, false);
301  break;
302 
303  default:
304  BL_LOG_ERROR << "Unimplemented node type: " << node.getType();
305  break;
306  }
307 }
308 
309 const Conversation::Node& ConversationNode::getValue() const { return current; }
310 
311 void ConversationNode::onItemChange() {
312  current.item().id = itemSelector->currentItem();
313  current.item().beforePrompt = itemBeforeCheck->getValue();
314  current.item().afterPrompt = itemAfterCheck->getValue();
315  onEdit();
316 }
317 
318 void ConversationNode::onMoneyChange() {
319  const std::string& val = moneyEntry->getInput();
320  current.money() = val.empty() ? 0 : std::atoi(val.c_str());
321  onEdit();
322 }
323 
324 void ConversationNode::onChoiceChange(unsigned int j, const std::string& t, unsigned int n) {
325  current.choices()[j].first = t;
326  current.choices()[j].second = n;
327  onEdit();
328  regenTree();
329 }
330 
331 void ConversationNode::onTypeChange() {
332  const Conversation::Node::Type t =
333  static_cast<Conversation::Node::Type>(typeEntry->getSelectedOption());
334 
335  Conversation::Node val(t);
336  val.next() = current.next();
337  val.nextOnPass() = current.nextOnPass();
338  val.nextOnReject() = current.nextOnReject();
339 
340  bool e = false;
341  if (current.getType() != t) {
342  update(index, val);
343  e = true;
344  }
345 
346  onEdit();
347  regenTree();
348  if (e) {
349  update(index, val);
350  syncJumps();
351  regenTree();
353  current.item().id = itemSelector->currentItem();
354  }
355  onEdit();
356  }
357 }
358 
359 void ConversationNode::onChoiceDelete(unsigned int j, std::list<Choice>::iterator it) {
360  it->content()->remove();
361  choices.erase(it);
362  current.choices().erase(current.choices().begin() + j);
363  unsigned int k = 0;
364  for (auto& choice : choices) {
365  choice.setIndex(k);
366  ++k;
367  }
368  onEdit();
369  regenTree();
370 }
371 
372 void ConversationNode::syncJumps() {
373  nextNode.sync();
374  passNext.sync();
375  failNext.sync();
376  for (auto& c : choices) { c.syncJump(); }
377 }
378 
379 ConversationNode::NodeConnector::NodeConnector(
380  const std::string& prompt, const NotifyCb& regenTree, const CreateNode& createNode,
381  const NotifyCb& syncJumpCb, const ChangeCb& changeCb, const SelectCb& selectCb,
382  const std::vector<core::file::Conversation::Node>* nodes)
383 : nodes(nodes) {
384  row = Box::create(LinePacker::create(LinePacker::Horizontal, 4.f));
385  row->pack(Label::create(prompt), false, true);
386  entry = ComboBox::create();
387  entry->setMaxHeight(150.f);
388  entry->getSignal(Event::ValueChanged).willAlwaysCall([this, changeCb](const Event&, Element*) {
389  changeCb(getSelected());
390  });
391  row->pack(entry, true, true);
392  Button::Ptr cb = Button::create("Create");
393  cb->setTooltip("Create a new node and jump to it");
394  cb->getSignal(Event::LeftClicked)
395  .willAlwaysCall(
396  [this, createNode, regenTree, syncJumpCb, changeCb](const Event&, Element*) {
397  const unsigned int nn = createNode(getSelected());
398  syncJumpCb();
399  setSelected(nn);
400  changeCb(nn);
401  regenTree();
402  });
403  row->pack(cb, false, true);
404  cb = Button::create("Goto");
405  cb->setTooltip("Select the node this points to");
406  cb->getSignal(Event::LeftClicked).willAlwaysCall([this, selectCb](const Event&, Element*) {
407  selectCb(getSelected());
408  });
409  row->pack(cb, false, true);
410  sync();
411 }
412 
413 void ConversationNode::NodeConnector::sync() {
414  const unsigned int oldSel = entry->getSelectedOption() == entry->optionCount() - 1 ?
415  ConversationEnd :
416  entry->getSelectedOption();
417  entry->clearOptions();
418  for (unsigned int i = 0; i < nodes->size(); ++i) {
419  entry->addOption(nodeToString(i, nodes->at(i)));
420  }
421  entry->addOption("End Conversation");
422  setSelected(oldSel);
423 }
424 
425 void ConversationNode::NodeConnector::setSelected(unsigned int i) {
426  entry->setSelectedOption(i >= nodes->size() ? nodes->size() : i, false);
427 }
428 
429 unsigned int ConversationNode::NodeConnector::getSelected() const {
430  const unsigned int s = static_cast<unsigned int>(entry->getSelectedOption());
431  return s < nodes->size() ? s : ConversationEnd;
432 }
433 
434 Box::Ptr& ConversationNode::NodeConnector::content() { return row; }
435 
436 ConversationNode::Choice::Choice(unsigned int i, const std::string& c, unsigned int jp,
437  const OnChange& changeCb, const OnDelete& delCb,
438  const NotifyCb& regenTree, const CreateNode& createNode,
439  const NotifyCb& syncJumpCb, const SelectCb& selectCb,
440  const std::vector<core::file::Conversation::Node>* nodes)
441 : onChange(changeCb)
442 , onDelete(delCb)
443 , index(i)
444 , jump(
445  "Next:", regenTree, createNode, syncJumpCb,
446  [this](unsigned int nj) { onChange(index, entry->getInput(), nj); }, selectCb, nodes) {
447  jump.setSelected(jp);
448 
449  box = Box::create(LinePacker::create(LinePacker::Vertical));
450  box->setColor(sf::Color::Transparent, sf::Color::Blue);
451  box->setOutlineThickness(1.f);
452 
453  Box::Ptr row = Box::create(LinePacker::create(LinePacker::Horizontal, 4.f));
454  row->pack(Label::create("Text:"), false, true);
455  entry = TextEntry::create();
456  entry->setInput(c);
457  entry->getSignal(Event::ValueChanged).willAlwaysCall([this](const Event&, Element*) {
458  onChange(index, entry->getInput(), jump.getSelected());
459  });
460  row->pack(entry, true, true);
461  Button::Ptr but = Button::create("Remove");
462  but->setColor(sf::Color::Red, sf::Color::Black);
463  but->getSignal(Event::LeftClicked).willAlwaysCall([this](const Event&, Element*) {
464  onDelete(index, it);
465  });
466  row->pack(but, false, true);
467 
468  box->pack(row, true, false);
469  box->pack(jump.content(), true, false);
470 }
471 
472 void ConversationNode::Choice::setIndex(unsigned int i) { index = i; }
473 
474 void ConversationNode::Choice::setIterator(std::list<Choice>::iterator i) { it = i; }
475 
476 Box::Ptr& ConversationNode::Choice::content() { return box; }
477 
478 void ConversationNode::Choice::syncJump() { jump.sync(); }
479 
480 } // namespace component
481 } // namespace editor
All classes and functionality used for implementing the game editor.
Definition: Tile.hpp:11
Stores a conversation that an NPC or trainer can have with the player.
Building block of conversations. A conversation is a tree of nodes and each node is an action that ha...
std::string & script()
Returns the script file of this node.
static std::string typeToString(Type type)
Converts a node type to a human readable string.
std::string & message()
Returns the message or prompt for this node.
std::uint32_t & nextOnPass()
Returns the next node if a check passes (ie take item or money)
std::uint32_t & nextOnReject()
Returns the next node if a check is rejected (ie not taking money or item)
std::uint32_t & money()
Returns the money requested or given, undefined behavior if not a money node.
Type getType() const
Returns the type of this node.
std::string & saveFlag()
Returns the save flag of this node.
Item & item()
Returns the item to give or take. Undefined behavior if not an item node.
std::uint32_t & next()
Returns the index of the next node in the case of a Talk, Give, and Script nodes.
std::vector< std::pair< std::string, std::uint32_t > > & choices()
Returns the choices if this is a prompt node. Undefined behavior if not.
Type
Represents the different effects that nodes can have.
@ TakeMoney
This will ask the player for money. They may refuse or not have enough.
@ RunScript
This will run a script. Must be a file.
@ CheckInteracted
This will check if interacted with the talking NPC and jump accordingly.
@ TakeItem
This will ask the player for an item. They may refuse or not have it.
@ GiveItem
This unconditionally gives the player an item.
@ SetSaveFlag
This will check if a flag exists and jump to the next node accordingly.
@ CheckSaveFlag
This will check if a flag exists and jump to the next node accordingly.
@ GiveMoney
This will unconditionally give money to the player.
Helper component for editing individual conversation nodes.
const core::file::Conversation::Node & getValue() const
Get the current node value.
std::function< unsigned int(unsigned int)> CreateNode
Called when a new node should be created.
void update(unsigned int i, const core::file::Conversation::Node &node)
Update the node editor content with the given node value.
std::function< void(unsigned int)> SelectCb
Called when a node is jumped to.
std::function< void(bool)> FocusCb
std::function< void()> NotifyCb
Called when the node value is modified.
void setParent(bl::gui::GUI *parent)
Set the parent GUI object.
ConversationNode(const FocusCb &focusCb, const NotifyCb &editCb, const NotifyCb &deleteCb, const NotifyCb &regenTree, const CreateNode &createNode, const SelectCb &selectCb, bl::gui::Box::Ptr &container, const std::vector< core::file::Conversation::Node > *nodes)
Construct a new Conversation Node editor.
static Ptr create(const ChangeCb &cb=[](core::item::Id) {})
Creates a new ItemSelector.
Definition: ItemSelector.cpp:9