LUA, Goody Huts, and You

zzragnar0kzz

Chieftain
Joined
May 2, 2021
Messages
22
Shout out to the Community in general, and to anyone with an unhealthy interest in Goody Huts in particular!

Posting to create a common knowledge repository relating to Goody Huts and Lua, and to provide examples of what can be accomplished when a reward is received.

To fully utilize the tribal village reward system within Lua, there are a few key points to be aware of:
  1. Numerous ingame Events may fire when a Goody Hut reward is received. Testing indicates that for nearly all rewards, all pertinent data can be acquired from just two: ImprovementActivated and GoodyHutReward. Exceptions exist; see below.
  2. For rewards where both Events fire, they do NOT always fire in a consistent order. Testing indicates that ~ 90% of the time ImprovementActivated will fire first, but that isn't a guarantee, and sometimes GoodyHutReward will fire first instead.
  3. When both Events fire, they frequently exhibit stacking behavior, meaning that when a Player receives multiple rewards, whichever Event fires first will stack first.
Exceptions to (1) above include the following:
  • Sumeria's ability which grants a free reward upon clearing a barbarian camp. Technically speaking, ImprovementActivated DOES fire here, but it does so for the cleared barbarian camp, not a Goody Hut. Practically speaking, GoodyHutReward is the only one of the two Events that provides relevant information.
  • The meteor strike reward in Gathering Storm. Of the two Events, it appears that only GoodyHutReward fires for this reward.
In regards to (3) above, testing indicates that if a target Player pops N Goody Huts in rapid succession, whichever Event fires first will execute N times, then the Event that fires second will execute N times. Using properly configured and maintained queues as described below, the arguments from the first Event can be correctly matched to the arguments from the second Event when it fires.

ImprovementActivated fires whenever any improvement is activated, which includes the Goody Hut. A properly-configured hook to this Event provides much, but not all, of the vital data needed to manipulate the granted reward using Lua, as follows:
Code:
function ImprovementActivatedListener( iX, iY, iOwnerID, iUnitID, iImprovementIndex, iImprovementOwnerID, iActivationType )
    -- function body
end
Of particular interest here are the following:
  • iX and iY provide direct map coordinates to the popped Goody Hut.
  • iOwnerID and iImprovementOwnerID can both be used to determine the true target Player, as needed. One of these should equal the value of iPlayerID as provided by the GoodyHutReward Event.
  • iUnitID here should be the same as the value provided by the GoodyHutReward Event. A value of -1 generally indicates the goody hut was popped by a method other than unit exploration, likely via border expansion.
  • iActivationType appears to be superfluous. A situation where it does not equal 0 has not yet been identified during testing.
GoodyHutReward fires whenever a Goody Hut reward is received. This includes the free reward from Sumeria's civilization ability, and the meteor strike reward. A properly-configured hook to this Event provides vital data that ImprovementActivated does not, as follows:
Code:
function GoodyHutRewardListener( iPlayerID, iUnitID, iTypeHash, iSubTypeHash )
    -- function body
end
Of particular interest here are the following:
  • iPlayerID is an additional means of determining the true target Player. It should be equal to the value of either iOwnerID or iImprovementOwnerID as provided by the ImprovementActivated Event. If it is greater than -1, it should represent the true target Player. Otherwise, iImprovementOwnerID likely represents the true target Player.
  • iUnitID here should be the same as the value provided by the ImprovementActivated Event. A value of -1 generally indicates the goody hut was popped by a method other than unit exploration, likely via border expansion.
  • iTypeHash indicates the received Goody Hut Type, or reward category.
  • iSubTypeHash indicates the received Goody Hut SubType, or specific reward.
