Swapping UnitArtDefines

Civitar

Adventurer
Joined
Mar 23, 2014
Messages
1,507
Location
Switzerland
For my Warhammer mod I want to represent a feature of certain units: they are armed with both hand weapons (usually spears/swords) and bows/crossbows.
I was thinking I could do two graphics for the unit (example High Elf Sea Guard), one carrying spear and shield with bow and quiver slung over the elf's back, and the other carrying the bow with spear and shield slung over his back. Then the spear-wielding version could be the default version, but when the player clicks on the ranged attack button the artdefines is swapped from ART_DEF_UNIT_HIGH_ELF_SEA_GUARD_SPEAR to ART_DEF_UNIT_HIGH_ELF_SEA_GUARD_BOW. Then when the ranged attack is finished the graphics change back. When the unit is attacked, though, the spear version remains.
I only have the very barest understanding of what Lua can do, so it seems like I need something like "When player clicks Bombard for Sea Guard change UnitArtInfo from spear to bow, when bombard finished change back". That's really all I'm looking for.
So please, can somebody provide the Lua necessary to do this?
Thanks for any help given.
 
The swapping graphics portion is similar to your Random Unit Models request, involving a second untrainable unit that temporarily replaces the first. However, I'm not sure how you could hook MISSION_RANGE_ATTACK without a DLL mod. Otherwise, you'd have to create a custom button.

EDIT: You know, an easy way would involve using a custom version of WorldView.lua. Since you're probably less concerned about maintaining compatibility with other mods since this is a more of a complete conversion, that might just be the way to go...

It's not quite the same thing, but it'd be much easier to have half of the unit members (in the rear of the formation) be carrying bows, and the other half carrying spears, and only the appropriate half is animated during the corresponding attack.
 
I thought about the randomizing unit models, but that is only done at the unit's creation. I really want it to look like the unit is switching between armaments.
Is creating a custom button/hooking into the MISSION_RANGE_ATTACK really so hard?
 
It looks like you missed the edit. But it looks like Bombardment.lua would be better than WorldView.lua. Add a copy of Assets\UI\InGame\Bombardment.lua to your mod, set it VFS=true, and then add a little bit of code to swap between the units.

You want to add this function, and another one very similar, i.e., SwapToMelee(), that swaps from UNIT_HIGH_ELF_SEA_GUARD_BOW back to UNIT_HIGH_ELF_SEA_GUARD [and you'd actually have to add a UNIT_HIGH_ELF_SEA_GUARD_BOW to the game in case that wasn't clear; you can make a copy with SQL as shown in the Random Unit Models thread]:
Code:
function SwapToRanged(iPlayer, pUnit)
    pPlayer = Players[iPlayer];
    if pPlayer:GetCivilizationType() == GameInfoTypes["CIVILIZATION_HIGH_ELF"] then
        if pUnit:GetUnitType() == GameInfoTypes["UNIT_HIGH_ELF_SEA_GUARD"] then
            local pUnitRanged = pPlayer:InitUnit(GameInfoTypes["UNIT_HIGH_ELF_SEA_GUARD_BOW"], pUnit:GetX(), pUnit:GetY())
            pUnitRanged:Convert(pUnit)
        end
    end
end

...then you put
Code:
    if (pHeadSelectedUnit) then
        SwapToRanged(Game.GetActivePlayer(), pHeadSelectedUnit);
    end
before the last end in the BeginRangedAttack function.

...Similarly, you'd place
Code:
    local pHeadSelectedUnit = UI.GetHeadSelectedUnit();
    if (pHeadSelectedUnit) then
        SwapToMelee(Game.GetActivePlayer(), pHeadSelectedUnit);
    end
at the beginning of the EndRangedAttack function.

EDIT: Hm, though you'd actually want to make sure the switch back to melee is delayed until after the animation completes; I think that EndRangedAttack() would probably fire too soon. You can always try it and see. If that doesn't work, maybe you can use Events.EndCombatSim? Or maybe someone who knows what the heck they're doing will come along. ;)

EDIT #2: Y'know, maybe you could just switch back at the end of the turn... though that wouldn't work if it has a multiple attack promotion... still, I think that'd be the best solution.
 
