Hegemony III: Clash of the Ancients

Hegemony III: Clash of the Ancients

Hegemony III Workshop
Post your maps and mods here ...
Learn More
Ijtzoi  [developer] 15 Jan 7, 2016 @ 8:19pm
Modding Tutorial #8: Quests (continues from Events)
Hello everyone, the modding blog is back from winter break, hope you all had a good one, for the eighth we'll be finishing off the unexplained bits of the quest that was added before the break! In addition, Rob's been putting out a video series (found here) focused on the map editor that you should all check out; map editing's really fun when you get into the swing of it.

Last time, we used the taskload function to load TaskFlowerOfItaly.xml into the Locris objective and discover the task in it.

Objectives

Objectives are a collection of tasks, largely used to keep the objective window organized; the default game has an objective for prelude tasks, for tutorial tasks, for global hegemony tasks, for the ancient rival event chain, and one for events about each faction. These objectives don't actually control anything about the tasks within, but can be expanded or hidden in the objective window. That said, you can define your own objectives and give the objective its own script to function more as a metatask if you want. If you do want to define an objective, you'll need to create an .xml file for it. These files are not loaded by default; to load one, use the objectiveload script function in a script somewhere (probably an even) with the filename passed in as a parameter: objectiveload("Example.xml"). An objective file should look something like this:
<objective> <name>Hegemony</name> <title>QuestHegemony::TITLE</title> <description>QuestHegemony::DESCRIPTION</description> </objective>
An "objective" tag with "name", "title", and "description" subtags. "name" is the unique internal name the code uses for this objective, make sure no other objective uses it, while "title" and "description" are the names of strings that show up in the objective window for this objective, and any number of objectives can have the same strings. You can also add in a "scriptinit" tag, which will run the lua script when the objective has been loaded.
<objective> <name>Example</name> <title>QuestHegemony::TITLE</title> <description>QuestHegemony::DESCRIPTION</description> <scriptinit>print("Example objective loaded and initialized!")</scriptinit> </objective>
You could also define tasks directly in the objective at the start rather than adding them in later via event; to do this, simply put a task tag within the objective.

Tasks

You'll be working with tasks more than with objectives; in Flower of Italy, we don't bother defining a new objective, but simply add a task to the default objective for the Locris faction. Tasks are what you probably think of when you think of quests in Hegemony III: examples of tasks are A Humble Request, Meeting the Neighbours (formerly known as General Tour), and Search and Rescue. They can be seen in the right of the screen in the objective tracker and are also stored as xml, either immediately in their objective's file, or in their own xml and later added to the objective with the taskload("ObjectiveName", "ExampleTaskFilename.xml") script command. A task xml should look like:
<task> <name>FlowerOfItaly</name> <title>QuestBurnLocris::FlowerOfItaly::TITLE</title> <description>QuestBurnLocris::FlowerOfItaly::DESCRIPTION</description> <critical>false</critical> <script> talkingheadnewobjective( getstring("PH", "QuestBurnLocris::FlowerOfItaly::Introduction"), getplayerfaction():getadvisor("spymaster") ); waitsubtasks(); </script> </task>
Looks pretty similar to the objective, "name" "title" and "description" all do the same things, though it's worth noting that the name only has to be unique within the objective. Any time you try to look up this task, you'll be using the objective name followed by a dot then the task name, so Locris.FlowerOfItaly, which count as a different name than Prelude.FlowerOfItaly even though both tasks have the same name.

The biggest difference between tasks and objectives is that tasks HAVE to have a script and will shut themselves down the moment this script is done. There will usually be a waitsubtasks() or waitsubtaskany() call in the task script; this will be the point at which the script will stop running and wait for a subtask to finish. If the task has more than one subtask, you'll have to choose between waitsubtaskany(), which will be done the moment any of the subtasks are done, or waitsubtasks(), which will wait for all of them. You usually use waitsubtaskany() to present the player with a choice and waitsubtasks() for things like conquering a whole bunch of cities. Anything you put before the wait call will be run the moment the task is discovered, so our talkingheadnewobjective() call (more on what that function does later) will happen immediately when the task is loaded. Anything put after the wait will run when the right amount of subtasks are completed, but since Locris.FlowerOfItaly has nothing after waitsubtasks(), the task will just consider itself over when the wait is completed and close itself.

Subtasks

