1 /*
2 Copyright © 2011-2012 Clint Bellanger
3 Copyright © 2012-2013 Henrik Andersson
4 Copyright © 2012 Stefan Beller
5 Copyright © 2012-2015 Justin Jacobs
6
7 This file is part of FLARE.
8
9 FLARE is free software: you can redistribute it and/or modify it under the terms
10 of the GNU General Public License as published by the Free Software Foundation,
11 either version 3 of the License, or (at your option) any later version.
12
13 FLARE is distributed in the hope that it will be useful, but WITHOUT ANY
14 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
15 PARTICULAR PURPOSE. See the GNU General Public License for more details.
16
17 You should have received a copy of the GNU General Public License along with
18 FLARE. If not, see http://www.gnu.org/licenses/
19 */
20
21 /**
22 * class NPC
23 */
24
25 #include "Animation.h"
26 #include "AnimationManager.h"
27 #include "AnimationSet.h"
28 #include "CampaignManager.h"
29 #include "EntityBehavior.h"
30 #include "EventManager.h"
31 #include "FileParser.h"
32 #include "ItemManager.h"
33 #include "LootManager.h"
34 #include "MapRenderer.h"
35 #include "MessageEngine.h"
36 #include "ModManager.h"
37 #include "NPC.h"
38 #include "RenderDevice.h"
39 #include "SharedGameResources.h"
40 #include "SharedResources.h"
41 #include "SoundManager.h"
42 #include "UtilsMath.h"
43 #include "UtilsParsing.h"
44
NPC(const Entity & e)45 NPC::NPC(const Entity& e)
46 : Entity(e)
47 , gfx("")
48 , vox_intro()
49 , vox_quests()
50 , name("")
51 , direction(0)
52 , show_on_minimap(true)
53 , npc_portrait(NULL)
54 , hero_portrait(NULL)
55 , talker(false)
56 , vendor(false)
57 , reset_buyback(true)
58 , stock()
59 , dialog()
60 {
61 stock.init(VENDOR_MAX_STOCK);
62 }
63
64 /**
65 * NPCs are stored in simple config files
66 *
67 * @param npc_id Config file for npc
68 */
load(const std::string & npc_id)69 void NPC::load(const std::string& npc_id) {
70
71 FileParser infile;
72 ItemStack stack;
73
74 portrait_filenames.resize(1);
75
76 // @CLASS NPC|Description of NPCs in npcs/
77 if (infile.open(npc_id, FileParser::MOD_FILE, FileParser::ERROR_NORMAL)) {
78 bool clear_random_table = true;
79
80 while (infile.next()) {
81 if (infile.section == "stats") {
82 // handled by StatBlock::load()
83 continue;
84 }
85 else if (infile.section == "dialog") {
86 if (infile.new_section) {
87 dialog.push_back(std::vector<EventComponent>());
88 }
89 EventComponent e;
90 e.type = EventComponent::NONE;
91 if (infile.key == "id") {
92 // @ATTR dialog.id|string|A unique identifer used to reference this dialog.
93 e.type = EventComponent::NPC_DIALOG_ID;
94 e.s = infile.val;
95 }
96 else if (infile.key == "him" || infile.key == "her") {
97 // @ATTR dialog.him|repeatable(string)|A line of dialog from the NPC.
98 // @ATTR dialog.her|repeatable(string)|A line of dialog from the NPC.
99 e.type = EventComponent::NPC_DIALOG_THEM;
100 e.s = msg->get(infile.val);
101 }
102 else if (infile.key == "you") {
103 // @ATTR dialog.you|repeatable(string)|A line of dialog from the player.
104 e.type = EventComponent::NPC_DIALOG_YOU;
105 e.s = msg->get(infile.val);
106 }
107 else if (infile.key == "voice") {
108 // @ATTR dialog.voice|repeatable(string)|Filename of a voice sound file to play.
109 e.type = EventComponent::NPC_VOICE;
110 e.x = loadSound(infile.val, VOX_QUEST);
111 }
112 else if (infile.key == "topic") {
113 // @ATTR dialog.topic|string|The name of this dialog topic. Displayed when picking a dialog tree.
114 e.type = EventComponent::NPC_DIALOG_TOPIC;
115 e.s = msg->get(infile.val);
116 }
117 else if (infile.key == "group") {
118 // @ATTR dialog.group|string|Dialog group.
119 e.type = EventComponent::NPC_DIALOG_GROUP;
120 e.s = infile.val;
121 }
122 else if (infile.key == "allow_movement") {
123 // @ATTR dialog.allow_movement|bool|Restrict the player's mvoement during dialog.
124 e.type = EventComponent::NPC_ALLOW_MOVEMENT;
125 e.s = infile.val;
126 }
127 else if (infile.key == "portrait_him" || infile.key == "portrait_her") {
128 // @ATTR dialog.portrait_him|repeatable(filename)|Filename of a portrait to display for the NPC during this dialog.
129 // @ATTR dialog.portrait_her|repeatable(filename)|Filename of a portrait to display for the NPC during this dialog.
130 e.type = EventComponent::NPC_PORTRAIT_THEM;
131 e.s = infile.val;
132 portrait_filenames.push_back(e.s);
133 }
134 else if (infile.key == "portrait_you") {
135 // @ATTR dialog.portrait_you|repeatable(filename)|Filename of a portrait to display for the player during this dialog.
136 e.type = EventComponent::NPC_PORTRAIT_YOU;
137 e.s = infile.val;
138 portrait_filenames.push_back(e.s);
139 }
140 else if (infile.key == "take_a_party") {
141 // @ATTR dialog.take_a_party|bool|Start/stop taking a party with player.
142 e.type = EventComponent::NPC_TAKE_A_PARTY;
143 e.x = Parse::toBool(infile.val);
144 }
145 else if (infile.key == "response") {
146 // @ATTR dialog.response|repeatable(string)|A dialog ID to present as a selectable response. This key must precede the dialog text line.
147 e.type = EventComponent::NPC_DIALOG_RESPONSE;
148 e.s = infile.val;
149 }
150 else if (infile.key == "response_only") {
151 // @ATTR dialog.response_only|bool|If true, this dialog topic will only appear when explicitly referenced with the "response" key.
152 e.type = EventComponent::NPC_DIALOG_RESPONSE_ONLY;
153 e.x = Parse::toBool(infile.val);
154 }
155 else {
156 Event ev;
157 EventManager::loadEventComponent(infile, &ev, NULL);
158
159 for (size_t i=0; i<ev.components.size(); ++i) {
160 if (ev.components[i].type != EventComponent::NONE) {
161 dialog.back().push_back(ev.components[i]);
162 }
163 }
164 }
165
166 if (e.type != EventComponent::NONE) {
167 dialog.back().push_back(e);
168 }
169 }
170 else if (infile.section.empty() || infile.section == "npc") {
171 filename = npc_id;
172
173 if (infile.new_section) {
174 // APPENDed file
175 clear_random_table = true;
176 }
177
178 if (infile.key == "name") {
179 // @ATTR name|string|NPC's name.
180 name = msg->get(infile.val);
181 }
182 else if (infile.key == "animations" || infile.key == "gfx") {
183 // TODO "gfx" is deprecated
184 }
185 else if (infile.key == "direction") {
186 // @ATTR direction|direction|The direction to use for this NPC's stance animation.
187 direction = Parse::toDirection(infile.val);
188 }
189 else if (infile.key == "show_on_minimap") {
190 // @ATTR show_on_minimap|bool|If true, this NPC will be shown on the minimap. The default is true.
191 show_on_minimap = Parse::toBool(infile.val);
192 }
193
194 // handle talkers
195 else if (infile.key == "talker") {
196 // @ATTR talker|bool|Allows this NPC to be talked to.
197 talker = Parse::toBool(infile.val);
198 }
199 else if (infile.key == "portrait") {
200 // @ATTR portrait|filename|Filename of the default portrait image.
201 portrait_filenames[0] = infile.val;
202 }
203
204 // handle vendors
205 else if (infile.key == "vendor") {
206 // @ATTR vendor|bool|Allows this NPC to buy/sell items.
207 vendor = Parse::toBool(infile.val);
208 }
209 else if (infile.key == "vendor_requires_status") {
210 // @ATTR vendor_requires_status|list(string)|The player must have these statuses in order to use this NPC as a vendor.
211 while (infile.val != "") {
212 vendor_requires_status.push_back(camp->registerStatus(Parse::popFirstString(infile.val)));
213 }
214 }
215 else if (infile.key == "vendor_requires_not_status") {
216 // @ATTR vendor_requires_not_status|list(string)|The player must not have these statuses in order to use this NPC as a vendor.
217 while (infile.val != "") {
218 vendor_requires_not_status.push_back(camp->registerStatus(Parse::popFirstString(infile.val)));
219 }
220 }
221 else if (infile.key == "constant_stock") {
222 // @ATTR constant_stock|repeatable(list(item_id))|A list of items this vendor has for sale. Quantity can be specified by appending ":Q" to the item_id, where Q is an integer.
223 while (infile.val != "") {
224 stack = Parse::toItemQuantityPair(Parse::popFirstString(infile.val));
225 stock.add(stack, ItemStorage::NO_SLOT);
226 }
227 }
228 else if (infile.key == "status_stock") {
229 // @ATTR status_stock|repeatable(string, list(item_id)) : Required status, Item(s)|A list of items this vendor will have for sale if the required status is met. Quantity can be specified by appending ":Q" to the item_id, where Q is an integer.
230 if (camp->checkStatus(camp->registerStatus(Parse::popFirstString(infile.val)))) {
231 while (infile.val != "") {
232 stack = Parse::toItemQuantityPair(Parse::popFirstString(infile.val));
233 stock.add(stack, ItemStorage::NO_SLOT);
234 }
235 }
236 }
237 else if (infile.key == "random_stock") {
238 // @ATTR random_stock|list(loot)|Use a loot table to add random items to the stock; either a filename or an inline definition.
239 if (clear_random_table) {
240 random_table.clear();
241 clear_random_table = false;
242 }
243
244 random_table.push_back(EventComponent());
245 loot->parseLoot(infile.val, &random_table.back(), &random_table);
246 }
247 else if (infile.key == "random_stock_count") {
248 // @ATTR random_stock_count|int, int : Min, Max|Sets the minimum (and optionally, the maximum) amount of random items this npc can have.
249 random_table_count.x = Parse::popFirstInt(infile.val);
250 random_table_count.y = Parse::popFirstInt(infile.val);
251 if (random_table_count.x != 0 || random_table_count.y != 0) {
252 random_table_count.x = std::max(random_table_count.x, 1);
253 random_table_count.y = std::max(random_table_count.y, random_table_count.x);
254 }
255 }
256
257 // handle vocals
258 else if (infile.key == "vox_intro") {
259 // @ATTR vox_intro|repeatable(filename)|Filename of a sound file to play when initially interacting with the NPC.
260 loadSound(infile.val, VOX_INTRO);
261 }
262
263 else {
264 infile.error("NPC: '%s' is not a valid key.", infile.key.c_str());
265 }
266 }
267 }
268 infile.close();
269 }
270
271 loadGraphics();
272
273 // fill inventory with items from random stock table
274 unsigned rand_count = Math::randBetween(random_table_count.x, random_table_count.y);
275
276 std::vector<ItemStack> rand_itemstacks;
277 for (unsigned i=0; i<rand_count; ++i) {
278 loot->checkLoot(random_table, NULL, &rand_itemstacks);
279 }
280 std::sort(rand_itemstacks.begin(), rand_itemstacks.end(), compareItemStack);
281 for (size_t i=0; i<rand_itemstacks.size(); ++i) {
282 stock.add(rand_itemstacks[i], ItemStorage::NO_SLOT);
283 }
284
285 // warn if dialog nodes lack a topic
286 std::string full_filename = mods->locate(npc_id);
287 for (size_t i=0; i<dialog.size(); ++i) {
288 std::string topic = getDialogTopic(static_cast<int>(i));
289 if (topic.empty()) {
290 Utils::logInfo("[%s] NPC: Dialog node %d does not have a topic.", full_filename.c_str(), i);
291 }
292 }
293 }
294
loadGraphics()295 void NPC::loadGraphics() {
296
297 if (stats.animations != "") {
298 anim->increaseCount(stats.animations);
299 animationSet = anim->getAnimationSet(stats.animations);
300 activeAnimation = animationSet->getAnimation("");
301 }
302
303 portraits.resize(portrait_filenames.size(), NULL);
304
305 for (size_t i = 0; i < portrait_filenames.size(); ++i) {
306 if (!portrait_filenames[i].empty()) {
307 Image *graphics;
308 graphics = render_device->loadImage(portrait_filenames[i], RenderDevice::ERROR_NORMAL);
309 if (graphics) {
310 portraits[i] = graphics->createSprite();
311 graphics->unref();
312 }
313 }
314 }
315 }
316
317 /**
318 * filename assumes the file is in soundfx/npcs/
319 * vox_type is a const int enum, see NPC.h
320 * returns -1 if not loaded or error.
321 * returns index in specific vector where to be found.
322 */
loadSound(const std::string & fname,int vox_type)323 int NPC::loadSound(const std::string& fname, int vox_type) {
324
325 SoundID a = snd->load(fname, "NPC voice");
326
327 if (!a)
328 return -1;
329
330 if (vox_type == VOX_INTRO) {
331 vox_intro.push_back(a);
332 return static_cast<int>(vox_intro.size()) - 1;
333 }
334
335 if (vox_type == VOX_QUEST) {
336 vox_quests.push_back(a);
337 return static_cast<int>(vox_quests.size()) - 1;
338 }
339 return -1;
340 }
341
logic()342 void NPC::logic() {
343 mapr->collider.unblock(stats.pos.x, stats.pos.y);
344
345 Entity::logic();
346 moveMapEvents();
347
348 if (!stats.hero_ally)
349 mapr->collider.block(stats.pos.x, stats.pos.y, true);
350 }
351
playSoundIntro()352 bool NPC::playSoundIntro() {
353 if (vox_intro.empty())
354 return false;
355
356 size_t roll = static_cast<size_t>(rand()) % vox_intro.size();
357 snd->play(vox_intro[roll], "NPC_VOX", snd->NO_POS, !snd->LOOP);
358 return true;
359 }
360
playSoundQuest(int id)361 bool NPC::playSoundQuest(int id) {
362 if (id < 0 || id >= static_cast<int>(vox_quests.size()))
363 return false;
364
365 snd->play(vox_quests[id], "NPC_VOX", snd->NO_POS, !snd->LOOP);
366 return true;
367 }
368
369 /**
370 * get list of available dialogs with NPC
371 */
getDialogNodes(std::vector<int> & result,bool allow_responses)372 void NPC::getDialogNodes(std::vector<int> &result, bool allow_responses) {
373 result.clear();
374 if (!talker)
375 return;
376
377 std::string group;
378 typedef std::vector<int> Dialogs;
379 typedef std::map<std::string, Dialogs > DialogGroups;
380 DialogGroups groups;
381
382 for (size_t i=dialog.size(); i>0; i--) {
383 bool is_available = true;
384 bool is_grouped = false;
385 for (size_t j=0; j<dialog[i-1].size(); j++) {
386 if (dialog[i-1][j].type == EventComponent::NPC_DIALOG_GROUP) {
387 is_grouped = true;
388 group = dialog[i-1][j].s;
389 }
390 else if (dialog[i-1][j].type == EventComponent::NPC_DIALOG_RESPONSE_ONLY) {
391 if (dialog[i-1][j].x && !allow_responses) {
392 is_available = false;
393 break;
394 }
395 }
396 else {
397 if (camp->checkAllRequirements(dialog[i-1][j]))
398 continue;
399
400 is_available = false;
401 break;
402 }
403 }
404
405 if (is_available) {
406 if (!is_grouped) {
407 result.push_back(static_cast<int>(i-1));
408 }
409 else {
410 DialogGroups::iterator it;
411 it = groups.find(group);
412 if (it == groups.end()) {
413 groups.insert(DialogGroups::value_type(group, Dialogs()));
414 }
415 else
416 it->second.push_back(static_cast<int>(i-1));
417
418 }
419 }
420 }
421
422 /* Iterate over dialoggroups and roll a dialog to add to result */
423 DialogGroups::iterator it;
424 it = groups.begin();
425 if (it == groups.end())
426 return;
427
428 while (it != groups.end()) {
429 /* roll a dialog for this group and add to result */
430 int di = it->second[rand() % it->second.size()];
431 result.push_back(di);
432 ++it;
433 }
434 }
435
getDialogResponses(std::vector<int> & result,size_t node_id,size_t event_cursor)436 void NPC::getDialogResponses(std::vector<int>& result, size_t node_id, size_t event_cursor) {
437 if (node_id >= dialog.size())
438 return;
439
440 if (event_cursor >= dialog[node_id].size())
441 return;
442
443 std::vector<size_t> response_ids;
444 for (size_t i = event_cursor; i > 0; i--) {
445 if (isDialogType(dialog[node_id][i-1].type))
446 break;
447
448 if (dialog[node_id][i-1].type == EventComponent::NPC_DIALOG_RESPONSE)
449 response_ids.push_back(i-1);
450 }
451
452 if (response_ids.empty())
453 return;
454
455 std::vector<int> nodes;
456 getDialogNodes(nodes, GET_RESPONSE_NODES);
457
458 for (size_t i = 0; i < response_ids.size(); i++) {
459 for (size_t j = 0; j < nodes.size(); j++) {
460 std::string id;
461
462 for (size_t k = 0; k < dialog[nodes[j]].size(); k++) {
463 if (dialog[nodes[j]][k].type == EventComponent::NPC_DIALOG_ID) {
464 id = dialog[nodes[j]][k].s;
465 break;
466 }
467 }
468
469 if (id.empty())
470 continue;
471
472 if (id == dialog[node_id][response_ids[i]].s) {
473 result.push_back(nodes[j]);
474 break;
475 }
476 }
477 }
478 }
479
getDialogTopic(unsigned int dialog_node)480 std::string NPC::getDialogTopic(unsigned int dialog_node) {
481 if (!talker)
482 return "";
483
484 for (unsigned int j=0; j<dialog[dialog_node].size(); j++) {
485 if (dialog[dialog_node][j].type == EventComponent::NPC_DIALOG_TOPIC)
486 return dialog[dialog_node][j].s;
487 }
488
489 return "";
490 }
491
492 /**
493 * Check if the hero can move during this dialog branch
494 */
checkMovement(unsigned int dialog_node)495 bool NPC::checkMovement(unsigned int dialog_node) {
496 if (dialog_node < dialog.size()) {
497 for (unsigned int i=0; i<dialog[dialog_node].size(); i++) {
498 if (dialog[dialog_node][i].type == EventComponent::NPC_ALLOW_MOVEMENT)
499 return Parse::toBool(dialog[dialog_node][i].s);
500 }
501 }
502 return true;
503 }
504
moveMapEvents()505 void NPC::moveMapEvents() {
506
507 // Update event position after NPC has moved
508 for (size_t i = 0; i < mapr->events.size(); i++)
509 {
510 if (mapr->events[i].type == filename)
511 {
512 mapr->events[i].location.x = static_cast<int>(stats.pos.x);
513 mapr->events[i].location.y = static_cast<int>(stats.pos.y);
514
515 mapr->events[i].hotspot.x = static_cast<int>(stats.pos.x);
516 mapr->events[i].hotspot.y = static_cast<int>(stats.pos.y);
517
518 mapr->events[i].center.x =
519 static_cast<float>(stats.pos.x) + static_cast<float>(mapr->events[i].hotspot.w)/2;
520 mapr->events[i].center.y =
521 static_cast<float>(stats.pos.y) + static_cast<float>(mapr->events[i].hotspot.h)/2;
522
523 for (size_t ci = 0; ci < mapr->events[i].components.size(); ci++)
524 {
525 if (mapr->events[i].components[ci].type == EventComponent::NPC_HOTSPOT)
526 {
527 mapr->events[i].components[ci].x = static_cast<int>(stats.pos.x);
528 mapr->events[i].components[ci].y = static_cast<int>(stats.pos.y);
529 }
530 }
531 }
532 }
533 }
534
checkVendor()535 bool NPC::checkVendor() {
536 if (!vendor)
537 return false;
538
539 for (size_t i = 0; i < vendor_requires_status.size(); ++i) {
540 if (!camp->checkStatus(vendor_requires_status[i]))
541 return false;
542 }
543
544 for (size_t i = 0; i < vendor_requires_not_status.size(); ++i) {
545 if (camp->checkStatus(vendor_requires_not_status[i]))
546 return false;
547 }
548
549 return true;
550 }
551
552 /**
553 * Process the current dialog
554 *
555 * Return false if the dialog has ended
556 */
processDialog(unsigned int dialog_node,unsigned int & event_cursor)557 bool NPC::processDialog(unsigned int dialog_node, unsigned int &event_cursor) {
558 if (dialog_node >= dialog.size())
559 return false;
560
561 npc_portrait = portraits[0];
562 hero_portrait = NULL;
563
564 while (event_cursor < dialog[dialog_node].size()) {
565
566 // we've already determined requirements are met, so skip these
567 if (dialog[dialog_node][event_cursor].type == EventComponent::REQUIRES_STATUS) {
568 // continue to next event component
569 }
570 else if (dialog[dialog_node][event_cursor].type == EventComponent::REQUIRES_NOT_STATUS) {
571 // continue to next event component
572 }
573 else if (dialog[dialog_node][event_cursor].type == EventComponent::REQUIRES_LEVEL) {
574 // continue to next event component
575 }
576 else if (dialog[dialog_node][event_cursor].type == EventComponent::REQUIRES_NOT_LEVEL) {
577 // continue to next event component
578 }
579 else if (dialog[dialog_node][event_cursor].type == EventComponent::REQUIRES_CURRENCY) {
580 // continue to next event component
581 }
582 else if (dialog[dialog_node][event_cursor].type == EventComponent::REQUIRES_NOT_CURRENCY) {
583 // continue to next event component
584 }
585 else if (dialog[dialog_node][event_cursor].type == EventComponent::REQUIRES_ITEM) {
586 // continue to next event component
587 }
588 else if (dialog[dialog_node][event_cursor].type == EventComponent::REQUIRES_NOT_ITEM) {
589 // continue to next event component
590 }
591 else if (dialog[dialog_node][event_cursor].type == EventComponent::REQUIRES_CLASS) {
592 // continue to next event component
593 }
594 else if (dialog[dialog_node][event_cursor].type == EventComponent::REQUIRES_NOT_CLASS) {
595 // continue to next event component
596 }
597 else if (dialog[dialog_node][event_cursor].type == EventComponent::NPC_DIALOG_ID) {
598 // continue to next event component
599 }
600 else if (dialog[dialog_node][event_cursor].type == EventComponent::NPC_DIALOG_RESPONSE) {
601 // continue to next event component
602 }
603 else if (dialog[dialog_node][event_cursor].type == EventComponent::NPC_DIALOG_RESPONSE_ONLY) {
604 // continue to next event component
605 }
606 else if (dialog[dialog_node][event_cursor].type == EventComponent::NPC_DIALOG_THEM) {
607 return true;
608 }
609 else if (dialog[dialog_node][event_cursor].type == EventComponent::NPC_DIALOG_YOU) {
610 return true;
611 }
612 else if (dialog[dialog_node][event_cursor].type == EventComponent::NPC_VOICE) {
613 playSoundQuest(dialog[dialog_node][event_cursor].x);
614 }
615 else if (dialog[dialog_node][event_cursor].type == EventComponent::NPC_PORTRAIT_THEM) {
616 npc_portrait = portraits[0];
617 for (size_t i = 0; i < portrait_filenames.size(); ++i) {
618 if (dialog[dialog_node][event_cursor].s == portrait_filenames[i]) {
619 npc_portrait = portraits[i];
620 break;
621 }
622 }
623 }
624 else if (dialog[dialog_node][event_cursor].type == EventComponent::NPC_PORTRAIT_YOU) {
625 hero_portrait = NULL;
626 for (size_t i = 0; i < portrait_filenames.size(); ++i) {
627 if (dialog[dialog_node][event_cursor].s == portrait_filenames[i]) {
628 hero_portrait = portraits[i];
629 break;
630 }
631 }
632 }
633 else if (dialog[dialog_node][event_cursor].type == EventComponent::NPC_TAKE_A_PARTY) {
634 stats.hero_ally = dialog[dialog_node][event_cursor].x == 0 ? false : true;
635 }
636 else if (dialog[dialog_node][event_cursor].type == EventComponent::NONE) {
637 // conversation ends
638 return false;
639 }
640
641 event_cursor++;
642 }
643 return false;
644 }
645
processEvent(unsigned int dialog_node,unsigned int cursor)646 void NPC::processEvent(unsigned int dialog_node, unsigned int cursor) {
647 if (dialog_node >= dialog.size())
648 return;
649
650 Event ev;
651
652 if (cursor < dialog[dialog_node].size() && isDialogType(dialog[dialog_node][cursor].type)) {
653 cursor++;
654 }
655
656 while (cursor < dialog[dialog_node].size() && !isDialogType(dialog[dialog_node][cursor].type)) {
657 ev.components.push_back(dialog[dialog_node][cursor]);
658 cursor++;
659 }
660
661 EventManager::executeEvent(ev);
662 }
663
isDialogType(const int & event_type)664 bool NPC::isDialogType(const int &event_type) {
665 return event_type == EventComponent::NPC_DIALOG_THEM || event_type == EventComponent::NPC_DIALOG_YOU;
666 }
667
~NPC()668 NPC::~NPC() {
669
670 for (size_t i = 0; i < portraits.size(); ++i) {
671 delete portraits[i];
672 }
673
674 if (stats.animations != "") {
675 anim->decreaseCount(stats.animations);
676 }
677
678 while (!vox_intro.empty()) {
679 snd->unload(vox_intro.back());
680 vox_intro.pop_back();
681 }
682 while (!vox_quests.empty()) {
683 snd->unload(vox_quests.back());
684 vox_quests.pop_back();
685 }
686 }
687