--[[
	Depending on the chosen ActionEvent, you can hook up to certain parts of the animation player, regardless if
	your own mod has initiated the animations first. ActionEvents are overarching functions that deals with
	more general logic and allows you to code in logic even if the animations are not owned by you. 

	ActionEvents.Perform - Runs whenever the animation successfully completes
	ActionEvents.Start - Runs whenever the animation starts
	ActionEvents.Stop - Runs whenever the animation is cancelled
	ActionEvents.Update - Runs whenever the animation is ongoing
	ActionEvents.WaitToStart - Runs whenever the animation is waiting to start

	-- @author QueuedResonance 2022, SolarEdge 2024 - 2025
]]

local ZomboWin = require("ZomboWin/ZomboWin");
local zwAniHan = require("ZomboWin/AnimationHandler");
local tLUA = require("ZomboWin/Temptation");
local zlM = require("ZomboWin/ZLMain");
local ActionEvents = ZomboWin.AnimationHandler.ActionEvents;
local ZomboWinDefeatConfig = ZomboWinDefeatConfig
require("ZomboWin/BodyCount");
require("ZomboWin/zombieUpdate");

local banditsEnabled = false;
if getActivatedMods():contains("\\Bandits2") == true then
	banditsEnabled = true;
	require("Bandits/BanditUpdate");
	require("Bandits/Bandit");
	require("Bandits/BanditBrain");
	require("Bandits/ZombiePrograms");
	require("Bandits/BanditPrograms");
	require("Bandits/BanditUtils");
	require("Bandits/BanditPlayer");
end

-- Drinker traits settings
local isSober = false;					--noob drinker bool, is set to 'true' when alcohol <= 0.
local isSoberMultiplier = 2.0;			--noob drinker multiplier. Takes first alcohol change and multiply by (this), then sets 'isSober' 'false'.
local lightDrinkerMultiplier = 0.0004;		--light drinker, not a real multiplier! Adds (this) while alcohol > 0.
local heavyDrinkerMultiplier = 0.006;		--heavy drinker, not a real multiplier! Adds -(this) while alcohol > 0.

-- Innocent trait settings
local bNoPantsPanic = .09;
local bNoShirtsPanic = .09;

local clothingHandler = {};
clothingHandler.checkShirts = {"Dress","ShortSleeveShirt","Tshirt","Shirt","Sweater","SweaterHat","JacketHat","Jacket","BathRobe","FullTop","FullSuit","FullSuitHead",};
clothingHandler.checkPants = {"Skirt","Pants","Dress","BathRobe",};
clothingHandler.defeatImminent = {"Underwear","UnderwearBottom","UnderwearTop","BodyCostume","TankTop","Legs5","Skirt","Pants","Legs1","Dress","Torso1Legs1","ShortSleeveShirt","Tshirt","Shirt","Sweater","SweaterHat","JacketHat","Jacket","BathRobe","FullTop","TorsoExtra","FullSuit","Mask","Hat","MaskEyes","MaskFull","FullHat","FullSuitHead",}

-- I will not implement food as male sex toys-regardless how you use them. You can do that yourself. 
local mST = {"HottieZ",}
local fST = {"HottieZ", "Zucchini_st", "Eggplant_st", "Corn_st","Banana_st",}

-- zombieUselessList; zombieToUselessList;
zUL = {};
zTUL = {};

local traitValues = {
	["cute"] = -25,
	["Fugly"] = 15,
	["Lucky"] = -15,
	["Unlucky"] = 15,
	["Everydrop"] = -10,
	["Virgin"] = -10,
	["Inexperienced"] = -3,
	["Experienced"] = -5,
	["Nymphomaniac"] = -8,
	["Innocent"] = -5,
	["Lewd"] = -2,
	["youngBuck"] = -8,
	["AARP"] = 8,
	["inPrime"] = -5,
	["spunkDefault"] = 0,
	["lowEffort"] = 5,
	["unkempt"] = 20,
	["dislikesShowers"] = 12,
	["neatTidy"] = -6,
	["lovesClean"] = -10,
	["Athletic"] = -10,
	["Fit"] = -8,
	["Smoker"] = 3,
	["smokesAfter"] = 3,
	["Stout"] = -2,
	["Strong"] = -5,
	["Feeble"] = 3,
	["Weak"] = 6,
	["Overweight"] = 4,
	["Obese"] = 10,
	["Unfit"] = 3,
	["Underweight"] = 2,
	["Very Underweight"] = 5,
	["Emaciated"] = 10,
}

function cFactCalculate(player, traitList)
	player:getModData().cFact = 0;
	
	for trait, value in pairs(traitList) do
        if player:HasTrait(trait) then
            player:getModData().cFact = player:getModData().cFact + value;
        end
    end
end

