local ent = ent
local scene = entities.currentScene()
local graph = scene:getComponent("Graph")
local physicsBody = ent:getComponent("PhysicsBody")
local metadataJson = ent:getComponent("Metadata")
local camera = ent:getComponent("Camera")
local cameraMetadata = ent:getComponent("PlayerCameraBehavior::Metadata")
local moveStates = ent:getComponent("PlayerMovementBehavior::Metadata")
local fixedCamera = cameraMetadata and cameraMetadata.fixed or false

-- setup all timers
local timers = {
	holp = Timer.new(),
	flashlight = Timer.new(),
	physcannon = Timer.new()
}
if not timers.holp:running() then timers.holp:start(); end
if not timers.flashlight:running() then timers.flashlight:start(); end
if not timers.physcannon:running() then timers.physcannon:start(); end

-- setup held object locals
local heldObject = {
	uid = 0,
	distance = 0,
	smoothSpeed = 4,
	scrollSpeed = 32,
	momentum = Vector3f(0,0,0),
	rotate = false,
}

-- setup light
local light = { entity = nil }
for k, v in pairs(ent:getChildren()) do
	if type(v) == "table" and v:name() == "Light" then light.entity = v end
end
if light.entity == nil then
	light.entity = ent:loadChild("ent://playerLight.json",true)
end

light.metadata = light.entity:getComponent("LightBehavior::Metadata")
light.transform = light.entity:getComponent("Transform")
light.power = light.metadata.power
light.metadata.power = 0

-- UI sound helpers
local playUiSound = function( key, loop )
	ent:callHook("sound:Emit.%UID%", {
		filename = string.resolveURI("/ui/" .. key .. ".ogg"),
		spatial = true, streamed = true, volume = "sfx", loop = loop or false
	}, 0)
end

local pullDistance = 24

