engine/bin/data/entities/scripts/player.lua

275 lines
8.3 KiB
Lua

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 )