City State Quests with XML and Lua

whoward69

DLL Minion
Joined
May 30, 2011
Messages
8,699
Location
Near Portsmouth, UK
(Requires v81 or later of "DLL - Various Mod Components", and probably of more interest to Total Conversion modders.)


Background

City State quests (clear a barbarian camp, bully another city state, discover the most techs, etc) are all hard-coded in the DLL.
Adding one is not possible (without modding the DLL), which for a Total Conversion means you're stuck with the standard quests, which may not be relevant for the milieu.
Custom quests, such as rescuing a maiden held captive by a dragon, finding all the "keys" to a magic box, or just sending a bard to entertain the king, are either not possible, or have to be faked with a lot of Lua using the standard quests as a framework.

V81 of "DLL - Various Mod Components" makes all of these (and many other) quests possible - via XML/SQL and Lua GameEvents.

Overview

A City State quest comprises
  1. Conditions that have to be met for the quest to be a candidate for handing out
  2. Biases that determine which of the City State types (mercantile, religious, etc) and personalities (friendly, hostile, etc) are most/least likely to hand out the quest
  3. Static information about the quest (duration, friendship gained, minimum number of participants, etc)
  4. Dynamic information about the active quest (location of the camp to be cleared, other City State to be bullied, etc)
  5. Conditions for the quest (once active) to be completed (won), cancelled (lost) or revoked (eg if the CS handing out the quest is bullied by the player)
  6. Messages to display when one of the above conditions are met
  7. An icon and tooltip to display for the quest (used only by the UI)

This information is now obtained by the quest logic in the DLL either from the Quests database table or via GameEvents.

Spoiler Quests table and GameEvents :

The definition of the new Quests table is
Code:
	<Table name="Quests">
		<Column name="ID" type="integer" primarykey="true" autoincrement="true"/>
		<Column name="Type" type="text" unique="true" notnull="true"/>

		<!-- Set to true if quest is backed by C++ code in the DLL -->
		<Column name="Internal" type="boolean" default="false"/>

		<!-- Set to false to disable this type of event -->
		<Column name="Enabled" type="boolean" default="true"/>
		<!-- Game option (if any) to disable this event for, eg GAMEOPTION_NO_POLICIES, GAMEOPTION_NO_RELIGION, GAMEOPTION_NO_SCIENCE -->
		<Column name="DisabledOnOption" type="text" default="NULL"/>

		<!-- Set to true for global (one quest applies to all players), false for personal (one quest per player) quests -->
		<Column name="Global" type="boolean" default="false"/>

		<!-- Set to false to override the default behaviour of revoking the quest in the player bullies the CS -->
		<Column name="RevokeOnBully" type="boolean" default="true"/>

		<!-- Set to true if this is a contest type quest (eg most culture, science, etc) - must also hook the QuestContestValue event -->
		<Column name="Contest" type="boolean" default="false"/>

		<!-- Mininimum number of majors the CS must have met before the quest can be considered, typically used for contest quests -->
		<Column name="MinPlayers" type="integer" default="1"/>

		<!-- Duration in turns this quest runs for; will be automatically adjusted for non-standard game speeds -->
		<Column name="Duration" type="integer" default="-1"/>

		<!-- Friendship/Influence gained on completion of the quest -->
		<Column name="Friendship" type="integer" default="0"/>

		<!-- Notification text pairs, set to NULL to force a QuestSendNotification event -->
		<Column name="StartSummary" type="text" default="NULL" reference="Language_en_US(Tag)"/>
		<Column name="StartMessage" type="text" default="NULL" reference="Language_en_US(Tag)"/>
		<Column name="FinishSummary" type="text" default="NULL" reference="Language_en_US(Tag)"/>
		<Column name="FinishMessage" type="text" default="NULL" reference="Language_en_US(Tag)"/>
		<Column name="CancelSummary" type="text" default="NULL" reference="Language_en_US(Tag)"/>
		<Column name="CancelMessage" type="text" default="NULL" reference="Language_en_US(Tag)"/>

		<!-- Personality biases, only one of these will ever apply -->
		<Column name="BiasFriendly" type="integer" default="100"/>
		<Column name="BiasHostile" type="integer" default="100"/>
		<Column name="BiasNeutral" type="integer" default="100"/>
		<Column name="BiasIrrational" type="integer" default="100"/>

		<!-- Trait biases, only one of these will ever apply -->
		<Column name="BiasMaritime" type="integer" default="100"/>
		<Column name="BiasMercantile" type="integer" default="100"/>
		<Column name="BiasCultured" type="integer" default="100"/>
		<Column name="BiasMilitaristic" type="integer" default="100"/>
		<Column name="BiasReligious" type="integer" default="100"/>

		<!-- UI related data, not processed by the DLL in any way -->
		<!-- The priority for displaying the quest,
		     there are "holes" in the standard quest sequence to permit custom quests to be inserted if needed -->
		<Column name="Priority" type="integer" default="100"/>
		<!-- The icon associated with the quest, or the function name to determine the icon -->
		<Column name="Icon" type="text" default="[ICON_TEAM_1]"/>
		<!-- The TXT_KEY associated with the quest (can use {1_TargetName:textkey} if there is a target for the quest (bully, find, etc)),
		     or the function name to determine the tooltip -->
		<Column name="Tooltip" type="text" default="TXT_KEY_CITY_STATE_QUEST_GENERIC_FORMAL"/>
	</Table>
and the new GameEvents are
Code:
    <!-- Events sent by City State quests (v81) -->
    <!--   ASSUMPTION: There is only one active quest of any given MinorCivQuestTypes per major, that is, (iPlayer, iCS, iQuest) is unique -->
    <!--   GameEvents.QuestIsAvailable.Add(function(iPlayer, iCS, iQuest, bNewQuest, iData1, iData2) return false end) -->
    <!--   GameEvents.QuestIsCompleted.Add(function(iPlayer, iCS, iQuest, bLastTurn) return false end) -->
    <!--   GameEvents.QuestIsRevoked.Add(function(iPlayer, iCS, iQuest) return false end) -->
    <!--   GameEvents.QuestIsExpired.Add(function(iPlayer, iCS, iQuest) return false end) -->
    <!--   GameEvents.QuestStart.Add(function(iPlayer, iCS, iQuest, bNewQuest, iStartTurn, iData1, iData2) end) -->
    <!--   GameEvents.QuestGetData.Add(function(iPlayer, iCS, iQuest, bData1) return 0 end) -->
    <!--   GameEvents.QuestSendNotification.Add(function(iPlayer, iCS, iQuest, iStartTurn, iEndTurn, iData1, iData2, bStarted, bFinished, sNames) end) -->
    <!--   GameEvents.QuestContestValue.Add(function(iPlayer, iCS, iQuest) return 0 end) -->