function offerSmex(player, bandit)	---to be used in performed()
	if not player:getModData().isOffering or player:getModData().isOffering == nil then return; else player:getModData().isOffering = false; end
	
	local rewardOption = (SandboxVars.ZomboWin.banditReward);
	local maxReward = -1;
	
	if rewardOption == 1 then
		maxReward = -1;
	elseif rewardOption == 2 then
		maxReward = 3;
	elseif rewardOption == 3 then
		maxReward = 5;
	end
	--[[
	> the count is reset but only for one bandit per [reset time] code needs to be adjusted
	> zombie:ModData() is reset every login, making abusing this reward system very easy or even unintentional
	]]
	--print("reward option: " .. maxReward);
	
	bandit:getModData().sFoods = bandit:getModData().sFoods or 0;	---counts up to max
	bandit:getModData().sFoodsNew = bandit:getModData().sFoodsNew or 0;		---counter for reset
	
	if maxReward ~= -1 and bandit:getModData().sFoods <= maxReward then 
		bandit:getModData().sFoods = (bandit:getModData().sFoods + 1) 
		--print("offerings: " .. bandit:getModData().sFoods .. " /" .. maxReward);
	end

	if maxReward ~= -1 and bandit:getModData().sFoods >= maxReward then 
		bandit:getModData().wasSubdued = true;
		slayerSpeak(bandit, "banRewardF");
		if bandit:getModData().sFoods > maxReward then 
			--print("maxed offerings: " .. bandit:getModData().sFoods .. " /" .. maxReward);
			return; 
		end
	end
	
	--print("bandit's reward count: " .. bandit:getModData().sFoods);
	
	local function reducer(item, amount)
		local hunger = item:getHungChange();
		local thirst = item:getThirstChange();
		local unhappy = item:getUnhappyChange();
		local boredom = item:getBoredomChange();
		local calories = item:getCalories();
		local carbs = item:getCarbohydrates();
		local fats = item:getLipids();
		local protein = item:getProteins();
		local stress = item:getStressChange();
		
		if hunger ~= nil then item:setHungChange(hunger/amount); end
		if thirst ~= nil then item:setThirstChange(thirst/amount); end
		if unhappy ~= nil then item:setUnhappyChange(unhappy/amount); end
		if boredom ~= nil then item:setBoredomChange(boredom/amount); end
		if calories ~= nil then item:setCalories(calories/amount); end
		if carbs ~= nil then item:setCarbohydrates(carbs/amount); end
		if fats ~= nil then item:setLipids(fats/amount); end
		if protein ~= nil then item:setProteins(protein/amount); end
		if stress ~= nil then item:setStressChange(stress/amount); end
	end
	
	local itemsT1 = {};
	--- T1 = tier 1, in case of future expansions
	table.insert(itemsT1, "Base.DogfoodOpen");
	table.insert(itemsT1, "Base.TunaTinOpen");
	table.insert(itemsT1, "Base.OpenBeans");
	table.insert(itemsT1, "Base.CannedCornedBeefOpen");
	table.insert(itemsT1, "Base.CannedChiliOpen");
	table.insert(itemsT1, "Base.CannedBologneseOpen");
	table.insert(itemsT1, "Base.CannedCarrotsOpen");
	table.insert(itemsT1, "Base.CannedCornOpen");
	table.insert(itemsT1, "Base.CannedMushroomSoupOpen");
	table.insert(itemsT1, "Base.CannedPeasOpen");
	table.insert(itemsT1, "Base.CannedPotatoOpen");
	table.insert(itemsT1, "Base.CannedSardinesOpen");
	table.insert(itemsT1, "Base.CannedTomatoOpen");
	table.insert(itemsT1, "Base.TinnedSoupOpen");
	table.insert(itemsT1, "Base.CannedFruitCocktailOpen");
	table.insert(itemsT1, "Base.CannedPeachesOpen");
	table.insert(itemsT1, "Base.CannedPineappleOpen");
	table.insert(itemsT1, "Base.JuiceBox");
	--table.insert(itemsT1, "Base.Pop2");		--broken b42
	table.insert(itemsT1, "Base.Crisps");
	table.insert(itemsT1, "Base.Crisps2");
	table.insert(itemsT1, "Base.BeerCan");
	table.insert(itemsT1, "Base.BeerBottle");
	--table.insert(itemsT1, "Base.WhiskeyFull");	---broken for b42
	table.insert(itemsT1, "Base.PopBottle");
	table.insert(itemsT1, "Base.Cigarettes");

	local lengthT1 = 0;
	local inventory = player:getInventory();

	for k, v in pairs(itemsT1) do lengthT1 = lengthT1 + 1; end
	local x = (ZombRand(lengthT1) + 1);
	if x == 0 then x = 1 end;
	local item = (itemsT1[x]);
	if item ~= nil then 
		local cFact = player:getModData().cFact or 0;
		local chance = ZombRand(60 + cFact)		--- positive cFact increases chance of not rolling 0.
		--print("cFact chance rolled: " .. chance .. " of " .. 60 + cFact);

		if item ~= "Base.Cigarettes" then
			local item = inventory:AddItem(item);
			
			if chance < 0 then chance = 0; end
			if chance ~= 0 then
				local bonus = 0;
				if cFact < 0 then bonus = ((cFact * 3) / 100); if bonus < -1 then bonus = -1; end end 
				local depletion = (ZombRand(2,5) + bonus)	--- cFact affects this value too!
				if depletion <= 1.09 then depletion = 1.1; end	--- because you can't divide by 0.
				--print("depletion: " .. depletion);
				reducer(item, depletion);
				--HaloTextHelper.addTextWithArrow(player, getText("Leftovers: ") .. item:getName(), true, HaloTextHelper.getColorGreen());
				----HaloTextHelper.addText(player, getText("Leftovers: ") .. item:getName(), --HaloTextHelper.getColorGreen());
			else
				--HaloTextHelper.addTextWithArrow(player, getText("Received: ") .. item:getName(), true, HaloTextHelper.getColorGreen());
			end
		else
			local loop = ZombRand(1,4);
			local item = " Cigarettes";
			
			for i = 1, loop do
				inventory:AddItem("Base.Cigarettes", 1);
			end
			--HaloTextHelper.addTextWithArrow(player, getText("Received: ") .. tostring(loop) .. getText(item), true, HaloTextHelper.getColorGreen());
		end
	end	
	---- additional codes here ---- codes below give "1" count of the item no matter the number within () but working if using to only give one! yay!
	--inventory:AddItem("Base.Nails", 1);				
end