Thus, by using hooks to both ImprovementActivated and GoodyHutReward, all needed data can be retrieved. No joke, these Events do NOT always fire in a consistent order. Since consistent order cannot be guaranteed, and since both Events provide vital data, and since they are prone to stacking, a system of queues may be employed in the hooks; whichever Event fires first will store its arguments in a queue to be used by the Event that fires second. To configure and use these queues, the following will be required:
  • Generic tables can be used for the queues.
  • To ensure that any improvement other than a goody hut, and certain barbarian camps, is ignored, the Index value of the goody hut improvement must be known.
  • The Index value of the barbarian camp improvement must also be known, to prevent any potential queue misalignments that might arise due to the Sumerian civilization being an active Player.
  • A simple boolean flag, to indicate whether a barbarian camp was cleared by the Sumerian civilization; this also helps prevent any potential queue misalignments.
  • To ensure that the meteor strike reward does not cause any queue misalignments, a means of identifying rewards by the provided hash values must be employed; tables can also be used for this purpose.
  • Defining any reward (sub)type values to be ignored or otherwise acted upon as strings makes it easy to introduce new checks for other rewards that should also be ignored or otherwise acted upon.
Initial global configuration:
Code:
tGoodyHutRewardQueue = {};
tImprovementActivatedQueue = {};

iGoodyHutIndex = GameInfo.Improvements["IMPROVEMENT_GOODY_HUT"].Index;

iBarbCampIndex = GameInfo.Improvements["IMPROVEMENT_BARBARIAN_CAMP"].Index;

bIsSumeria = false;

sMeteorType = "METEOR_GOODIES";
sMeteorSubType = "METEOR_GRANT_GOODIES";

tGoodyHutTypes = {};
for row in GameInfo.GoodyHuts() do
    tGoodyHutTypes[DB.MakeHash(row.GoodyHutType)] = {
        GoodyHutType = row.GoodyHutType,
        Weight = row.Weight
    };
end

tGoodyHutRewards = {};
for row in GameInfo.GoodyHutSubTypes() do
    tGoodyHutRewards[DB.MakeHash(row.SubTypeGoodyHut)] = {
        GoodyHut = row.GoodyHut,
        SubTypeGoodyHut = row.SubTypeGoodyHut,
        Weight = row.Weight,
        ModifierID = row.ModifierID
    };
end
Now the listeners for each Event can be modified to act only when needed, and to store their values in the correct queue and/or pull corresponding values from the other queue as needed.