So we know that tasks wait for subtasks to finish, but what's a subtask look like? Once again, they're xml, and like tasks, they can either be stored directly in the task they belong to or loaded into it later with the subtaskload("Locris.FlowerOfItaly", "ExampleSubtaskFilename.xml") command, using the task's full name (which includes the name of the objective they're in) for the first parameter. Here's a subtask:
<subtask> <name>Subtask1</name> <status>SubtaskCaptureSpecific</status> <script> trackself(); local locris = getentitybytag("locris"); subtaskposfollow( whoami(), locris ); waitcapture( locris ); taskload("Locris", "Resources/Objectives/BurnLocrisBurn/TaskPluckingThePetals.xml"); taskdiscover("Locris.PluckingThePetals", false); </script> </subtask>
Like most subtasks, it's just a "name", "status" and "script". The "name" operates under similar rules to the task, where its name has to be unique within its task's subtasks, the "script" is a lua script that starts running the moment the subtask is loaded (which is when the task is loaded), and the "status" is the name of a string that will show up under the task's name. For example, A Humble Request's subtask uses the SubtaskDefeatNFaction, which is the part in the tracker that says "Defeat Locrian units (4/5)" or whatever the appropriate faction and numbers are. This subtask is about capturing the city of Locris, so we use the string SubtaskCaptureSpecific, which will look like Capture [name]:h3_gototag:.

There are a couple of script commands in this subtask's script that're only ever used in subtasks, so it's worth looking at. The trackself() command just puts the task in the tracker on the right side of the screen; by default, tasks start untracked and thus the player needs to go to the objective window and click the tracker checkbox for the task themselves if they want to keep track of this, but the trackself() puts it there for them.

Afterwards, we call subtaskposfollow(whoami(), locris). The whoami() function is a quick way to get the full name of the subtask rather than typing out "Locris.FlowerOfItaly.Subtask1", and one that would remain accurate if the name were changed somehow (by adding the task to a different objective, for example) and so is preferred, but using "Locris.FlowerOfItaly.Subtask1" would work too. subtaskposfollow() is a function that takes two things, the name of a subtask (handled by whoami()) and an entity to follow (the locris script entity we saved off) and will then start placing map markers over that entity which remind the player it's the target of a quest.

waitcapture(locris) is similar to the task's waitsubtasks() in that the script will stop and wait until something happens, but it waits until the entity passed in is captured by the player. This one can be used outside of subtasks, but it contains a small bonus subtask-only feature where it includes calls to the status functions.

Status Functions, Anchors and Placeholders

statusstring, statusstringref and statusint are subtask-only functions that will replace a certain placeholder in the status string with whatever you've passed in, statusint will use a number, statusstringref takes the name of a string and uses whatever's in that string while statusstring just uses whatever you pass it. If we look in the status string we're using in Locris.FlowerOfItaly.Subtask1, we see the following:
Capture <placeholder name="name"/> <a placeholder:scripttag="gototag"><img src='icons/goto' colour='inherit'/></a>
This contains two placeholders, '<placeholder name="name"/>' and 'placeholder:scripttag="gototag"'. The first one, named "name", is in its own placeholder tag; if we call statusstring("name", "Example"), the entire tag would be replaced by "Example" and the end product would look like Capture Example:h3_gototag:. The waitcapture function automatically calls statusstringref with the string the object it's waiting to get captured uses as its name, so that's why the subtask will read Capture Locris:h3_gototag: instead of Capture [name]:h3_gototag:.

The other placeholder is for an attribute to an "a" tag. "a" tags, generally called "anchors", make all the text inside the tag interactive: <a gototag="locris">Click Here</a> would show up as "Click Here" that, when clicked, goes to an entity with the tag "locris". There're a few different tags you can put in anchors, like "gototag" which goes to an entity with the given tag, "gotoid" goes to an entity with a given numerical id, "selectid" which selects an entity with a given id, or "script" which executes a script given. Our placeholder will add a "gototag" attribute to the anchor tag with whatever is handed to it; waitcapture() will automatically take the tag of the entity handed to it and call statusstring("scripttag", "whateverthetagwas"), which turns the anchor from <a placeholder:scripttag="gototag"> to <a gototag="whateverthetagwas"> or in our case <a gototag="locris">, wrapping the img tag inside the anchor tag in a link that will make the camera zoom to Locris. The img tag is comparatively simple, needing only a src attribute containing an atlas reference for the image to lift and a colour attribute saying it should inherit the colour of the link.
Last edited by Ijtzoi; Jan 7, 2016 @ 8:23pm
< >
Showing 1-6 of 6 comments
Ijtzoi  [developer] 15 Jan 7, 2016 @ 8:19pm 
Advanced Task Details
So waitcapture(locris) will have the subtask pause until the city is captured, but then the rest of the script will execute. You may have noticed that we then run the commands to add another task; when Locris gets captured, we don't add any reward immediately but add another quest. This one will be a more complicated one that gives the player a choice; either crush Locris beneath their boot and extract the wealth from their temples, or let them live unmolested under their benevolent rule [other player choices may or may not impact benevolence of rule].