function disabler(player, bool)	--- Called from action.Perform(), action.Stop(), and ZomboWinTimed.lua
	player:setIgnoreContextKey(bool);
    player:setIgnoreInputsForDirection(bool);
    player:setAuthorizeMeleeAction(not bool);
	player:setAuthorizeShoveStomp(not bool);
    player:setIgnoreAimingInput(bool);
	player:setIgnoreMovement(bool);
	player:setBannedAttacking(bool);
    player:setCanShout(not bool);
    player:setBlockMovement(bool);
    player:setAllowRun(not bool);
    player:setAllowSprint(not bool);
    player:setCollidable(not bool);
	
	player:getModData().ZomboWinSexScene = (bool);
end

function zwPlayerReset(player)	--- Called from action.Perform(), action.Stop(), and ZomboWinTimed.lua
	player:getModData().isPartner = nil;
	player:getModData().isOffering = false;
	player:getModData().isConsent = false;
	player:getModData().zwisPerformed = false;
end
--------------------------------------------------------------------------------------------------------------------------------

	-------------------------------------------------- ActionStageEvent end functions --------------------------------------------------
	function isCursed(player)	--- Death occurs only from zombie defeats ---
		if player:HasTrait("itFollows") then 
			local partner = player:getModData().isPartner;
			if partner ~= nil then
				if instanceof(partner, "IsoZombie") then
					local isBandit = partner:getVariableBoolean("Bandit")
					if not isBandit then
						local isSubdued = partner:getModData().isSubdued;
						if not isSubdued then
							player:setHealth(0)
							return "DEAD";
						end
					end
				end
			end
		end
	end
	function isVirgin(player)
		if player:HasTrait("Virgin") then
			local bodyDamage = player:getBodyDamage()
			local unhappiness = bodyDamage:getUnhappynessLevel()
			local isConsent = player:getModData().isConsent;
			local hasPartner = player:getModData().isPartner;
			local hasPerformed = player:getModData().zwisPerformed;

			if hasPartner ~= nil then 
				player:getTraits():remove("Virgin")
				player:getTraits():add("Inexperienced")
				if not isConsent then
					bodyDamage:setUnhappynessLevel(unhappiness + 100); 
				elseif hasPerformed then
					bodyDamage:setUnhappynessLevel(unhappiness - 100); 
				end
			end
		end
	end
	function isNecro(player)
		local bodyDamage = player:getBodyDamage()
		local unhappiness = bodyDamage:getUnhappynessLevel()
		local hasPerformed = player:getModData().zwisPerformed;
		if player:HasTrait("Necrophiliac") and hasPerformed then
			local partner = player:getModData().isPartner;
			if partner ~= nil then
				if partner:isZombie() then
					local isBandit = partner:getVariableBoolean("Bandit")
					if not isBandit then
						--HaloTextHelper.addTextWithArrow(player, getText("Necrophiliac"), true, HaloTextHelper.getColorGreen());
						bodyDamage:setUnhappynessLevel(unhappiness - 30)
					end
				end
			end
		end
	end
	function isSwallower(player)
		local isMainHeroFemale = player:isFemale()
		local isConsent = player:getModData().isConsent;
		if player:HasTrait("Everydrop") then 
			local partner = player:getModData().isPartner;
			if partner ~= nil then
				if instanceof(partner, "IsoZombie") then
					local isSubdued = partner:getModData().isSubdued;
					if isSubdued or isConsent then
						if isMainHeroFemale then	---only females coz I dont have MaleGulp01---
							getSoundManager():PlayWorldSound('FeGulp01', true, player:getCurrentSquare(), 0, 4, 1, false)
							local currentHunger = player:getStats():getHunger()
							player:getStats():setHunger(currentHunger - 0.040)
						end
					end
				end
			end
		end
	end
	function isSmokesAfter(player)
		if player:HasTrait("smokesAfter") then 
			local stress = player:getStats():getStressFromCigarettes();
			local addedStress = 0.25;
			
			if stress < addedStress then
				player:getStats():setStressFromCigarettes(addedStress);
			else
				player:getStats():setStressFromCigarettes(stress + addedStress);
			end
		end
	end
	function isPacify(player)
		if player:HasTrait("pacify") then
			local partner = player:getModData().isPartner;
			if partner ~= nil then
				if instanceof(partner, "IsoZombie") then
					local isBandit = partner:getVariableBoolean("Bandit")
					if not isBandit then
						--HaloTextHelper.addTextWithArrow(player, getText("Pacified"), true, HaloTextHelper.getColorGreen());
						getSoundManager():PlayWorldSound('pacify01', true, player:getCurrentSquare(), 0, 4, 1, false)
						partner:setUseless(true);
						partner:getModData().isSubdued = true;
					elseif Bandit.IsHostile(partner) then 
						--HaloTextHelper.addTextWithArrow(player, getText("Pacified"), true, HaloTextHelper.getColorGreen());
						getSoundManager():PlayWorldSound('pacify01', true, player:getCurrentSquare(), 0, 4, 1, false)						
						Bandit.SetHostile(partner, false);
					end
				end
			end
		end
	end
	function isSuccubus(player)	--- Supposed to kill ALL including friendlies...not players ---
		local isMainHeroFemale = player:isFemale()
		if player:HasTrait("succubus") then
			local partner = player:getModData().isPartner;
			if partner ~= nil then
				if instanceof(partner, "IsoZombie") then
					if isMainHeroFemale then
						getSoundManager():PlayWorldSound('succubus01', true, player:getCurrentSquare(), 0, 4, 1, false)
					else
						getSoundManager():PlayWorldSound('succubus02', true, player:getCurrentSquare(), 0, 4, 1, false)
					end
					partner:Kill(player);
				end
			end
		end
	end
	function isSTD(player)	--- Excluded from functionLists, WIP ---
		if player:HasTrait("gonorrhea") then
			zombie:SetOnFire();
			zombie:setOnFire(true);
		end
	end
	function isTraumatized(player)	--- does Traumatized trait reset? ---
		local sexTrauma = (SandboxVars.ZomboWin.tLength);
		local isConsent = player:getModData().isConsent;
		local hasPartner = player:getModData().isPartner;
		if sexTrauma ~= 1 then 
			local partner = player:getModData().isPartner;
			if partner ~= nil then
				if instanceof(partner, "IsoZombie") then
					if not isConsent then
						if not player:HasTrait("Lewd") and not player:HasTrait("Nymphomaniac") and not player:HasTrait("dpress") then
							local isBandit = partner:getVariableBoolean("Bandit")
							if not isBandit then
								if not player:HasTrait("Necrophiliac") then
									if not player:HasTrait("Traumatized") then
										--HaloTextHelper.addTextWithArrow(player, getText("Traumatized"), true, HaloTextHelper.getColorRed());
										player:getTraits():add("Traumatized");
									end
								end
							else
								if not player:HasTrait("Traumatized") then
									--HaloTextHelper.addTextWithArrow(player, getText("Traumatized"), true, HaloTextHelper.getColorRed());
									player:getTraits():add("Traumatized");
								end
							end
						end
					end
				end
			end
		end
		if sexTrauma == 1 then 
			if target:HasTrait("Traumatized") then
				target:getTraits():remove("Traumatized");
			end
		end
	end

	local allFunctionLists = {isCursed, isVirgin, isNecro, isSwallower, isSmokesAfter, isPacify, isSuccubus, isTraumatized};
	local negativeFunctionLists = {isCursed, isVirgin, isTraumatized};
	
	local function callEndFunctions(player)
		if player:getModData().zwisPerformed then
			for _, func in ipairs(allFunctionLists) do
				local result = func(player)
				if result == "DEAD" then
					return;
				end
			end
		else
			for _, func in ipairs(negativeFunctionLists) do
				local result = func(player)
				if result == "DEAD" then
					return;
				end
			end
		end
	end
	--- note: "if instanceof(partner, "IsoZombie") then" == "if partner:isZombie() then" --- idk why i used instanceof lol
	
	local function cleanup(zActor, zombie)
		zActor:setX(zActor:getX() + 99)

		local function _deletezActor(tick)
			ISTimedActionQueue.clear(zActor)
			zActor:setInvincible(false)
			zActor:setInvisible(false)
			zActor:setGhostMode(false)
			zActor:removeFromWorld()
			zActor:removeFromSquare()
			Events.OnTick.Remove(_deletezActor)
		end

		--zombie:getModData().isSubdued = zombie:getModData().isSubdued or false;	---maybe dont need?
		zombie:getModData().ZomboWinSexScene = false;
		if zombie:getModData().isSubdued == true then
			zombie:setX(zombie:getX() - 99)
			zombie:setCanWalk(false);
			zombie:setInvincible(false)
			zombie:setInvisible(false)
			zombie:setNoDamage(false)
		else
			zombie:setX(zombie:getX() - 99)
			zombie:getModData().isWatching = false;
			zombie:setUseless(false)
			zombie:setCanWalk(true);
			zombie:setInvincible(false)
			zombie:setInvisible(false)
			zombie:setNoDamage(false)
		end
		
		Events.OnTick.Add(_deletezActor)
	end
	--------------------------------------------------------------------------------------------------------------------------------