So if I understand correctly, I put this at the beginning of Bombardment.lua:
Code:
function SwapToRanged(iPlayer, pUnit)
    pPlayer = Players[iPlayer];
    if pPlayer:GetCivilizationType() == GameInfoTypes["CIVILIZATION_HIGH_ELF"] then
        if pUnit:GetUnitType() == GameInfoTypes["UNIT_HIGH_ELF_SEA_GUARD"] then
            local pUnitRanged = pPlayer:InitUnit(GameInfoTypes["UNIT_HIGH_ELF_SEA_GUARD_BOW"], pUnit:GetX(), pUnit:GetY())
            pUnitRanged:Convert(pUnit)
        end
    end
end
function SwapToMelee(iPlayer, pUnit)
    pPlayer = Players[iPlayer];
    if pPlayer:GetCivilizationType() == GameInfoTypes["CIVILIZATION_HIGH_ELF"] then
        if pUnit:GetUnitType() == GameInfoTypes["UNIT_HIGH_ELF_SEA_GUARD_BOW"] then
            local pUnitMelee = pPlayer:InitUnit(GameInfoTypes["UNIT_HIGH_ELF_SEA_GUARD"], pUnit:GetX(), pUnit:GetY())
            pUnitMelee:Convert(pUnit)
        end
    end
end
This defines the functions to be used in the next step, right?
Then I add this:
Code:
if (pHeadSelectedUnit) then
        SwapToRanged(Game.GetActivePlayer(), pHeadSelectedUnit);
    end
to the BeginRangedAttack function to perform the functions above when I click on the Bombard button? And finally I add this:
Code:
local pHeadSelectedUnit = UI.GetHeadSelectedUnit();
    if (pHeadSelectedUnit) then
        SwapToMelee(Game.GetActivePlayer(), pHeadSelectedUnit);
    end
to the EndRangedAttack function to ensure that the unit changes back, right?
Is this correct or am I misunderstanding you? I actually seem to be getting some of the function mechanics though, maybe a light is dawning... right.:lol:
Also I know this is similar to Random Unit Models... yes I am very interested in the appearances of the units, however did you guess?;)

EDIT: I just realized I never actually told you that the randomizing function worked for me... thanks!
EDIT2: I tried doing what I describe in the above post, and it worked in part: when I clicked on the Bombard button, the Warrior was replaced by an Archer, and once the Archer had bombarded somebody it changed back into a Warrior (there was no problem with the timing of the change). However if the Archer didn't bombard anybody it stayed an Archer, and also when the unit changed all earned promotions/XP were lost.
So what I still need is something to ensure that the Archer changes back at the end of the turn if it hasn't already, and to keep all promotions and XP through the changes.
 
Right, then you will have to use a function in WorldView.lua or InGame.lua after all so you can intercept the cancelation of the ranged attack...

Unit.Convert should have copied the unit attributes... Oh, is it losing promotions that are melee-only? Does your temporary archer not have a melee Combat entry in the Units table?
 
Unit.Convert isn't in any of the code you gave me. Post #5 has exactly what I put in Bombardment.lua.
The Warrior seems to literally be getting killed, replaced with a brand-new Archer, then the first time that the Archer fires it is killed and replaced with a brand-new Warrior.
Do you happen to have a function for WorldView.lua? I don't think I've ever messed with that... just with UnitPanel.lua.
 
Unit.Convert is there. It's called by pUnitRanged:Convert(pUnit)
In conjunction with InitUnit, we're basically doing a create + kill and replace, but it's supposed to copy the attributes across before doing so.

I'll find you a function when I get home if you can't find it, but I know there's a couple that change from INTERFACEMODE_RANGE_ATTACK back to INTERFACEMODE_SELECTION if canceled (one for keyboard, one for mouse) that might work.
 
