Showing posts with label lua. Show all posts
Showing posts with label lua. Show all posts

Sunday, October 10, 2021

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 smashed window, just when picking up broken glass off the ground.

You deal with broken glass whenever you smash a window.  There's a bit of broken glass remaining in the window frame, and glass on the ground by the smashed window.  You might want to get rid of that broken glass, and I used to think that required leather gloves to prevent injuries.  I was wrong.

Picking Up Broken Glass - Wear Gloves (Any)

You might want to pick up the glass off the ground.  When you or a zombie walks over it, it makes a sound.  Some people strategically place the glass as a sort of alarm for when zombies are near.  Some people just don't like the mess.

If you're not wearing gloves when you pick up the glass, you may injure yourself.

It starts in ISWorldObjectContextMenu.  If you are interacting with brokenGlass, you start executing some code:

    -- broken glass interaction
--    if brokenGlass and playerObj:getClothingItem_Hands() then
	if brokenGlass then
--        local itemName = playerObj:getClothingItem_Hands():getName()
--        if itemName ~= "Fingerless Gloves" then
            context:addOption(getText("ContextMenu_PickupBrokenGlass"), worldObjects, ISWorldObjectContextMenu.onPickupBrokenGlass, brokenGlass, player)
--        end
    end

You notice that there's some commented out code where Fingerless Gloves don't protect you when you pick up broken glass.  I don't know if this is old code or just code that hasn't been implemented yet.  If they put this functionality in (not here, though), I hope they include surgical gloves and long gloves.

The code calls onPickupBrokenGlass

ISWorldObjectContextMenu.onPickupBrokenGlass = function(worldobjects, brokenGlass, player)
    local playerObj = getSpecificPlayer(player)
    if luautils.walkAdj(playerObj, brokenGlass:getSquare()) then
        ISTimedActionQueue.add(ISPickupBrokenGlass:new(playerObj, brokenGlass, 100));
    end
end

That calls ISPickupBrokenGlass, and in the perform() method we see what happens:

function ISPickupBrokenGlass:perform()
	-- add random damage to hands if no gloves (done in pickUpMoveable)
	if ISMoveableTools.isObjectMoveable(self.glass) then
        local moveable = ISMoveableTools.isObjectMoveable(self.glass)
        moveable:pickUpMoveable( self.character, self.square, self.glass, true )
    end

	-- needed to remove from queue / start next.
	ISBaseTimedAction.perform(self)
end

That calls ISMoveableTools.isObjectMoveable to retrieve the glass (a ISMoveableSpriteProps object) (code not included here), and then actually picks it up in ISMoveableSpriteProps:

        elseif self.isoType == "IsoBrokenGlass" then
            -- add random damage to hands if no gloves
            if not _character:getClothingItem_Hands() and ZombRand(3) == 0 then
                local handPart = _character:getBodyDamage():getBodyPart(BodyPartType.FromIndex(ZombRand(BodyPartType.ToIndex(BodyPartType.Hand_L),BodyPartType.ToIndex(BodyPartType.Hand_R) + 1)))
                handPart:setScratched(true, true);
                -- possible glass in hands
                if ZombRand(5) == 0 then
                    handPart:setHaveGlass(true);
                end
            end

This means that ANY clothing on the hands protects you when picking up broken glass.  So you don't need leather gloves - fingerless gloves or surgical gloves or whatever will protect your hands completely from the broken glass.

Removing Broken Glass From a Window - Don't Need Gloves

You should all already know that if you climb through a broken window, there's a good chance you're going to get injured from the broken glass.  So you want to remove the glass from the window before you climb through, especially if it's a window you're going to be climbing through regularly (like at your base).

I haven't found any code that might injure a character removing glass from a broken window.

function ISRemoveBrokenGlass:perform()
	self.window:removeBrokenGlass()

	-- needed to remove from queue / start next.
	ISBaseTimedAction.perform(self)
end

Unlike what we found in the ISPickupBrokenGlass:perform() method, nothing here has any opportunity to injure the character.  If the removeBrokenGlass() method could injure the character, it would pass in the character object.