table.insert(ActionEvents.WaitToStart, function(action)
	local character = action.character;
	local otherCharacter = character:getModData().zActor;
	local hasPartner = character:getModData().isPartner;	---in zDefeat, this is a zombie
	
	if otherCharacter ~= nil then
		if hasPartner:isZombie() and not hasPartner:getVariableBoolean("bandit") then
			hasPartner:setInvisible(true);
			hasPartner:getModData().isWatching = true;
			otherCharacter:setGhostMode(true);
			otherCharacter:setInvisible(false);
			hasPartner:setX(hasPartner:getX() + 99);
			hasPartner:setInvisible(true);
			otherCharacter:setInvisible(false);
		end
	end
	
end)

table.insert(ActionEvents.Start, function(action)
	local character = action.character
	local otherCharacter = character:getModData().zActor;
	local isOffering = character:getModData().isOffering;
	local hasPartner = character:getModData().isPartner;
	local isConsent = character:getModData().isConsent;
	
	print("zD cp. 0");
	if otherCharacter ~= nil then
		print("zD cp. 1");
		if hasPartner:isZombie() and not hasPartner:getVariableBoolean("bandit") then
			print("setX + 99");

		else
			print("negative on the zombie");
		end
	else
		print("negative on otherCharacter");
	end
	----------------------------------------------- Consent Logic: inStart -----------------------------------------------
	if not isConsent then	--- consent is also set true in ZLMain ---
		if isOffering or hasPartner == nil then
			character:getModData().isConsent = true;
		else
			if hasPartner ~= nil then
				if instanceof(hasPartner, "IsoZombie") then
					if not hasPartner:getModData().isSubdued then
						character:getModData().isConsent = false;
					else
						character:getModData().isConsent = true;
					end
				end
			end
		end
	end
	-----------------------------------------------------------------------------------------------------------------------

		------------------- Bystander Logic: inStart -------------------
		if #zUL > 0 then
			for i = #zUL, 1, -1 do
				local enemy = zUL[i]
				if enemy ~= zombie then
					enemy:setUseless(false);
					enemy:getModData().isWatching = true;
				end
			end
		end
		zUL = {};
		zTUL = {};
		
		local enemies = character:getSpottedList()
		local maxDistance = 4

		for i = 0, enemies:size() - 1 do
			local enemy = enemies:get(i)
			local isBandit = enemy:getVariableBoolean("Bandit");
			if enemy:isZombie() and enemy:DistTo(character) <= maxDistance and not isBandit and hasPartner ~= enemy then
				enemy:setStaggerBack(true)
				--enemy:setKnockedDown(true)
			end
		end
		---------------------------------------------------------------
	
	------------------- cFact logic: inStart -------------------
	if isOffering then	
		cFactCalculate(character, traitValues);		--- resets cFact .. anything affecting cFact MUST go below this line of code!

		------------------- cFact dirty and bloody: inStart -------------------
		local filth = character:getHumanVisual();
		local dirtL = 0;
		local bloodL = 0;
		local totalFilth = 0;
		
		local function round(number)
			return math.floor(number + 0.5);
		end
		
		for i = 0, BloodBodyPartType.MAX:index() - 1 do
			local bloodBodyPartType = BloodBodyPartType.FromIndex(i)
			dirtL = dirtL + filth:getDirt(bloodBodyPartType);
		end
		
		for i = 0, BloodBodyPartType.MAX:index() - 1 do
			local bloodBodyPartType = BloodBodyPartType.FromIndex(i)
			bloodL = bloodL + filth:getBlood(bloodBodyPartType);
		end
		
		totalFilth = round(dirtL + bloodL);
		character:getModData().cFact = character:getModData().cFact + totalFilth;
		--[[
		print("dirt level: " .. dirtL);
		print("blood level: " .. bloodL);
		print("total filth: " .. totalFilth);
		print("accum cFact: " .. player:getModData().cFact);
		]]
	end
	------------------------------------------------------------
		
		disabler(character, true);
end)