The listener for GoodyHutReward should first check the boolean flag, and if that flag is true it should set it to false and abort. It should then check the reward (sub)type hash values, and if they correspond to the meteor strike reward, it should abort. If it passes both checks without aborting, it should store its current arguments in a table and check the ImprovementActivated queue to determine which Event fired first for the current reward. If the ImprovementActivated queue is empty, insert the current arguments table into the GoodyHutReward queue at the end; if the ImprovementActivated queue is NOT empty, remove the first set of arguments from it and add those to the current arguments table, then pass the consolidated arguments to another function for further processing. The updated listener looks like this:
Code:
function GoodyHutRewardListener( iPlayerID, iUnitID, iTypeHash, iSubTypeHash )
    if (bIsSumeria) then
        bIsSumeria = false;
        return;
    elseif (tGoodyHutTypes[iTypeHash].GoodyHutType == sMeteorType) and (tGoodyHutRewards[iSubTypeHash].SubTypeGoodyHut == sMeteorSubType) then
        return;
    end
    local tCurrentArgs = {};
    tCurrentArgs.PlayerID = iPlayerID;
    tCurrentArgs.UnitID_GHR = iUnitID;
    tCurrentArgs.TypeHash = iTypeHash;
    tCurrentArgs.SubTypeHash = iSubTypeHash;
    if (#tImprovementActivatedQueue == 0) then
        table.insert(tGoodyHutRewardQueue, tCurrentArgs);
    elseif (#tImprovementActivatedQueue > 0) then
        for k, v in pairs(tImprovementActivatedQueue[1]) do
            tCurrentArgs[k] = v;
        end
        table.remove(tImprovementActivatedQueue, 1);
        ValidateGoodyHutReward(tCurrentArgs);
    end
end
The listener for ImprovementActivated should first check the activated improvement index to determine what type of improvement was activated. If the activated improvement was a barbarian camp, it should check the value of iOwnerID, and if that is > -1, it will try to determine if this value represents the Sumerian civilization, setting the boolean flag to true if it does. If iOwnerID is NOT > -1, it will repeat the same check using iImprovementOwnerID instead. It should then abort. If the activated improvement is NOT a barbarian camp, and is also NOT a goody hut, it should abort. If it passes both checks without aborting, it should store its current arguments in a table and check the GoodyHutReward queue to determine which Event fired first for the current reward. If the GoodyHutReward queue is empty, insert the current arguments table into the ImprovementActivated queue at the end; if the GoodyHutReward queue is NOT empty, remove the first set of arguments from it and add those to the current arguments table, then pass the consolidated arguments to another function for further processing. The updated listener looks like this:
Code:
function ImprovementActivatedListener( iX, iY, iOwnerID, iUnitID, iImprovementIndex, iImprovementOwnerID, iActivationType )
    if (iImprovementIndex == iBarbCampIndex) then
        if (iOwnerID > -1) then
            local pPlayer = Players[iOwnerID];
            local pPlayerConfig = PlayerConfigurations[iOwnerID];
            if (pPlayer ~= nil) and (pPlayerConfig ~= nil) then
                local sCivTypeName = pPlayerConfig:GetCivilizationTypeName();
                if (sCivTypeName == "CIVILIZATION_SUMERIA") then
                    bIsSumeria = true;
                end
            end
        elseif (iImprovementOwnerID > -1) then
            local pPlayer = Players[iImprovementOwnerID];
            local pPlayerConfig = PlayerConfigurations[iImprovementOwnerID];
            if (pPlayer ~= nil) and (pPlayerConfig ~= nil) then
                local sCivTypeName = pPlayerConfig:GetCivilizationTypeName();
                if (sCivTypeName == "CIVILIZATION_SUMERIA") then
                    bIsSumeria = true;
                end
            end
        end
        return;
    elseif (iImprovementIndex ~= iGoodyHutIndex) then
        return;
    end
    local tCurrentArgs = {};
    tCurrentArgs.PlotX = iX;
    tCurrentArgs.PlotY = iY;
    tCurrentArgs.OwnerID = iOwnerID;
    tCurrentArgs.UnitID_IA = iUnitID;
    tCurrentArgs.ImprovementIndex = iImprovementIndex;
    tCurrentArgs.ImprovementOwnerID = iImprovementOwnerID;
    tCurrentArgs.ActivationType = iActivationType;
    if (#tGoodyHutRewardQueue == 0) then
        table.insert(tImprovementActivatedQueue, tCurrentArgs);
    elseif (#tGoodyHutRewardQueue > 0) then
        for k, v in pairs(tGoodyHutRewardQueue[1]) do
            tCurrentArgs[k] = v;
        end
        table.remove(tGoodyHutRewardQueue, 1);
        ValidateGoodyHutReward(tCurrentArgs);
    end
end
Alternately, individual arguments can be stored/passed in lieu of a table of arguments. With the above, or similar, code in place, whichever Event fires first will store its arguments in the appropriate queue, and those arguments will be retrieved and used by whichever Event fires second. Any queued Event arguments should be removed from the appropriate queue in the order they were added.

So, why go through all of this hassle to gather and consolidate Event arguments? Once all the arguments from both Events have been consolidated, they can be passed to another function that performs whatever actual magic is desired, like shown by the calls to ValidateGoodyHutReward in the code above. This function might have a basic structure similar to the following:
Code:
function ValidateGoodyHutReward( tArgs )
    -- function body
end
The above example receives a table as an argument. Individual arguments can be passed to this function instead, if desired, but this configuration quickly and simply receives every stored argument from both Event listeners, so it is by far the easiest choice.

For a more concrete example, a future update to EGHV will utilize a system like this to enable the correct application of several enhanced goody hut rewards that cannot be granted using the built-in Modifiers system alone:
  • Hostile villagers as a "reward" from a goody hut. In addition to the required Lua, this requires additional database configuration in the form of a new goody hut type and individual weighted subtypes for each such desired reward. See Addendum 2 in this thread for an example.
  • A hidden building that becomes available when a particular reward is first received by a Player, and that upgrades to a more advanced version each subsequent time that reward is received by the same Player. This requires extensive additional database configuration in the form of multiple new buildings and corresponding Modifiers in multiple database tables, a new goody hut type, and a single new subtype to act as a gateway. Unlocking and upgrading the building involves attaching Modifiers to the target Player using Lua, so a generic Modifier can be used for the reward. * TO-DO: Add an Addendum to provide an example *
Utilizing the above examples, the core of the validation function can thus be expressed by the following:
Code:
function ValidateGoodyHutReward( tArgs )
    local iPlayerID = -2;
    if (tArgs.PlayerID > -1) then iPlayerID = tArgs.PlayerID;
    elseif (tArgs.OwnerID > -1) then iPlayerID = tArgs.OwnerID;
    elseif (tArgs.ImprovementOwnerID > -1) then iPlayerID = tArgs.ImprovementOwnerID;
    end
    if (iPlayerID == -2) then
        tGoodyHutRewardQueue = {};
        tImprovementActivatedQueue = {};
        return;
    end
    local sRewardSubType = tGoodyHutRewards[tArgs.SubTypeHash].SubTypeGoodyHut;
    local sVillagerSecrets = "GOODYHUT_UNLOCK_VILLAGER_SECRETS";
    local tHostileVillagers = {
        ["GOODYHUT_LOW_HOSTILITY_VILLAGERS"] = 1,
        ["GOODYHUT_MID_HOSTILITY_VILLAGERS"] = 2,
        ["GOODYHUT_HIGH_HOSTILITY_VILLAGERS"] = 3,
        ["GOODYHUT_MAX_HOSTILITY_VILLAGERS"] = 4
    };
    if (sRewardSubType == sVillagerSecrets) then
        UnlockVillagerSecrets(iPlayerID, sRewardSubType);
    elseif (tHostileVillagers[sRewardSubType] ~= nil) then
        CreateHostileVillagers(tArgs.PlotX, tArgs.PlotY, iPlayerID, sRewardSubType);
    end
end
To prevent constant resource recycling, sVillagerSecrets and tHostileVillagers above can instead be defined with the other global configuration settings. Any needed arguments are retrieved from the consolidated set and passed to an appropriate implementation function as shown. The possibilities here are nearly endless.

Future edits and/or posts will clarify and/or correct existing information, and/or add new information. Questions, comments, and corrections are welcome.
 
Last edited:
Numerous edits to the OP to clarify information, to clean up existing code samples, and to provide new code samples.
 
Last edited:
Numerous edits to the OP to clarify and correct information, to provide new information, and to update and clean up existing code samples.
 
Correction 1: Regarding the iUnitID values provided by the two Events, I did not accurately pinpoint the source of the only-sometimes discrepancy I was seeing between these values until earlier today. It turns out that the GoodyHutReward Event fires when Sumeria clears a barbarian camp, since their civilization ability grants a free random goody hut reward whenever a barbarian camp is cleared. Since an actual goody hut was not also being popped, this was causing a misalignment between the queues. All of this is obvious with the benefit of hindsight, but since Sumeria was not always an active Player in my tests, the discrepancy would not always manifest itself. The moral of the story is that testing and troubleshooting are both easier and more reliable when the game environment is consistent. Further testing now shows no discrepancy between iUnitID values when Sumeria is properly accounted for. The OP has been edited to account for Sumeria's ability where necessary.
 
Last edited:
Addendum 1: The plot coordinates provided by ImprovementActivated can also be retrieved by using the iUnitID value provided by GoodyHutReward, IF that value AND the value of iPlayerID are NOT -1. Since ImprovementActivated always provides the direct coordinates to the target plot without any additional processing, it is easier to retrieve them from it. Being able to always accurately identify the Player that received the reward is a nice bonus.
 
Addendum 2: I was a fan of the hostile villager "rewards" in Civilization IV, and IMO the lack of any such rewards in this game is an oversight. YMMV in this regard, of course, but since Lua provides a way to implement such a reward, let's correct that oversight as an example of what we can do with this system.

Before any hostile villagers can be placed, appropriate "rewards" must be defined. This example requires:
  • one new Goody Hut Type
  • a component Goody Hut SubType for each individual "reward". This example requires four such SubTypes
  • a ModifierID to be used by each configured SubType. This example only requires one such ModifierID, for reasons described below
  • any ModifierArguments set(s) to be used by any configured ModifierID(s). This example requires one such set
Most Goody Hut rewards have a unique ModifierID; it is this ModifierID that supplies the effect of a granted reward. Since the actual work of placing any hostile unit(s) must be done by Lua outside of the Modifiers system, a single dummy modifier that does nothing ingame may be used here, and that modifier may be shared by each defined hostiles "reward". With that said, this example instead uses a single modifier that grants a very small amount of experience to the popping unit, if applicable.

To configure the above, the following modifications to the Gameplay database must be made:
Code:
REPLACE INTO Types
    (Type, Kind)
VALUES
    ('GOODYHUT_HOSTILES', 'KIND_GOODY_HUT');

REPLACE INTO GoodyHuts
    (GoodyHutType, Weight)
VALUES
    ('GOODYHUT_HOSTILES', 100);

REPLACE INTO GoodyHutSubTypes
    (GoodyHut, SubTypeGoodyHut, Description, Weight, Turn, MinOneCity, RequiresUnit, ModifierID)
VALUES
    ('GOODYHUT_HOSTILES', 'GOODYHUT_LOW_HOSTILITY_VILLAGERS', 'LOC_GOODYHUT_SPAWN_HOSTILE_VILLAGERS_DESCRIPTION', 40, 0, 1, 1, 'GOODY_SPAWN_HOSTILES'),
    ('GOODYHUT_HOSTILES', 'GOODYHUT_MID_HOSTILITY_VILLAGERS', 'LOC_GOODYHUT_SPAWN_HOSTILE_VILLAGERS_DESCRIPTION', 30, 0, 1, 1, 'GOODY_SPAWN_HOSTILES'),
    ('GOODYHUT_HOSTILES', 'GOODYHUT_HIGH_HOSTILITY_VILLAGERS', 'LOC_GOODYHUT_SPAWN_HOSTILE_VILLAGERS_DESCRIPTION', 20, 0, 1, 1, 'GOODY_SPAWN_HOSTILES'),
    ('GOODYHUT_HOSTILES', 'GOODYHUT_MAX_HOSTILITY_VILLAGERS', 'LOC_GOODYHUT_SPAWN_HOSTILE_VILLAGERS_DESCRIPTION', 10, 0, 1, 1, 'GOODY_SPAWN_HOSTILES');

REPLACE INTO Modifiers
    (ModifierId, ModifierType, RunOnce, Permanent, SubjectRequirementSetId)
VALUES
    ('GOODY_SPAWN_HOSTILES', 'MODIFIER_PLAYER_UNIT_ADJUST_GRANT_EXPERIENCE', 1, 1, NULL);

REPLACE INTO ModifierArguments
    (ModifierId, Name, Value, Extra)
VALUES
    ('GOODY_SPAWN_HOSTILES', 'Amount', 5, NULL);
To facilitate the CreateHostileVillagers() function, additional global Lua configuration is required. Specifically, at a minimum the following will be required:
  • a lookup table keyed to the SubTypeGoodyHut values defined above.
  • one or more table(s) of hostile units to spawn, keyed to Era. This example uses two such tables: one for melee units and one for ranged units. This allows somewhat contemporary unit(s) to be spawned as hostiles.
  • the current global or player Era.
  • the selected ruleset. This facilitates determining the player Era if that must be used.
  • the Barbarian Player ID. This is probably 63, but the actual value will be confirmed.
For a more robust experience, the following can also be implemented:
  • one or more table(s) of hostile mounted units to spawn, keyed to Era. If the Horses resource is detected in a nearby plot, these units will serve as potential substitutions for any hostile unit(s) which may spawn. If the Gathering Storm ruleset is in use, then alter certain keys in these table(s) to accommodate new contemporary unit(s) in appropriate Era(s). This example uses two such tables: one for heavy cavalry (replacing melee) units and one for light cavalry (replacing ranged) units.
  • the Index value of the Horses resource. This will be used to detect the presence of Horses in any nearby plot(s).
Global configuration, including robust options:
Code:
sRuleset = GameConfiguration.GetValue("RULESET");

iBarbarianPlayer = 63;
for p = 63, 0, -1 do
    local pPlayer = Players[p];
    if (pPlayer ~= nil) and pPlayer:IsBarbarian() then
        iBarbarianPlayer = p;
    end
end

iHorsesIndex = GameInfo.Resources["RESOURCE_HORSES"].Index;

tHostileVillagers = {
    ["GOODYHUT_LOW_HOSTILITY_VILLAGERS"] = 1,
    ["GOODYHUT_MID_HOSTILITY_VILLAGERS"] = 2,
    ["GOODYHUT_HIGH_HOSTILITY_VILLAGERS"] = 3,
    ["GOODYHUT_MAX_HOSTILITY_VILLAGERS"] = 4
};

tHostileMeleeByEra = {
    [0] = "UNIT_WARRIOR",
    [1] = "UNIT_WARRIOR",
    [2] = "UNIT_SWORDSMAN",
    [3] = "UNIT_MAN_AT_ARMS",
    [4] = "UNIT_MUSKETMAN",
    [5] = "UNIT_LINE_INFANTRY",
    [6] = "UNIT_INFANTRY",
    [7] = "UNIT_INFANTRY",
    [8] = "UNIT_INFANTRY"
};

tHostileRangedByEra = {
    [0] = "UNIT_SLINGER",
    [1] = "UNIT_ARCHER",
    [2] = "UNIT_ARCHER",
    [3] = "UNIT_CROSSBOWMAN",
    [4] = "UNIT_CROSSBOWMAN",
    [5] = "UNIT_FIELD_CANNON",
    [6] = "UNIT_FIELD_CANNON",
    [7] = "UNIT_MACHINE_GUN",
    [8] = "UNIT_MACHINE_GUN"
};

tHostileHeavyCavalryByEra = {
    [0] = "UNIT_BARBARIAN_HORSEMAN",
    [1] = "UNIT_BARBARIAN_HORSEMAN",
    [2] = "UNIT_KNIGHT",
    [3] = "UNIT_KNIGHT",
    [4] = "UNIT_KNIGHT",
    [5] = "UNIT_KNIGHT",
    [6] = "UNIT_KNIGHT",
    [7] = "UNIT_KNIGHT",
    [8] = "UNIT_KNIGHT"
};

if (sRuleset == "RULESET_EXPANSION_2")
    then for e = 6, 8, 1 do
        tHostileHeavyCavalryByEra[e] = "UNIT_CUIRASSIER";
    end
end

tHostileLightCavalryByEra = {
    [0] = "UNIT_BARBARIAN_HORSE_ARCHER",
    [1] = "UNIT_BARBARIAN_HORSE_ARCHER",
    [2] = "UNIT_BARBARIAN_HORSE_ARCHER",
    [3] = "UNIT_HORSEMAN",
    [4] = "UNIT_HORSEMAN",
    [5] = "UNIT_HORSEMAN",
    [6] = "UNIT_CAVALRY",
    [7] = "UNIT_CAVALRY",
    [8] = "UNIT_CAVALRY"
};

if (sRuleset == "RULESET_EXPANSION_2")
    then for e = 5, 6, 1 do
        tHostileLightCavalryByEra[e] = "UNIT_COURSER";
    end
end
With the above in place, the CreateHostileVillagers() function is fairly straightforward. The updated version, including robust options:
Code:
function CreateHostileVillagers( iX, iY, iPlayerID, sRewardSubType )
    local pPlayer = Players[iPlayerID];
    local iEra = (sRuleset == "RULESET_STANDARD") and pPlayer:GetEras():GetEra() or Game.GetEras():GetCurrentEra();
    local iHostilityLevel = tHostileVillagers[sRewardSubType];
    local iMeleeHostiles = 1;
    local iRangedHostiles = 0;
    local bSpawnBarbCamp = (iHostilityLevel == 4) and true or false;

    if (iHostilityLevel == 4) then
        iRangedHostiles = 1;
    elseif (iHostilityLevel == 3) then
        iMeleeHostiles = 2;
        iRangedHostiles = 1;
    elseif (iHostilityLevel == 2) then
        iRangedHostiles = 1;
    end

    local tValidBarbCampPlots, bIsHorsesNearby = ValidateAdjacentPlots(iX, iY);

    if (bSpawnBarbCamp) then
        if tValidBarbCampPlots and #tValidBarbCampPlots > 0 then
            local iSpawnPlotIndex = TerrainBuilder.GetRandomNumber(#tValidBarbCampPlots, "New barbarian camp plot index") + 1;
            local pSpawnPlot = tValidBarbCampPlots[iSpawnPlotIndex];
            table.remove(tValidBarbCampPlots, iSpawnPlotIndex);
            ImprovementBuilder.SetImprovementType(pSpawnPlot, iBarbCampIndex, -1);
        else
            iMeleeHostiles = 2;
            iRangedHostiles = 2;
        end
    end

    local sMeleeHostile = tHostileMeleeByEra[iEra];
    local sRangedHostile = tHostileRangedByEra[iEra];
    local sHeavyCavalryHostile = tHostileHeavyCavalryByEra[iEra];
    local sLightCavalryHostile = tHostileLightCavalryByEra[iEra];

    if (iMeleeHostiles > 0) then
        for p = 1, iMeleeHostiles do
            local iSpawnPlotIndex = TerrainBuilder.GetRandomNumber(#tValidBarbCampPlots, "Melee hostiles plot index") + 1;
            local pSpawnPlot = tValidBarbCampPlots[iSpawnPlotIndex];
            table.remove(tValidBarbCampPlots, iSpawnPlotIndex);
            local sX, sY = pSpawnPlot:GetX(), pSpawnPlot:GetY();
            if bIsHorsesNearby then
                local bMountedSpawn = ((TerrainBuilder.GetRandomNumber(100, "Mounted melee/heavy cavalry hostiles coin flip") % 2) > 0) and true or false;
                if bMountedSpawn then
                    UnitManager.InitUnitValidAdjacentHex(iBarbarianPlayer, sHeavyCavalryHostile, sX, sY, 1);
                else
                    UnitManager.InitUnitValidAdjacentHex(iBarbarianPlayer, sMeleeHostile, sX, sY, 1);
                end
            else
                UnitManager.InitUnitValidAdjacentHex(iBarbarianPlayer, sMeleeHostile, sX, sY, 1);
            end
        end
    end

    if (iRangedHostiles > 0) then
        for p = 1, iRangedHostiles do
            local iSpawnPlotIndex = TerrainBuilder.GetRandomNumber(#tValidBarbCampPlots, "Ranged hostiles plot index") + 1;
            local pSpawnPlot = tValidBarbCampPlots[iSpawnPlotIndex];
            table.remove(tValidBarbCampPlots, iSpawnPlotIndex);
            local sX, sY = pSpawnPlot:GetX(), pSpawnPlot:GetY();
            if bIsHorsesNearby then
                local bMountedSpawn = ((TerrainBuilder.GetRandomNumber(100, "Mounted ranged/light cavalry hostiles coin flip") % 2) > 0) and true or false;
                if bMountedSpawn then
                    UnitManager.InitUnitValidAdjacentHex(iBarbarianPlayer, sLightCavalryHostile, sX, sY, 1);
                else
                    UnitManager.InitUnitValidAdjacentHex(iBarbarianPlayer, sRangedHostile, sX, sY, 1);
                end
            else
                UnitManager.InitUnitValidAdjacentHex(iBarbarianPlayer, sRangedHostile, sX, sY, 1);
            end
        end
    end
end
This will set the current global or player Era based on the selected ruleset. The hostility level is determined by the supplied sRewardSubType value:
  1. Low - 1 melee unit
  2. Moderate - 1 melee and 1 ranged units
  3. High - 2 melee and 1 ranged units
  4. Max - will attempt to spawn a new barbarian camp. If successful, will also spawn 1 melee and 1 ranged units for good measure; if NOT successful, will instead attempt to spawn 2 melee and 2 ranged units
A helper function, ValidateAdjacentPlots(), will be executed to identify valid plots for any camp and/or unit(s) to place. This function will also check any identified plot(s) for the presence of the Horses resource. A new camp and/or the indicated number of unit(s) will then be placed in (a) random valid plot(s) adjacent to the just-popped goody hut or within a short distance; if Horses were found, there is a ~ 50% chance that any spawned unit will be replaced with its mounted substitute. The selected plot will be removed from the pool of valid plots to eliminate any chance of being chosen multiple times.

ValidateAdjacentPlots() is easy to configure to search multiple levels of adjacent plots. To search 3 levels deep (e.g. the plots directly adjacent to the current plot, the plots directly adjacent to each of those plots, and the plots directly adjacent to each of THOSE plots, the following configuration may be employed:
Code:
function ValidateAdjacentPlots( iX, iY )
    local tValidPlots = {};
    local bIsHorsesNearby = false;
    for dPriAdj = 0, DirectionTypes.NUM_DIRECTION_TYPES -1, 1 do
        local pPriAdjacentPlot = Map.GetAdjacentPlot(iX, iY, dPriAdj);
        if pPriAdjacentPlot then
            local jX, jY = pPriAdjacentPlot:GetX(), pPriAdjacentPlot:GetY();
            for dSecAdj = 0, DirectionTypes.NUM_DIRECTION_TYPES -1, 1 do
                local pSecAdjacentPlot = Map.GetAdjacentPlot(jX, jY, dSecAdj);
                if pSecAdjacentPlot then
                    local kX, kY = pSecAdjacentPlot:GetX(), pSecAdjacentPlot:GetY();
                    for dTerAdj = 0, DirectionTypes.NUM_DIRECTION_TYPES -1, 1 do
                        local pTerAdjacentPlot = Map.GetAdjacentPlot(kX, kY, dTerAdj);
                        if pTerAdjacentPlot then
                            local zX, zY = pTerAdjacentPlot:GetX(), pTerAdjacentPlot:GetY();
                            if ImprovementBuilder.CanHaveImprovement(pTerAdjacentPlot, GUE.BarbCampIndex, -1) then
                                local bIsDuplicate = false;
                                for i, v in ipairs(tValidPlots) do if (v == pTerAdjacentPlot) then bIsDuplicate = true; end end
                                if not bIsDuplicate then
                                    table.insert(tValidPlots, pTerAdjacentPlot);
                                end
                            end
                            if not bIsHorsesNearby then
                                local pPlotResource = pTerAdjacentPlot:GetResourceType();
                                if (pPlotResource ~= 1) and (pPlotResource == GUE.HorsesIndex) then
                                    bIsHorsesNearby = true;
                                end
                            end
                        end
                    end
                end
            end
        end
    end
    return tValidPlots, bIsHorsesNearby;
end
In theory, since barbarian camps are placed directly into the plot used as an argument, this should enable a barbarian camp to spawn up to 3 plots away from the popped Goody Hut. Since any hostile units are placed into a plot adjacent to the plot used as an argument, it is theoretically possible that any hostile units spawn up to 4 plots away.

Not enough? With additional configuration, even further customizations are possible:
  • by tracking any plots in which a barbarian camp and/or units are spawned, notifications can be generated and sent to the appropriate Player, providing ingame alerts that hostile villagers have appeared nearby.
  • by adding additional pertinent data to the previously defined tables of Goody Hut (Sub)Types, and employing a math-heavy method of determining villager "hostility" levels, hostile villagers can be configured to potentially (or always) appear in addition to (following) any other reward. Have your cake and eat it, too, in a manner of speaking.
Examples of these may be addressed in a future Addendum, if demand dictates.
 
Last edited:
Top Bottom