Afterword

So wearing any gloves is better than wearing no gloves.  Sure, long gloves don't offer any scratch or bite resistance, but they'll protect your hands when you pick up glass.


Thanks for reading!  If you notice a mistake or have any questions I might answer by looking through the code, let me know!

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!

Thursday, September 30, 2021

Can You Drink Tainted Water?

Can you drink tainted water safely?  Yes

If you are drinking it directly from the tainted source (i.e. river or lake), you will barely get sick (probably not even enough to notice it) and will completely recover before you're thirsty again.

If you are drinking it from a bottle, it can be safe if you only drink a small amount.  The more you drink, the sicker you will get.  If you drink too much, you can die.


Drinking Tainted Water From a River

When you drink water from a river (or lake) the code starts in the ISWorldObjectContextMenu.lua file, in onDrink:

ISWorldObjectContextMenu.onDrink = function(worldobjects, waterObject, player)
    local playerObj = getSpecificPlayer(player)
	if not waterObject:getSquare() or not luautils.walkAdj(playerObj, waterObject:getSquare(), true) then
		return
	end
	local waterAvailable = waterObject:getWaterAmount()
	local thirst = playerObj:getStats():getThirst()
	local waterNeeded = math.floor((thirst + 0.005) / 0.1)
	local waterConsumed = math.min(waterNeeded, waterAvailable)
	ISTimedActionQueue.add(ISTakeWaterAction:new(playerObj, nil, waterConsumed, waterObject, (waterConsumed * 10) + 15, nil));
end

When you drink from a river, you have all the water you need to we're mainly concerned about your thirst and waterNeeded.

Thirst is a value between 0 and 1, and represents how thirsty you are.

WaterNeeded takes your thirst (0-1) to calculate the amount of water you need, so becomes a value between 0 and 10.

WaterConsumed, in this case, is equal to waterNeeded

We send waterConsumed to ISTakeWaterAction.lua, inside ISTakeWaterAction:perform()

        if self.waterObject:isTaintedWater() then
            self.character:getBodyDamage():setPoisonLevel(self.character:getBodyDamage():getPoisonLevel() + self.waterUnit);
        end

We see the consequences of drinking the tainted water.  The amount you drank (10 units max) is added to the current poison level.

That means, if you were completely healthy before, you can have a maximum PoisonLevel of 10.  What does that mean?

We can look in the BodyDamage class, in the update() method, to see:

        if (this.PoisonLevel > 0.0f) {
            if (this.PoisonLevel > 10.0f && this.getParentChar().getMoodles().getMoodleLevel(MoodleType.Sick) >= 1) {
                f += 0.0035f * Math.min(this.PoisonLevel / 10.0f, 3.0f) * GameTime.instance.getMultiplier();
            }
            float f3 = 0.0f;
            if (this.getParentChar().getMoodles().getMoodleLevel(MoodleType.FoodEaten) > 0) {
                f3 = 1.5E-4f * (float)this.getParentChar().getMoodles().getMoodleLevel(MoodleType.FoodEaten);
            }
            this.PoisonLevel = (float)((double)this.PoisonLevel - ((double)f3 + ZomboidGlobals.PoisonLevelDecrease * (double)GameTime.instance.getMultiplier()));
            if (this.PoisonLevel < 0.0f) {
                this.PoisonLevel = 0.0f;
            }
            this.setFoodSicknessLevel(this.getFoodSicknessLevel() + this.getInfectionGrowthRate() * (float)(2 + Math.round(this.PoisonLevel / 10.0f)) * GameTime.instance.getMultiplier());
            if (this.getFoodSicknessLevel() > 100.0f) {
                this.setFoodSicknessLevel(100.0f);
            }
        }

Being healthy and drinking directly from the river gives us a maximum PoisonLevel of 10, so there is no effect here.

And you can see here that the PoisonLevel goes down.

So unless you were already poisoned, you will take no damage.  And by the time you are naturally thirsty again (unless you do something to increase your thirst like eat something that increases your thirst), your poison level will have dropped to 0, so it will be safe for you to drink from the river again.