table.insert(ActionEvents.Update, function(action)
	local character = action.character;
	local otherCharacter = character:getModData().zActor;

	local stats = character:getStats();
	local bodyDamage = character:getBodyDamage();
	local unhappiness = bodyDamage:getUnhappynessLevel();
	local boredom = bodyDamage:getBoredomLevel();
	local wetness = bodyDamage:getWetness();
	
	local isConsent = character:getModData().isConsent;
	local isMainHeroFemale = character:isFemale();
	local hasPartner = character:getModData().isPartner;
	--print("isOffering: " .. tostring(isConsent));

		------------------- Health stabilizer: inUpdate -------------------
		local currentHP = character:getBodyDamage():getHealth();
		--print("inUpdate: currentHP: " .. currentHP);
		
		if character:getModData().animHP == nil then character:getModData().animHP = currentHP; end
		
		if character:getModData().animHP > currentHP then	
			local difHP = character:getModData().animHP - currentHP;
			character:getBodyDamage():AddGeneralHealth(difHP);
		end
		-------------------------------------------------------------------

	------------------- Sexual Endurance debuff: inUpdate -------------------
	local endurance = character:getStats():getEndurance();
	local enduranceLoss = 0.00010;
	--should rework this for both sexes!
	if character:HasTrait("youngBuck") then
		enduranceLoss = 0.00004;
	elseif character:HasTrait("AARP") then
		enduranceLoss = 0.00014;
	elseif character:HasTrait("inPrime") then
		enduranceLoss = 0.00008;
	elseif character:HasTrait("spunkDefault") then
		enduranceLoss = 0.00010;
	elseif character:HasTrait("lowEffort") then
		enduranceLoss = 0.00002;
	end
	
	-- Sexual Endurance: female modifier
	if isMainHeroFemale then enduranceLoss = (enduranceLoss / 4); end
	
	character:getStats():setEndurance(endurance - enduranceLoss);
	if character:getStats():getEndurance() < 0.0 then
		character:getStats():setEndurance(0.0);
	end
	-------------------------------------------------------------------------
	
		------------------- Stress and Unhappiness debuff: inUpdate -------------------
		if not isConsent and not character:HasTrait("Lewd") and not character:HasTrait("Nymphomaniac") then
			local bodyDamage = character:getBodyDamage()
			local unhappiness = bodyDamage:getUnhappynessLevel()
			local stats = character:getStats()
			local stressAdd = 0;
			local unhappinessAdd = 0;
			
			if character:HasTrait("Wimp") then
				stressAdd = 0.006;
				unhappinessAdd =  0.08;
			else
				stressAdd = 0.003;
				unhappinessAdd =  0.04;
			end
			
		-- Stress and Unhappiness: male modifier
			if not isMainHeroFemale then stressAdd = (0.0); unhappinessAdd = (0.0); end
			
		-- Stress and Unhappiness: trait modifier
			if character:HasTrait("Masochist") or character:HasTrait("Nymphomaniac") then stressAdd = 0.0015; unhappinessAdd = 0.02; end
			
			stats:setStress(stats:getStress() + stressAdd);
			bodyDamage:setUnhappynessLevel(unhappiness + unhappinessAdd);
		end
		-------------------------------------------------------------------------------
	
	--------------------------------- Pain debuff, Groin: inUpdate --------------------------------------
	if not isConsent then
		local Groin = character:getBodyDamage():getBodyPart(BodyPartType.FromString("Groin"))
		local pain = 0.03;
		-- Default pain is: 0.03
		-- Why no switch in lua!?
		if character:HasTrait("Virgin") then
		pain = 0.08;
		elseif character:HasTrait("Inexperienced") then
		pain = 0.04;
		elseif character:HasTrait("Experienced") then
		pain = 0.02;
		end
		
		if character:HasTrait("Lewd") or character:HasTrait("Nymphomaniac") then
			pain = 0.01;
		end
		
		-- Pain: male modifier
		if not isMainHeroFemale then pain = 0; end
		
		Groin:setAdditionalPain(Groin:getAdditionalPain() + pain)
		if Groin:getAdditionalPain() >= 100 then
			Groin:setAdditionalPain(100)
		end
	end
	----------------------------------------------------------------------------------------------------

		--------------------------------------------------------- Sickness debuff ---------------------------------------------------------
		
		local maxSick = SandboxVars.ZomboWin.dSickness

		if hasPartner ~= nil then
			if maxSick ~= 1 and hasPartner:isZombie() and not hasPartner:getVariableBoolean("Bandit") then
				local bodyDamage = character:getBodyDamage()
				local sickness = bodyDamage:getFoodSicknessLevel()
				local defeatSickness = 0

				-- Determine additional sickness based on traits
				local traitSicknessModifiers = {
					unkempt         = 0.07,
					dislikesShowers = 0.04,
					neatTidy        = 0.01,
					lovesClean      = 0.005,
				}

				for trait, value in pairs(traitSicknessModifiers) do
					if character:HasTrait(trait) then
						defeatSickness = value
						break
					end
				end

				-- Default value if no trait matched
				if defeatSickness == 0 then
					defeatSickness = 0.03
				end

				-- Thresholds by sickness level
				local sicknessThresholds = {
					[2] = 49,
					[3] = 74,
					[4] = 89,
					[5] = 99,
				}

				if not character:HasTrait("unblessing") then
					local threshold = sicknessThresholds[maxSick]
					if threshold and sickness <= threshold then
						bodyDamage:setFoodSicknessLevel(sickness + defeatSickness)
					end
				end
			end
		end
		----------------------------------------------------------------------------------------------------------------------------------

	------------------- Bystander Logic: inUpdate -------------------
	local enemies = character:getSpottedList()
	local maxDistance = 4

	for i = 0, enemies:size() - 1 do
		local enemy = enemies:get(i)
		local isBandit = enemy:getVariableBoolean("Bandit");
		if enemy:isAlive() and enemy:isZombie() and enemy:DistTo(character) <= maxDistance and not isBandit and hasPartner ~= enemy then
			if enemy:DistTo(character) <= (maxDistance/2) then
				enemy:setStaggerBack(true);
			end
			if not zTUL[enemy] and not enemy:isUseless() then
				enemy:getModData().isWatching = true;
				--enemy:setUseless(true); --- applied elsewhere
				table.insert(zUL, enemy);
				zTUL[enemy] = true;
			end
			--enemy:setKnockedDown(true);
		end
	end
	--------------------------------------------------------

		-------------------------------------------------- Misc: inUpdate --------------------------------------------------
		if character:HasTrait("Sexaddict") then
			stats:setStress(stats:getStress() - 0.002)
			bodyDamage:setUnhappynessLevel(unhappiness - 0.005)
		else
			stats:setStress(stats:getStress() - 0.001)
			bodyDamage:setUnhappynessLevel(unhappiness - 0.0025)
		end

		--- Decrease boredom
		bodyDamage:setBoredomLevel(boredom - 0.005)
		--- Increase wetness ... maybe sweat, maybe something else is wet, who knows?
		if wetness < 40 then
			bodyDamage:setWetness(wetness + 0.1);
		end
		--------------------------------------------------------------------------------------------------------------------
end)