As we touched on before, quests that give a task use waitsubtaskany() and close after one of their subtasks completes. So in TaskPluckingThePetals.xml, rather than use waitsubtasks() like we did in TaskFlowerOfItaly.xml, we'll use waitsubtaskany() and have multiple subtasks, then once the player completes one the other is gone. One of these subtasks will have its script be to go through with the looting, one will be not to do anything. In addition, we'll add a time limit to this task so that it'll auto-cancel if you take too long and the moment of conquest passes, since it stops feeling appropriate after Locris has been a loyal subject for years.

To add a time limit, you add a new tag to your task called "maxtime" and set its contents for the number of days you want the task to exist before time runs out. If you want, you can add a cancelscript tag which will run if the time runs out or if the player manually cancels. To make deciding not to loot a somewhat competitive option, we'll create a cancelscript which assimilates the city a little and gives it a population growth bonus, since they're keeping temples to gods of fertility and family.

For the actual subtasks, the positive subtask's script will be based mainly on the sleepuntil function. This function is similar to waitcapture or waitsubtasks, but waits until any wake event is triggered. A wake event can be triggered either by the game (this is actually how the waitcapture function detects there's been a capture) or via script; we'll be waiting for a custom-defined wake event called "PluckThePetals", and in our status string, add an anchor script to trigger it. Thus the subtask's script will wait until the player clicks the anchor in the subtask's status. We can add that anchor script to the string by wrapping the text with <a script="wakeevent([[PluckThePetals]])">____</a> (note that [[ and ]] can be used in lua to replace " when you need to make sure it doesn't think you're closing a different quote). After that fires, we'll give the player the gold from looting by calling the givegold() command on the player faction and raise its hostility with all the greek factions by starting a loop through every faction, checking if it's greek and isn't the player, and if so raising its hostility through the inchostility function.

The other subtask just needs to make the cancelscript run, since it's just the player actively choosing not to do it. Strictly speaking, it doesn't need to exist since manual cancel options and time limits can have it run if the player would prefer this option, but it's better to make the player's choices obvious. Its status string can just be the default SubtaskReject string, which already has a placeholder for scripts we can fill with a request to cancel this task using the taskcancel() command with the task's name passed in. The actual script for this subtask isn't too important if we do it correctly; all we want is for this subtask to stay paused for the entirety of the task and give the player a chance to cancel the task. I set it to call delaydays() with 7 passed in, which means it will wait out the time limit and can't unpause early.

Of course, both when we start the task and when it's completed, it would be nice to have popups tell the player what's going on, which is where talkinghead functions come in. Talking heads is our name for the advisors or ambassadors that sometimes come up to tell the player about something; they are brought up with a talkinghead function. There are a few talkinghead functions, but they're all pretty similar; the first parameter is the text that they say. You'll want to use the getstring command to retrieve the contents of anything you put in stringwrangler, getstring works by taking first "PH" then the name of the string you want to retrieve. The second parameter of the talkinghead functions describes who the advisor is; their name, their job title, what faction they work for, etc. You can get advisor data by retrieving a faction entity (usually through getplayerfaction() or getfactionbyname()) and calling the getadvisor function with either "diplomat", "spymaster", "general" or "governor" passed in as its only parameter. The talking head will then pause until the player clicks okay, at which point the script will resume. The talking head functions that follow that format are talkingheadrewardcity, talkingheadrewardgold, talkingheadnewobjective, and talkingheadnews, and the main difference between them is that rewardcity says "A NEW CITY HAS JOINED OUR FACTION!" while newobjective says "NEW OBJECTIVE", rewardgold takes a third parameter for an amount of gold to replace any "gold" placeholders with and news has none of these bells and whistles.