So drinking from a tainted natural water source, like a river, is safe so long as you were already healthy.  You will recover from the minor negative effect before you are thirsty again.


Drinking Tainted Water From a Bottle

Let's quickly compare this to drinking tainted water from a bottle, which starts in ISInventoryPaneContextMenu.lua:

ISInventoryPaneContextMenu.onDrinkForThirst = function(waterContainer, playerObj)
    local thirst = playerObj:getStats():getThirst()
    local units = math.min(math.ceil(thirst / 0.1), 10)
    units = math.min(units, waterContainer:getDrainableUsesInt())
    ISInventoryPaneContextMenu.transferIfNeeded(playerObj, waterContainer)
    ISTimedActionQueue.add(ISDrinkFromBottle:new(playerObj, waterContainer, units))
end

It calculates the number of units in much the same way as drinking from the river, except it limits the amount you can drink to how much is available in the container.  But the maximum is 10 units, so let's look at that.  

We move to ISDrinkFromBottle:

function ISDrinkFromBottle:drink(food, percentage)
    -- calcul the percentage drank
    if percentage > 0.95 then
        percentage = 1.0;
    end
    local uses = math.floor(self.uses * percentage + 0.001);

    for i=1,uses do
        if not self.character:getInventory():contains(self.item) then
            break
        end
        if self.character:getStats():getThirst() > 0 then
            self.character:getStats():setThirst(self.character:getStats():getThirst() - 0.1);
            if self.character:getStats():getThirst() < 0 then
                self.character:getStats():setThirst(0);
            end
            if self.item:isTaintedWater() then
                self.character:getBodyDamage():setPoisonLevel(self.character:getBodyDamage():getPoisonLevel() + 10);
            end
            self.item:Use();
        end
    end

end

Here you can see that it adds 10 to the PoisonLevel for EACH USE of the bottle.  In this example, that's 10 times because that's how much water you want to drink at maximum thirst.  That's 100 poison, which is enough to kill you.  But you can also see that if you hadn't been very thirsty, and had only wanted 1 unit, that would have corresponded to a poison value of 10 which we saw earlier is easily handled.


What About Prone To Illness or Weak Stomach?

It looks to me like Prone To Illness only affects colds (so not food sickness) and mortality length.

And it looks to me like Weak Stomach only affects foods you eat (so not water).

So I don't think either of those traits affects drinking tainted water.


Extra:  Lemongrass

I'll also mention here that in the IsoGameCharacter class it shows that when you eat lemongrass (ReduceFoodSickness level of 12), it reduces both your food sickness level and your poison level.  In case you want to carry some lemongrass for emergencies.

        if (this.BodyDamage.getFoodSicknessLevel() > 0.0f && (float)food.getReduceFoodSickness() > 0.0f && this.effectiveEdibleBuffTimer <= 0.0f) {
            float f4 = this.BodyDamage.getFoodSicknessLevel();
            this.BodyDamage.setFoodSicknessLevel(this.BodyDamage.getFoodSicknessLevel() - (float)food.getReduceFoodSickness() * f);
            if (this.BodyDamage.getFoodSicknessLevel() < 0.0f) {
                this.BodyDamage.setFoodSicknessLevel(0.0f);
            }
            float f5 = this.BodyDamage.getPoisonLevel();
            this.BodyDamage.setPoisonLevel(this.BodyDamage.getPoisonLevel() - (float)food.getReduceFoodSickness() * f);
            if (this.BodyDamage.getPoisonLevel() < 0.0f) {
                this.BodyDamage.setPoisonLevel(0.0f);
            }
            this.effectiveEdibleBuffTimer = this.Traits.IronGut.isSet() ? Rand.Next((float)80.0f, (float)150.0f) : (this.Traits.WeakStomach.isSet() ? Rand.Next((float)120.0f, (float)230.0f) : Rand.Next((float)200.0f, (float)280.0f));
        }


Thanks for reading!  If you see any mistakes or if you have any questions you'd like me to answer by looking at the code, let me know!

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...