-- @author DoomTero 2026

require "ZomboRut/ZomboRutBodyCount"
require "ZomboRut/ZR_Surrender"

local ZomboRutScene = require "ZomboRut/ZomboRutScene"

local function bumpStat(player, statKey, delta)
	if player == nil then return end
	local stats = player:getStats(); if stats == nil then return end
	local stat = CharacterStat[statKey]; if stat == nil then return end
	local scaled = delta * (stat:getMaximumValue() / 100.0)
	stats:set(stat, stats:get(stat) + scaled)
end

local function bumpPanicFloor(player, floor)
	if player == nil then return end
	local stats = player:getStats(); if stats == nil then return end
	local current = stats:get(CharacterStat.PANIC)
	if current < floor then stats:set(CharacterStat.PANIC, floor) end
end

local function addItemSafe(player, itemFullType)
	if player == nil then return end
	local inv = player:getInventory(); if inv == nil then return end
	pcall(function() inv:AddItem(itemFullType) end)
end

local function addXpBoost(player, perkKey, xp)
	if player == nil then return end
	local perk = Perks[perkKey]; if perk == nil then return end
	pcall(function() player:getXp():AddXP(perk, xp) end)
end

local function hasTrait(player, name)
	return ZR_Trait and ZR_Trait.has and ZR_Trait.has(player, name)
end

local SURRENDER_START = {
	{ "Wimp", function(p)
		bumpStat(p, "PANIC", 30)
		bumpStat(p, "UNHAPPINESS", 10)
	end },
	{ "dominate", function(p)
		bumpStat(p, "PANIC", 20)
	end },
}

local SCENE_START = {
	{ "Innocent", function(p) bumpStat(p, "PANIC", 30) end },
}

local SURRENDER_TICK = {
	{ "Wimp", function(p) bumpPanicFloor(p, 60) end },
}

local SCENE_TICK = {
	{ "Masochist",    function(p) bumpStat(p, "PAIN",   -1); bumpStat(p, "STRESS", -1) end },
	{ "Sexaddict",    function(p) bumpStat(p, "STRESS", -1) end },
	{ "Nymphomaniac", function(p) bumpStat(p, "STRESS", -2); bumpStat(p, "UNHAPPINESS", -1) end },
}

local VICIOUS_HP_FLOOR = 50
local VICIOUS_HP_PER_TICK = 0.025
local function viciousZombieTick(player, active)
	if not hasTrait(player, "viciousZombies") then return end
	if not active or not active.sceneId then return end
	if not active.sceneId:find("Defeat", 1, true) then return end
	local bd = player:getBodyDamage(); if bd == nil then return end
	if bd:getHealth() <= VICIOUS_HP_FLOOR then return end
	if bd.ReduceGeneralHealth then bd:ReduceGeneralHealth(VICIOUS_HP_PER_TICK) end
end

local SCENE_END = {
	{ "Sexaddict",    function(p) bumpStat(p, "UNHAPPINESS", -10); bumpStat(p, "STRESS", -10) end },
	{ "Nymphomaniac", function(p) bumpStat(p, "UNHAPPINESS", -15); bumpStat(p, "STRESS", -15) end },
	{ "Lewd",         function(p) bumpStat(p, "UNHAPPINESS",  -5); bumpStat(p, "STRESS",  -5) end },
	{ "submissive",   function(p) bumpStat(p, "STRESS",       -3) end },
	{ "Masochist",    function(p) bumpStat(p, "UNHAPPINESS",  -5) end },
	{ "Experienced",  function(p) bumpStat(p, "UNHAPPINESS",  -3) end },

	{ "Innocent",     function(p) bumpStat(p, "UNHAPPINESS", 15); bumpStat(p, "STRESS", 10) end },
	{ "Virgin",       function(p) bumpStat(p, "UNHAPPINESS", 25); bumpStat(p, "PAIN",   10) end },
	{ "Inexperienced",function(p) bumpStat(p, "UNHAPPINESS", 10) end },
	{ "dpress",       function(p) bumpStat(p, "UNHAPPINESS",  5) end },

	{ "succubus",     function(p)
		local stats = p:getStats(); if stats == nil then return end
		bumpStat(p, "THIRST", -2)
		bumpStat(p, "HUNGER", -1)
		bumpStat(p, "ENDURANCE", 10)
	end },
	{ "smokesAfter",  function(p) addItemSafe(p, "Base.Cigarettes") end },

	{ "inPrime",      function(p) bumpStat(p, "ENDURANCE", -15) end },
	{ "spunkDefault", function(p) bumpStat(p, "ENDURANCE", -20) end },
	{ "AARP",         function(p) bumpStat(p, "ENDURANCE", -35) end },

	{ "betterAlive",  function(p) addXpBoost(p, "Strength", 5); addXpBoost(p, "Fitness", 5) end, releasedOnly = true },
}