Additional quest related API methods are also available
Code:
	LUAAPIEXTN(DoMinorCivStartQuestForPlayer, void, iMajor, iQuest);
	LUAAPIEXTN(GetQuestTurnsDuration, int, iMajor, iQuest);
	LUAAPIEXTN(IsEverBulliedByMajor, bool, iPlayer);
	LUAAPIEXTN(IsRecentlyBulliedByMajor, bool, iPlayer);
	LUAAPIEXTN(AddQuestNotification, void, iCS, sMessage, sSummary, iPlotX, iPlotY, bNewQuest);
And also an API to permit control of the AIs economic and military strategy
Code:
	LUAAPIEXTN(GetActiveEconomicStrategies, table);
	LUAAPIEXTN(IsActiveEconomicStrategy, bool, iStrategy);
	LUAAPIEXTN(ActivateEconomicStrategy, void, iStrategy);
	LUAAPIEXTN(DeactivateEconomicStrategy, void, iStrategy);

	LUAAPIEXTN(GetActiveMilitaryStrategies, table);
	LUAAPIEXTN(IsActiveMilitaryStrategy, bool, iStrategy);
	LUAAPIEXTN(ActivateMilitaryStrategy, void, iStrategy);
	LUAAPIEXTN(DeactivateMilitaryStrategy, void, iStrategy);
We'll cover (most of) these in detail in the following three example quests.


Types of Quests

There are three categories of City State quests - Global (eg build a World Wonder), Global Race (eg gain the most techs) and Personal (eg bully another City State or discover a Natural Wonder).
Not all database columns and GameEvents are relevant to each type of quest, but every custom quest will have an entry in the Quests table and implement one or more quest GameEvent handlers.

Example 1: Global Quest - Circumnavigate the Globe

The aim of this quest is to be the first to circumnavigate the globe.

The Quests database table entry is
Code:
	<Quests>
		<Row>
			<Type>MINOR_CIV_QUEST_CIRCUMNAVIGATE</Type>
			<Global>true</Global>
			<MinPlayers>2</MinPlayers>
			<RevokeOnBully>false</RevokeOnBully>
			<Friendship>50</Friendship>

			<BiasMaritime>300</BiasMaritime>
			<BiasMercantile>200</BiasMercantile>

			<!-- While we could easily use simple TXT_KEYs here, we'll use the Lua GameEvent (as this is an example!)
			<StartSummary>TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_START_S</StartSummary>
			<StartMessage>TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_START</StartMessage -->

			<FinishSummary>TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_FINISH_S</FinishSummary>
			<FinishMessage>TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_FINISH</FinishMessage>
			<CancelSummary>TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_CANCEL_S</CancelSummary>
			<CancelMessage>TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_CANCEL</CancelMessage>

			<Priority>150</Priority>
			<!-- To prove a point, we'll get the icon and tooltip via Lua functions - see CityStateQuests_Helpers_Circumnavigate.lua -->
			<!--Icon>[ICON_TRADE]</Icon-->
			<Icon>GetCircumnavigateQuestIcon</Icon>
			<!--Tooltip>TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_FORMAL</Tooltip-->
			<Tooltip>GetCircumnavigateQuestTooltip</Tooltip>
		</Row>
	</Quests>

The unique Type for this quest is MINOR_CIV_QUEST_CIRCUMNAVIGATE, a unique id will be automatically assigned by the database, when you need it, use GameInfoTypes.MINOR_CIV_QUEST_CIRCUMNAVIGATE as you usually would.

This quest is only awarded once by a City State, and if it's already active, other players can join in. This makes it a global quest, so we set Global to true. Also, we need more than one player who could complete the quest, so we set MinPlayers to 2 to indicate that we want at least two players (human or AI) to participate.

By default, if a player bullies a City State after a quest has be handed out, that quest will be revoked. As we don't want that behaviour, we override it by setting RevokeOnBully to false.

The player that completes the quest typically gains a friendship boost with the City State, for this quest that boost will be 50 and this is set by the Friendship value.

As this is an exploration/trading type quest, we'll increase the liklihood that the quest will be handed out by a maritime or mercantile City State. The default biases are 100, so 300 means 3 times more likely, while 50 is half as likely. Biases cannot be negative. A bias of 0 means that the quest will never be handed out by a City State with the associated type or personality.

When the City State hands out (starts) the quest, a notification is generated. A notification comprises two text strings - the message and the summary. If these messages are simple strings, we can place their TXT_KEY entries directly into the StartSummary and StartMessage values for the quest. However, if the strings are not simple, omitting the entries will cause a GameEvent to be generated, in which we can format the strings as desired and generate the notification from Lua.
The start message and summary for this quest are simple, but as this is an example, we'll omit them to show how to generate the notification via Lua.

When the quest is complete, the player has either won (they finished the quest) or lost (someone else finished it, so the quest for them has been cancelled). Again, a notification is sent, and we either put the TXT_KEYs directly into the quest entry in the database table or omit them to use the GameEvent. As we're already showing how to generate the start notification from Lua, we'll use TXT_KEYs for these four entries.