For the introduction, we'll use talkingheadnewobjective and add a string with a player advisor pointing out that they could loot the temples, then just wait on any of the subtasks. We'll add a talkingheadnews to the accept script, have a Locrian point out that the player's committed an atrocity and everyone who loves the greek gods will hate them for it, and add a talkingheadnews to the cancelscript where one of the player's advisors state how grateful the people of Locris are.

Our final task looks like:
<task> <name>PluckingThePetals</name> <title>QuestBurnLocris::PluckingThePetals::TITLE</title> <description>QuestBurnLocris::PluckingThePetals::DESCRIPTION</description> <critical>false</critical> <subtask> <name>Subtask1</name> <status>QuestBurnLocris::PluckingThePetals::DoIt</status> <script> trackself(); local player = getplayerfaction(); statusint("gold", 500 * player:numcities()); sleepuntil("PluckThePetals"); player:givegold(500 * player:numcities()); for faction in listfactions() do if faction:getfactiongroup() == "greek" and faction ~= player then faction:inchostility(player, 0.5); player:setfactionrelation( faction, "atwar" ); end end talkingheadnews(getstring("PH", "QuestBurnLocris::PluckingThePetals::YouDidIt"), getfactionbyname("Locris"):getadvisor("diplomat")); </script> </subtask> <subtask> <name>Subtask2</name> <status string:script="taskcancel([[Locris.PluckingThePetals]])">SubtaskReject</status> <script> trackself(); delaydays(7); taskcancel([[Locris.PluckingThePetals]]); </script> </subtask> <cancelscript> getentitybytag("locris"):changeassimilation(200); getentitybytag("locris"):setattribute("populationgrowth", 0.25, "mult"); talkingheadnews(getstring("PH", "QuestBurnLocris::PluckingThePetals::YouDidnt"), getplayerfaction():getadvisor("governor")); </cancelscript> <maxtime>7</maxtime> <script> talkingheadnewobjective( getstring("PH", "QuestBurnLocris::PluckingThePetals::Introduction"), getplayerfaction():getadvisor("diplomat") ); waitsubtaskany(); </script> </task>

So that's how the quests from the Flower of Italy mod were put together. I know I sort of quickly blasted through a lot there, but if you have any questions or anything seems unclear, please ask on the forums or through email at chris@longbowgames.com and I'll do my best to go over whatever you asked about in greater detail.
Last edited by Ijtzoi; Jan 7, 2016 @ 8:21pm
Fristi61 10 Sep 2, 2016 @ 12:29pm 
Quick question: what exactly does the <critical> subtag do?
Slightly longer question: Are there any other subtags like it we can use (I also found <cancellable>, for instance)?
Ijtzoi  [developer] 15 Sep 2, 2016 @ 4:27pm 
Critical determines whether it's a Hegemony objective, and if it is, it won't be cancellable and will have the H symbol next to its name.

Other tags not covered in the above tutorial include
  1. <reward>, which defines the name of a string that shows up in the objective window for the task's reward
  2. <faction> which makes a faction logo appear next to the task name
  3. <startundiscovered>, which can be set to false to make it discover immediately
  4. <checkscript> for a script that runs every 5 days (meant to be used to check if the task is still valid)
  5. <timeoutscript> can be specified if you want a different script to run if the time runs out (normally it just uses <cancelscript>)

Subtasks can also have some extra tags if you want them to show icons on the map.
  1. <mappos> gives a position on the map for the image
  2. <mapsize> is the size of the image
  3. <mapfactionfilter> is part of another way of going about it, asking the game to mark all items of a certain faction by its numerical id
  4. <mapobjtypefilter> is the other part, further filtering it by type so that it can show all Roman bridges or all Tarentine cities. Valid types are: brigade, city, building, resourcefactory, farm, mine, cattlefarm, fort, bridge, traderoute
Last edited by Ijtzoi; Nov 28, 2017 @ 1:38pm
Fristi61 10 Nov 28, 2017 @ 10:12am 
So, I was wondering what's going wrong here/if what I'm attempting to do is possible:

I am creating an objective that basically asks the player to conquer any 15 out of a predetermined 21 cities.
I have actually managed to get it to work, however the one thing I am struggling with is making the objective icon on the map actually appear over all 21 cities.

