Implement a quest system and job board

- The job board is loosely based on Exile III's job board; the dialog is converted from the one contained as a relic of E3 in BoE
- Quest system is loosely based on a mix of Exile III jobs and Blades of Avernum quests
- Talking to a monster (even a hostile one) can now trigger an arbitrary special node

Dialog engine:
- LED's now support wrapped labels
This commit is contained in:
2015-02-01 01:30:15 -05:00
parent db627e54a8
commit 4efcb08932
35 changed files with 714 additions and 52 deletions

View File

@@ -137,12 +137,18 @@ the party is in town A, the response is Text 1. Otherwise, its Text 2.</p>
</ol>
<p>The possible shop types are as follows:</p>
<ol start='0'>
<li>Ordinary items shop; B is the number of the first item from the scenario's item list.</li>
<li>Mage spells shop; B is the number of the first mage spell (0 - 61, but generally not lower than 30).</li>
<li>Priest spells shop; B is the number of the first priest spell (0 - 61, but generally not lower than 30).</li>
<li>Ordinary items shop; B is the number of the first item from the scenario's item
list.</li>
<li>Mage spells shop; B is the number of the first mage spell (0 - 61, but generally not
lower than 30).</li>
<li>Priest spells shop; B is the number of the first priest spell (0 - 61, but generally
not lower than 30).</li>
<li>Alchemy shop; B is the number of the first recipe (0 - 19).</li>
<li>Healing shop; B and C are ignored.</li>
<li>Random Items Shop, type 0. This brings up a shop window where the party can buy up to 10 randomly chosen items. These items are changed every 3000 moves, and are often magical. B and C are ignored for these shops.</li>
<li>Random Items Shop, type 0. This brings up a shop window where the party can buy up to
10 randomly chosen items. These items are changed every 3000 moves, and are often magical.
There are five separate random lists maintained by the game. B and C are ignored for these
shops.</li>
<li>Random Items Shop, type 1.</li>
<li>Random Items Shop, type 2.</li>
<li>Random Items Shop, type 3.</li>
@@ -154,20 +160,12 @@ is the response, the player gets to shop in a store called Fred's Fish. The pric
quite cheap, and the player can buy items 193-207.</p>
<p><b>Node Type 8 - Training</b> The training window immediately comes up. Text 1 &amp; 2
are ignored.</p>
<p><b>Node Type 9 - Mage Spell Shop</b> Shop where the party can buy mage spells. A is the
cost adjustment (Range 0 ... 6, see above). B is the number of the first spell sold in the
shop (press the Choose button to select). C is the total number of spells sold in the
shop, taken from the list of spells in the game, starting with B. Text 1 is the name of
the shop.</p>
<p><b>Node Type 10 - Priest Spell Shop</b> Exactly like Mage Spell Shop (above) but with
Priest spells.</p>
<p><b>Node Type 11 - Alchemy Shop </b>Shop where the party can buy alchemy recipes. A is
the cost adjustment (Range 0 ... 6, see above. B is the number of the first recipe sold in
the shop (press the Choose button to select). C is the total number of recipes sold in the
shop, taken from the list of recipes in the game, starting with B. Text 1 is the name of
the shop.</p>
<p><b>Node Type 12 - Healer </b> Brings up the healing screen. A is the cost adjustment
(Range 0 ... 6, see above) and Text 1 is the name of the healer.</p>
<p><b>Node Type 9 - Job Board</b> Brings up the job board, where the player can choose to
accept a job or quest. This could be something simple like delivering a message or
package, or it could be something plot-relevant. A is the number of the job board to
show. If the job board is angry at you for failing too many jobs, no job board will be
shown and the response will be Text 2. Text 1 is currently ignored, but reserved to be a
name for the job board.</p>
<h3>Item Button Talking Nodes</h3>
@@ -230,11 +228,13 @@ the number of the special item being sold. If they already have it, they are tol
already have that". Otherwise, the cost of the item is B gold. If the party can afford it,
they are told Text 1. Otherwise, they are told Text 2.</p>
<p>Note: If you set the cost to 0, the party is always given the item.</p>
<p><b>Node Type 23 - Special Item Shop</b> This brings up a shop window where the party
can buy up to 10 randomly chosen items. These items are changed every 3000 moves, and are
often magical. There are 5 different, independently maintained lists of items the shop can
give. A is the cost adjustment of the shop (Range 0 ... 6, see above) and B is list of
items to sell from (0 .. 4).</p>
<p><b>Node Type 23 - Receive Quest</b> If quest A has been completed, the response is Text
2 and nothing special happens. Otherwise, the party is given quest A (if they hadn't
already received it) and the response is Text 1.</p>
<p>Note: This node is not set up for the possibility that the quest somehow failed. If the
quest is one that has a deadline or other failure condition, it might be better to instead
use node types 29 or 30 to call a special node and check the quest's status before giving
it.</p>
<p><b>Node Type 24 - Reveal Town Location</b> Charges the party money, and enables them to
enter a hidden town. The cost is A gold. If the party can afford it, they are told Text 1,
and they will be able to see and enter town/dungeon number B. Otherwise, they are told

View File

@@ -468,6 +468,26 @@ to cause the game to crash since it won't know which town to get the node from.
<dt>Extra 1a:</dt><dd>This specifies the personality to use for the conversation. A
personality is defined as part of a town; see the chapter on Dialogue for
details.</dd></dd>
<dt>Type 45: Update Quest</dt><dd>This special node allows you to set the status of one of
the scenario's quests or jobs.
<dl>
<dt>Extra 1a:</dt><dd>The quest to update.</dd>
<dt>Extra 1b:</dt><dd>The new status for the quest:
<ol start='0'>
<li>Marks the quest as not started. This might be useful if the quest is on a job board
and you want it to be repeatable. Note however that marking a quest available does not
grant automatic rewards.</li>
<li>Marks the quest as started. If it wasn't already started, the game automatically fills
in the quest's start day, and if Extra 2a is non-negative, the quest is considered to have
come from the job board with that number.</li>
<li>Marks the quest as complete. If the quest specifies automatic rewards, the game grants
these to the party.</li>
<li>Marks the quest as failed. If it came from a job board, the game increases the anger
level of that job board. You can set Extra 2a if you want to increase the anger level even
more. A job board won't offer any jobs if its anger level is 50 or more.</li>
</ol>
</dd></dd>
</dl>
<h3>One-Shot Specials</h3>
@@ -1192,22 +1212,33 @@ is called.
<dt>Extra 1a:</dt><dd>If the Stuff Done flag equals this value, call special in Extra
1b.</dd></dd>
<dt>Type 156: If Context?</dt>Result depends on how the special node was called.
<dt>Type 156: If Context?</dt><dd>Result depends on how the special node was called.
<dl>
<dt>Extra 1a:</dt><dd>The context to test for. Click Choose to select one. If you want to check whether the party is in town, in combat, or outdoors, you probably want one of the first three options. To test for Ritual of Sanctification, use the Targeting Spell option and set Extra 1b to 108. Most of these contexts arise from special nodes assigned while editing monsters, items, town details, and other things, rather than special nodes assigned to a terrain space.</dd>
<dt>Extra 1b:</dt><dd>The meaning of this field depends on the context. Usually it's not used. For the three movement contexts, 0 means you can enter the space and 1 means you can't. For the targeting spell context, setting this to something other than -1 means that the special in Extra 1c will only be called if the spell that was cast is equal to the spell that has the given number. Add 100 to indicate a priest spell. Item-only spells such as Wrack or Strengthen Target can also be tested for; just enter the same spell ID you would enter for the item ability.
<dt>Extra 1b:</dt><dd>The meaning of this field depends on the context. Usually it's not used. For the three movement contexts, 0 means you can enter the space and 1 means you can't. For the targeting spell context, setting this to something other than -1 means that the special in Extra 1c will only be called if the spell that was cast is equal to the spell that has the given number. Add 100 to indicate a priest spell. Item-only spells such as Wrack or Strengthen Target can also be tested for; just enter the same spell ID you would enter for the item ability.</dd></dd>
<dt>Type 157: If Numeric Response?</dt>Result depends on a number entered by the player.
<dt>Type 157: If Numeric Response?</dt><dd>Result depends on a number entered by the player.
<!-- TODO: Document this. -->
</dd>
<dt>Type 158: In Boat?</dt>Result depends whether the player is in a boat.
<dt>Type 158: In Boat?</dt><dd>Result depends whether the player is in a boat.
<dl>
<dt>Extra 1b:</dt><dd>If left at -1, the special in Extra 1c is called if the player is in any boat. Otherwise, the special in Extra 1c is only called if the player is in the boat with that number.</dd></dd>
<dt>Extra 1b:</dt><dd>If left at -1, the special in Extra 1c is called if the player is in
any boat. Otherwise, the special in Extra 1c is only called if the player is in the boat
with that number.</dd></dd>
<dt>Type 158: On Horse?</dt>Result depends whether the player is on horseback.
<dt>Type 158: On Horse?</dt><dd>Result depends whether the player is on horseback.
<dl>
<dt>Extra 1b:</dt><dd>If left at -1, the special in Extra 1c is called if the player is on any horses. Otherwise, the special in Extra 1c is only called if the player is on the horses with that number.</dd></dd>
<dt>Extra 1b:</dt><dd>If left at -1, the special in Extra 1c is called if the player is on
any horses. Otherwise, the special in Extra 1c is only called if the player is on the
horses with that number.</dd></dd>
<dt>Type 159: Quest Status?</dt><dd>Result depends on the status of a quest.
<dl>
<dt>Extra 1a:</dt><dd>The quest to check.</dd>
<dt>Extra 1b:</dt><dd>If the quest has this status (0 - not started, 1 - started, 2 -
completed, 3 - failed), the special in Extra 1c is called.</dd>
<dt>Jumpto:</dt><dd>Otherwise, this special is called.</dd></dd>
</dl>
<h3>Town Mode Specials</h3>

View File

@@ -129,6 +129,15 @@
<sector-start x="20" y="20"/>
<!-- Definitions of special items, if any -->
<specials/>
<!-- Define quests here -->
<quest start-with='false'>
<!-- Quests can have a deadline, and an event that waives the deadline -->
<deadline relative='true' waive-if='5'>12</deadline>
<!-- Quests can have an automatic reward -->
<reward xp='12000' gold='250'/>
<name>Sample Quest</name>
<description>Your mission, if you choose to accept it, is...!!??</description>
</quest>
</game>
<editor>

View File

@@ -136,6 +136,32 @@
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="quest" maxOccurs="unbounded">
<xs:complexType>
<xs:sequence>
<xs:element name='deadline' minOccurs='0'>
<xs:complexType>
<xs:simpleContent>
<xs:extension base='xs:integer'>
<xs:attribute name="relative" type="bool"/>
<xs:attribute name='waive-if' type='xs:integer'/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
<xs:element name='reward' minOccurs='0'>
<xs:complexType>
<xs:attribute name='xp' type='xs:integer'/>
<xs:attribute name='gold' type='xs:integer'/>
</xs:complexType>
</xs:element>
<xs:element name='bank' type='xs:integer' minOccurs='0' maxOccurs='2'/>
<xs:element name="name" type="xs:string"/>
<xs:element name="description" type="xs:string"/>
</xs:sequence>
<xs:attribute name="start-with" type="bool"/>
</xs:complexType>
</xs:element>
<xs:element name="timer" minOccurs="0" maxOccurs="20">
<xs:complexType>
<xs:simpleContent>

View File

@@ -111,8 +111,10 @@
<xsl:if test='./@state'>
background-image: url('img/button/led-<xsl:value-of select='./@state'/>.png');
</xsl:if>
background-position: left top;
left: <xsl:value-of select='./@left'/>px; top: <xsl:value-of select='./@top'/>px;
width: <xsl:value-of select='./@width'/>px;
height: <xsl:value-of select='./@height'/>px;
</xsl:attribute>
<xsl:value-of select='.'/>
</div>

View File

@@ -0,0 +1,30 @@
<?xml version='1.0' encoding='UTF-8' standalone='no'?>
<?xml-stylesheet href="dialog.xsl" type="text/xsl"?>
<dialog defbtn='okay' debug='true'>
<pict type='dlog' num='16' top='8' left='8'/>
<text size='large' top='8' left='50' width='100' height='17'>Editing Quests</text>
<text top='8' left='200' width='100' height='16'>Quest number:</text>
<text name='num' top='8' left='300' width='100' height='16'/>
<text top='58' left='50' width='132' height='16'>Name of Quest:</text>
<text top='85' left='50' width='118' height='16'>Quest Description:</text>
<field name='name' top='57' left='205' width='252' height='16'/>
<field name='descr' top='84' left='205' width='252' height='104'/>
<text top='209' left='50' width='160' height='28'>Must be completed by day:<br/>(-1 for no deadline)</text>
<field name='chop' top='208' left='205' width='110' height='16'/>
<text top='247' left='50' width='150' height='28'>Event to waive deadline:<br/>(-1 for none)</text>
<field name='evt' top='246' left='205' width='100' height='16'/>
<text size='large' top='280' left='50' width='220' height='17'>Reward for completing:</text>
<text top='307' left='50' width='150' height='16'>Experience Points:</text>
<field name='xp' top='306' left='205' width='110' height='16'/>
<text top='333' left='50' width='150' height='16'>Gold:</text>
<field name='gold' top='332' left='205' width='110' height='16'/>
<led name='rel' wrap='true' top='208' left='330' width='120' height='28'>Deadline is relative to start day</led>
<led name='start' wrap='true' top='246' left='330' width='120' height='28'>Player is given quest when scenario starts</led>
<led name='inbank' top='285' left='330' width='120'>Include in a job bank:</led>
<field name='bank1' top='306' left='344' width='110' height='16'/>
<field name='bank2' top='332' left='344' width='110' height='16'/>
<button name='left' type='left' def-key='left' top='358' left='50'/>
<button name='right' type='right' def-key='right' top='358' left='115'/>
<button name='cancel' type='regular' def-key='esc' top='358' left='322'>Cancel</button>
<button name='okay' type='regular' top='358' left='387'>OK</button>
</dialog>

View File

@@ -0,0 +1,18 @@
<?xml version='1.0' encoding='UTF-8' standalone='no'?>
<?xml-stylesheet href="dialog.xsl" type="text/xsl"?>
<dialog defbtn='done' debug='true'>
<pict type='dlog' num='3' top='9' left='9'/>
<text size='large' top='9' left='54' width='161' height='19'>THE JOB BOARD:</text>
<text top='9' left='230' width='92' height='19'>Current day:</text>
<text name='day' top='9' left='324' width='92' height='19'/>
<text name='feedback' framed='true' top='313' left='19' width='183' height='18'/>
<text name='job1' top='38' left='54' width='364' height='60'/>
<button name='take1' type='regular' top='78' left='426'>Take</button>
<text name='job2' top='104' left='54' width='364' height='60'/>
<button name='take2' type='regular' top='144' left='426'>Take</button>
<text name='job3' top='170' left='54' width='364' height='60'/>
<button name='take3' type='regular' top='210' left='426'>Take</button>
<text name='job4' top='236' left='54' width='364' height='60'/>
<button name='take4' type='regular' top='276' left='426'>Take</button>
<button name='done' type='done' top='305' left='426'/>
</dialog>

View File

@@ -0,0 +1,14 @@
<?xml version='1.0' encoding='UTF-8' standalone='no'?>
<?xml-stylesheet href="dialog.xsl" type="text/xsl"?>
<dialog defbtn='done' debug='true'>
<pict type='dlog' num='21' top='8' left='8'/>
<text name='name' framed='true' top='9' left='55' width='257' height='19'/>
<text name='descr' framed='true' top='39' left='55' width='257' height='87'/>
<text top='136' left='55' width='80' height='16'>Received on:</text>
<text name='start' framed='true' top='136' left='140' width='100' height='16'/>
<text top='156' left='55' width='80' height='16'>Deadline:</text>
<text name='chop' framed='true' top='156' left='140' width='100' height='16'/>
<text top='176' left='55' width='80' height='16'>Pay:</text>
<text name='pay' framed='true' top='176' left='140' width='100' height='16'/>
<button name='done' type='done' top='208' left='251'/>
</dialog>

View File

@@ -242,6 +242,7 @@
</xs:attribute>
<xs:attributeGroup ref="rect"/>
<xs:attributeGroup ref="font"/>
<xs:attribute name="wrap" default="false" type="bool"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>

View File

@@ -5,7 +5,7 @@ Looking (Outdoors)
Looking (Town/Combat)
Entering town
Leaving town
Talking
In conversation
Using special item
Town event timer triggered
Scenario event timer triggered
@@ -22,3 +22,4 @@ Attacking at melee
Being attacked at melee
Attacking at range
Being attacked at range
Initiating conversation

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

After

Width:  |  Height:  |  Size: 234 KiB

View File

@@ -553,6 +553,14 @@ static void handle_talk(location destination, bool& did_something, bool& need_re
if(univ.town.monst[i].on_space(destination)) {
did_something = true;
need_redraw = true;
if(univ.town.monst[i].special_on_talk >= 0) {
short s1, s2, s3;
run_special(eSpecCtx::HAIL, 2, univ.town.monst[i].special_on_talk, univ.town.monst[i].cur_loc, &s1, &s2, &s3);
if(s3 > 0)
need_redraw = true;
if(s1 > 0)
break;
}
if(univ.town.monst[i].attitude % 2 == 1) {
add_string_to_buf(" Creature is hostile.");
} else if(univ.town.monst[i].summon_time > 0 || univ.town.monst[i].personality < 0) {
@@ -1344,8 +1352,7 @@ bool handle_action(sf::Event event) {
set_stat_window(ITEM_WIN_SPECIAL);
break;
case 7:
// TODO: Jobs! Or maybe quests!
//set_stat_window(ITEM_WIN_QUESTS);
set_stat_window(ITEM_WIN_QUESTS);
break;
case 8: // help
cChoiceDlog("help-inventory").show();
@@ -1384,7 +1391,7 @@ bool handle_action(sf::Event event) {
if(stat_window == ITEM_WIN_SPECIAL)
put_spec_item_info(spec_item_array[item_hit]);
else if(stat_window == ITEM_WIN_QUESTS)
; // TODO: Implement quests view
put_quest_info(spec_item_array[item_hit]);
else display_pc_item(stat_window, item_hit,univ.party[stat_window].items[item_hit],0);
break;
case 5: // sell? That this code was reached indicates that the item was sellable

View File

@@ -523,6 +523,67 @@ void end_talk_mode() {
redraw_screen(REFRESH_TERRAIN | REFRESH_BAR);
}
static void fill_job_bank(cDialog& me, job_bank_t& bank, std::string) {
// TODO: Maybe customize the icon?
// TODO: Allow custom title?
me["day"].setTextToNum(calc_day());
for(int i = 0; i < 4; i++) {
std::string id = std::to_string(i + 1);
if(bank.jobs[i] >= 0 && bank.jobs[i] < univ.scenario.quests.size()) {
cQuest& quest = univ.scenario.quests[bank.jobs[i]];
std::string description = quest.descr;
if(quest.deadline > 0) {
if(quest.flags % 10 == 1)
description += " Must be completed in " + std::to_string(quest.deadline) + " days.";
else description += " Must be completed by day " + std::to_string(quest.deadline) + ".";
}
description += " Pay is " + std::to_string(quest.gold) + " gold.";
me["take" + id].show();
me["job" + id].setText(description);
} else {
me["take" + id].hide();
me["job" + id].setText("");
}
}
}
static void show_job_bank(int which_bank, std::string title) {
cDialog job_dlg("job-bank");
job_dlg.attachClickHandlers([&](cDialog& me, std::string hit, eKeyMod) -> bool {
int which = hit[4] - '1';
me["prompt"].setText("Job accepted.");
job_bank_t& bank = univ.party.job_banks[which_bank];
univ.party.quest_status[bank.jobs[which]] = eQuestStatus::STARTED;
univ.party.quest_source[bank.jobs[which]] = store_personality;
univ.party.quest_start[bank.jobs[which]] = calc_day();
// Now, if there are spare jobs available, fill in. Otherwise, clear space.
if(bank.jobs[4] >= 0)
std::swap(bank.jobs[which], bank.jobs[4]);
else if(bank.jobs[5] >= 0)
std::swap(bank.jobs[which], bank.jobs[5]);
fill_job_bank(me, bank, title);
return true;
}, {"take1", "take2", "take3", "take4"});
job_dlg["done"].attachClickHandler(std::bind(&cDialog::toast, &job_dlg, false));
if(which_bank >= univ.party.job_banks.size())
univ.party.job_banks.resize(which_bank + 1);
if(!univ.party.job_banks[which_bank].inited)
generate_job_bank(which_bank, univ.party.job_banks[which_bank]);
fill_job_bank(job_dlg, univ.party.job_banks[which_bank], title);
int anger = univ.party.job_banks[which_bank].anger;
if(anger >= 0 && anger < 10) {
job_dlg["prompt"].setText("Dispatcher is neutral towards you.");
} else if(anger >= 10 && anger < 20) {
job_dlg["prompt"].setText("Dispatcher is a little annoyed at you.");
} else if(anger >= 20 && anger < 35) {
job_dlg["prompt"].setText("Dispatcher is annoyed at you.");
} else job_dlg["prompt"].setText("Dispatcher is rather angry at you.");
job_dlg.run();
}
void handle_talk_event(location p) {
short i,get_pc,s1 = -1,s2 = -1,s3 = -1;
char asked[4];
@@ -727,6 +788,17 @@ void handle_talk_event(location p) {
start_shop_mode(shop,b,b + c - 1,a,save_talk_str1.c_str());
strnum1 = -1;
return;
case eTalkNode::JOB_BANK:
if(a < univ.party.job_banks.size() && univ.party.job_banks[a].anger >= 50) {
strnum1 = strnum2;
save_talk_str1 = save_talk_str2;
strnum2 = 0;
save_talk_str2 = "";
break;
} else {
show_job_bank(a, save_talk_str1.c_str());
return;
}
case eTalkNode::SELL_WEAPONS:
strnum1 = -1;
stat_screen_mode = MODE_SELL_WEAP;
@@ -853,6 +925,30 @@ void handle_talk_event(location p) {
strnum2 = 0;
save_talk_str2 = "";
break;
case eTalkNode::RECEIVE_QUEST:
if(a < 0 || a >= univ.scenario.quests.size()) {
giveError("Tried to give a nonexistent quest!");
return;
}
switch(univ.party.quest_status[a]) {
case eQuestStatus::AVAILABLE:
univ.party.quest_status[a] = eQuestStatus::STARTED;
univ.party.quest_source[a] = -1;
univ.party.quest_start[a] = calc_day();
break;
case eQuestStatus::STARTED:
break;
case eQuestStatus::COMPLETED:
strnum1 = strnum2;
save_talk_str1 = save_talk_str2;
break;
case eQuestStatus::FAILED:
// TODO: How to handle this?
return;
}
strnum2 = 0;
save_talk_str2 = "";
break;
case eTalkNode::BUY_TOWN_LOC:
if(univ.party.can_find_town[b]) {
// TODO: Uh, is something supposed to happen here?

View File

@@ -849,6 +849,23 @@ void give_help(short help1, short help2, cDialog& parent) {
give_help(help1, help2, &parent);
}
void put_quest_info(short which_i) {
cQuest& quest = univ.scenario.quests[which_i];
cDialog quest_dlg("quest-info");
quest_dlg["name"].setText(quest.name);
quest_dlg["descr"].setText(quest.descr);
int start = univ.party.quest_start[which_i];
quest_dlg["start"].setText("Day " + std::to_string(start));
if(quest.deadline > 0)
quest_dlg["chop"].setText("Day " + std::to_string(quest.deadline + (quest.flags % 10) * start));
else quest_dlg["chop"].setText("None");
if(quest.gold > 0)
quest_dlg["pay"].setText(std::to_string(quest.gold) + " gold");
else quest_dlg["pay"].setText("Unknown");
quest_dlg["done"].attachClickHandler(std::bind(&cDialog::toast, &quest_dlg, false));
quest_dlg.run();
}
void put_spec_item_info (short which_i) {
cStrDlog display_strings(univ.scenario.special_items[which_i].descr,"",
univ.scenario.special_items[which_i].name,univ.scenario.intro_pic,PIC_SCEN);

View File

@@ -24,6 +24,7 @@ void add_to_journal(short event);
void give_help(short help1,short help2,class cDialog& parent_num);
void give_help(short help1,short help2);
void put_spec_item_info (short which_i);
void put_quest_info(short which_i);
// These are defined in pc.editors.cpp since they are also used by the character editor
void pick_race_abil(cPlayer *pc,short mode);

View File

@@ -920,6 +920,20 @@ cItem return_treasure(short loot) {
}
void generate_job_bank(int which, job_bank_t& bank) {
std::fill(bank.jobs.begin(), bank.jobs.end(), -1);
bank.inited = true;
size_t iSlot = 0;
for(size_t i = 0; iSlot < 4 && i < univ.scenario.quests.size(); i++) {
if(univ.scenario.quests[i].bank1 != which && univ.scenario.quests[i].bank2 != which)
continue;
if(univ.party.quest_status[i] != eQuestStatus::AVAILABLE)
continue;
if(get_ran(1,1,100) <= 50 - bank.anger)
bank.jobs[iSlot++] = i;
}
}
void refresh_store_items() {
short i,j;
short loot_index[10] = {1,1,1,1,2,2,2,3,3,4};
@@ -934,6 +948,9 @@ void refresh_store_items() {
univ.party.magic_store_items[i][j].ident = true;
}
for(i = 0; i < univ.party.job_banks.size(); i++) {
generate_job_bank(i, univ.party.job_banks[i]);
}
}

View File

@@ -33,6 +33,7 @@ void reset_item_max();
short item_val(cItem item);
void place_treasure(location where,short level,short loot,short mode);
cItem return_treasure(short loot);
void generate_job_bank(int which, job_bank_t& bank);
void refresh_store_items();
std::string get_text_response(std::string prompt = "", pic_num_t pic = 16);
short get_num_response(short min, short max, std::string prompt);

View File

@@ -171,6 +171,13 @@ static void init_party_scen_data() {
univ.party.party_event_timers.clear();
for(i = 0; i < 50; i++)
univ.party.spec_items[i] = univ.scenario.special_items[i].flags >= 10;
for(i = 0; i < univ.scenario.quests.size(); i++) {
if(univ.scenario.quests[i].flags >= 10) {
univ.party.quest_status[i] = eQuestStatus::STARTED;
univ.party.quest_start[i] = 1;
univ.party.quest_source[i] = -1;
}
}
for(i = 0; i < 200; i++)
univ.party.m_killed[i] = 0;

View File

@@ -1816,6 +1816,45 @@ void special_increase_age(long length, bool queue) {
location null_loc; // TODO: Should we pass the party's location here? It doesn't quite make sense to me though...
unsigned long age_before = univ.party.age - length;
unsigned long current_age = univ.party.age;
bool failed_job = false;
for(auto p : univ.party.quest_status) {
if(p.second != eQuestStatus::STARTED)
continue;
cQuest& quest = univ.scenario.quests[p.first];
if(quest.deadline <= 0)
continue;
bool is_relative = quest.flags % 10;
int deadline = quest.deadline + is_relative * univ.party.quest_start[p.first];
if(day_reached(deadline + 1, quest.event)) {
p.second = eQuestStatus::FAILED;
if(univ.party.quest_source[p.first] >= 0) {
int bank = univ.party.quest_source[p.first];
// Safety valve in case it was given by a special node
if(bank >= univ.party.job_banks.size())
univ.party.job_banks.resize(bank + 1);
int add_anger = 1;
if(quest.flags % 10 == 1) {
if(quest.deadline < 20)
add_anger++;
if(quest.deadline < 10)
add_anger++;
if(quest.deadline < 5)
add_anger++;
} else if(quest.deadline - univ.party.quest_start[p.first] > 20)
add_anger++;
univ.party.job_banks[bank].anger += add_anger;
}
if(!failed_job)
add_string_to_buf("The deadline for one of your quests has passed.",2);
failed_job = true;
}
}
// Angered job boards slowly forgive you
if(univ.party.age % 30 == 0)
for(i = 0; i < univ.party.job_banks.size(); i++)
move_to_zero(univ.party.job_banks[i].anger);
if(is_town() || (is_combat() && which_combat_type == 1)) {
for(i = 0; i < 8; i++)
@@ -2399,6 +2438,42 @@ void general_spec(eSpecCtx which_mode,cSpecial cur_node,short cur_spec_type,
start_talk_mode(i, spec.ex1a, spec.ex1b, spec.pic);
*next_spec = -1;
break;
case eSpecType::UPDATE_QUEST:
if(spec.ex1a < 0 || spec.ex1a >= univ.scenario.quests.size()) {
giveError("The scenario tried to update a non-existent quest.");
break;
}
if(spec.ex1b < 0 || spec.ex1b > 3) {
giveError("Invalid quest status (range 0 .. 3).");
break;
}
if(spec.ex1b == int(eQuestStatus::STARTED) && univ.party.quest_status[spec.ex1a] != eQuestStatus::STARTED) {
univ.party.quest_start[spec.ex1a] = calc_day();
univ.party.quest_source[spec.ex1a] = max(-1,spec.ex2a);
if(univ.party.quest_source[spec.ex1a] >= univ.party.job_banks.size())
univ.party.job_banks.resize(univ.party.quest_source[spec.ex1a] + 1);
}
univ.party.quest_status[spec.ex1a] = eQuestStatus(spec.ex1b);
switch(univ.party.quest_status[spec.ex1a]) {
case eQuestStatus::STARTED: add_string_to_buf("You have received a quest."); break;
case eQuestStatus::AVAILABLE: break; // TODO: Should this award XP/gold if the quest was previously started?
case eQuestStatus::FAILED:
add_string_to_buf("You have failed to complete a quest.");
if(univ.party.quest_source[spec.ex1a] >= 0 && univ.party.quest_source[spec.ex1a] < univ.party.job_banks.size())
univ.party.job_banks[univ.party.quest_source[spec.ex1a]].anger += spec.ex2a < 0 ? 1 : spec.ex2a;
break;
case eQuestStatus::COMPLETED:
add_string_to_buf("You have completed a quest!");
if(univ.scenario.quests[spec.ex1a].gold > 0) {
int gold = univ.scenario.quests[spec.ex1a].gold;
add_string_to_buf(" Received " + std::to_string(gold) + " as a reward.");
give_gold(gold, true);
}
if(univ.scenario.quests[spec.ex1a].xp > 0)
award_party_xp(univ.scenario.quests[spec.ex1a].xp);
break;
}
break;
default:
giveError("Special node type \"" + (*cur_node.type).name() + "\" is either miscategorized or unimplemented!");
break;
@@ -3457,6 +3532,18 @@ void ifthen_spec(eSpecCtx which_mode,cSpecial cur_node,short cur_spec_type,
if(spec.ex2c == 1 && k > j) *next_spec = spec.ex1b;
if(spec.ex2c == 2 && k >= j) *next_spec = spec.ex1b;
break;
case eSpecType::IF_QUEST:
if(spec.ex1a < 0 || spec.ex1a >= univ.scenario.quests.size()) {
giveError("The scenario tried to update a non-existent quest.");
break;
}
if(spec.ex1b < 0 || spec.ex1b > 3) {
giveError("Invalid quest status (range 0 .. 3).");
break;
}
if(univ.party.quest_status[spec.ex1a] == eQuestStatus(spec.ex1b))
*next_spec = spec.ex1c;
break;
case eSpecType::IF_CONTEXT:
// TODO: Test this. In particular, test that the legacy behaviour is correct.
j = -1;
@@ -3581,6 +3668,10 @@ void ifthen_spec(eSpecCtx which_mode,cSpecial cur_node,short cur_spec_type,
if(which_mode == eSpecCtx::ATTACKED_RANGE)
*next_spec = spec.ex1c;
break;
case 25: // Initiating conversation
if(which_mode == eSpecCtx::HAIL)
*next_spec = spec.ex1c;
break;
}
if(j >= 0) *a = j;
break;

View File

@@ -256,7 +256,19 @@ void put_item_screen(short screen_num,short suppress_buttons) {
}
break;
case ITEM_WIN_QUESTS:
// TODO: Implement quest list
style.font = FONT_BOLD;
style.colour = sf::Color::White;
win_draw_string(item_stats_gworld,upper_frame_rect,"Quests/Jobs:",eTextMode::WRAP,style);
style.colour = sf::Color::Black;
for(i = 0; i < 8; i++) {
i_num = i + item_offset;
if(spec_item_array[i_num] >= 0) {
// 2nd condition above is quite kludgy, in case it gets here with array all 0's
win_draw_string(item_stats_gworld,item_buttons[i][0],univ.scenario.quests[spec_item_array[i_num]].name,eTextMode::WRAP,style);
place_item_button(3,i,4,0);
}
}
break;
default: // on an items page
@@ -517,8 +529,15 @@ void set_stat_window(short new_stat) {
item_sbar->setMaximum(array_pos);
break;
case ITEM_WIN_QUESTS:
item_sbar->setMaximum(2);
item_sbar->setPageSize(2);
for(i = 0; i < 60; i++)
spec_item_array[i] = -1;
for(i = 0; i < 50; i++)
if(univ.party.quest_status[i] == eQuestStatus::STARTED) {
spec_item_array[array_pos] = i;
array_pos++;
}
array_pos = max(0,array_pos - 8);
item_sbar->setMaximum(array_pos);
break;
default:
item_sbar->setMaximum(16);

View File

@@ -399,6 +399,7 @@ cTownperson::cTownperson() {
monster_time = 0;
personality = -1;
special_on_kill = -1;
special_on_talk = -1;
}
cTownperson::cTownperson(location loc, mon_num_t num, const cMonster& monst) : cTownperson() {

View File

@@ -162,7 +162,7 @@ public:
short spec1, spec2;
short spec_enc_code, time_code;
short monster_time, personality;
short special_on_kill;
short special_on_kill, special_on_talk;
pic_num_t facial_pic;
void append(legacy::creature_start_type old);

View File

@@ -552,6 +552,8 @@ void cParty::writeTo(std::ostream& file) const {
file << "SCENARIO " << scen_name << '\n';
file << "WON " << scen_won << '\n';
file << "PLAYED " << scen_played << '\n';
for(auto p : quest_status)
file << "QUEST " << p.first << ' ' << p.second << ' ' << quest_start.at(p.first) << ' ' << quest_source.at(p.first) << '\n';
for(auto iter = campaign_flags.begin(); iter != campaign_flags.end(); iter++){
std::string campaign_id = maybe_quote_string(iter->first);
// Okay, we have the campaign ID in a state such that reading it back in will restore the original ID.
@@ -586,6 +588,13 @@ void cParty::writeTo(std::ostream& file) const {
file << '\f';
}
file << '\f';
for(int i = 0; i < job_banks.size(); i++) {
file << "JOBBANK " << i << ' ' << job_banks[i].anger << '\n';
if(!job_banks[i].inited) continue;
for(int j = 0; j < 6; j++)
file << "JOB " << j << ' ' << job_banks[i].jobs[j] << '\n';
}
file << '\f';
for(int i = 0; i < 10; i++)
if(out_c[i].exists){
file << "ENCOUNTER " << i << "\n";
@@ -756,6 +765,10 @@ void cParty::readFrom(std::istream& file){
int i;
sin >> i;
sin >> m_killed[i];
} else if(cur == "QUEST") {
int i;
sin >> i;
sin >> quest_status[i] >> quest_start[i] >> quest_source[i];
}else if(cur == "KILLS")
sin >> total_m_killed;
else if(cur == "DAMAGE")
@@ -872,6 +885,27 @@ void cParty::readFrom(std::istream& file){
bin >> std::ws;
getline(bin, note.the_str1);
getline(bin, note.the_str2);
} else if(cur == "JOB_BANK") {
int i;
bin >> i;
if(i < 0) continue;
if(i >= job_banks.size())
job_banks.resize(i + 1);
bin >> job_banks[i].anger;
job_banks[i].inited = false;
while(bin) {
getline(bin, cur);
std::istringstream sin(cur);
sin >> cur;
if(cur == "JOB") {
job_banks[i].inited = true;
int j;
sin >> j;
if(j < 0 || j >= 6)
continue;
sin >> job_banks[i].jobs[j];
}
}
}
bin.clear();
}
@@ -1068,3 +1102,32 @@ std::ostream& operator<<(std::ostream& out, ePartyStatus type) {
return out;
}
std::istream& operator>>(std::istream& in, eQuestStatus& type) {
std::string name;
in >> name;
if(name == "avail") type = eQuestStatus::AVAILABLE;
else if(name == "start") type = eQuestStatus::STARTED;
else if(name == "done") type = eQuestStatus::COMPLETED;
else if(name == "fail") type = eQuestStatus::FAILED;
else in.setstate(std::ios_base::failbit);
return in;
}
std::ostream& operator<<(std::ostream& out, eQuestStatus type) {
switch(type) {
case eQuestStatus::AVAILABLE:
out << "avail";
break;
case eQuestStatus::STARTED:
out << "start";
break;
case eQuestStatus::COMPLETED:
out << "done";
break;
case eQuestStatus::FAILED:
out << "fail";
break;
}
return out;
}

View File

@@ -36,6 +36,14 @@ struct campaign_flag_type{
unsigned char idx[25][25];
};
struct job_bank_t {
std::array<int,6> jobs;
int anger = 0;
bool inited = false;
};
enum class eQuestStatus {AVAILABLE, STARTED, COMPLETED, FAILED};
class cUniverse;
class cParty : public iLiving {
@@ -85,6 +93,7 @@ public:
short in_horse;
cOutdoors::cCreature out_c[10];
std::array<std::array<cItem,10>,5> magic_store_items;
std::vector<job_bank_t> job_banks;
mon_num_t imprisoned_monst[4]; // Soul Crystal
char m_noted[256]; // has the monster been scried?
char m_seen[256]; // has the monster ever been seen? (this used to have the above meaning)
@@ -92,6 +101,10 @@ public:
std::vector<cEncNote> special_notes;
std::vector<cConvers> talk_save;
std::map<ePartyStatus,short> status;
// Quest stuff
std::map<int, eQuestStatus> quest_status;
std::map<int, int> quest_start; // the day the quest was started; used for quests with relative deadlines
std::map<int, int> quest_source; // if gotten from a job board, this is the number of the job board; otherwise -1
location left_at;
size_t left_in;
eDirection direction;
@@ -204,5 +217,7 @@ std::istream& operator>>(std::istream& in, eEncNoteType& type);
std::ostream& operator<<(std::ostream& out, eEncNoteType type);
std::istream& operator>>(std::istream& in, ePartyStatus& type);
std::ostream& operator<<(std::ostream& out, ePartyStatus type);
std::istream& operator>>(std::istream& in, eQuestStatus& type);
std::ostream& operator<<(std::ostream& out, eQuestStatus type);
#endif

View File

@@ -34,6 +34,17 @@ struct scenario_header_flags {
unsigned char ver[3],min_run_ver,prog_make_ver[3],num_towns;
};
class cQuest {
public:
short flags = 0; // 0 - absolute deadline, 1 - relative to when quest started, +10 - start quest when scenario starts
short deadline = -1;
short event = -1; // if this event occurs before the deadline, then the deadline is waived
short xp = 0, gold = 0; // automatically award this much XP and gold to the party when the quest is marked complete
short bank1 = -1, bank2 = -1; // which job bank(s) this quest is in; -1 for none
std::string name;
std::string descr;
};
class cScenario {
public:
class cItemStorage {
@@ -57,7 +68,8 @@ public:
short flag_to_add_to_town[10][2];
rectangle store_item_rects[3];
short store_item_towns[3];
cSpecItem special_items[50];
std::array<cSpecItem,50> special_items;
std::vector<cQuest> quests;
short rating,uses_custom_graphics;
std::vector<ePicType> custom_graphics;
std::array<cMonster,256> scen_monsters;

View File

@@ -507,7 +507,7 @@ enum class eSpecCtx {
TOWN_LOOK = 4,
ENTER_TOWN = 5,
LEAVE_TOWN = 6,
TALK = 7,
TALK = 7, // Special called during conversation
USE_SPEC_ITEM = 8,
TOWN_TIMER = 9,
SCEN_TIMER = 10,
@@ -525,6 +525,7 @@ enum class eSpecCtx {
ATTACKING_RANGE = 22,
ATTACKED_MELEE = 23,
ATTACKED_RANGE = 24,
HAIL = 25, // Special called by trying to initiate conversation
};
enum class eSpecType {
@@ -574,6 +575,7 @@ enum class eSpecType {
APPEND_TER = 42,
PAUSE = 43,
START_TALK = 44,
UPDATE_QUEST = 45,
ONCE_GIVE_ITEM = 50,
ONCE_GIVE_SPEC_ITEM = 51,
ONCE_NULL = 52,
@@ -644,6 +646,7 @@ enum class eSpecType {
IF_NUM_RESPONSE = 157,
IF_IN_BOAT = 158,
IF_ON_HORSE = 159,
IF_QUEST = 160,
MAKE_TOWN_HOSTILE = 170,
TOWN_RUN_MISSILE = 171,
TOWN_MONST_ATTACK = 172,
@@ -699,13 +702,13 @@ enum class eSpecCat {
inline eSpecCat getNodeCategory(eSpecType node) {
int code = (int) node;
if(code >= 0 && code <= 44)
if(code >= 0 && code <= 45)
return eSpecCat::GENERAL;
if(code >= 50 && code <= 63)
return eSpecCat::ONCE;
if(code >= 80 && code <= 105)
return eSpecCat::AFFECT;
if(code >= 130 && code <= 159)
if(code >= 130 && code <= 160)
return eSpecCat::IF_THEN;
if(code >= 170 && code <= 199)
return eSpecCat::TOWN;
@@ -726,6 +729,7 @@ enum class eTalkNode {
DEP_ON_TOWN = 6,
SHOP = 7,
TRAINING = 8,
JOB_BANK = 9,
SELL_WEAPONS = 13,
SELL_ARMOR = 14,
SELL_ITEMS = 15,
@@ -736,6 +740,7 @@ enum class eTalkNode {
BUY_SHIP = 20,
BUY_HORSE = 21,
BUY_SPEC_ITEM = 22,
RECEIVE_QUEST = 23,
BUY_TOWN_LOC = 24,
END_FORCE = 25,
END_FIGHT = 26,

View File

@@ -80,7 +80,7 @@ void cButton::draw(){
style.lineHeight = 8;
eTextMode textMode = eTextMode::CENTRE;
if(type == BTN_TINY) {
textMode = eTextMode::LEFT_TOP;
textMode = wrapLabel ? eTextMode::WRAP : eTextMode::LEFT_TOP;
to_rect.left += 18;
style.colour = textClr;
} else if(type == BTN_PUSH) {
@@ -236,12 +236,14 @@ bool cLed::triggerClickHandler(cDialog& me, std::string id, eKeyMod mods){
void cLed::setFormat(eFormat prop, short val) throw(xUnsupportedProp){
if(prop == TXT_FONT) textFont = (eFont) val;
else if(prop == TXT_SIZE) textSize = val;
else if(prop == TXT_WRAP) wrapLabel = val;
else throw xUnsupportedProp(prop);
}
short cLed::getFormat(eFormat prop) throw(xUnsupportedProp){
if(prop == TXT_FONT) return textFont;
else if(prop == TXT_SIZE) return textSize;
else if(prop == TXT_WRAP) return wrapLabel;
else throw xUnsupportedProp(prop);
}
@@ -263,7 +265,7 @@ void cLed::draw(){
style.colour = textClr;
to_rect.right = frame.right;
to_rect.left = frame.left + 18; // Possibly could be 20
win_draw_string(*inWindow,to_rect,lbl,eTextMode::LEFT_TOP,style);
win_draw_string(*inWindow,to_rect,lbl,wrapLabel ? eTextMode::WRAP : eTextMode::LEFT_TOP,style);
}
}

View File

@@ -85,11 +85,12 @@ protected:
/// @param t The type of control. Should be either CTRL_LED or CTRL_BTN.
cButton(cDialog* parent,eControlType t);
private:
bool wrapLabel;
bool labelWithKey;
std::string fromList;
static rectangle btnRects[13][2];
protected:
/// Determines whether the button's label should be word wrapped.
bool wrapLabel;
/// The button's text colour; only used by LED and tiny buttons
sf::Color textClr;
/// The index in buttons of the texture for each button type.

View File

@@ -612,6 +612,12 @@ template<> pair<string,cLed*> cDialog::parse(Element& who /*LED*/){
throw xBadVal("led",name,val,attr->Row(),attr->Column(),fname);
}
p.second->setColour(clr);
} else if(name == "wrap") {
std::string val;
attr->GetValue(&val);
if(val == "true")
p.second->setFormat(TXT_WRAP, true);
else p.second->setFormat(TXT_WRAP, false);
}else if(name == "top"){
attr->GetValue(&frame.top), foundTop = true;
}else if(name == "left"){

View File

@@ -221,7 +221,10 @@ bool handle_action(location the_point,sf::Event /*event*/) {
case 7:
start_special_item_editing();
break;
case 11: // pick out
case 8:
start_quest_editing();
break;
case 12: // pick out
if(change_made) {
if(!save_check("save-section-confirm"))
break;
@@ -233,12 +236,12 @@ bool handle_action(location the_point,sf::Event /*event*/) {
set_up_main_screen();
}
break;
case 12: // edit outdoors
case 13: // edit outdoors
start_out_edit();
mouse_button_held = false;
return false;
break;
case 16: // pick town
case 17: // pick town
if(change_made) {
if(!save_check("save-section-confirm"))
break;
@@ -250,12 +253,12 @@ bool handle_action(location the_point,sf::Event /*event*/) {
set_up_main_screen();
}
break;
case 17: // edit town
case 18: // edit town
start_town_edit();
mouse_button_held = false;
return false;
break;
case 18:
case 19:
start_dialogue_editing(0);
break;
@@ -382,6 +385,17 @@ bool handle_action(location the_point,sf::Event /*event*/) {
else edit_text_str(j,5);
start_string_editing(5,1);
break;
case 16:
if(option_hit) {
if(j == scenario.quests.size() - 1)
scenario.quests.pop_back();
else {
scenario.quests[j] = cQuest();
scenario.quests[j].name = "Unused Quest";
}
} else edit_quest(j);
start_quest_editing();
break;
}
mouse_button_held = false;
}
@@ -2925,6 +2939,7 @@ void set_up_main_screen() {
set_lb(-1,11,"Create New Town",0);
set_lb(-1,11,"Edit Scenario Text",0);
set_lb(-1,11,"Edit Special Items",0);
set_lb(-1,11,"Edit Quests",0);
set_lb(-1,1,"",0);
set_lb(-1,1,"Outdoors Options",0);
sprintf((char *) message," Section x = %d, y = %d",(short) cur_out.x,(short) cur_out.y);
@@ -3105,6 +3120,27 @@ void start_special_item_editing() {
set_lb(NLS - 3,0,"",1);
}
void start_quest_editing() {
int num_options = scenario.quests.size() + 1;
if(overall_mode < MODE_MAIN_SCREEN)
set_up_main_screen();
overall_mode = MODE_MAIN_SCREEN;
right_sbar->show();
right_sbar->setPosition(0);
reset_rb();
right_sbar->setMaximum(num_options - NRSONPAGE);
for(int i = 0; i < num_options; i++) {
std::string title;
if(i == scenario.quests.size())
title = "Create New Quest";
else title = scenario.quests[i].name;
title = std::to_string(i) + " - " + title;
set_rb(i, 16000 + i, title.c_str(), 0);
}
redraw_screen();
set_lb(NLS - 3, 1, "Command-click or right-click to delete", 1);
}
extern size_t num_strs(short mode); // defined in scen.keydlgs.cpp
// mode 0 - scen 1 - out 2 - town 3 - journal

View File

@@ -46,6 +46,7 @@ void start_terrain_editing();
void start_monster_editing(short just_redo_text);
void start_item_editing(short just_redo_text);
void start_special_item_editing();
void start_quest_editing();
void start_string_editing(short mode,short just_redo_text);
void start_special_editing(short mode,short just_redo_text);
void town_entry(location spot_hit);

View File

@@ -23,6 +23,7 @@ bool left_buttons_active = 1,right_buttons_active = 0;
extern short left_button_status[NLS]; // 0 - clear, 1 - text, 2 - title text, 3 - tabbed text, +10 - button
extern short right_button_status[NRS];
extern std::shared_ptr<cScrollbar> right_sbar;
// Button status:
// 0 - clear
// 1000 + x - terrain type x
// 2000 + x - monster type x
@@ -34,9 +35,12 @@ extern std::shared_ptr<cScrollbar> right_sbar;
// 8000 + x - out string x
// 9000 + x - town string x
// 10000 + x - scen. special item x
// 11000 + x - charter intro c
// 11000 + x - journal entry x
// 12000 + x - dialogue node x
// 13000 + x - basic dialogue node x
// 14000 + x - outdoor sign x
// 15000 + x - town sign x
// 16000 + x - quest x
// for following, lb stands for left button(s)

View File

@@ -1989,6 +1989,105 @@ void edit_spec_item(short which_item) {
item_dlg.run();
}
static void put_quest_in_dlog(cDialog& me, const cQuest& quest, size_t which_quest) {
me["num"].setText(std::to_string(which_quest) + " of " + std::to_string(scenario.quests.size()));
me["name"].setText(quest.name);
me["descr"].setText(quest.descr);
me["chop"].setTextToNum(quest.deadline);
me["evt"].setTextToNum(quest.event);
me["xp"].setTextToNum(quest.xp);
me["gold"].setTextToNum(quest.gold);
me["bank1"].setTextToNum(quest.bank1);
me["bank2"].setTextToNum(quest.bank2);
dynamic_cast<cLed&>(me["rel"]).setState(quest.flags % 10 == 1 ? led_red : led_off);
dynamic_cast<cLed&>(me["start"]).setState(quest.flags >= 10 ? led_red : led_off);
dynamic_cast<cLed&>(me["inbank"]).setState(quest.bank1 >= 0 || quest.bank2 >= 0 ? led_red : led_off);
if(quest.bank1 < 0 && quest.bank2 < 0) {
me["bank1"].hide();
me["bank2"].hide();
} else {
me["bank1"].show();
me["bank2"].show();
}
}
static bool save_quest_from_dlog(cDialog& me, cQuest& quest, size_t which_quest, bool close) {
if(!me.toast(true)) return false;
quest.name = me["name"].getText();
quest.descr = me["descr"].getText();
quest.deadline = me["chop"].getTextAsNum();
quest.event = me["evt"].getTextAsNum();
quest.xp = me["xp"].getTextAsNum();
quest.gold = me["gold"].getTextAsNum();
quest.flags = dynamic_cast<cLed&>(me["rel"]).getState() == led_red;
if(dynamic_cast<cLed&>(me["start"]).getState() == led_red)
quest.flags += 10;
if(dynamic_cast<cLed&>(me["inbank"]).getState() == led_red) {
quest.bank1 = me["bank1"].getTextAsNum();
quest.bank2 = me["bank2"].getTextAsNum();
} else quest.bank1 = quest.bank2 = -1;
scenario.quests[which_quest] = quest;
if(!close) me.untoast();
return true;
}
static bool change_quest_dlog_page(cDialog& me, std::string dir, cQuest& quest, size_t& which_quest) {
if(!save_quest_from_dlog(me, quest, which_quest, false))
return true;
if(dir == "left") {
if(which_quest == 0)
which_quest = scenario.quests.size();
which_quest--;
} else if(dir == "right") {
which_quest++;
if(which_quest == scenario.quests.size())
which_quest = 0;
}
quest = scenario.quests[which_quest];
put_quest_in_dlog(me, quest, which_quest);
return true;
}
void edit_quest(size_t which_quest) {
using namespace std::placeholders;
if(which_quest == scenario.quests.size()) {
scenario.quests.resize(which_quest + 1);
scenario.quests[which_quest].name = "New Quest";
}
cQuest quest = scenario.quests[which_quest];
cDialog quest_dlg("edit-quest");
quest_dlg["cancel"].attachClickHandler(std::bind(&cDialog::toast, _1, false));
quest_dlg["okay"].attachClickHandler(std::bind(save_quest_from_dlog, _1, std::ref(quest), std::ref(which_quest), true));
quest_dlg["inbank"].attachFocusHandler([](cDialog& me, std::string, bool losing) -> bool {
if(losing) {
me["bank1"].hide();
me["bank2"].hide();
} else {
me["bank1"].show();
me["bank2"].show();
}
return true;
});
// TODO: Some focus handlers
if(scenario.quests.size() == 1) {
quest_dlg["left"].hide();
quest_dlg["right"].hide();
} else {
quest_dlg.attachClickHandlers(std::bind(change_quest_dlog_page, _1, _2, std::ref(quest), std::ref(which_quest)), {"left", "right"});
}
put_quest_in_dlog(quest_dlg, quest, which_quest);
quest_dlg.run();
}
static void put_save_rects_in_dlog(cDialog& me) {
short i;

View File

@@ -8,6 +8,7 @@ cMonster edit_monst_abil(cMonster starting_record,short which_monst,cDialog& par
short edit_item_type(short which_item);
cItem edit_item_abil(cItem starting_record,short which_item,cDialog& parent);
void edit_spec_item(short which_item);
void edit_quest(size_t which_quest);
void edit_save_rects();
void edit_horses();
void edit_add_town();

View File

@@ -189,6 +189,34 @@ static void writeScenarioToXml(ticpp::Printer&& data) {
data.CloseElement("item");
}
data.CloseElement("specials");
for(size_t i = 0; i < scenario.quests.size(); i++) {
cQuest& quest = scenario.quests[i];
data.OpenElement("quest");
data.PushAttribute("start-with", boolstr(quest.flags / 10));
if(quest.deadline >= 0) {
data.OpenElement("deadline");
data.PushAttribute("relative", boolstr(quest.flags % 10));
if(quest.event >= 0)
data.PushAttribute("waive-if", quest.event);
data.PushText(quest.deadline);
data.CloseElement("deadline");
}
if(quest.xp > 0 || quest.gold > 0) {
data.OpenElement("reward");
if(quest.xp > 0)
data.PushAttribute("xp", quest.xp);
if(quest.gold > 0)
data.PushAttribute("gold", quest.gold);
data.CloseElement("reward");
}
if(quest.bank1 >= 0)
data.PushElement("bank", quest.bank1);
if(quest.bank2 >= 0)
data.PushElement("bank", quest.bank2);
data.PushElement("name", quest.name);
data.PushElement("description", quest.descr);
data.CloseElement("quest");
}
for(int i = 0; i < 20; i++) {
if(scenario.scenario_timer_times[i] > 0) {
data.OpenElement("timer");