table.insert(ActionEvents.Perform, function(action)
    local character = action.character
	local otherCharacter = character:getModData().zActor;
	local pHand = character:getPrimaryHandItem();
	--local sHand = character:getSecondaryHandItem();	--- not yet implemented
	local stats = character:getStats()
	local bodyDamage = character:getBodyDamage()
	local unhappiness = bodyDamage:getUnhappynessLevel()
	local boredom = bodyDamage:getBoredomLevel()
	--local wetness = bodyDamage:getWetness()
	local stress = stats:getStress(); --- stats:setStress(stress - 0.0012);
	--local drunk = stats:getDrunkenness(); --- drunken multiplier coming soon
	local isOffering = character:getModData().isOffering;
	local hasPartner = character:getModData().isPartner;

		---------------------- Health stabilizer: inPerform ------------------------
		character:getModData().animHP = nil;
		----------------------------------------------------------------------------
	
	---------------------------- Section: Solo Toys ----------------------------
	if not isOffering and hasPartner == nil then
		local function toyless(player)
			--print("toyless. current boredom: " .. boredom);
			bodyDamage:setBoredomLevel(boredom - 20);
			stats:setStress(stress - 15);
			if not player:isFemale() then tCounterReset(player); end
		end
		------------------------------- if toy used -------------------------------
		local function checkItemInList(itemUsed)
			--print("checkItem called");
			local var = {};
			local usingToy = false;
			if character:isFemale() then var = fST; else var = mST; end
			--print("female check, passed. " .. tostring(var));
			for i, item in ipairs(var) do
				--print("iterated item: " .. item);
				if item == itemUsed then
					usingToy = true;
					--print("Item found: " .. item)
					--HaloTextHelper.addTextWithArrow(character, getText("IGUI_ZW_toysatisfied"), true, HaloTextHelper.getColorGreen());
					--print("toyed. current boredom: " .. boredom);
					bodyDamage:setBoredomLevel(boredom - 65);
					stats:setStress(stress - 35);
					if var==mST then tCounterReduce(character, 150); end
					break;
				end
			end
			if usingToy == false then
				toyless(character);
				return;
			end
		end

		if pHand ~= nil then 
			local itemUsed = pHand:getType()
			--print("toy used: " .. itemUsed);
			checkItemInList(itemUsed)
		else
			toyless(character);
			--print("pHand nil.");
		end
	end
	-------------------------------------------------------------------------------
	
		---------------------------- Section: Couples ----------------------------
		if isOffering and hasPartner ~= nil then
			increaseCount(character, hasPartner);		---ref: BodyCount
			offerSmex(character, hasPartner); 
			
			if character:HasTrait("Everydrop") then
				--getSoundManager():PlayWorldSound('FeGulp01', true, character:getCurrentSquare(), 0, 4, 1, false);
				stats:setHunger(stats:getHunger() - 0.040);
			end
		end
		---------------------------------------------------------------------------

	-------------------- Bystander Logic: inPerform --------------------
	for i = 1, #zUL do
		local enemy = zUL[i]
		enemy:getModData().isWatching = false;
		enemy:setUseless(false)
		enemy:setCanWalk(true)
		enemy:setNoTeeth(false)
		--table.remove(zUL, i)
	end
	-----------------------------------------------------------------

		------- Clean Up -------
		if otherCharacter ~= nil then
			if hasPartner:isZombie() and not hasPartner:getVariableBoolean("bandit") then cleanup(otherCharacter, hasPartner); end
		end
		character:getModData().zwisPerformed = true;
		callEndFunctions(character);
		disabler(character, false);
		zwPlayerReset(character);
		------------------------
end)