local function tickFlashlight( transform, axes, inputState )
	if light.enabled then
		local center = transform.position
		local direction = axes.forward * 8
		local _, depth = physicsBody:rayCast(center, direction)
		depth = math.clamp(depth, 0, 0.5)
		light.transform.position = center + direction * (depth - 0.25)
	end

	-- Flashlight uses 'F' key (we can keep this manual since it's game-specific, or add it to C++ input state)
	if timers.flashlight:elapsed() > 0.5 and inputs.key("F") then
		timers.flashlight:reset()
		light.enabled = (light.metadata.power ~= light.power)
		light.metadata.power = light.enabled and light.power or 0
		playUiSound("flashlight")
	end
end

local function tickGravGun( transform, axes, inputState )
	local mouse1 = inputs.key("Mouse1") or inputs.key("L_TRIGGER")
	local mouse2 = inputs.key("Mouse2") or inputs.key("R_TRIGGER")
	local mouse3 = inputs.key("Mouse3")
	local wheel = inputs.analog("MouseWheel")

	if heldObject.uid == 0 then
		if mouse2 then
			local prop, depth = physicsBody:rayCast( transform.position, axes.forward * pullDistance )
			if depth >= 0 and prop and not string.matched( prop:name(), "/^worldspawn/" ) then
				local propPhysics = prop:getComponent("PhysicsBody")
				local distanceSquared = (prop:getComponent("Transform").position - transform.position):magnitude()
				propPhysics:applyImpulse( axes.forward * -propPhysics:getMass() * 500 / distanceSquared )

				if timers.physcannon:elapsed() > 1.0 then
					timers.physcannon:reset()
					playUiSound("phys_tooHeavy")
				end
			end
		end
	else
		-- Held object logic (unchanged from your original, just cleaner scope)
		if wheel ~= 0 then heldObject.distance = heldObject.distance + (wheel / 120 * heldObject.scrollSpeed) * time.delta() end
		if mouse3 then heldObject.rotate = not heldObject.rotate end

		local prop = entities.get( heldObject.uid )
		local propTransform = prop:getComponent("Transform")
		local propPhysics = prop:getComponent("PhysicsBody")

		if mouse1 and timers.physcannon:elapsed() > 0.5 then
			timers.physcannon:reset()
			heldObject.uid = 0
			propPhysics:enableGravity(true)
			propPhysics:applyImpulse( axes.forward * propPhysics:getMass() * 50 )
			playUiSound("phys_launch"..math.random(1,4))
		else
			if heldObject.rotate then propTransform.orientation = camera:getTransform():flatten().orientation end

			local target = transform.position + (axes.forward * heldObject.distance)
			local offset = target - propTransform:flatten().position
			propPhysics:applyImpulse((offset * 15.0 + propPhysics:getVelocity() * -2.0) * propPhysics:getMass() * time.delta())
		end
	end
end

local footstepTimer = 0.0
local surfaceTypes = {
	"chainlink",
	"concrete",
	"dirt",
	"duct",
	"grass",
	"gravel",
	"hardboot_generic",
	"ladder",
	"metal",
	"metalgrate",
	"mud",
	"sand",
	"slosh",
	"snow",
	"softshoe_generic",
	"tile",
	"wade",
	"wood",
	"woodpanel"
}

local playFootstepSound = function( surface, isCrouching )
	local variant = math.random(1, 4)
	local path = "valve://sound/player/footsteps/" .. surface .. tostring(variant) .. ".wav"
	local pitch = 0.95 + (math.random() * 0.10)
	local vol = 1.0
	
	if isCrouching then vol = 0.5 end

	ent:callHook("sound:Emit.%UID%", {
		filename = string.resolveURI(path, metadataJson["system"]["root"]),
		spatial = true,
		streamed = false,
		volume = vol,
		pitch = pitch,
		loop = false
	}, 0)
end

local function getFloorSurface()
	local surface = "concrete"
	local collisionEvents = physicsBody:getCollisionEvents()
	for i, event in ipairs( collisionEvents ) do
		if event.normal.y <= -0.7 then
			local tri = event.featureA or event.featureB
			local other = event.a == ent and event.a or event.b
			local collider = other:getCollider()

			if tri ~= nil and collider.type == ShapeType.MESH then
				local mesh = collider:asMesh()
				local drawCommand = mesh:fetchDrawCommand( tri )
				local instance = graph:getInstance( drawCommand.instanceID )
				local materialName = string.lower( graph:getMaterialName( instance.materialID ) )

				for _, key in ipairs(surfaceTypes) do
					if string.find(materialName, key) then
						return key
					end
				end
			end
			break
		end
	end
	return surface
end

local airTime = 0.0

local tickFootsteps = function( inputState )
	local isWalking = moveStates.walking
	local isRunning = moveStates.running
	local isCrouching = moveStates.crouching
	local isFloored = moveStates.floored
	local isNoclipped = moveStates.noclipped

	if not isNoclipped then
		if not isFloored then
			airTime = airTime + time.delta()
		end

		if isFloored and not wasFloored then
			if airTime > 0.15 then
				playFootstepSound( getFloorSurface(), isCrouching )
				footstepTimer = isRunning and 0.3 or 0.45
			end
			airTime = 0.0
		end

		if not isFloored and wasFloored and inputState.jump then
			playFootstepSound( getFloorSurface(), isCrouching )
		end
	end
	wasFloored = isFloored

	if not isWalking or isNoclipped then
		footstepTimer = 0.0
		return
	end

	footstepTimer = footstepTimer - time.delta()

	if not isFloored then return end

	if footstepTimer > 0.0 then return end

	playFootstepSound( getFloorSurface(), isCrouching )

	if isRunning then
		footstepTimer = 0.3
	elseif isCrouching then
		footstepTimer = 0.6
	else
		footstepTimer = 0.45
	end
end

ent:addHook( "entity:Use.%UID%", function( payload )
	if payload.user ~= ent:uid() then return end

	local validUse = false
	if heldObject.uid == 0 and payload.depth > 0 then
		local prop = entities.get( payload.uid )
		if prop:getComponent("Metadata")["holdable"] then
			validUse = true
			heldObject.uid = payload.uid
			heldObject.distance = (ent:getComponent("Transform").position - prop:getComponent("Transform"):flatten().position):norm()
			prop:getComponent("PhysicsBody"):enableGravity(false)
		else
			validUse = not string.matched( prop:name(), "/^worldspawn/" )
		end
	elseif heldObject.uid ~= 0 then
		validUse = true
		local prop = entities.get( heldObject.uid )
		prop:getComponent("PhysicsBody"):enableGravity(true)
		heldObject.uid = 0
	end
	playUiSound(validUse and "select" or "deny")
end )

-- on tick
ent:bind( "tick", function(self)
	local inputs = ent:getComponent("PlayerInputBehavior::Metadata")
	if not inputs or not inputs.control then return end

	local flattenedTransform = fixedCamera and ent:getComponent("Transform"):flatten() or camera:getTransform():flatten()
	local axes = flattenedTransform:axes()

	-- update flashlight
	tickFlashlight( flattenedTransform, axes, inputs )
	
	-- update HOLP
	tickGravGun( flattenedTransform, axes, inputs )

	-- play footsteps
	tickFootsteps( inputs )
end )