local SURRENDER_END = {
	{ "Wimp", function(p)
		local stats = p:getStats(); if stats == nil then return end
		local panic = stats:get(CharacterStat.PANIC)
		if panic > 80 then
			local traits = p:getCharacterTraits()
			if traits and ZR_Trait and ZR_Trait.Traumatized
				and not ZR_Trait.has(p, "Traumatized")
			then
				traits:add(ZR_Trait.Traumatized)
				p:Say("I won't forget this...")
				if ZR_AutoSurrender and ZR_AutoSurrender.invalidateMods then
					ZR_AutoSurrender.invalidateMods(p)
				end
			end
		end
		bumpStat(p, "STRESS", 5)
	end },
}

local function dispatch(table_, player, ...)
	for i = 1, #table_ do
		local entry = table_[i]
		local trait, fn = entry[1], entry[2]
		if trait == "_always" then
			fn(player, ...)
		elseif hasTrait(player, trait) then
			fn(player, ...)
		end
	end
end

local function dispatchSceneEnd(player, _active, result)
	for i = 1, #SCENE_END do
		local entry = SCENE_END[i]
		local trait, fn = entry[1], entry[2]
		if entry.releasedOnly and result ~= "Released" then
		elseif trait == "_always" then
			fn(player)
		elseif hasTrait(player, trait) then
			fn(player)
		end
	end
end

local function onTick(player)
	if player == nil then return end
	if player.isNpc and player:isNpc() then return end

	if ZomboRutScene.isPlayerInScene(player) then
		local active = ZomboRutScene._active and ZomboRutScene._active[player]
		dispatch(SCENE_TICK, player)
		viciousZombieTick(player, active)
		return
	end

	if ZR_Surrender and ZR_Surrender.isActive and ZR_Surrender.isActive(player)
		and ZR_Surrender.getPhase(player) == "surrender"
	then
		dispatch(SURRENDER_TICK, player)
	end
end

Events.OnPlayerUpdate.Add(onTick)

local function registerSurrenderHooks()
	if not ZR_Surrender or not ZR_Surrender.registerStartCallback then return end
	if ZR_Surrender._traitEffectsRegistered then return end
	ZR_Surrender._traitEffectsRegistered = true

	ZR_Surrender.registerStartCallback(function(player, _st)
		dispatch(SURRENDER_START, player)
	end)

	ZR_Surrender.registerEndCallback(function(player, _st)
		dispatch(SURRENDER_END, player)
	end)
end

registerSurrenderHooks()
Events.OnGameStart.Add(registerSurrenderHooks)

local SUCCUBUS_LIMIT = 4
local SUCCUBUS_WINDOW_HOURS = 12