I haven't tested it, and I'm not sure whether it covers the case where the bombardment is canceled by mouse. These changes should be instead of the ones we made in Bombardment.lua (you'll need to move the 2 functions as well). If this doesn't work for you, upload your test mod so I can play with it.

In WorldView.lua, line 335:
Code:
InterfaceModeMessageHandler[InterfaceModeTypes.INTERFACEMODE_RANGE_ATTACK][KeyEvents.KeyDown] = 
function( wParam, lParam )
	[COLOR=red][B]local pHeadSelectedUnit = UI.GetHeadSelectedUnit();[/B][/COLOR]
	if ( wParam == Keys.VK_ESCAPE ) then	
		[COLOR=red][B]if (pHeadSelectedUnit) then
			SwapToMelee(Game.GetActivePlayer(), pHeadSelectedUnit);
		end[/B][/COLOR]
		UI.SetInterfaceMode(InterfaceModeTypes.INTERFACEMODE_SELECTION);
		return true;
	elseif (wParam == Keys.VK_NUMPAD1 or wParam == Keys.VK_NUMPAD3 or wParam == Keys.VK_NUMPAD4 or wParam == Keys.VK_NUMPAD6 or wParam == Keys.VK_NUMPAD7 or wParam == Keys.VK_NUMPAD8 ) then
		[COLOR=red][B]if (pHeadSelectedUnit) then
			SwapToMelee(Game.GetActivePlayer(), pHeadSelectedUnit);
		end[/B][/COLOR]
		UI.SetInterfaceMode(InterfaceModeTypes.INTERFACEMODE_SELECTION);
		return DefaultMessageHandler[KeyEvents.KeyDown]( wParam, lParam );
	else
		return DefaultMessageHandler[KeyEvents.KeyDown]( wParam, lParam );
	end
end

Then in RangeAttack(), which starts on line 447:
Code:
	-- should handle the case of units bombarding tiles when they are already at war
	if pHeadSelectedUnit and pHeadSelectedUnit:CanRangeStrikeAt(plotX, plotY, true, true) then
		[COLOR=red][B]SwapToRanged(Game.GetActivePlayer(), pHeadSelectedUnit);[/B][/COLOR]
		Game.SelectionListGameNetMessage(GameMessageTypes.GAMEMESSAGE_PUSH_MISSION, MissionTypes.MISSION_RANGE_ATTACK, plotX, plotY, 0, false, bShift);
		UI.SetInterfaceMode(InterfaceModeTypes.INTERFACEMODE_SELECTION);
		[COLOR=red][B]SwapToMelee(Game.GetActivePlayer(), pHeadSelectedUnit);[/B][/COLOR]
		return true;
	-- Unit Range Strike - special case for declaring war with range strike
	elseif pHeadSelectedUnit and pHeadSelectedUnit:CanRangeStrikeAt(plotX, plotY, false, true) then
		-- Is there someone here that we COULD bombard, perhaps?
		local eRivalTeam = pHeadSelectedUnit:GetDeclareWarRangeStrike(plot);
		if (eRivalTeam ~= -1) then
			[COLOR=red][B]SwapToRanged(Game.GetActivePlayer(), pHeadSelectedUnit);[/B][/COLOR]
			UIManager:SetUICursor(0);
							
			local popupInfo = 
			{
				Type  = ButtonPopupTypes.BUTTONPOPUP_DECLAREWARRANGESTRIKE,
				Data1 = eRivalTeam,
				Data2 = plotX,
				Data3 = plotY
			};
			Events.SerialEventGameMessagePopup(popupInfo);
			UI.SetInterfaceMode(InterfaceModeTypes.INTERFACEMODE_SELECTION);
			[COLOR=red][B]SwapToMelee(Game.GetActivePlayer(), pHeadSelectedUnit);[/B][/COLOR]
			return true;	
		end
	end
						
	return true;
 
Out of curiosity, did this work?

Anyway, I'm responding here to the request for religion-specific units you made in the Warhammer thread:
I'd really like to figure out if it's possible to make which cult a city follows affect what units it can produce...


Spoiler one method :
[EDIT: Never mind, forget this spoiler. See below.]

First, add this mod snippet to your mod (SerialEventUnitCreatedGood.lua and Promotion_Created.xml).

Then add this lua. I've included a variation of Unit_Spawn.lua to prevent unnecessary code duplication [you should delete Unit_Spawn.lua from your mod]:
Code:
function SwapUnit(playerID, unitID, hexVec, unitType, cultureType, civID, primaryColor, secondaryColor, unitFlagIndex, fogState, selected, military, notInvisible)
    local player = Players[playerID]
    local unit = player:GetUnitByID(unitID)
    if(player == nil or
        unit == nil or
	unit:IsDead()) then
        return
    end

--------------
-- Choose random Spawn unit
--------------
    if unit:GetUnitType() == GameInfoTypes["UNIT_SPAWN"] then
        local spawn = { "UNIT_SPAWN_V1", "UNIT_SPAWN_V2", "UNIT_SPAWN_V3", "UNIT_SPAWN_V4" }
        local unitRnd = player:InitUnit(GameInfo.Units[spawn[math.random(#spawn)]].ID, unit:GetX(), unit:GetY())
        unitRnd:Convert(unit)

--------------
-- Swap to religion-specific unit
--------------
    elseif unit:GetUnitType() == GameInfoTypes["UNIT_GENERIC"] then
        for city in player:Cities() do
            if (city:GetX() == unit:GetX() and city:GetY() == unit:GetY()) then
                if (city:GetReligiousMajority() == GameInfoTypes["RELIGION_XXX"]) then
                    local newUnit = player:InitUnit(GameInfoTypes["UNIT_SPECIFIC"], unit:GetX(), unit:GetY())
                    newUnit:Convert(unit)
                end
            end
        end
    end
end
LuaEvents.SerialEventUnitCreatedGood.Add(SwapUnit)

Then we need to revise the production popup to reflect the unit that will be built...


EDIT: Never mind, forget all that. This is a much simpler method:
Code:
function ReligionSpecificUnits(playerID, cityID, unitType)
    local majorityReligion = Cities[cityID]:GetReligiousMajority()

-- Turn off the generics for cities with specific religions
    if (majorityReligion == GameInfoTypes["RELIGION_XXX"] or majorityReligion == GameInfoTypes["RELIGION_YYY"]) then
        if (unitType == GameInfoTypes["UNIT_GENERIC1"] or unitType == GameInfoTypes["UNIT_GENERIC2"]) then
            return false
        end
    end

-- Turn off the specifics for all but the correct religion
    if (unitType == GameInfoTypes["UNIT_SPECIFIC_XXX1"]) then
        if not (majorityReligion == GameInfoTypes["RELIGION_XXX"]) then
            return false
        end
    elseif (unitType == GameInfoTypes["UNIT_SPECIFIC_YYY1"]) then
        if not (majorityReligion == GameInfoTypes["RELIGION_YYY"]) then
            return false
        end
    elseif (unitType == GameInfoTypes["UNIT_SPECIFIC_XXX2"]) then
        if not (majorityReligion == GameInfoTypes["RELIGION_XXX"]) then
            return false
        end
    elseif (unitType == GameInfoTypes["UNIT_SPECIFIC_YYY2"]) then
        if not (majorityReligion == GameInfoTypes["RELIGION_YYY"]) then
            return false
        end
    end
    return true
end
GameEvents.CityCanTrain.Add(ReligiousSpecificUnits)

This method intercepts the check to see whether a specific unit can be built in a given city. The default is true, i.e., the city can build the unit. We then have to specify where that is false; i.e., where the generic units aren't appropriate because the city has the corresponding religious majority, or as to the specific units in all cases EXCEPT where the city has the corresponding religious majority.

Your specific units need not be uniques (they could have a different unit class from the generic ones), nor do they need to be set to -1 Cost or anything like that. They're just regular units that can or can't be chosen from the list as appropriate.

EDIT #2: Oops. Needed some small fixes.
 
Wow, thanks Nutty! I'll try plugging in a couple of units, can you tell me if I've done it right?
Code:
function ReligionSpecificUnits(playerID, cityID, unitType)
    local majorityReligion = Cities[cityID]:GetReligiousMajority()

-- Turn off the generics for cities with specific religions
    if (majorityReligion == GameInfoTypes["RELIGION_KHORNE"] or majorityReligion == GameInfoTypes["RELIGION_NURGLE"]) then
        if (unitType == GameInfoTypes["UNIT_CHAOS_MARAUDERS"] or unitType == GameInfoTypes["UNIT_CHAOS_WARRIORS"]) then
            return false
        end
    end

-- Turn off the specifics for all but the correct religion
    if (unitType == GameInfoTypes["UNIT_KHORNE_BLOODLETTERS"]) then
        if not (majorityReligion == GameInfoTypes["RELIGION_KHORNE"]) then
            return false
        end
    elseif (unitType == GameInfoTypes["UNIT_NURGLE_PLAGUEBEARERS"]) then
        if not (majorityReligion == GameInfoTypes["RELIGION_NURGLE"]) then
            return false
        end
    end
    return true
end
GameEvents.CityCanTrain.Add(ReligiousSpecificUnits)
This tiny bit should make the Chaos Marauders and Chaos Warriors unbuildable for civs with the Khorne or Nurgle religions, and make the Plaguebearers buildable for Nurgle and Bloodletters for Khorne. Does it actually do that?
I'll kind of have to use different UnitClasses so as to make the Bloodletters and Plaguebearers (both are horrendous demons) unbuildable by other civs even when they adopt the right religion. Most civs keep going as normal but of course won't build, say, the Templars of Sigmar if they worship Slaanesh.
Also, can I add more "if unitType==GameInfoTypes[""] or if unitType==GameInfoTypes[""]" as needed?
 
Sorry, I missed your reply!

Does it actually do that?
Yes, that's right.

I'll kind of have to use different UnitClasses so as to make the Bloodletters and Plaguebearers (both are horrendous demons) unbuildable by other civs even when they adopt the right religion.
You could use this technique and add a check for the civ type as well, rather than setting the unitclass override to null for those civs that aren't ever allowed to train them. Either way works.

However, I assumed you were going to edit (or otherwise intercept) ChooseReligionPopup.lua and constrain which religions are available to the different civs, and therefore wouldn't need a civ check as part of this function. As a matter of strategy, I wouldn't want to choose a religion where I didn't get all the benefits of that religion (unless religion didn't matter to me and I was just preventing another civ from getting those benefits).

Also, can I add more "if unitType==GameInfoTypes[""] or if unitType==GameInfoTypes[""]" as needed?
Of course, that's why I wrote it that way (use "elseif" if you're talking about the second check; for the first you just add more "or"s). If you need to have more than a few, though, then I'd make it a table, or even add a new column to the database and check against that. Let me know if you want to go that route.
 
Alright, sorry for the delay in responding.
I'll go with this bit of Lua and then the UnitClassOverrides for civs... best not to complicate it with even more Lua. Also, the religion-specific units Lua: should I just add it with IngameUIAddin or if not what file do I put it in?
I'll test the ranged/melee swapping again, but I seem to recall it didn't work.
 
Out of curiosity, did this work?

Anyway, I'm responding here to the request for religion-specific units you made in the Warhammer thread:



Spoiler one method :
[EDIT: Never mind, forget this spoiler. See below.]

First, add this mod snippet to your mod (SerialEventUnitCreatedGood.lua and Promotion_Created.xml).

Then add this lua. I've included a variation of Unit_Spawn.lua to prevent unnecessary code duplication [you should delete Unit_Spawn.lua from your mod]:
Code:
function SwapUnit(playerID, unitID, hexVec, unitType, cultureType, civID, primaryColor, secondaryColor, unitFlagIndex, fogState, selected, military, notInvisible)
    local player = Players[playerID]
    local unit = player:GetUnitByID(unitID)
    if(player == nil or
        unit == nil or
	unit:IsDead()) then
        return
    end

--------------
-- Choose random Spawn unit
--------------
    if unit:GetUnitType() == GameInfoTypes["UNIT_SPAWN"] then
        local spawn = { "UNIT_SPAWN_V1", "UNIT_SPAWN_V2", "UNIT_SPAWN_V3", "UNIT_SPAWN_V4" }
        local unitRnd = player:InitUnit(GameInfo.Units[spawn[math.random(#spawn)]].ID, unit:GetX(), unit:GetY())
        unitRnd:Convert(unit)

--------------
-- Swap to religion-specific unit
--------------
    elseif unit:GetUnitType() == GameInfoTypes["UNIT_GENERIC"] then
        for city in player:Cities() do
            if (city:GetX() == unit:GetX() and city:GetY() == unit:GetY()) then
                if (city:GetReligiousMajority() == GameInfoTypes["RELIGION_XXX"]) then
                    local newUnit = player:InitUnit(GameInfoTypes["UNIT_SPECIFIC"], unit:GetX(), unit:GetY())
                    newUnit:Convert(unit)
                end
            end
        end
    end
end
LuaEvents.SerialEventUnitCreatedGood.Add(SwapUnit)

Then we need to revise the production popup to reflect the unit that will be built...


EDIT: Never mind, forget all that. This is a much simpler method:
Code:
function ReligionSpecificUnits(playerID, cityID, unitType)
    local majorityReligion = Cities[cityID]:GetReligiousMajority()

-- Turn off the generics for cities with specific religions
    if (majorityReligion == GameInfoTypes["RELIGION_XXX"] or majorityReligion == GameInfoTypes["RELIGION_YYY"]) then
        if (unitType == GameInfoTypes["UNIT_GENERIC1"] or unitType == GameInfoTypes["UNIT_GENERIC2"]) then
            return false
        end
    end

-- Turn off the specifics for all but the correct religion
    if (unitType == GameInfoTypes["UNIT_SPECIFIC_XXX1"]) then
        if not (majorityReligion == GameInfoTypes["RELIGION_XXX"]) then
            return false
        end
    elseif (unitType == GameInfoTypes["UNIT_SPECIFIC_YYY1"]) then
        if not (majorityReligion == GameInfoTypes["RELIGION_YYY"]) then
            return false
        end
    elseif (unitType == GameInfoTypes["UNIT_SPECIFIC_XXX2"]) then
        if not (majorityReligion == GameInfoTypes["RELIGION_XXX"]) then
            return false
        end
    elseif (unitType == GameInfoTypes["UNIT_SPECIFIC_YYY2"]) then
        if not (majorityReligion == GameInfoTypes["RELIGION_YYY"]) then
            return false
        end
    end
    return true
end
GameEvents.CityCanTrain.Add(ReligiousSpecificUnits)

This method intercepts the check to see whether a specific unit can be built in a given city. The default is true, i.e., the city can build the unit. We then have to specify where that is false; i.e., where the generic units aren't appropriate because the city has the corresponding religious majority, or as to the specific units in all cases EXCEPT where the city has the corresponding religious majority.

Your specific units need not be uniques (they could have a different unit class from the generic ones), nor do they need to be set to -1 Cost or anything like that. They're just regular units that can or can't be chosen from the list as appropriate.

EDIT #2: Oops. Needed some small fixes.

Can this code be used for buildings as well? (If yes, I'm asuming all that's needed is to swap out everywhere "unit" or UNIT" is defined, and swap for "building" or "BUILDING"?)
 
I think I may have done something wrong. I have:
Code:
function ReligionSpecificUnits(playerID, cityID, unitType)
    local majorityReligion = Cities[cityID]:GetReligiousMajority()


-- Turn off the specifics for all but the correct religion
    if (unitType == GameInfoTypes["UNIT_SEEKERS_OF_TRUTH"]) then
        if not (majorityReligion == GameInfoTypes["RELIGION_CHANTRY"]) then
            return false
        end
    elseif (unitType == GameInfoTypes["UNIT_LEGION_OF_THE_DEAD"]) then
        if not (majorityReligion == GameInfoTypes["RELIGION_PARAGONS"]) then
            return false
        end
    end
    return true
end
GameEvents.CityCanTrain.Add(ReligiousSpecificUnits)

and I'm wanting to make it so that only those that follow the Chantry can build the Seekers of Truth, and only those that follow the Paragons can build the Legion of the Dead. I just tested it out, and while neither units is build-able in a city with no religion, both can be built in a city that follows the Chantry.

(I can link the full mod if need be)
 
Back
Top Bottom