That's it as far as the quest logic in the DLL is concerned, but the UI needs three additional pieces of information. For the UI, the quest needs an icon, a tooltip and a relative place in a list of quests to be displayed.
The Priority entry is where in a list of quests this quest appears - see the entries for the standard City State quests in the Quests table to work out about where you want your custom quest to appear in the list - 150 is after the "Build a Road" quest.
The Icon entry is either an [ICON_XYZ] string or the name of a Lua function to determine the icon. If the entry starts with a square bracket ([), this is a specific icon, otherwise it's the name of a function.
Similarly, the Tooltip entry is either a TXT_KEY or the name of a Lua function to determine the tooltip. If the entry starts with the eight characters TXT_KEY_, this is a simple text string, otherwise it's the name of a function.
For this quest we could have used a simple icon and tooltip entry, but as this is an example, we'll use Lua functions to generate the icon and tooltip.

As we've just mentioned them, we'll look at the functions to get the icon and tooltip for the UI.

To use these you'll need to include the modded version of the CityStateStatusHelper.lua file into your mod. If you don't override this core file, nothing bad will happen, you just won't see any custom quests in the City State popups (or other mods that use the helpers, eg the City States screen of my "UI - Trade Opportunities" mod).

The modded CityStateStatusHelper.lua file includes all files that match the pattern "CityStateQuests_Helpers_{something}.lua", so we need to create a "CityStateQuests_Helpers_Circumnavigate.lua" file and set it to be VFS=true (do NOT add it as an InGameUIAddin).

Each function receives five parameters that we could use to return a different icon (see the standard GetReligionQuestIcon function in CityStateStatusHelper.lua file) or format the tooltip (see the GetTourismContestQuestTooltip function below)

Code:
--
-- This file MUST follow the naming pattern of "CityStateQuests_Helpers_{something}.lua"
--

function GetCircumnavigateQuestIcon(iMajor, iMinor, iQuest, iData1, iData2)
  return "[ICON_TRADE]"
end

function GetCircumnavigateQuestTooltip(iMajor, iMinor, iQuest, iData1, iData2)
  return Locale.Lookup("TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_FORMAL")
end

Our TXT_KEY is very simple

Code:
	<Language_en_US>
		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_FORMAL">
			<Text>They want the map circumnavigated.</Text>
		</Row>
	</Language_en_US>

As we're looking at TXT_KEYs the ones for the won (finished) and lost (cancelled) notifications are

Code:
	<Language_en_US>
		<!-- We can ONLY use the {1_MinorName:textkey} replacement, and {2_InfluenceReward} for _FINISH, no others are available -->
		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_FINISH">
			<Text>You have successfully circumnavigated the map, much to the delight of {1_MinorName:textkey}! Your [ICON_INFLUENCE] Influence over them has increased by [COLOR_POSITIVE_TEXT]{2_InfluenceReward}[ENDCOLOR].</Text>
		</Row>
		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_FINISH_S">
			<Text>Map circumnavigated for {1_MinorName:textkey}!</Text>
		</Row>
		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_CANCEL">
			<Text>The map has been circumnavigated by someone else!</Text>
		</Row>
		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_CANCEL_S">
			<Text>Someone Else Circumnavigated For {1_MinorName:textkey}</Text>
		</Row>
	</Language_en_US>

Note that we get the City State name passed as a parameter/substition ({1_MinorName:textkey}) for these TXT_KEYs, while the finished pair also receive the amount of friendship/influence gained ({2_InfluenceReward}).

And now for the GameEvent handlers.

Every custom quest must handle the QuestIsAvailable GameEvent - this event is used to ascertain if the quest can be started by a specific player for a specific City State. We are going to keep this example simple, so our only condition for starting the quest is that the globe must not have already been circumnavigated and that the player must be in the Medieval Era

Code:
local iThisQuest = GameInfoTypes.MINOR_CIV_QUEST_CIRCUMNAVIGATE
local iEraMinimum = GameInfoTypes.ERA_MEDIEVAL

function OnQuestIsAvailable(iPlayer, iCS, iQuest, bNewQuest, iData1, iData2)
	if (iQuest == iThisQuest) then
		-- If the map has already been circumnavigated, this quest is no longer available
		if (iPlayerCircumnavigating == -1) then
			-- Only let the player join in if they are in (or past) the appropriate era
			-- We could do something more complex here like checking for the ability to cross oceans (but that assumes a map with at least some water)
			-- or we could check the width of the map being greater than a certain value and WrapX being true
			return Players[iPlayer]:GetCurrentEra() >= iEraMinimum
		end
	end
	
	return false
end
GameEvents.QuestIsAvailable.Add(OnQuestIsAvailable)

We'll worry about how iPlayerCircumnavigating gets set later, but it records which player, if anyone, was the first to circumnavigate the globe.

As we didn't specify values in the Quests entry for StartSummary and StartMessage, we need to handle the QuestSendNotification GameEvent to generate the notification to the player that the City State has issued the challenge

Code:
function OnQuestSendNotification(iPlayer, iCS, iQuest, iStartTurn, iEndTurn, iData1, iData2, bStart, bFinish, sNames)
	if (iQuest == iThisQuest) then
		if (bStart) then
			-- Notify the player the quest has started
			-- If both StartMessage and StartSummary are present in the quest definition, this will never occur
			
			-- While we could handle this with simple TXT_KEYs, we'll manually send the notification here as an example
			local sMinor = Players[iCS]:GetName()
			local sMessage, sSummary
			if (Map.IsWrapX()) then
				-- Map wraps, so we'll ask for circumnavigation
				sMessage = "TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_START"
				sSummary = "TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_START_S"
			else
				-- Map is a single continent, so we'll ask the player to cross it
				-- To implement this, needs MUCH more work
				sMessage = "TXT_KEY_MINOR_CIV_QUEST_TRANSCONTINENT_START"
				sSummary = "TXT_KEY_MINOR_CIV_QUEST_TRANSCONTINENT_START_S"
			end

			Players[iPlayer]:AddQuestNotification(iCS, Locale.ConvertTextKey(sMessage, sMinor), Locale.ConvertTextKey(sSummary, sMinor), -1, -1, true)
		end
	end
end
GameEvents.QuestSendNotification.Add(OnQuestSendNotification)

Note that the same GameEvent is used for all three notifications (if the TXT_KEYs aren't in the Quests table), you differentiate the notification to send with the bStart and bFinish parameters (these will NEVER both be true).

Also note the new API method on the Player object pPlayer:AddQuestNotification(iCS, sMsg, sSum, iX, iY, bNewQuest) for sending the notification. While you can use the standard pPlayer:AddNotification() method, it is NOT recommended as it will not produce a notification that behaves the same as the standard City State quests.

This custom quest must also handle the QuestIsCompleted and QuestIsExpired GameEvents. The former returns true if the player has won, the latter returns true if the player has lost, return false means that the quest is ongoing. Our handlers just need to ascertain if the player who actually circumnavigated the globe is the current player.

Code:
function OnQuestIsCompleted(iPlayer, iCS, iQuest, bLastTurn)
	if (iQuest == iThisQuest) then
		-- Have we completed the quest by being the first player to circumnavigate the map?
		return (iPlayer == iPlayerCircumnavigating)
	end
	
	return false
end
GameEvents.QuestIsCompleted.Add(OnQuestIsCompleted)

function OnQuestIsExpired(iPlayer, iCS, iQuest)
	if (iQuest == iThisQuest) then
		-- Did someone beat us to it?
		return (iPlayerCircumnavigating ~= -1 and iPlayerCircumnavigating ~= iPlayer)
	end
	
	return false
end
GameEvents.QuestIsExpired.Add(OnQuestIsExpired)

So the City State issues the challenge and waits for someone to circumnavigate the globe. Presumably the player will build a ship and start exploring ... but what about the AI?
The DLL sends a QuestStart GameEvent at the commencement of the quest, so we can hook this and add some logic to give the AI a nudge in the right direction. We could check its units for some ships capable of exploration and if not locate a coastal city or three and add an exploration type ship to the build queue and hope they get built and then explore. Alternatively, we can just make sure the AI strategy to recon the sea is active (via a new API method).

Code:
function OnQuestStart(iPlayer, iCS, iQuest, bNewQuest, iStartTurn, iData1, iData2)
	if (iQuest == iThisQuest) then
		if (not Players[iPlayer]:IsHuman()) then
			-- Direct the AI to attempt to circumnavigate by activating the strategy to recon at sea
			Players[iPlayer]:ActivateEconomicStrategy(GameInfoTypes.ECONOMICAISTRATEGY_NEED_RECON_SEA)
		end
	end
end
GameEvents.QuestStart.Add(OnQuestStart)

Finally, we need to look at how the iPlayerCircumnavigating variable gets set. We use the CircumnavigatedGlobe GameEvent along with some simple data persistence

Code:
--
-- House-keeping of who circumnavigated first
--

local modDB = Modding.OpenSaveData()
local iPlayerCircumnavigating = modDB.GetValue("MINOR_CIV_QUEST_CIRCUMNAVIGATE_PLAYER") or -1

function OnCircumnavigatedGlobe(iTeam)
	-- We get the team that circumnavigated the globe, so we'll give the win to the team leader
	-- As an example this is acceptable, a production mod may want to give the win to all team members,
	-- or track the current player and just give it to them
	iPlayerCircumnavigating = Players[Teams[iTeam]:GetLeaderID()]:GetID()
	
	modDB.SetValue("MINOR_CIV_QUEST_CIRCUMNAVIGATE_PLAYER", iPlayerCircumnavigating)
	
	GameEvents.CircumnavigatedGlobe.Remove(OnCircumnavigatedGlobe)
end
if (iPlayerCircumnavigating == -1) then
	GameEvents.CircumnavigatedGlobe.Add(OnCircumnavigatedGlobe)
end

The complete code is in the "Quests - Circumnavigate" mod
 
Example 2: Global Race - Tourism

The aim of this quest is to accumulate the most (base) Tourism over a fix number of turns - very much like the Culture, Faith and Tech races.

The Quests database table entry is
Code:
	<Quests>
		<Row>
			<Type>MINOR_CIV_QUEST_TOURISM_CONTEST</Type>
			<!-- If we can't generate culture, we can't defend against tourism, so consequently we don't generate any -->
			<DisabledOnOption>GAMEOPTION_NO_POLICIES</DisabledOnOption>
			
			<Global>true</Global>
			<Contest>true</Contest>
			<Duration>30</Duration>
			<MinPlayers>3</MinPlayers>
			<RevokeOnBully>false</RevokeOnBully>
			<Friendship>40</Friendship>

			<FinishSummary>TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_FINISH_S</FinishSummary>
			<FinishMessage>TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_FINISH</FinishMessage>
			<CancelSummary>TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_CANCEL_S</CancelSummary>
			<CancelMessage>TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_CANCEL</CancelMessage>

			<BiasFriendly>200</BiasFriendly>
			<BiasHostile>50</BiasHostile>
			<BiasCultured>300</BiasCultured>
			<BiasMercantile>200</BiasMercantile>
			<BiasMaritime>150</BiasMaritime>
			<BiasMilitaristic>75</BiasMilitaristic>

			<Priority>4</Priority>
			<Icon>[ICON_TOURISM]</Icon>
			<Tooltip>GetTourismContestQuestTooltip</Tooltip>
		</Row>
	</Quests>

The DisabledOnOption entry refers to a Game Option, which if set to true, disables the quest. In this case, if the player has disabled culture (policies), it also disables tourism, so no one will be generating any, so the race will be a dead heat with everyone in it finishing on zero!

This is a global race type quest, so we need to set both Global and Contest to true and give the Duration - 30 turns. Note that the duration should be set for Standard game speed, the DLL will automatically adjust it for the actual game speed.
MinPlayers, RevokeOnBully and Friendship are the same as for the Circumnavigate quest.

Notifications for win (finish) and lose (cancel) can be handled by simple TXT_KEYs, but we need to include duration into the start notification, so we'll handle that via the QuestSendNotification GameEvent.

Biases favour friendly cultured City States and avoid hostile militaristic ones.

The UI values place the icon just after the other three global races, but as the tooltip needs to include if we are winning or losing, we need a function to generate it.

The associated TXT_KEYs are
Code:
	<Language_en_US>
		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_FINISH">
			<Text>You have impressed {1_MinorCivName:textkey} with your tourism!  They turn a blind eye to the tourism of other civilizations, and your [ICON_INFLUENCE] Influence over them has increased by [COLOR_POSITIVE_TEXT]{2_InfluenceReward}[ENDCOLOR].  Civilizations that succeeded (ties are allowed):[NEWLINE]</Text>
		</Row>
		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_FINISH_S">
			<Text>{1_MinorCivName:textkey} is in awe of you!</Text>
		</Row>
		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_CANCEL">
			<Text>Another civilization has impressed {1_MinorCivName:textkey} with its tourism.  Your tourism growth was not enough, and your [ICON_INFLUENCE] Influence remains the same as before.  Civilizations that succeeded (ties are allowed):[NEWLINE]</Text>
		</Row>
		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_CANCEL_S">
			<Text>{1_MinorCivName:textkey} looks elsewhere</Text>
		</Row>
	</Language_en_US>

Note that because this is a contest type quest, the DLL will automatically add the list of winners to the end of the notification messages.

Our tooltip function is

Code:
function GetTourismContestQuestTooltip(iMajor, iMinor, iQuest, iData1, iData2)
  local pMinor = Players[iMinor]

  -- We need this value for both winning and losing tooltips
  local iMajorScore = pMinor:GetMinorCivContestValueForPlayer(iMajor, iQuest)

  -- Is the player (one of) the contest leader(s)
  if (pMinor:IsMinorCivContestLeader(iMajor, iQuest)) then
    -- Yes, so tell them they are winning
    return Locale.Lookup("TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_WINNING_FORMAL", iMajorScore)
  else
    -- No, so get the leaders score ...
    local iLeaderScore = pMinor:GetMinorCivContestValueForLeader(iQuest)
	
	-- ... and tell them how much they are losing by
    return Locale.Lookup("TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_LOSING_FORMAL", iLeaderScore, iMajorScore)
  end
end

which is pretty much a copy of the standard tooltip for a culture race, and the associated TXT_KEYs are

Code:
	<Language_en_US>
		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_FORMAL">
			<Text>They will reward the player with the largest Tourism growth.</Text>
		</Row>
		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_WINNING_FORMAL">
			<Text>{TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_FORMAL} So far, you have the lead with [ICON_TOURISM][COLOR_POSITIVE_TEXT]{1_PlayerScore}[ENDCOLOR].</Text>
		</Row>
		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_LOSING_FORMAL">
			<Text>{TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_FORMAL} So far, the leader has [ICON_TOURISM]{1_LeaderScore} and you have [ICON_TOURISM][COLOR_POSITIVE_TEXT]{2_PlayerScore}[ENDCOLOR].  </Text>
		</Row>
	</Language_en_US>

In the same way that we had to track which player, if any, had circumnavigated the globe, we also need to track how much tourism a player has generated during the race. But not just on a per player basis. Multiple City States can start a tourism race on different turns with different players particiapting in the races, and these races can (and will) overlap. A single City State will never start another tourism race until the first has finished. So we need to record tourism generated per player per City State. The code to do this is as follows,

Code:
--
-- House-keeping of how much tourism has been generated by each major for each quest
--

local modDB = Modding.OpenSaveData()
local cachedTourism = {}

function GetKey(iPlayer, iCS)
	return string.format("MINOR_CIV_QUEST_TOURISM_%i_%i", iPlayer, iCS)
end

function GetTotalTourism(iPlayer, iCS)
	if (cachedTourism[iPlayer] == nil) then
		cachedTourism[iPlayer] = {}
	end
	
	if (cachedTourism[iPlayer][iCS] == nil) then
		cachedTourism[iPlayer][iCS] = (modDB.GetValue(GetKey(iPlayer, iCS)) or 0)
	end
	
	return cachedTourism[iPlayer][iCS]
end

function SetTotalTourism(iPlayer, iCS, iValue)
	modDB.SetValue(GetKey(iPlayer, iCS), iValue)

	if (cachedTourism[iPlayer] == nil) then
		cachedTourism[iPlayer] = {}
	end

	cachedTourism[iPlayer][iCS] = iValue
end

function ChangeTotalTourism(iPlayer, iCS, iValue)
	SetTotalTourism(iPlayer, iCS, GetTotalTourism(iPlayer, iCS) + iValue)
end

function OnPlayerDoTurn(iPlayer)
	if (iPlayer > 0 and iPlayer <= GameDefines.MAX_MAJOR_CIVS) then
		local pPlayer = Players[iPlayer-1]
		if (pPlayer:IsAlive() and pPlayer:GetCurrentEra() >= iEraMinimum) then
			local iTourism = pPlayer:GetTourism()
			
			if (iTourism > 0) then
				for iCS = GameDefines.MAX_MAJOR_CIVS, GameDefines.MAX_CIV_PLAYERS-1, 1 do
					if (Players[iCS]:IsAlive()) then
						ChangeTotalTourism(iPlayer-1, iCS, iTourism)
					end
				end
			end
		end
	end
end
GameEvents.PlayerDoTurn.Add(OnPlayerDoTurn)

We can now use SetTotalTourism() and GetTotalTourism() to track the races.

Every custom quest must handle the QuestIsAvailable event, we'll keep our conditions simple and just require the player to have started the Industrial Era.

Code:
local iThisQuest = GameInfoTypes.MINOR_CIV_QUEST_TOURISM_CONTEST
local iEraMinimum = GameInfoTypes.ERA_INDUSTRIAL

function OnQuestIsAvailable(iPlayer, iCS, iQuest, bNewQuest, iData1, iData2)
	if (iQuest == iThisQuest) then
		-- Only let the player join in if they are in (or past) the appropriate era
		return Players[iPlayer]:GetCurrentEra() >= iEraMinimum
	end
	
	return false
end
GameEvents.QuestIsAvailable.Add(OnQuestIsAvailable)

When the quest starts, we need to reset the tourism generated by this player for the City State handing out the quest ...

Code:
function OnQuestStart(iPlayer, iCS, iQuest, bNewQuest, iStartTurn, iData1, iData2)
	if (iQuest == iThisQuest) then
		SetTotalTourism(iPlayer, iCS, 0)
	end
end
GameEvents.QuestStart.Add(OnQuestStart)

... and fire the starting gun!

Code:
function OnQuestSendNotification(iPlayer, iCS, iQuest, iStartTurn, iEndTurn, iData1, iData2, bStart, bFinish, sNames)
	if (iQuest == iThisQuest) then
		if (bStart) then
			local pMinor = Players[iCS]

			local iTurnsRemaining = iEndTurn - Game.GetGameTurn()
			local iTurnsDuration = iEndTurn - iStartTurn  -- Don't be tempted to do a database lookup here, as that doesn't allow for game speed scaling
			local sMinor = pMinor:GetName()
			
			local sMessage = Locale.ConvertTextKey("TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_START", iTurnsDuration, iTurnsRemaining, sMinor)
			local sSummary = Locale.ConvertTextKey("TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_START_S", sMinor)

			Players[iPlayer]:AddQuestNotification(iCS, sMessage, sSummary, -1, -1, true)
		end
	end
end
GameEvents.QuestSendNotification.Add(OnQuestSendNotification)

And the associated TXT_KEYs

Code:
	<Language_en_US>
		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_START">
			<Text>The desire to travel is flourishing in {3_MinorCivName:textkey}, and they seek tourism guidance.  Whoever can produce the most [ICON_TOURISM] Tourism in a period of {2_TurnsDuration} turns will gain [ICON_INFLUENCE] Influence with them.  [COLOR_POSITIVE_TEXT]{1_TurnsRemaining} turns remaining.[ENDCOLOR]</Text>
		</Row>
		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_START_S">
			<Text>{1_MinorCivName:textkey} longs for tourism!</Text>
		</Row>
	</Language_en_US>

At various times, the DLL will need to know the player's "score" for a race, and sends the QuestContestValue GameEvent to find out. So we need to handle this event and just return the accumulated tourism value.

Code:
function OnQuestContestValue(iPlayer, iCS, iQuest)
	if (iQuest == iThisQuest) then
		return GetTotalTourism(iPlayer, iCS)
	end
	
	return 0
end
GameEvents.QuestContestValue.Add(OnQuestContestValue)

Finally, we need to ascertain if the player won (there can be more than one winner), but we only check if this is the last turn of the race

Code:
function OnQuestIsCompleted(iPlayer, iCS, iQuest, bLastTurn)
	if (iQuest == iThisQuest) then
		-- Is it time to compare the scores?
		if (bLastTurn) then
			return Players[iCS]:IsMinorCivContestLeader(iPlayer, iQuest)
		end
	end
	
	return false
end
GameEvents.QuestIsCompleted.Add(OnQuestIsCompleted)

We could have checked the player's score (total accrued tourism) against all the other players' scores (allowing for equal first places) directly, but why bother when there's an API method that does it for us!

Note that we don't handle the QuestIsExpired event, as any player in the race who didn't win automatically loses at the end of the race

The complete code is in the "Quests - Tourism Contest" mod
 
Example 3: Personal Quest - National Wonder

The aim of this quest is to build a National Wonder

The Quests database table entry is
Code:
	<Quests>
		<Row>
			<Type>MINOR_CIV_QUEST_NATIONAL_WONDER</Type>
			<Friendship>30</Friendship>
			<BiasFriendly>200</BiasFriendly>
			<BiasNeutral>150</BiasNeutral>
			<BiasHostile>50</BiasHostile>
			<BiasCultured>150</BiasCultured>
			<Priority>56</Priority>
			<Icon>[ICON_GOLDEN_AGE]</Icon>
			<Tooltip>GetNationalWonderQuestTooltip</Tooltip>
		</Row>
	</Quests>

which is about as simple as it gets!

As the quest is open ended, the player cannot lose (so we don't need a cancel notification) and as the name of the national wonder has to be formmated into the start and finish notifications we must send those via the QuestSendNotification GameEvent.

The UI tooltip code is

Code:
function GetNationalWonderQuestTooltip(iMajor, iMinor, iQuest, iData1, iData2)
  return Locale.Lookup("TXT_KEY_MINOR_CIV_QUEST_NATIONAL_WONDER_FORMAL", GameInfo.Buildings[Players[iMajor]:GetSpecificBuildingType(GameInfo.BuildingClasses[iData1].Type)].Description)
end

Code:
	<Language_en_US>
		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_NATIONAL_WONDER_FORMAL">
			<Text>They want you to build {1_WonderName:textkey}.</Text>
		</Row>
	</Language_en_US>

which just converts the national wonder class into a civ specific building and formats the building's description into the notification message.

We'll look at how the required National Wonder building class id gets into the iData1 value in a bit.

But first, some housekeeping. We need to track what National Wonders a player has left that could be the target of the City State quest

Code:
--
-- House-keeping of who has built what national wonders
--

-- Find all the national wonder building classes
local nationalWonderClasses = {}
for row in DB.Query("SELECT ID, Type FROM BuildingClasses WHERE MaxPlayerInstances=1") do
	nationalWonderClasses[row.Type] = row.ID
end

-- Find all the national wonders already constructed by players
local playerNationalWonders = {}
for iPlayer = 0, GameDefines.MAX_CIV_PLAYERS-1, 1 do
	playerNationalWonders[iPlayer] = {}
	
	local pPlayer = Players[iPlayer]
	if (pPlayer:IsAlive()) then
		for nwClass, nwID in pairs(nationalWonderClasses) do
			playerNationalWonders[iPlayer][nwClass] = (pPlayer:GetBuildingClassCount(nwID) > 0)
		end
	end
end

-- Watch for new national wonders being built
function OnCityConstructed(iPlayer, iCity, iBuilding)
	local class = GameInfo.Buildings[iBuilding].BuildingClass
	
	if (nationalWonderClasses[class]) then
		playerNationalWonders[iPlayer][class] = true
	end
end
GameEvents.CityConstructed.Add(OnCityConstructed)

We don't need to persist any data, as we can reconstruct the lists every time the game is reloaded (we'll gloss over National Wonders destroyed in razed cities).

The code to ascertain if the quest is available to the player is

Code:
local iThisQuest = GameInfoTypes.MINOR_CIV_QUEST_NATIONAL_WONDER
local iEraMinimum = GameInfoTypes.ERA_MEDIEVAL

function OnQuestIsAvailable(iPlayer, iCS, iQuest, bNewQuest, iData1, iData2)
	if (iQuest == iThisQuest) then
		-- Only let the player join in if they haven't bullied the City State recently
		if (not Players[iCS]:IsRecentlyBulliedByMajor(iPlayer)) then
			-- Only let the player join in if they are in (or past) the appropriate era
			if (Players[iPlayer]:GetCurrentEra() >= iEraMinimum) then
				for _, bConstructed in pairs(playerNationalWonders[iPlayer]) do
					-- We don't care what the National Wonder is, just that there is something left.  In a real mod this would be much more selective.
					if (not bConstructed) then
						return true
					end
				end
			end
		end
	end
	
	return false
end
GameEvents.QuestIsAvailable.Add(OnQuestIsAvailable)

Note that this code doesn't pick which National Wonder to construct, it just checks that any National Wonder could be constructed.

So how do we pick a National Wonder to construct?

When a quest starts, we can store two integers as data to the quest - typically this is an (X, Y) pair (eg the location of the camp to be cleared), or the id of a target player/City State. (If you need to store more data, persist it into the mod database and store the unique key into the quest data.)
After sending the QuestStart GameEvent, the DLL immediately sends TWO QuestGetData events. We will use this event to decide what National Wonder to request and store the decision into the iData1 value.

Code:
function OnQuestGetData(iPlayer, iCS, iQuest, bData1)
	if (iQuest == iThisQuest and bData1) then
		local nwAvailable = {}

		-- Find all the unbuilt national wonders
		for nwClass, bConstructed in pairs(playerNationalWonders[iPlayer]) do
			if (not bConstructed) then
				table.insert(nwAvailable, nwClass)
			end
		end

		-- Pick one at random
		return nationalWonderClasses[nwAvailable[Game.Rand(#nwAvailable, "National Wonder for quest")+1]]
	end
	
	return 0
end
GameEvents.QuestGetData.Add(OnQuestGetData)

Note the limitation in using an accumulator event, if the value zero is meaningful you will have to take other steps to record it - fortunately no National Wonder has a building class id of 0

Now we have the selected National Wonder stashed away in iData1, we can generate the start and finish notifications using that information

Code:
function OnQuestSendNotification(iPlayer, iCS, iQuest, iStartTurn, iEndTurn, iData1, iData2, bStart, bFinish, sNames)
	if (iQuest == iThisQuest) then
		local sMinor = Players[iCS]:GetName()
		local sBuilding = GameInfo.Buildings[Players[iPlayer]:GetSpecificBuildingType(GameInfo.BuildingClasses[iData1].Type)].Description

		if (bStart) then
			-- Notify the player the quest has started
			local sMessage = Locale.ConvertTextKey("TXT_KEY_MINOR_CIV_QUEST_NATIONAL_WONDER_START", sMinor, sBuilding)
			local sSummary = Locale.ConvertTextKey("TXT_KEY_MINOR_CIV_QUEST_NATIONAL_WONDER_START_S", sMinor, sBuilding)

			Players[iPlayer]:AddQuestNotification(iCS, sMessage, sSummary, -1, -1, true)
		elseif (bFinish) then
			-- Influence boost has already been handled via the Friendship entry in the quest definition
			
			-- Notify the player the quest has finished
			local sMessage = Locale.ConvertTextKey("TXT_KEY_MINOR_CIV_QUEST_NATIONAL_WONDER_FINISH", sMinor, sBuilding, GameInfo.Quests[iQuest].Friendship)
			local sSummary = Locale.ConvertTextKey("TXT_KEY_MINOR_CIV_QUEST_NATIONAL_WONDER_FINISH_S", sMinor, sBuilding)

			Players[iPlayer]:AddQuestNotification(iCS, sMessage, sSummary, -1, -1, false)
		end
	end
end
GameEvents.QuestSendNotification.Add(OnQuestSendNotification)

And the associated TXT_KEYs

Code:
	<Language_en_US>
		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_NATIONAL_WONDER_START">
			<Text>{1_MinorName:textkey} wants you to build {2_WonderName:textkey}.</Text>
		</Row>
		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_NATIONAL_WONDER_START_S">
			<Text>{1_MinorName:textkey} desires {2_WonderName:textkey}</Text>
		</Row>
		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_NATIONAL_WONDER_FINISH">
			<Text>As {1_MinorName:textkey} requested, you have successfully constructed {2_WonderName:textkey}! Your [ICON_INFLUENCE] Influence over them has increased by [COLOR_POSITIVE_TEXT]{3_InfluenceReward}[ENDCOLOR].</Text>
		</Row>
		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_NATIONAL_WONDER_FINISH_S">
			<Text>{2_WonderName:textkey} Constructed for {1_MinorName:textkey}!</Text>
		</Row>
	</Language_en_US>

Finally we need to indicate when the quest is complete - simply when the player constructs the National Wonder.

Code:
function OnQuestIsCompleted(iPlayer, iCS, iQuest, bLastTurn)
	if (iQuest == iThisQuest) then
		-- Have we built the national wonder yet?
		return (Players[iPlayer]:GetBuildingClassCount(Players[iCS]:GetQuestData1(iPlayer, iQuest)) > 0)
	end
	
	return false
end
GameEvents.QuestIsCompleted.Add(OnQuestIsCompleted)

It would appear that there is no incentive for the player to rush build the National Wonder, except that it is taking up a City State quest slot and there are a limited number of those per player.

The complete code is in the "Quests - National Wonder" mod
 
Writing Efficient Quest Event Handlers

Quest events fire frequently, it is therefore of utmost importance that their handlers are coded as efficiently as possible. Assuming we have M majors, N city states and Q custom quests, each event handler can potentially trigger N*M*Q times per turn, where N*M*(Q-1) of those will NOT be for the specific event, so it is imperative to "early out" if the event is not for the specific quest.

EVERY quest event handler should therefore be wrapped with a test for the specific event
Code:
function OnQuestEvent(...)
	if (iQuest == iThisQuest) then
	end
	
	return {default}
end

Furthermore, iThisEvent should be a file scoped variable
Code:
local iThisEvent = GameInfoTypes.MINOR_CIV_QUEST_MY_QUEST
and not use "GameInfoTypes.MINOR_CIV_QUEST_MY_QUEST" directly in the condition (as that requires a table lookup everytime)

Avoid "mustering the troops" style programming.
Code:
local a = ...1
local b = ...2
local c = ...3

if (a and b and c) then
end

If a is false, b and c are never needed; if b is false, c is never needed. But we will still evaluate and assign both b and c, instead use
Code:
local a = ...1
if (a) then
	local b = ...2
	if (b) then
		local c = ...3
		if (c) then
		end
	end
end

Avoid unnecssary variable usage. While it may be tempting to write
Code:
local a = (iQuest == iThisQuest)
if (a) then
end
in a non-compiled langauge such as Lua, this forces the interpreter to create a variable that's only ever used once, and this takes (albeit a very small amount of) time - which over N*M*Q cycles WILL add up.
If the right hand side of the assignment to a is trivial, ALWAYS use the expression directly, NEVER create a single use variable
Code:
if (iQuest == iThisQuest) then
end

Assuming c is a function of b, it would be better to write
Code:
if (iQuest == iThisQuest) then
	local b = ...2
	if (b) then
		if ({uses b}...3) then
			return true
		end
	end
end

return false

Typically b and c will be functions of Players[iPlayer], so it is tempting to write
Code:
if (iQuest == iThisQuest) then
	local pPlayer = Players[iPlayer]
		
	if ({uses pPlayer}...2) then
		if ({uses pPlayer}...3) then
			return true
		end
	end
end

return false

However, if b is a single method call on pPlayer that fails most of the time (eg checking to see if the player is an ally of the City State), creating the pPlayer variable should be delayed
Code:
if (iQuest == iThisQuest) then
	if (Players[iPlayer]:SomeMethod(...)) then
		local pPlayer = Players[iPlayer]
		
		if ({uses pPlayer}...3) then
			return true
		end
	end
end

return false

Take advantage of lazy evaluation. In the condition "a and b", b will never be evaluated if a is false (as "false and anything" is always false), which means we can compress the last code to
Code:
if (iQuest == iThisQuest and Players[iPlayer]:SomeMethod(...)) then
	local pPlayer = Players[iPlayer]
		
	if ({uses pPlayer}...3) then
		return true
	end
end

return false
which gives the interpreter one less block (the second if ... then ... end block statement) to parse.

Avoid using "if (condition) then return true end" constructs as the last statement within a TestAny (ie all Quest) handlers, just return the outcome of the condition
Code:
if (iQuest == iThisQuest and Players[iPlayer]:SomeMethod(...)) then
	local pPlayer = Players[iPlayer]
			
	return ({uses pPlayer}...3)
end

return false

and if ...3 only uses pPlayer once, do not create the variable
Code:
if (iQuest == iThisQuest and Players[iPlayer]:SomeMethod(...)) then
	return Players[iPlayer]:IsSomething(...)
end

return false

Finally, NEVER use "if (condition) then return true else return false end" constructs, always use "return (condition)"
And NEVER, NEVER, NEVER use "if (not condition) then return false else return true end" constructs, always use "return (not condition)"


Hooking Quest Event Handlers Efficiently

Even with efficient "early out" code, the most efficient way to process events is not to - at least not if you don't have to.

Most event handlers are coded as
Code:
function OnSomeEvent(...)
end
GameEvents.SomeEvent.Add(OnSomeEvent)

That is, they are installed as the game starts up and left installed for the entire duration of the game.

In the case of quests, we can usually delay installing the handlers until the entry conditions are met, and for some global events, we can uninstall the handlers after the event is complete.

Consider a quest that cannot be started until some era (or tech) is reached by one or more players. We do not need to hook the events until at least one player has reached the required era(tech).
Code:
function OnSomeEvent(...)
end
	
for iPlayer = 0, GameDefines.MAX_MAJOR_CIVS-1, 1 do
	if (Players[iPlayer]:GetCurrentEra() >= iRequiredEra) then
		GameEvents.SomeEvent.Add(OnSomeEvent)
		break
	end
end

But we will also need to add an era(tech) event handler (which will trigger far less often than the quest event(s))
Code:
function OnTeamSetEra(iTeam, iEra, bFirst)
	if (iEra >= iRequiredEra) then
		-- Install the quest event handler(s)
		GameEvents.SomeEvent.Add(OnSomeEvent)

		-- Remove ourselves
		GameEvents.TeamSetEra.Remove(OnTeamSetEra)
	end
end
GameEvents.TeamSetEra.Add(OnTeamSetEra)

Removing the handlers for one-off (global) quests can also be done, but should be deferred until the barbarian player's turn, in order to give every player and City State the opportunity to complete/expire any active quests - see the "Quests - Circumnavigate" complete mod for an example of how to do this.
 
Some other ideas for custom quests (thanks to bane_ for some of these)
  • Rescue a princess (captured civilian) from a dragon (very powerful barbarian unit limited to tiles around a specific camp)
  • Free a City State prince (destroy another CS, or capture a civilian from another CS/Civ)
  • Spread the message (convert X cities (of a specific civ) to the religion of the City State)
  • Bounty (kill/capture X units)
  • Mysterious Gift (send a unit to a designated plot to find out what (if anything) is there)
  • Stop the message (destroy all trade routes from a specific civ to the City State)
  • Reunite the people (be the first civ to send a unit to each City State of a certain type/personality)
All are now possible
 
Wow, this is fantastic. I can't wait until the CP updates their DLL so I can try out your quest mini-mods. Nice work whoward!
 
Wow, awesomely done whoward (since 'nicely done' wouldn't be a sufficient statement)! :D

Some ideas for a militaristic city state quest:
  • The player who trains the most military units gains influence with the city state
  • Barbarian slaughterer: The player who kills the most Barbarians (including those not near/in CS's territory), gains influence
 
This functionality is very interesting, and relevant to the plans of future development of my mod :)
 
  • Rescue a princess (captured civilian) from a dragon (very powerful barbarian unit limited to tiles around a specific camp)
  • snip

Is this possible directly? I can think of a way to 'move it back' if it leaves the stipulated range with UnitSetXY, but I'm interested to know if one can actually 'lock' an unit to specific tiles (or even no tile, with moves only in order to attack).
 
Code:
    <!-- Event sent to ascertain if a unit can move into a given plot (v19) -->
    <!--   GameEvents.CanMoveInto.Add(function(iPlayer, iUnit, iPlotX, iPlotY, bAttack, bDeclareWar) return true end) -->
    <!--   Also requires each unit type to receive the event to be enabled via the new SendCanMoveIntoEvent column in the Units table -->
    <!-- See also: "Units - Railroad Artillery" -->
    <!-- See also: "Civilization - Morindim" -->
    <Row Class="3" Name="EVENTS_CAN_MOVE_INTO" Value="0" DbUpdates="1"/>

In the Unit definition
Code:
<SendCanMoveIntoEvent>true</SendCanMoveIntoEvent>

Restricts a unit to the railways
Code:
--
-- This event is only sent for units where their SendCanMoveIntoEvent column in the Units table is true
--
function OnCanMoveInto(iPlayer, iUnit, iPlotX, iPlotY, bAttack, bDeclareWar)
    local pUnit = Players[iPlayer]:GetUnitByID(iUnit)

    if (pUnit:GetUnitClassType() == iClassRailroadArtillery) then
        local pPlot = Map.GetPlot(iPlotX, iPlotY)
        return (pPlot:GetRouteType() == iRouteRailroad)
    end

    return true
end
GameEvents.CanMoveInto.Add(OnCanMoveInto)
 
This is awesome - wish I had an nth of your ability! Is it possible to have quests set by your own cities? I'm thinking along the lines of their demands for resources that can start a "we love the king day"... I guess it's a totally different bit of DLL modding to do that?

Is there a way to mod so that, for example, adopting the exploration policy tree could make it possible that one of your own cities would challenge you to circumnavigate the globe?
 
make it possible that one of your own cities would challenge you to circumnavigate the globe?

I guess it's a totally different bit of DLL modding to do that?

You've answered your own question :)

Yes it would be possible. No you couldn't do it with the City State quest code directly (and by implication what the features/functions described in this thread enables).
 
Top Bottom