Showing posts with label mods. Show all posts
Showing posts with label mods. Show all posts

Sunday, October 3, 2021

How Strong Are My Walls? (How Wall Health is Calculated in Build 41.55)

Build 41.55 changed how wall health is calculated.  There are three main differences:

  • The health increase per level was reduced from 40 to 20 (under default settings)
  • The level at which the wall could first be built is no longer a factor in calculating health
  • Upgraded walls no longer have more health than originally built walls

If you want more details about how wall health was calculated in 41.54 and earlier, check out my old blog post.

Here are the factors that influence wall health:

  • Wall Type
  • Character Skill Level
  • Handy Trait (only for log walls)
  • For upgrades, the health of the original wall
  • Sandbox Option: Player-Built Construction Strength

For the purposes of this entire post, let's assume the Sandbox Options are all set to default.

The strongest wall is the Level 2 Metal Wall, built with a metal frame, but considering the scarcity of the resources required to build them, I think most people will stick with wooden walls.  Log walls (without the handy trait) are only sometimes stronger than wooden walls, and require much more wood (but fewer nails).

How Wall Health is Determined in the Code

As in older builds, the code for wood (and log) walls starts in ISBuildMenu.lua.  Metal walls are handled in ISBlacksmithMenu.lua.  Metal walls are handled similarly to wood walls in lua, and use the same code when we get to the java, so we'll only look at the wood wall code here.

Once we get past determining the skills and materials required to build the wall, the stats for the wall frame gets set in onWoodenWallFrame:

ISBuildMenu.onWoodenWallFrame = function(worldobjects, sprite, player)
-- sprite, northSprite, corner
    local wall = ISWoodenWall:new(sprite.sprite, sprite.northSprite, sprite.corner, ISBuildMenu.isNailsBoxNeededOpening(2));
    wall.canBarricade = false
    wall.name = "WoodenWallFrame";
    -- set up the required material
    wall.modData["xp:Woodwork"] = 5;
    wall.modData["need:Base.Plank"] = "2";
    wall.modData["need:Base.Nails"] = "2";
    wall.health = 50;
    wall.player = player;
    getCell():setDrag(wall, player);
end

So the frame's health is 50.  

Wall frames are built in ISBuildMenu like a lot of constructions (stairs, crates, rain collectors).  Unlike in older builds, there is no health difference between upgrading a wall and building the wall outright.  They are handled in ISBuildMenu:

ISBuildMenu.onMultiStageBuild = function(worldobjects, stage, item, player)

The definitions of these are in the scripts, in multistagebuild.txt.

Here is the definition of a level 1 wall:

    multistagebuild CreateWoodenWall_1
    {
        PreviousStage:WoodenWallFrame;MetalWallFrame,
        Name:WoodenWallLvl1,
        TimeNeeded:250,
        BonusHealth:400,
        SkillRequired:Woodwork=2,
        ItemsRequired:Base.Plank=2;Base.Nails=4,
        ItemsToKeep:Base.Hammer,
        Sprite:walls_exterior_wooden_01_44,
        NorthSprite:walls_exterior_wooden_01_45,
        CraftingSound:Hammering,
        ID:Create Wooden Wall Lvl 1,
        XP:Woodwork=10,
    }

It's pretty clear what most of the items in this definition means.  BonusHealth is what is important for this discussion.

You can look at multistagebuild.txt if you want to see all the values, but here are the health values for the walls and upgrades:

Bonus Health

Level 1 Wall: 400
Level 2 Wall: 500
Level 3 Wall: 600
Level 1 Metal Wall: 650
Level 2 Metal Wall: 750

Upgrade from L1 Wall to L2 Wall: +100
Upgrade from L1 Wall to L3 Wall: +200
Upgrade from L2 Wall to L3 Wall: +100
Upgrade from L1 Metal Wall to L2 Metal Wall: +100

