3 #include <BLIB/Scripts.hpp>
12 using namespace bl::gui;
15 const std::string EmptyFile =
"<no file selected>";
17 std::string makePath(
const std::string& local) {
24 node.next() = 9999999;
29 bool nodeTerminates(
const std::vector<core::file::Conversation::Node>& nodes,
unsigned int i) {
30 std::queue<unsigned int> toVisit;
31 std::vector<bool> visited(nodes.size(),
false);
36 std::vector<unsigned int> next;
38 while (!toVisit.empty()) {
39 const unsigned int n = toVisit.front();
43 for (
unsigned int j : next) {
44 if (j >= nodes.size())
return true;
61 , value(DefaultConversation)
63 , nodeBox(Box::create(LinePacker::create(LinePacker::Vertical, 4.f)))
64 , nodeComponent([this](bool f) { window->setForceFocus(f); },
67 value.setNode(currentNode, nodeComponent.getValue());
70 if (value.nodes().size() > 1 &&
71 1 == bl::dialog::tinyfd_messageBox(
72 "Warning",
"Delete conversation node?",
"yesno",
"warning", 0)) {
73 value.deleteNode(currentNode);
74 if (currentNode >= value.nodes().size()) {
75 currentNode = value.nodes().size() - 1;
78 window->queueUpdateAction(std::bind(&ConversationWindow::sync,
this));
81 [
this]() { treeComponent->update(value.nodes()); },
82 [
this](
unsigned int current) {
85 node.nextOnPass() = current;
86 value.appendNode(node);
87 return value.nodes().size() - 1;
89 std::bind(&ConversationWindow::setSelected,
this, std::placeholders::_1), nodeBox,
93 [
this](
const std::string& c) {
95 if (bl::util::FileUtil::getExtension(conv) !=
97 filePickerMode != FilePickerMode::OpenExisting) {
102 fileLabel->setText(conv);
103 window->setForceFocus(
true);
104 switch (filePickerMode) {
105 case FilePickerMode::MakeNew:
106 value = DefaultConversation;
108 window->queueUpdateAction(std::bind(&ConversationWindow::sync,
this));
111 case FilePickerMode::OpenExisting:
113 if (!value.load(makePath(conv))) {
114 const std::string msg =
"Failed to load conversation: " + conv;
115 bl::dialog::tinyfd_messageBox(
"Error", msg.c_str(),
"ok",
"error", 1);
116 value = DefaultConversation;
117 fileLabel->setText(EmptyFile);
120 window->queueUpdateAction(std::bind(&ConversationWindow::sync,
this));
122 case FilePickerMode::SetFile:
130 window->setForceFocus(
true);
133 window = Window::create(LinePacker::create(LinePacker::Vertical),
"Conversation Editor");
134 window->getSignal(Event::Closed).willAlwaysCall([
this](
const Event&, Element*) {
136 window->setForceFocus(
false);
140 Box::Ptr row = Box::create(LinePacker::create(LinePacker::Horizontal, 4.f));
141 Button::Ptr but = Button::create(
"New");
142 but->getSignal(Event::LeftClicked).willAlwaysCall([
this](
const Event&, Element*) {
143 filePickerMode = FilePickerMode::MakeNew;
144 if (!dirty || confirmDiscard()) {
145 window->setForceFocus(
false);
146 filePicker.open(FilePicker::CreateNew,
"New Conversation", parent,
true);
149 row->pack(but,
false,
true);
150 but = Button::create(
"Open");
151 but->getSignal(Event::LeftClicked).willAlwaysCall([
this](
const Event&, Element*) {
152 filePickerMode = FilePickerMode::OpenExisting;
153 if (!dirty || confirmDiscard()) {
154 window->setForceFocus(
false);
155 filePicker.open(FilePicker::PickExisting,
"Open Conversation", parent,
true);
158 row->pack(but,
false,
true);
159 but = Button::create(
"Set File");
160 but->setTooltip(
"Change the file to save to");
161 but->getSignal(Event::LeftClicked).willAlwaysCall([
this](
const Event&, Element*) {
162 filePickerMode = FilePickerMode::SetFile;
163 window->setForceFocus(
false);
164 filePicker.open(FilePicker::CreateNew,
"Change File", parent,
true);
166 row->pack(but,
false,
true);
167 saveBut = Button::create(
"Save");
168 saveBut->getSignal(Event::LeftClicked).willAlwaysCall([
this](
const Event&, Element*) {
170 if (value.
save(makePath(fileLabel->getText()))) { makeClean(); }
171 else { bl::dialog::tinyfd_messageBox(
"Error",
"Failed to save",
"ok",
"error", 1); }
174 row->pack(saveBut,
false,
true);
175 fileLabel = Label::create(EmptyFile);
176 fileLabel->setColor(sf::Color::Cyan, sf::Color::Transparent);
177 row->pack(fileLabel,
true,
true);
178 window->pack(row,
true,
false);
181 std::bind(&ConversationWindow::setSelected,
this, std::placeholders::_1));
182 treeComponent->setRequisition({500.f, 500.f});
184 row = Box::create(LinePacker::create(LinePacker::Horizontal, 8.f));
185 row->pack(treeComponent,
true,
true);
186 nodeBox->setRequisition({270.f, 500.f});
187 nodeBox->setColor(sf::Color::Transparent, sf::Color::Black);
188 nodeBox->setOutlineThickness(2.f);
189 row->pack(nodeBox,
true,
true);
190 window->pack(row,
true,
true);
192 row = Box::create(LinePacker::create(
193 LinePacker::Horizontal, 0.f, LinePacker::Compact, LinePacker::RightAlign));
194 but = Button::create(
"Use Conversation");
195 but->getSignal(Event::LeftClicked).willAlwaysCall([
this](
const Event&, Element*) {
197 if (!dirty || confirmDiscard()) {
198 window->setForceFocus(
false);
200 selectCb(fileLabel->getText());
204 but->setColor(sf::Color(52, 235, 222), sf::Color::Black);
206 window->pack(row,
true,
false);
208 window->setPosition({100.f, 100.f});
211 void ConversationWindow::sync() {
212 nodeComponent.
update(currentNode, value.
nodes().at(currentNode));
213 treeComponent->update(value.
nodes());
214 treeComponent->setSelected(currentNode);
220 if (current.empty() || !value.
load(makePath(current))) { value = DefaultConversation; }
224 fileLabel->setText(current);
227 parent->pack(window);
228 window->setForceFocus(
true);
231 bool ConversationWindow::confirmDiscard()
const {
232 return !dirty || 1 == bl::dialog::tinyfd_messageBox(
233 "Warning",
"Discard unsaved changes?",
"yesno",
"warning", 0);
236 bool ConversationWindow::validate()
const {
237 const auto error = [](
const std::string& e) {
238 bl::dialog::tinyfd_messageBox(
"Error", e.c_str(),
"ok",
"error", 1);
241 const auto warning = [](
const std::string& e) {
242 bl::dialog::tinyfd_messageBox(
"Warning", e.c_str(),
"ok",
"warning", 1);
246 unsigned int i) ->
bool {
248 using OutputCb = std::function<void(
const std::string&)>;
250 const auto output = [i](
const OutputCb& cb,
const std::string& m) {
251 std::string o =
"Node " + std::to_string(i) +
": " + m;
252 for (
unsigned int i = 0; i < o.size(); ++i) {
253 if (o[i] ==
'\'') o[i] =
'`';
256 o.insert(i,
"<QUOTE>");
262 switch (n.getType()) {
265 if (n.message().empty()) { output(warning,
"Message is empty"); }
266 if (n.getType() == T::Prompt) {
267 if (n.choices().empty()) {
268 output(error,
"Prompt has no choices");
271 for (
const auto& c : n.choices()) {
272 if (c.first.empty()) {
273 output(error,
"Choice has empty text");
283 output(error,
"Invalid item");
290 if (n.money() == 0) {
291 output(error,
"Invalid money amount");
297 bl::script::Script check(n.script());
298 if (!check.valid()) {
299 output(error,
"SyntaxError: " + check.errorMessage());
306 case T::CheckSaveFlag:
307 if (n.saveFlag().empty()) {
308 output(error,
"Save flag cannot be empty");
313 case T::CheckInteracted:
317 output(error,
"Unknown node type");
322 unsigned int validated = 0;
323 std::queue<unsigned int> toVisit;
324 std::vector<bool> visited(value.
nodes().size(),
false);
329 std::vector<unsigned int> next;
331 while (!toVisit.empty()) {
332 const unsigned int n = toVisit.front();
337 if (!checkNode(node, n))
return false;
338 if (!nodeTerminates(value.
nodes(), n)) {
339 error(
"Conversation end is unreachable from node " + std::to_string(n));
345 error(
"Node " + std::to_string(n) +
" has no jumps");
349 for (
unsigned int j : next) {
350 if (j >= value.
nodes().size())
continue;
358 if (validated != value.
nodes().size()) {
359 warning(
"There are " + std::to_string(value.
nodes().size() - validated) +
360 " unreachable nodes");
366 void ConversationWindow::makeClean() {
368 saveBut->setColor(sf::Color::Green, sf::Color::Black);
371 void ConversationWindow::makeDirty() {
373 saveBut->setColor(sf::Color::Red, sf::Color::Black);
376 void ConversationWindow::setSelected(
unsigned int i) {
377 if (i >= value.
nodes().size()) {
378 bl::dialog::tinyfd_messageBox(
"Error",
"Invalid node",
"ok",
"error", 1);
382 window->queueUpdateAction(
383 [
this]() { nodeComponent.
update(currentNode, value.
nodes().at(currentNode)); });
384 treeComponent->setSelected(i);
All classes and functionality used for implementing the game editor.
Stores a conversation that an NPC or trainer can have with the player.
static void getNextJumps(const Node &node, std::vector< unsigned int > &jumps)
Populates the jumps vector with the indices reachable from the given node.
bool load(const std::string &file)
Loads the conversation from the given file.
const std::vector< Node > & nodes() const
Returns the list of nodes in the conversation.
bool save(const std::string &file) const
Saves the conversation to the given file.
void appendNode(const Node &node)
Appends the given node to the list of nodes.
Building block of conversations. A conversation is a tree of nodes and each node is an action that ha...
std::uint32_t & nextOnReject()
Returns the next node if a check is rejected (ie not taking money or item)
Type
Represents the different effects that nodes can have.
static const std::string & ConversationPath()
static const std::string & ConversationFileExtension()
void update(unsigned int i, const core::file::Conversation::Node &node)
Update the node editor content with the given node value.
void setParent(bl::gui::GUI *parent)
Set the parent GUI object.
static Ptr create(const ClickCb &clickCb)
Construct a new Conversation Tree component.
std::function< void(const std::string &)> SelectCb
Callback for when a conversation is chosen.
std::function< void()> CancelCb
Callback for when the window is closed with no choice made.
ConversationWindow(const SelectCb &onSelect, const CancelCb &onCancel)
Initialize a new conversation window.
void open(bl::gui::GUI *parent, const std::string ¤t)
Actually opens the window and initializes with a conversation.