table.insert(ActionEvents.Stop, function(action)
	local character = action.character
	local otherCharacter = character:getModData().zActor;
	local hasPartner = character:getModData().isPartner;
		
		---------------------- Health stabilizer: inStop ------------------------
		character:getModData().animHP = nil;
		----------------------------------------------------------------------------

	-------------------- Bystander Logic: inStop --------------------
	for i = 1, #zUL do
		local enemy = zUL[i]
		enemy:getModData().isWatching = false;
		enemy:setUseless(false)
		enemy:setCanWalk(true)
		enemy:setNoTeeth(false)
		--table.remove(zUL, i)
	end
	-----------------------------------------------------------------

		------- Clean Up -------
		if otherCharacter ~= nil then
			if hasPartner:isZombie() and not hasPartner:getVariableBoolean("bandit") then cleanup(otherCharacter, hasPartner); end
		end
		character:getModData().zwisPerformed = false;
		callEndFunctions(character);
		disabler(character, false);
		zwPlayerReset(character);
		------------------------
end)

local function _onPlayerUpdate(character)
	local stats = character:getStats();
	local bodyDamage = character:getBodyDamage();
	local unhappiness = bodyDamage:getUnhappynessLevel();
	local boredom = bodyDamage:getBoredomLevel();
	local stress = stats:getStress();
	local drunk = stats:getDrunkenness();
	local panic = stats:getPanic();
	local sickness = bodyDamage:getFoodSicknessLevel()
	
	if character:HasTrait("Traumatized") then
		if drunk < 10 then
			if unhappiness <= 60 then
				bodyDamage:setUnhappynessLevel(unhappiness + 0.045);
			elseif unhappiness > 60 and unhappiness < 100 then
				bodyDamage:setUnhappynessLevel(unhappiness + 0.0003); -- @60 slower
			end
			
			if stress <= 0.50 then
				stats:setStress(stress + 0.00045);
			elseif stress > 0.50 and stress < 1.0 then
				stats:setStress(stress + 0.00002); -- @50 slower
			end
			
		elseif drunk > 30 and drunk <= 50 then
			if unhappiness > 0 then
				bodyDamage:setUnhappynessLevel(unhappiness - 0.06);
			end
			if stress > 0 then
				stats:setStress(stress - 0.0006);
			end
		elseif drunk > 50 then
			if unhappiness > 0 then
				bodyDamage:setUnhappynessLevel(unhappiness - 0.12);
			end
			if stress > 0 then
				stats:setStress(stress - 0.0012);
			end
		end
	end
	
	if character:HasTrait("dpress") then
		if unhappiness < 58 then
			bodyDamage:setUnhappynessLevel(unhappiness + 0.005);
		end
	end
	
	if character:HasTrait("unblessing") and bodyDamage:isInfected() then
		bodyDamage:setInfected(false);
		bodyDamage:setInfectionTime(-1);
		--[[
		if bodyDamage:getInfectionTime() == {-1} then
			bodyDamage:setInfectionTime(7);
		end]]
		
		---- seems infection level is predetermined/ calculated.
		---- always turn off infection and do our own calculations!
	end
	if character:HasTrait("unblessing") then
		local ubMS = (ZomboWinDefeatConfig.ModOptions.options.dropdown6 * 1);
		local ubM = 0.0004		---this doesn't seem optimized.
		if ubMS == 1 then ubM = 0.0004; elseif ubMS == 2 then ubM = 0.0002; elseif ubMS == 3 then ubM = 0.0008; end

		bodyDamage:setInfectionLevel(bodyDamage:getInfectionLevel() + ubM);
		---make option for this too
	end
	
	-- Drinker traits
	
		-- Drinker noob
	if character:HasTrait("drinkerNoob") then
		if drunk > 0 and isSober then
			isSober = false;
			stats:setDrunkenness(drunk * isSoberMultiplier);
			if drunk > 100 then
				stats:setDrunkenness(100);
			end
		end
		
		if drunk > 50 and drunk <= 70 and sickness < 50 then
			bodyDamage:setFoodSicknessLevel(sickness + 0.0032)
		elseif drunk > 70 and sickness < 50 then
			bodyDamage:setFoodSicknessLevel(sickness + 0.0064)
		end
		
		-- this must stay down here
		if drunk == 0 then
			isSober = true;
		else
			isSober = false;
		end
	end
	
		-- Drinker lightweight
	if character:HasTrait("drinkerLight") then
		if drunk > 0 then
			stats:setDrunkenness(drunk + lightDrinkerMultiplier);
		end
		if drunk > 50 and drunk <= 70 and sickness < 50 then
			bodyDamage:setFoodSicknessLevel(sickness + 0.0064)
		elseif drunk > 70 and sickness < 50 then
			bodyDamage:setFoodSicknessLevel(sickness + 0.0128)
		end
		--[[if sickness > 50 then
			bodyDamage:setFoodSicknessLevel(50)
		end]]
	end
	
		-- Drinker heavyweight ... Note: You need to set min. cap limiters.
	if character:HasTrait("drinkerHeavy") then
		if drunk > 0 then
			stats:setDrunkenness(drunk - heavyDrinkerMultiplier);
		end
		if drunk > 30 and drunk <= 70 then
			bodyDamage:setFoodSicknessLevel(sickness - 0.0032) --its a game, it doesn't need to make sense.
			stats:setStress(stats:getStress() - 0.0006)
			bodyDamage:setUnhappynessLevel(unhappiness - 0.06)
		elseif drunk > 70 then
			bodyDamage:setFoodSicknessLevel(sickness - 0.0016) --but heres the kicker, being too drunk is bad.
			stats:setStress(stats:getStress() - 0.0003)
			bodyDamage:setUnhappynessLevel(unhappiness - 0.03)
		end
	end
	
	-- Innocent Naked, outdoors
	if character:HasTrait("Innocent") then
		--print("Character has Innocent trait.");
		if character:isOutside() then
			
			--print("Chracter is outside.");
			local player = getPlayer();
			local bNoPants = true;
			local bNoShirts = true;
			local bNoNothing = true;
			local wornItems = player:getWornItems();
			if wornItems then
				--print("Character is wearing something.");
			end
			if wornItems then
		--- Loop through the character's currently equipped items for shirts
				for i = wornItems:size(), 1, -1 do
					local wornItem = wornItems:get(i - 1) --- Get the current worn item

					if wornItem then
						local item = wornItem:getItem() --- Receive item data
						local location = wornItem:getLocation() --- Receive location of where this item is located on the body
						if item:IsClothing() then --- Make sure it is a clothing piece
							--- Iterate through the shirts clothing list
							local isWearingShirt = false
							
							for v = 1, #clothingHandler.checkShirts do
								if bNoShirts then
									local bodyPart = clothingHandler.checkShirts[v]

									if location == bodyPart then
										bNoShirts = false;
										isWearingShirt = true;
									end
								end
							end
							
							if isWearingShirt then
								--print("Character is wearing a shirt.");
								break;
							end
						end
					end
				end
			end			
			if wornItems then
		--- Loop through the character's currently equipped items for pants
				for i = wornItems:size(), 1, -1 do
					local wornItem = wornItems:get(i - 1) --- Get the current worn item

					if wornItem then
						local item = wornItem:getItem() --- Receive item data
						local location = wornItem:getLocation() --- Receive location of where this item is located on the body
						if item:IsClothing() then --- Make sure it is a clothing piece
							--- Iterate through the pants clothing list
							local isWearingPants = false
							
							for v = 1, #clothingHandler.checkPants do
								if bNoPants then
									local bodyPart = clothingHandler.checkPants[v]

									if location == bodyPart then
										bNoPants = false;
										isWearingPants = true;
									end
								end
							end
							
							if isWearingPants then
								--print("Character is wearing a pants.");
								break;
							end
						end
					end
				end
			end
			if wornItems then
		--- Loop through the character's currently equipped items for anything!!!
				for i = wornItems:size(), 1, -1 do
					local wornItem = wornItems:get(i - 1) --- Get the current worn item

					if wornItem then
						local item = wornItem:getItem() --- Receive item data
						local location = wornItem:getLocation() --- Receive location of where this item is located on the body
						if item:IsClothing() then --- Make sure it is a clothing piece
							--- Iterate through the pants clothing list
							local isWearingNothing = false
							
							for v = 1, #clothingHandler.defeatImminent do
								if bNoNothing then
									local bodyPart = clothingHandler.defeatImminent[v]

									if location == bodyPart then
										bNoNothing = false;
										isWearingNothing = true;
									end
								end
							end
							
							if isWearingNothing then
								--print("Character is wearing at least something.");
								break;
							end
						end
					end
				end
			end
			-- Induce panic, no pants
			if bNoPants then
				if panic < 6 then
				stats:setPanic(6);
				end
				if panic < 30 then
					--print("No pants, panic increasing.");
					stats:setPanic(panic + bNoPantsPanic);
				end
			end
			-- Induce panic, no shirts; only females
			if bNoShirts and player:isFemale() then
				if panic < 6 then
				stats:setPanic(6);
				end
				if panic < 30 then
					--print("No shirts and female, panic increasing.");
					stats:setPanic(panic + bNoShirtsPanic);
				end
			end
			-- Induce panic, no anything; defeat imminent!
			if bNoNothing then
				if panic < 99 then
				stats:setPanic(100);
				end
			end
		end
	end
	
	-- Sex Addict + Universal
	if not character:getModData().ZomboWinSexScene then
		if character:HasTrait("Sexaddict") then
			bodyDamage:setUnhappynessLevel(unhappiness + 0.0004)
		else
			bodyDamage:setUnhappynessLevel(unhappiness + 0.0002)
		end
	end
end

--- Hook up event listeners
Events.OnPlayerUpdate.Add(_onPlayerUpdate) 
--Events.OnZombieUpdate.Add(_withSexperiment)	---need to learn profession making in b42