The code that handles setting the stats of these walls isn't in the lua, it is in the java.  It is in the MultiStageBuilding class.  This is used for both wood and metal walls:

        public void doStage(IsoGameCharacter isoGameCharacter, IsoThumpable isoThumpable, boolean bl) {
            int n = isoThumpable.getHealth();
            int n2 = isoThumpable.getMaxHealth();
            String string = this.sprite;
            if (isoThumpable.north) {
                string = this.northSprite;
            }
            IsoThumpable isoThumpable2 = new IsoThumpable(IsoWorld.instance.getCell(), isoThumpable.square, string, isoThumpable.north, isoThumpable.getTable());
            isoThumpable2.setCanBePlastered(this.canBePlastered);
            if ("doorframe".equals(this.wallType)) {
                isoThumpable2.setIsDoorFrame(true);
                isoThumpable2.setCanPassThrough(true);
                isoThumpable2.setIsThumpable(isoThumpable.isThumpable());
            }
            int n3 = this.bonusHealth;
            switch (SandboxOptions.instance.ConstructionBonusPoints.getValue()) {
                // Code edited out by Ed because we are using default values
            }
            Iterator<String> iterator = this.perks.keySet().iterator();
            int n4 = 20;
            switch (SandboxOptions.instance.ConstructionBonusPoints.getValue()) {
                // Code edited out by Ed because we are using default values
            }
            int n5 = 0;
            if (this.bonusHealthSkill) {
                while (iterator.hasNext()) {
                    String string2 = iterator.next();
                    n5 += isoGameCharacter.getPerkLevel(PerkFactory.Perks.FromString(string2)) * n4;
                }
            }
            isoThumpable2.setMaxHealth(n2 + n3 + n5);
            isoThumpable2.setHealth(n + n3 + n5);

When you build a level 1 wood wall from a wood frame here are the steps for setting the health:

  • Get the health of the wood frame (this includes any damage taken)
  • Get the maximum health of the wood frame (we know this is 50)
  • Get the Bonus Health value of the level 1 wall (we know this is 400)

Then we look at the "perks" which is basically the skills required to build this wall.  In this case, it is carpentry.  We take the character's level, multiplied by 20.  

Then for the maximum health we add up the max health, bonus health, and multiplied level modifier.

And for the current health, we add up health, bonus health, and multiplied level modifier.

Remember that when you are upgrading a wall, it is starting with the health of the original wall.  So if you build a L1 wall when you have L2 Carpentry, it will start out at 490 HP.  If you wait til L10 to upgrade the wall to L2, it will still only have 590 HP.

Note that except for log walls, the handy trait does not factor in calculating wall health.

What About Metal Walls and Log Walls?

Although metal walls get their start in the ISBlacksmithMenu.lua code, they also end up in the MultiStageBuilding class so they have the same calculations, with different numbers.  

By the way, metal frames have a health of 120 compared to the wood frame health of 50.  And you can build wood walls over metal frames.  So if you want to build strongest wall, start with the strongest frame.

Log walls fall under what appears to be the old system of calculating health.  Much of the code for the old system of constructing wood walls is still there, just unreachable because of comments.  When we want to calculate a log wall's health:

-- return the health of the new wall, it's 200 + 100 per carpentry lvl
function ISWoodenWall:getHealth()
    if self.sprite == "carpentry_02_80" then -- log walls are stronger
	    return 400 + buildUtil.getWoodHealth(self);
    else
        return 200 + buildUtil.getWoodHealth(self);
    end
end

The comment there is in the 41.55 version of the file, and is wrong.

-- give the health of the item, depending on you're carpentry level + if you're a construction worker
buildUtil.getWoodHealth = function(ISItem)
	if not ISItem or not ISItem.player then
		return 100;
	end
	local playerObj = getSpecificPlayer(ISItem.player)
	local health = (playerObj:getPerkLevel(Perks.Woodwork) * 50);
	if playerObj:HasTrait("Handy") then
		health = health + 100;
	end
	return health;
end

We can see here that for the log walls, 50 per carpentry level gets added to the health, with an additional 100 if the character is handy.  So a handy character with level 10 carpentry makes 1000 health log walls.  

Although handy isn't currently being used to calculate wall health, maybe they'll add it back in in the future.

Mod: How Strong Is That Wall?



After my first post on wall health, I created a mod that lets you see how strong your walls are.  Look at the mod through this link!


Thanks for reading this!  If you see any mistakes, or if you have any questions that I might be able to answer by looking at the code, let me know!

Friday, September 3, 2021

Why My Mods Didn't Support Controllers

Twitch streamer Djackzz discovered that my mods (in particular Playable Arcade Machines, Simple Playable Pianos, and Silly Object Sounds) didn't work with the controller.  He discovered this while playing co-op on his stream.

Since not many people play Project Zomboid co-op, I didn't consider this a high priority.  Plus, before 41.51 I couldn't get my Xbox One controller to work with Project Zomboid (I use Linux as my OS).

Since 41.53 has been out for over a month now I decided it was time to find my Xbox One controller and fix this bug.  It took me over an hour to find my controller, because instead of being in the first places I looked (with my cables, with my extra computer parts, in a bin with assorted tech junk), it was on top of my Xbox.

Playable Arcade Machines was my first mod, and one of the most important things my mod does is create a menu item to "Play" the particular game.  I don't remember where I looked for examples of that, but the good examples basically looked like this one that I'm taking from the ISBBQMenu:

function ISBBQMenu.OnFillWorldObjectContextMenu(player, context, worldobjects, test)

	if test and ISWorldObjectContextMenu.Test then return true end

	local bbq = nil

	local objects = {}
	for _,object in ipairs(worldobjects) do
		local square = object:getSquare()
		if square then
			for i=1,square:getObjects():size() do
				local object2 = square:getObjects():get(i-1)
				if instanceof(object2, "IsoBarbecue") then
					bbq = object2
				end
			end
		end
	end

	if not bbq then return end

Not knowing anything about Project Zomboid mods I guessed the following:

  • player - Was an object related to the player
  • context - had to do something with the "context menu" I was trying to add my commands to
  • worldobjects - some kind of key value pair object containing a bunch of objects (probably near the character)
  • test - test variable I didn't need to worry about

Briefly, this code cycles through all of the worldobjects to see if any of them are on squares that also contain a BBQ.  Once the code figures out which object is the BBQ, it can then add the menu item for the BBQ Info.

But I also saw some mod examples that didn't go through this whole process to find the object it wanted to activate.  Instead of looping through a bunch of worldobjects, they just picked the first one:

	local thisObject = worldobjects[1];

When I tested out using just this in my Playable Arcade Machines mod, it worked.  And from a time-complexity standpoint, it was much better.  I didn't know if worldobjects was large or small.  If it were large, it seemed like a huge waste of time to search through all of the values in worldobjects when worldobjects[1] had what I wanted!  I mean, just the name "worldobjects" implies it could be everything in the world!  So I did the same for my piano mod, and for my silly object sounds mod.

But for controller it didn't work, I would later find.  A mouse can usually pick the exact object I want to activate, so worldobjects[1] usually works.  

But the controller can't know the exact pixel you've selected - you can only stand near the object you want to activate.  You might be standing in the right spot, and pointing to the right object.  But it's really hard to tell.  Through experimentation it looks to me like worldobjects[1] is the ground square in front of the character all the time, and I don't really think worldobjects[1] will ever be the arcade machine (or piano) in front of the character.  Also in my experiments, it looks like it only pulls around 5 objects into worldobjects, so it isn't examining too many more squares.

So for my Simple Playable Pianos mod, I changed it to loop through all the worldobjects:

	for _,object in ipairs(worldobjects) do
		local square = object:getSquare()
		if square then
			for i=1,square:getObjects():size() do
				local thisObject = square:getObjects():get(i-1)
				
				local properties = thisObject:getSprite():getProperties()

				if properties ~= nil then
					local groupName = nil
					local customName = nil
					local thisSpriteName = nil
					
					local thisSprite = thisObject:getSprite()
					if thisSprite:getName() then
						thisSpriteName = thisSprite:getName()
						--print("PseudoEdSPP: Sprite Name is " .. spriteName)
					end
					
					if properties:Is("GroupName") then
						groupName = properties:Val("GroupName")
						--print("PseudoEdSPP: Sprite GroupName: " .. groupName);
					end
					
					if properties:Is("CustomName") then
						customName = properties:Val("CustomName")
						--print("PseudoEdSPP: Sprite CustomName: " .. customName);
					end
					
					-- For Pianos, Custom Name = "Piano".
					-- In vanilla PZ, GroupName = "Western", but leave that open for modders
					if customName == "Piano" then
						piano = thisObject;
						spriteName = thisSpriteName;
					end
				end
			end
		end
	end

So now it follows these basic steps:

  • for each of the world objects
  • select the square the object is on
  • check all the objects on that square, looking for a piano

And if if finds the object is a piano, it stores it as piano so I can add the menu items later...

It might be cycling through a few more objects than necessary for mouse users, but this is necessary for controllers.  I'll be fixing the rest of my mods in the next few days.


Thanks for reading!  If you notice any mistakes, or if you have any questions you'd like me to answer, please let me know!


I'm trying to do some modding so I may not be posting as often as I have been recently.  But maybe I will post.  I'm trying to figure it out.



Do Gloves Protect You From Broken Glass?

Yes, gloves protect you from handling broken glass - any pair of gloves.  But gloves are not needed when removing broken glass from a smashe...