I've basically tried to call 'subtaskposfollow' multiple times, once for each city, but it seems to just overwrite itself each time, so I only end up with the icon over ibossim (the last in the 'coastcities' list).

I don't think I can split this up into subtasks for each city with their own subtaskposfollows because from what I can tell you can either make the task require just one of the subtasks or all of them, not any other amount (like 15).

<subtask> <name>Subtask1</name> <status>QuestIberiaFactObj::CaptureCoast::Subtask</status> <script> <![CDATA[ trackself(); local coastcities = { "malaka", "kart", "sexi", "abdera", "barea", "mastia", "akraleuka", "hemeroskopeion", "ildum", "hibera", "kese", "subur", "laie", "ailuron", "emporion", "rhode", "arse", "pollentia", "palma", "mago", "ibossim" }; for i, city in pairs(coastcities) do subtaskposfollow( whoami(), getentitybytag(city)); end local count = 0 statusint("count", count); while (count < 15) do local capturedcity = sleepuntil("capture"); coroutine.yield(); if (capturedcity:getentitytype() == "citycommon") then count = 0; for i, city in pairs(coastcities) do if (getentitybytag(city):getfaction() == getplayerfaction()) then count = count + 1; end end end statusint("count", count); end ]]> </script> </subtask>

(steam bleeps out some of the city names for some accidental similarities to... other things)

Any ideas? Is it just not possible?
Last edited by Fristi61; Nov 28, 2017 @ 10:15am
Ijtzoi  [developer] 15 Nov 28, 2017 @ 1:20pm 
There currently isn't any way to follow more than one item with a single subtask; the only exception to this is attempting to follow all items of a certain type by a certain faction (e.g. all Veientine cities).

That said, it is possible to make tasks do something other than wait for only one or all of its subtasks. The task isn't considered done until the task's script, the one that has "waitsubtaskany" or "waitsubtasks" calls, successfully completes its last line. If a task script has
local tasksdone = 0; while tasksdone < 15 do waitsubtaskany(); tasksdone = tasksdone + 1; end
that should do it. Admittedly having 21 subtasks seems like it'd take up a lot of screen space, but it should at least function.
Fristi61 10 Nov 29, 2017 @ 5:16am 
Thanks! That'll actually be quite helpful for other 'do a specific amount of subtasks' objectives.

In this case it has the problem of 21 'capture' subtasks being on the screen at once and not communicating clearly that the player only need 15 of them. Also the idea is to have the player hold 15 of them at a time whereas this way it wouldn't check if any of the cities have been lost since first capturing it in the subtask.

But, after sleeping on it for a night and some more experimentation today, I found a pretty good solution!

I basically kept my original subtask, minus the 'subtaskposfollow' bit.

Then I added 21 subtasks like this, one for each city.

<subtask> <name>Subtask2</name> <status>IberiaStringEmpty</status> <script> <![CDATA[ subtaskposfollow( whoami(), getentitybytag("malaka") ); repeat local capturedcity = sleepuntil("capture"); coroutine.yield(); if (capturedcity:getentitytype() == "citycommon") then if (getentitybytag("malaka"):getfaction() == getplayerfaction()) then subtaskmapsize(whoami(), 2); else subtaskmapsize(whoami(), 15); end end until (0 > 1); ]]> </script> </subtask>

And finally used a simple 'waitsubtaskany' in the task script.

Subtasks 2-22 all can't be completed (until the fabric of logic itself changes and 0 becomes a higher number than 1), so it forces the player to complete subtask 1, which counts the amount of cities currently owned as before, and therefore takes into account when a city gets lost, and shows the player through its string how many out of 15 he is currently controlling.

Subtasks 2-22 also all point to an empty string, which means they are invisible and don't take
up any room on the screen when playing. (I was surprised this worked, they don't even take up an empty line or anything like that.)

So what subtasks 2-22 do is just display the icons on the map over the cities (and it checks to see if the player owns it, in which case it will minimize the icon), without further affecting the objective or the player's screen.


So, quite messy, but it seems to work perfectly in-game.

Thanks as always for the help and being so hands-on with it!
Last edited by Fristi61; Nov 29, 2017 @ 5:17am
< >
Showing 1-6 of 6 comments
Per page: 1530 50