local function succubusKill(player, partner, result)
	if result ~= "Released" then return end
	if not hasTrait(player, "succubus") then return end
	if partner == nil or not partner.isZombie or not partner:isZombie() then return end
	if partner:getVariableBoolean("Bandit") then return end

	local now = getGameTime():getWorldAgeHours()
	local md = player:getModData()
	local prior = md.succubusKills or {}

	local kept = {}
	for i = 1, #prior do
		if now - prior[i] < SUCCUBUS_WINDOW_HOURS then
			kept[#kept + 1] = prior[i]
		end
	end

	if #kept >= SUCCUBUS_LIMIT then
		md.succubusKills = kept
		return
	end

	kept[#kept + 1] = now
	md.succubusKills = kept

	pcall(function() if partner.Kill                 then partner:Kill(nil, false)        end end)
	pcall(function() if partner.becomeCorpseSilently then partner:becomeCorpseSilently() end end)
end

local SICKNESS_BY_LEVEL = { [1] = 0, [2] = 1, [3] = 3, [4] = 6, [5] = 10 }
local SICKNESS_SOFT_CAP = 85
local function zombieSexSickness(player, partner, result, active)
	if result ~= "Released" then return end
	if partner == nil then return end
	if not partner:isZombie() then return end
	if partner:getVariableBoolean("Bandit") then return end
	if active and active.ctx and active.ctx.condomActive then return end
	local level = (SandboxVars.ZomboRut and SandboxVars.ZomboRut.zombieSexSickness) or 3
	local base  = SICKNESS_BY_LEVEL[level] or 3
	if base == 0 then return end
	local mult = 1.0
	if hasTrait(player, "unkempt") then mult = 3.0
	elseif hasTrait(player, "neatTidy") then mult = 0.5
	elseif hasTrait(player, "lovesClean") then mult = 0.2 end
	local stats = player:getStats(); if stats == nil then return end
	local current = stats:get(CharacterStat.FOOD_SICKNESS)
	if current >= SICKNESS_SOFT_CAP then return end
	local target = current + base * mult
	if target > SICKNESS_SOFT_CAP then target = SICKNESS_SOFT_CAP end
	stats:set(CharacterStat.FOOD_SICKNESS, target)
end

local DEFEAT_GROIN_PAIN_BASE = 30
local function defeatGroinPain(player, active, result)
	if result ~= "Released" then return end
	if not active or not active.sceneId then return end
	if not active.sceneId:find("Defeat", 1, true) then return end
	if hasTrait(player, "Rapist") then return end
	if hasTrait(player, "Lewd")   then return end
	local bd = player:getBodyDamage(); if bd == nil then return end
	local bp = bd:getBodyPart(BodyPartType.Groin); if bp == nil then return end

	local delta = DEFEAT_GROIN_PAIN_BASE
	if hasTrait(player, "Masochist") then delta = delta * 0.1
	elseif hasTrait(player, "Virgin") then delta = delta * 2.0
	elseif hasTrait(player, "Inexperienced") then delta = delta * 1.25
	end

	local current = 0
	pcall(function() current = bp:getAdditionalPain() or 0 end)
	local target = current + delta
	if target > 100 then target = 100 end
	pcall(function() bp:setAdditionalPain(target) end)
end

local CORPSE_UNHAPPINESS_DEFAULT  =  10
local CORPSE_UNHAPPINESS_NECRO    = -15
local function corpseSceneMood(player, active, result)
	if result ~= "Released" then return end
	if not active or active.liveOriginal then return end
	if hasTrait(player, "Necrophiliac") then
		bumpStat(player, "UNHAPPINESS", CORPSE_UNHAPPINESS_NECRO)
	else
		bumpStat(player, "UNHAPPINESS", CORPSE_UNHAPPINESS_DEFAULT)
	end
end

local function wimpDefeatPenalty(player, active, result)
	if result ~= "Released" then return end
	if not hasTrait(player, "Wimp") then return end
	if not active or not active.sceneId then return end
	if not active.sceneId:find("Defeat", 1, true) then return end
	bumpStat(player, "UNHAPPINESS", 30)
	bumpStat(player, "STRESS",      20)
end

local function rapistDefeatMood(player, active, result)
	if result ~= "Released" then return end
	if not hasTrait(player, "Rapist") then return end
	if not active or not active.sceneId then return end
	if not active.sceneId:find("Defeat", 1, true) then return end
	bumpStat(player, "STRESS",      -15)
	bumpStat(player, "UNHAPPINESS", -40)
end

local function itFollowsDeath(player, active, result)
	if result ~= "Released" then return end
	if not hasTrait(player, "itFollows") then return end
	if not active or not active.sceneId then return end
	if not active.sceneId:find("Defeat", 1, true) then return end
	pcall(function()
		if player.Kill then player:Kill(nil, false) end
	end)
end

local function defeatTraumatize(player, active, result)
	if result ~= "Released" then return end
	if not active or not active.sceneId then return end
	if not active.sceneId:find("Defeat", 1, true) then return end
	if hasTrait(player, "Traumatized") then return end
	if hasTrait(player, "Nymphomaniac") then return end
	if hasTrait(player, "dpress")        then return end
	local lewd = hasTrait(player, "Lewd")
	local trigger = false
	if hasTrait(player, "Virgin") then
		trigger = true
	elseif (hasTrait(player, "Inexperienced") or hasTrait(player, "Experienced")) and not lewd then
		trigger = true
	end
	if not trigger then return end
	local traits = player:getCharacterTraits()
	if traits and ZR_Trait and ZR_Trait.Traumatized then
		traits:add(ZR_Trait.Traumatized)
		player:Say("I won't forget this...")
		if ZR_AutoSurrender and ZR_AutoSurrender.invalidateMods then
			ZR_AutoSurrender.invalidateMods(player)
		end
	end
end

local function loseVirgin(player, active, result)
	if result ~= "Released" then return end
	if not hasTrait(player, "Virgin") then return end
	local traits = player:getCharacterTraits()
	if traits and ZR_Trait and ZR_Trait.Virgin then
		traits:remove(ZR_Trait.Virgin)
		player:Say("It's done. No going back.")
		if ZR_AutoSurrender and ZR_AutoSurrender.invalidateMods then
			ZR_AutoSurrender.invalidateMods(player)
		end
	end
end

local function youngBuckEffects(player, active, result)
	if result ~= "Released" then return end
	if not hasTrait(player, "youngBuck") then return end
	bumpStat(player, "ENDURANCE", -5)
	addXpBoost(player, "Fitness", 1)
	local sceneId = active and active.sceneId or ""
	if type(sceneId) == "string" and not sceneId:find("Defeat", 1, true) then
		bumpStat(player, "STRESS", -2)
	end
end

local function lowEffortEffects(player, active, result)
	if result ~= "Released" then return end
	if not hasTrait(player, "lowEffort") then return end
	local sceneId = active and active.sceneId or ""
	if type(sceneId) ~= "string" or not sceneId:find("Defeat", 1, true) then return end
	bumpStat(player, "ENDURANCE",   -5)
	bumpStat(player, "UNHAPPINESS", -2)
end

local function swingerVarietyMood(player, active, result)
	if result ~= "Released" then return end
	if not hasTrait(player, "swinger") then return end
	local partner = active and (active.liveOriginal or active.attackerZombie)
	if partner == nil then return end
	if RunID and RunID(player, partner) then
		bumpStat(player, "STRESS",      -5)
		bumpStat(player, "UNHAPPINESS", -3)
	end
end

local INNOCENT_NIGHTMARE_HOURS = 30
local INNOCENT_NIGHTMARE_UNHAPPY_PER_HOUR = 1.0
local INNOCENT_NIGHTMARE_STRESS_PER_HOUR  = 0.5

local function innocentNightmareStart(player, active, result)
	if result ~= "Released" then return end
	if not hasTrait(player, "Innocent") then return end
	if not active or not active.sceneId then return end
	if not active.sceneId:find("Defeat", 1, true) then return end
	pcall(function()
		player:getModData().zr_innocentNightmaresUntil =
			getGameTime():getWorldAgeHours() + INNOCENT_NIGHTMARE_HOURS
	end)
end

local SEXADDICT_GRACE_HOURS         = 12
local SEXADDICT_RISE_UNHAPPY        = 1.5
local PROMISCUOUS_GRACE_HOURS       = 18
local PROMISCUOUS_RISE_STRESS       = 1.0

local function recordSceneTimestamp(player, active, result)
	if result ~= "Released" then return end
	pcall(function()
		player:getModData().zr_lastSceneHour = getGameTime():getWorldAgeHours()
	end)
end

local function hourlyTraitTick()
	local player = getPlayer()
	if player == nil or player:isDead() then return end
	if player.isNpc and player:isNpc() then return end
	local stats = player:getStats(); if stats == nil then return end
	local md    = player:getModData() or {}
	local now   = getGameTime():getWorldAgeHours()

	if hasTrait(player, "Innocent") then
		local until_h = md.zr_innocentNightmaresUntil
		if until_h ~= nil then
			if now < until_h then
				bumpStat(player, "UNHAPPINESS", INNOCENT_NIGHTMARE_UNHAPPY_PER_HOUR)
				bumpStat(player, "STRESS",      INNOCENT_NIGHTMARE_STRESS_PER_HOUR)
			else
				md.zr_innocentNightmaresUntil = nil
			end
		end
	end

	if hasTrait(player, "Sexaddict") then
		local last = md.zr_lastSceneHour
		if last ~= nil and (now - last) >= SEXADDICT_GRACE_HOURS then
			bumpStat(player, "UNHAPPINESS", SEXADDICT_RISE_UNHAPPY)
		end
	end

	if hasTrait(player, "promiscuous") then
		local last = md.zr_lastSceneHour
		if last ~= nil and (now - last) >= PROMISCUOUS_GRACE_HOURS then
			bumpStat(player, "STRESS", PROMISCUOUS_RISE_STRESS)
		end
	end
end

Events.EveryHours.Add(hourlyTraitTick)

local function partnerIsMale(active)
	local partner = active and (active.liveOriginal or active.attackerZombie)
	if partner == nil then return false end
	local female = false
	pcall(function()
		if partner.isFemale then female = partner:isFemale() end
	end)
	return not female
end

local function proteinDietEffect(player, active, result)
	if result ~= "Released" then return end
	if not hasTrait(player, "proteinDiet") then return end
	if not active or not active.sceneId then return end
	if not active.sceneId:find("Defeat", 1, true) then return end
	if not partnerIsMale(active) then return end
	bumpStat(player, "HUNGER", -10)
	pcall(function()
		local nutr = player.getNutrition and player:getNutrition()
		if nutr == nil then return end
		if nutr.setProteins and nutr.getProteins then
			nutr:setProteins((nutr:getProteins() or 0) + 25)
		end
		if nutr.setCalories and nutr.getCalories then
			nutr:setCalories((nutr:getCalories() or 0) + 80)
		end
	end)
end

local THIRST_CHANGE_PER_SCENE = 0.125

local function hydrationJunkieEffect(player, _active, result)
	if result ~= "Released" then return end
	if not hasTrait(player, "hydration") then return end
	bumpStat(player, "THIRST", -THIRST_CHANGE_PER_SCENE)
end

local function drainedEffect(player, _active, result)
	if result ~= "Released" then return end
	if not hasTrait(player, "drained") then return end
	bumpStat(player, "THIRST", THIRST_CHANGE_PER_SCENE)
end

local FIGHTER_PARTS = {
	"Hand_L", "Hand_R", "ForeArm_L", "ForeArm_R",
	"UpperArm_L", "UpperArm_R", "Torso_Upper", "Torso_Lower",
	"Groin", "UpperLeg_L", "UpperLeg_R", "LowerLeg_L", "LowerLeg_R",
}
local FIGHTER_SCRATCH_CHANCE_PCT = 50
local FIGHTER_BLEED_CHANCE_PCT   =  0
local FIGHTER_KILL_CHANCE_PCT    = 50
local FIGHTER_SCRATCH_MAX        =  2

local function fighterScratches(player, active, result)
	if result ~= "Released" then return end
	if not hasTrait(player, "fighter") then return end
	if not active or not active.sceneId then return end
	if not active.sceneId:find("Defeat", 1, true) then return end
	local bd = player:getBodyDamage(); if bd == nil then return end

	local partner = active.liveOriginal or active.attackerZombie
	if partner ~= nil and ZombRand(100) < FIGHTER_KILL_CHANCE_PCT then
		pcall(function() if partner.Kill                 then partner:Kill(nil, false)        end end)
		pcall(function() if partner.becomeCorpseSilently then partner:becomeCorpseSilently() end end)
	end

	if ZombRand(100) >= FIGHTER_SCRATCH_CHANCE_PCT then return end

	local count = ZombRand(FIGHTER_SCRATCH_MAX) + 1
	local pickedNames = {}
	local guard = 0
	while #pickedNames < count and guard < 50 do
		guard = guard + 1
		local name = FIGHTER_PARTS[ZombRand(#FIGHTER_PARTS) + 1]
		local already = false
		for _, n in ipairs(pickedNames) do
			if n == name then already = true; break end
		end
		if not already then pickedNames[#pickedNames + 1] = name end
	end

	for _, name in ipairs(pickedNames) do
		local part = BodyPartType[name]
		if part ~= nil then
			local bp = bd:getBodyPart(part)
			if bp ~= nil then
				pcall(function() bp:setScratched(true, true) end)
				pcall(function() bp:setBleeding(false) end)
				pcall(function() bp:setBleedingTime(0) end)
			end
		end
	end
end

local GLASS_BONES_PARTS = {
	"ForeArm_L", "ForeArm_R", "UpperArm_L", "UpperArm_R",
	"UpperLeg_L", "UpperLeg_R", "LowerLeg_L", "LowerLeg_R",
}
local GLASS_BONES_CHANCE_PCT = 15

local function glassBonesFracture(player, active, result)
	if result ~= "Released" then return end
	if not hasTrait(player, "glassBones") then return end
	if not active or not active.sceneId then return end
	if not active.sceneId:find("Defeat", 1, true) then return end
	if ZombRand(100) >= GLASS_BONES_CHANCE_PCT then return end
	local bd = player:getBodyDamage(); if bd == nil then return end
	local name = GLASS_BONES_PARTS[ZombRand(#GLASS_BONES_PARTS) + 1]
	local part = BodyPartType[name]; if part == nil then return end
	local bp = bd:getBodyPart(part); if bp == nil then return end
	pcall(function() bp:setFractureTime(ZombRand(30, 60)) end)
end

local function cardioJunkieXP(player, active, result)
	if result ~= "Released" then return end
	if not hasTrait(player, "cardioJunkie") then return end
	if not active or not active.sceneId then return end
	if not active.sceneId:find("Defeat", 1, true) then return end
	addXpBoost(player, "Fitness",  3)
	addXpBoost(player, "Strength", 2)
end

ZomboRutScene.registerSceneStartCallback(function(player, active)
	local sceneId = active and active.sceneId or ""
	if type(sceneId) ~= "string" or not sceneId:find("Defeat", 1, true) then return end
	dispatch(SCENE_START, player)
end)

ZomboRutScene.registerSceneEndCallback(function(player, active, result)
	dispatchSceneEnd(player, active, result)
	local partner = active and (active.liveOriginal or active.attackerZombie)
	zombieSexSickness(player, partner, result, active)
	defeatGroinPain(player, active, result)
	corpseSceneMood(player, active, result)
	wimpDefeatPenalty(player, active, result)
	rapistDefeatMood(player, active, result)
	defeatTraumatize(player, active, result)
	loseVirgin(player, active, result)
	youngBuckEffects(player, active, result)
	lowEffortEffects(player, active, result)
	swingerVarietyMood(player, active, result)
	innocentNightmareStart(player, active, result)
	proteinDietEffect(player, active, result)
	hydrationJunkieEffect(player, active, result)
	drainedEffect(player, active, result)
	fighterScratches(player, active, result)
	glassBonesFracture(player, active, result)
	cardioJunkieXP(player, active, result)
	recordSceneTimestamp(player, active, result)
	itFollowsDeath(player, active, result)
	if partner and increaseCount then
		pcall(increaseCount, player, partner)
	end
	succubusKill(player, partner, result)
end)
