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!

Sunday, September 26, 2021

Barbells and Dumbells Are More Rare in 41.54 than 41.53

I like making bases in school, and I used to find the occasional barbell and dumbbell in the lockers.

I haven't found them recently though.

Have I just been unlucky lately?  Was I imagining things before?

No.  The distributions have changed.  Here is a clip from the ProceduralDistributions.lua file in 41.54:

    CrateFitnessWeights = {
        rolls = 4,
        items = {
            "DumbBell", 10,
            "BarBell", 4,
            "Hat_Sweatband", 8,
            "Shoes_BlueTrainers", 4,
            "Shoes_RedTrainers", 4,
            "Shoes_TrainerTINT", 8,
            "Shorts_LongSport", 6,
            "Shorts_ShortSport", 8,
            "Socks_Long", 6,
            "Tshirt_Sport", 8,
            "Tshirt_SportDECAL", 6,
            "Vest_DefaultTEXTURE_TINT", 10,
            "Vest_DefaultTEXTURE_TINT", 10,
        },

That is the only place in the 41.54 lua files that mention DumbBells and BarBells.

And where can "CrateFitnessWeights" be found?  If you look in Distributions.lua, you see they are found in the following locations where crates are found:

  • Bedrooms
  • Garages
  • Storage Units


For 41.53, the barbells and dumbells can be found in more locations.  Here's what I see when I grep in 41.53:

grep -ri "barbell"
Distributions.lua:                "BarBell", 0.3,
Distributions.lua:                "BarBell", 0.3,
Distributions.lua:                "BarBell", 0.5,
ProceduralDistributions.lua:                "BarBell", 100,
ProceduralDistributions.lua:            "BarBell", 0.3,
ProceduralDistributions.lua:            "BarBell", 0.3,
ProceduralDistributions.lua:            "BarBell", 0.2,
ProceduralDistributions.lua:            "BarBell", 0.2,
ProceduralDistributions.lua:            "BarBell", 0.3,

If you actually look in the 41.53 Distributions.lua and ProceduralDistributions.lua files, you see them in:

  • Change Room Lockers
  • Police Storage Lockers
  • Security Lockers
  • Fitness Crates (like in 41.54, but with different weights)
  • Lockers
  • Classy Lockers
  • Men's Wardrobes
  • Classy Men's Wardrobes
  • Redneck Wardrobes

(But not Women's Wardrobes - that's a bit sexist, eh?)

So you are less likely to find barbells and dumbbells in 41.54.


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!

Saturday, September 18, 2021

Bandanas Deflect Zombie Attacks 90% of the Time! (Not anymore - fixed in 41.55)

Edit 2021-10-27: Indie Stone released Project Zomboid build 41.56, and at first look I think they've fixed the bug correctly.  I'll write a post that goes into detail soon.

Edit 2021-09-30: In 41.55 they have "fixed" this bug by changing the code so that falling clothing no longer protects you.  They also adjusted some of the chancetofall rates on some helmets so that they never fall.  They have not fixed the bug that essentially made chancetofall actually represent the chance that the item would stay on.  I've covered this in a new post.

Bandanas are the most OP protective clothing in all Project Zomboid!  They deflect almost 90% of front attacks from a single zombie!

Glasses deflect almost 45% of all front attacks from a single zombie, and you can START with glasses in vanilla Project Zomboid.  That's better than taking the (fixed in 41.54) Thick Skinned trait!

I'll have more examples of the total protection you get from some vanilla Project Zomboid clothing at the end, but let me explain how this works.

I covered head protection in another blog post, and at the time I thought that helmets/hats could only fall off during an attack to the head or neck.  I didn't look carefully enough at the code, because masks and glasses also can fall off, and all of those clothing items can fall off during ANY zombie attack.

When any piece of clothing falls off, that means that a zombie attack hit your character and the falling clothing nullified the attack.  

So at every opportunity, you should wear a hat/helmet, glasses, and a bandana mask.  Each of those has a chance of completely nullifying a zombie attack that would otherwise hit you!


BodyDamage Class

Let's go back to the BodyDamage class to demonstrate how hats/helmets/glasses/bandanas can fall and nullify "successful" zombie attacks:

        if (Rand.Next(100) > n4) {
            n = 1;
            boolean bl4 = false;
            if (this.getParentChar().helmetFall(n3 == BodyPartType.ToIndex(BodyPartType.Neck) || n3 == BodyPartType.ToIndex(BodyPartType.Head))) {
                return false;
            }

In the BodyDamage class, once your character is hit by a zombie attack one of the first things the code does is check getParentChar().helmetFall.

It sends TRUE if the target of the attack was either the neck or head.  In my earlier post, I misinterpreted this to mean that helmetFall was only called if the attack was the neck or head - I was wrong about that.

And you can see here that if helmetFall is true, this method returns false, meaning the attack is nullified and no damage is recorded on your character.


IsoGameCharacter Class

So let's take a closer look at helmetFall, in the IsoGameCharacter class:

    public boolean helmetFall(boolean bl, String string) {
        IsoPlayer isoPlayer = (IsoPlayer)Type.tryCastTo((Object)this, IsoPlayer.class);
        boolean bl2 = false;
        InventoryItem inventoryItem = null;
        IsoZombie isoZombie = (IsoZombie)Type.tryCastTo((Object)this, IsoZombie.class);
        if (isoZombie != null && !isoZombie.isUsingWornItems()) {
	    // Code skipped because this is only for Zombies
        } else if (this.getWornItems() != null && !this.getWornItems().isEmpty()) {
            for (int i = 0; i < this.getWornItems().size(); ++i) {
                WornItem wornItem = this.getWornItems().get(i);
                InventoryItem inventoryItem3 = wornItem.getItem();
                String string2 = wornItem.getLocation();
                if (!(inventoryItem3 instanceof Clothing)) continue;
                int n = ((Clothing)inventoryItem3).getChanceToFall();
                if (bl) {
                    n += 40;
                }
                if (inventoryItem3.getType().equals(string)) {
                    n = 100;
                }
                if (((Clothing)inventoryItem3).getChanceToFall() <= 0 || Rand.Next((int)100) < n) continue;
                IsoFallingClothing isoFallingClothing = new IsoFallingClothing(this.getCell(), this.getX(), this.getY(), PZMath.min((float)(this.getZ() + 0.4f), (float)((float)((int)this.getZ()) + 0.95f)), Rand.Next((float)-0.2f, (float)0.2f), Rand.Next((float)-0.2f, (float)0.2f), inventoryItem3);

Remember that bl was TRUE if the target was the head or neck.  But even attacks on other body parts make it to this part of the code (with bl set to FALSE).

We skip over some code because it only applies to zombies.  Maybe I'll go over that in another post.  It actually doesn't protect the zombies as much from attacks.

Then we go through every piece of clothing worn by the character.  This includes every piece of clothing, not just helmets and hats.

We skip the code if it isn't a piece of clothing, and continue to the next piece of clothing.

Then we get the ChanceToFall, and call that n.

(As an example, let's look at the Bandana Mask, which has a chance to fall of 5 - at the end of this entry I have a list of all vanilla items that can fall, with their ChanceToFall value).

Here we see bl used.  If the target was the head or neck, bl is true, and n is increased by 40 (to 45 for our example).

And then there is the decision line - if a random d100 roll is less than n (45 if the target was the head/neck, 5 otherwise), then we continue.  Continue means we go back and look at the next item of clothing in the list - nothing happens, and we go to the next piece of clothing worn by the character.

And if we get past the d100 check, the clothing falls.  In the Bandana example, this means that Bandanas stay on 5% of the time, and when the target is the head or neck they stay on 45% of the time.

As I've reported before, this is a bug.  If the Bandana Mask should have the low 5% chance of falling, not staying on.  So this line should read Rand.Next((int)100>= n.


How Much Protection Is This?

In my last post, I calculated that a single zombie attacking from the front targets the neck 6.4% of the time, and the head or neck 12.7% of the time.  There's a little rounding there, so I'm just going to use the slightly more accurate figure of 6.36% for head hits (and neck hits) in this example.

Bandanas (as a mask or on your head) have a ChanceToFall of 5, meaning there is a 55% chance to fall from a head/neck attack, and a 95% chance to fall for other body parts.  If a head is targeted 6.36% of the time and the neck is targeted 6.36% of the time, then all other body parts together are targeted 87.28% of the time.

  • Head/Neck:  55% chance to fall * 12.72 chance of being targeted
  • Other: 95% chance to fall * 87.28% chance of being targeted
Total Protection:  89.912%

Glasses have a ChanceToFall of 50.

Total Protection = 10% * .1272 + 50% * .8728 = 44.912%

Reflective Ski Glasses (and Safety Goggles and some other eyewear) have a ChanceToFall of 20.

Total Protection = 40% * .1272 + 80% * .8728 = 71.312%

Baseball Caps (and Tinfoil Hats) have a ChanceToFall of 80, so the protection isn't great.  If you are hit in the head or neck while wearing them, they never fall off.  Plus they don't have any bite or scratch protection.  Still they, are better than nothing because when other parts of your body are hit, they might fall off.  Plus, you can start with them.

Total Protection = 20% * .8728 = 17.456%

Calculating the protection from items like the Riot Helmet was a little more complicated, because even when they don't fall off of your head they offer 100% protection to the head, so that modifies the protection a little.  I ran some items through my spreadsheet (with slightly more accurate numbers):

  • Riot Helmet - 96.52% Total Protection
  • Football Helmet - 94.65% Total Protection
  • Military Helmet - 88.01% Total Protection
  • Hard Hat or Firefighter Helmet - 78.73% Total Protection

Also remember that these values combine if you are wearing more than one.

If you are wearing a head bandana + a bandana mask + regular glasses, the total protection is actually

1 - (1-0.89912) * (1-0.89912) * (1-0.44912) = 99.44%!

If you start with a baseball cap + regular glasses, you get to start with a protection value of

1 - (1-0.17456) * (1-0.44912) = 54.53%!

 

Comments On the Bug

So one of the reasons this is such a big deal is because of the bug I mentioned.  Things that should stay on almost all the time are falling off, and items that should fall easily are staying on.  

I suspect that Indie Stone will rework this mechanic a little more than just fixing the specific bug I mentioned.  If they ONLY fix my bug, baseball caps will become pretty OP, falling off 100% of the time the head or neck is attacked, and 80% of the time other body parts are attacked.  But at least baseball caps are supposed to be fairly easy to fall off in real life.  Bugged Football Helmets fall off to 57% of head/neck attacks, and 97% to other attacks.

Whatever work they do to this part of the code, I'll cover it when it changes.  I suspect glasses will still be worth wearing, even if they aren't as powerful as they are now. 


Vanilla Items That Can Fall

Here's a list of vanilla items that can fall, and the base % chance that they will fall.  Remember, while this bug still exists, it is actually the base % chance that they won't fall.  For the best benefit right now, pick items with a low percentage.  Once the bug is fixed, I'll update this blog post and tell everyone to pick items with a high percentage chance of falling.

Military Helmet                       10
Bandana (Head)                         5
Baseball Cap                          80
KY Baseball Cap                       80
Baseball Cap                          80
Army Baseball Cap                     80
Kentucky Baseball Helmet              10
Riverside Rangers Baseball Helmet     10
Z Hurricanes Baseball Helmet          10
Beanie Hat                            40
Beret                                 80
Army Beret                            80
Bicycle Helmet                        20
Chef Hat                              80
Cowboy Hat                            80
Crash Helmet                          10
Motorcycle Helmet                      3
Police Motorcycle Helmet              10
USA Crash Helmet                      10
Ear Muffs                             80
Ear Protectors                        80
Fast Food Server Hat                  80
Ice Cream Server Hat                  80
Spiffo's Server Hat                   80
Fedora                                80
Black Band Fedora                     80
Firefighter Helmet                    20
Football Helmet                        3
Tartan Golf Cap                       80
Golf Cap                              80
Hard Hat                              20
Mining Helmet                         20
Hockey Helmet                         10
Jockey Helmet - 1                     10
Jockey Helmet - 2                     10
Jockey Helmet - 3                     10
Jockey Helmet - 4                     10
Jockey Helmet - 5                     10
Jockey Helmet - 6                     10
Police Trooper Hat                    70
Police Deputy Hat                     70
Raccoon Hat                           30
Ranger Hat                            70
Riding Helmet                         10
Riot Helmet                            1
Santa Hat                             80
Green Santa Hat                       80
Shower Cap                            80
Airforce Helmet                       10
Spiffo Suit Head                      10
Summer Hat                            80
Medical Cap                           80
Sweatband                             50
Wedding Veil                          80
Welder Mask                           30
Winter Hat                            40
Woolly Hat                            40
Bucket Hat                            80
Bonnie Hat                            80
Visor                                 80
Peaked Army Cap                       80
Bandana (Tied)                         5
Bandana (Face)                         5
Newspaper Hat                         80
Party Hat with Stars                  80
Coloured Party Hat                    80
Tin Foil Hat                          80
Hockey Goalie Mask                    30

Reflective Ski Sunglasses             20
Aviator Glasses                       50
Glasses                               50
Reading Glasses                       50
Safety Goggles                        20
Shooting Glasses                      20
Ski Goggles                           20
Sunglasses                            50
Swimming Goggles                      30

Although Riot Helmets, Football Helmets, and Motorcycle Helmets fall more easily than the bandana, they already offered 100% protection to the head, so I don't consider them as OP as bandanas.


Thanks for reading!  If you see any mistakes, or if you have a question you'd like me to answer, let me know in the comments!

Friday, September 17, 2021

How Often Do Zombies Hit the Neck?

From what I've seen on streams, lots of Project Zomboid characters run around without any neck protection.  Neck protection is not as common as head or torso protection, so this is understandable.  What I wanted to find out was how often zombie attacks hit a character's neck.  Fortunately, this is in the code.

The BodyDamage class handles this, in the AddRandomDamageFromZombie method.

There are a number of variables can affect this, so let's start by making a few assumptions:

  • The zombie is standing (not crawling)
  • The zombie is attacking you from the front (not from behind)
  • Only 1 zombie is attacking

        if (!isoZombie.bCrawling) {
            n3 = Rand.Next((int)BodyPartType.ToIndex(BodyPartType.Hand_L), (int)(BodyPartType.ToIndex(BodyPartType.Groin) + 1));
            f = 10.0f * (float)n7;
            if (bl2) {
                f += 5.0f;
            }
            if (bl3) {
                f += 2.0f;
            }
            if (bl2 && (float)Rand.Next((int)100) < f) {
                n3 = BodyPartType.ToIndex(BodyPartType.Neck);
            }
            if (n3 == BodyPartType.ToIndex(BodyPartType.Head) || n3 == BodyPartType.ToIndex(BodyPartType.Neck)) {
                n = 70;
                if (bl2) {
                    n = 90;
                }
                if (bl3) {
                    n = 80;
                }
                if (Rand.Next((int)100) > n) {
                    bl = false;
                    while (!bl) {
                        bl = true;
                        n3 = Rand.Next((int)BodyPartType.ToIndex(BodyPartType.MAX));
                        if (n3 != BodyPartType.ToIndex(BodyPartType.Head) && n3 != BodyPartType.ToIndex(BodyPartType.Neck)) continue;
                        bl = false;
                    }
                }
            }
        }

First it picks a random body part between the Left Hand and the Groin.  You can see in the BodyPartType class that there are 11 body parts in between those, and the neck is one of them.

bl2 and bl3 come from being attacked from behind or the side, so we ignore that for this example.  You may notice that if you are attacked from behind (bl2), there is a chance that the hit location gets reassigned to the neck.  So there are more neck hits when you are attacked from behind.  But like I said, we're ignoring attacks from behind or the side for this example.

So if the randomly determined body part is the head or neck, there is a 30% (random 100 roll > 70) chance the target will be forcibly reassigned to be something other than the head or neck.  So there is a 70% chance a neck attack will stay a neck attack.

So, the result is (1/11) * .7 = 6.4%

6.4% of the time, a single zombie attacking from the front will attack the neck.

Because it might be important later (stay tuned), let me also figure out how often a single zombie attacking from the front will attack the neck OR head...

(2/11) * .7 = 12.7%

12.7% of the time, a single zombie attacking from the front will attack the neck or head.


No earth shattering news here, but it's a little interesting.  And I plan on using this information in a future investigation.


Thanks for reading!  If you see a mistake I've made, or if you have any questions you'd like me to answer by looking in 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...