#include "ui/ui_helpers.lua" #include "ui/ui_extensions.lua" #include "script/common.lua" #include "horseinput.lua" local DIRECTION_BACKWARD = 1 local DIRECTION_FORWARD = 2 local DIRECTION_NONE = 3 FORWARD_SPEED = 10 BACKWARD_SPEED = 5 STRENGTH = 1.5 JUMP_TIME = 1.2 body = 0 head = 0 legs = {} animAngle = 0 animScale = 1 speed = 0 contact = false lookingDirection = Vec(0, -90, 0) scroll = 10 isJump = false jumpTimer = 0 linVel = 0 angVel = 0 speed = 0 outlineAlpha = 0 canDrawHints = false direction = DIRECTION_NONE camHeight = GetFloatParam("camHeight", 2.4) scrollInCave = GetFloatParam("scrollInCave", 10) scrollcamDif = GetFloatParam("scrollcamDif", 0.05) local velocity = 0 --local isDrive = false function rndVec(length) local v = VecNormalize(Vec(math.random(-100,100), math.random(-100,100), math.random(-100,100))) return VecScale(v, length) end function rnd(mi, ma) return math.random(1000)/1000*(ma-mi) + mi end function mix(a, b, t) return a*(1-t)+b*t end function Clamp(value, min, max) return math.min(math.max(value, min), max) end function init() body = FindBody("body") main = FindShape("main") RemoveTag(main, "interact") allShapes = FindShapes() for i = 1, #allShapes do SetTag(allShapes[i], "horse") end allBodies = FindBodies() --vehicle = FindVehicle() local bodyTransform = GetBodyTransform(body) head = {} head.body = FindBody("head") head.lt = TransformToLocalTransform(bodyTransform, GetBodyTransform(head.body)) legs[1] = { { body = FindBody("fur") }, { body = FindBody("flr") } } legs[2] = { { body = FindBody("ful") }, { body = FindBody("fll") } } legs[3] = { { body = FindBody("bur") }, { body = FindBody("blr") } } legs[4] = { { body = FindBody("bul") }, { body = FindBody("bll") } } for i=1,4 do legs[i][1].lt = TransformToLocalTransform(bodyTransform, GetBodyTransform(legs[i][1].body)) legs[i][2].lt = TransformToLocalTransform(GetBodyTransform(legs[i][1].body), GetBodyTransform(legs[i][2].body)) end local t = clamp((math.abs(speed)-5)/3, 0, 1) legs[1].offset = 0 legs[2].offset = mix(2, 1.1, t) legs[3].offset = mix(4, 2, t) legs[4].offset = mix(6, 3.1, t) runSnd = LoadLoop("MOD/sound/horse/galopp.ogg") jumpSnd = LoadSound("MOD/sound/horse/horse_pair_vehicle-gearshift0") idleSnd = LoadSound("MOD/sound/horse/grunt0") --rwSnd = LoadSound("MOD/sound/horse/horse_pair_vehicle-brake") playerSitSound = LoadSound("MOD/sound/horse/horse_pair_vehicle-ignition0") playerStopSound = LoadSound("MOD/sound/horse/horse_pair_vehicle-turnoff0") backSnd = LoadLoop("MOD/sound/horse/horse-reverse-walk.ogg") lookingDirection = QuatEuler(GetCameraTransform().rot) local x,y,z = GetQuatEuler(bodyTransform.rot) isDrive = false isActive = true idletimer = 0 idletimermax = math.random(2,8) trotEffect = LoadHaptic("MOD/haptic/horse/trot.xml") jumpEffect = LoadHaptic("MOD/haptic/horse/jump.xml") trotEffectTimer = 0 scrolldif = 0 playerTool = 0 end function tick(dt) if isDrive then if IsPointInWater(Vec(GetBodyTransform(head.body).pos[1], GetBodyTransform(head.body).pos[2]+2.5, GetBodyTransform(head.body).pos[3])) then --Delete(body) SetString("game.player.tool", playerTool) isDrive = false isActive = false end if IsHandleValid(body) then CameraControl(GetBodyTransform(body), dt) else for i=1, #allBodies do Delete(allBodies[i]) end PlaySound(playerSitSound, GetBodyTransform(body).pos) isDrive = false SetString("game.player.tool", playerTool) end if contact then if speed > 0 then PlayLoop(runSnd, GetBodyTransform(body).pos, clamp(speed/10, 0, 1)) else PlayLoop(backSnd, GetBodyTransform(body).pos, clamp(math.abs(speed)/3, 0, 1)) end end if LastInputDevice() == UI_DEVICE_GAMEPAD then input.layout = "gamepad" else input.layout = "keyboard" end end if GetPlayerGrabShape() == main then ReleasePlayerGrab() end if isDrive then if InputPressed(input.keys[input.layout].vexit) or GetFloat("game.player.health") == 0 or GetString("level.state") ~= "" then RemoveTag(body, "autobreak") local t = GetBodyTransform(body) t.pos[2] = t.pos[2] + 1 t.rot = GetCameraTransform().rot SetPlayerTransform(t) SetString("game.player.tool", playerTool) PlaySound(playerStopSound, GetBodyTransform(body).pos) isDrive = false end else if GetPlayerInteractShape() == main and InputPressed(input.keys[input.layout].vexit) and isActive then local x,y,z = GetQuatEuler(GetCameraTransform().rot) lookingDirection = Vec(x,y,z) cameraPos = GetCameraTransform().pos isDrive = true playerTool = GetString("game.player.tool") SetString("game.player.tool", 0) PlaySound(playerSitSound, GetBodyTransform(body).pos) SetTag(body, "autobreak") end end velocity = VecLength(GetBodyVelocity(body)) --Leg offset depends on speed if velocity > 0.02 then local t = clamp((math.abs(speed)-5)/3, 0, 1) legs[1].offset = 0 legs[2].offset = mix(2, 1.1, t) legs[3].offset = mix(4, 2, t) legs[4].offset = mix(6, 3.1, t) setTransforms(dt) else setTransforms(dt) end -- process interact tag local inside = isDrive local interactable = not GetBool("game.disableinteract") local hasTag = HasTag(body, "interact") if not interactable and hasTag then RemoveTag(main, "interact") end if interactable then if not inside and not hasTag then SetTag(main, "interact", "loc@HORSE_TXT_RIDE") end if inside and hasTag then RemoveTag(main, "interact") end end end function CameraControl(tr, dt) if GetBool("game.map.enabled") then canDrawHints = false return end --if GetFloat("game.map.fade") == 0 then scroll = Clamp(scroll - InputValue("mousewheel"), 1.5, 11) lookingDirection = VecLerp(lookingDirection, VecSub(lookingDirection, Vec(InputValue("cameray")*100, InputValue("camerax")*100, 0)), 0.0166667*20) local minangle = -90 if scroll < 4 then minangle = 0 end lookingDirection[1] = Clamp(lookingDirection[1], minangle, -15) tr.rot = QuatEuler(lookingDirection[1], lookingDirection[2], lookingDirection[3]) if GetBool("level.horseCam") then if scrolldif < scrollInCave then scrolldif = scrolldif + scrollcamDif end local dif = scroll - scrolldif dif = Clamp(dif, 1.5, 11) cameraPos = TransformToParentPoint(tr, Vec(0, camHeight, dif-0.7)) else if scrolldif > 0 then scrolldif = scrolldif - scrollcamDif end local dif = scroll - scrolldif dif = Clamp(dif, 1.5, 11) cameraPos = TransformToParentPoint(tr, Vec(0, camHeight, dif-0.7)) end SetCameraTransform(Transform(cameraPos, QuatEuler(lookingDirection[1],lookingDirection[2],0))) if not GetBool("level.horseCam") then SetPivotClipBody(body, 0) SetPivotClipBody(head.body) for i=1,4 do SetPivotClipBody(legs[i][1].body) SetPivotClipBody(legs[i][2].body) end end local dist = VecLength(VecSub(GetBodyTransform(body).pos, cameraPos)) local a = clamp((25 - dist) / 5.0, 0.0, 1.0) if canBeSeenByPlayer() then a = 0 end outlineAlpha = outlineAlpha + clamp(a - outlineAlpha, -0.1, 0.1) drawOutline() canDrawHints = true --end end function canBeSeenByPlayer() for i=1, #allShapes do if IsShapeVisible(allShapes[i], 15, true) then return true end end return false end function drawOutline() for i=1, #allBodies do DrawBodyOutline(allBodies[i], 1, 1, 1,outlineAlpha*0.5) end end function mix(a, b, t) return a*(1-t)+b*t end function setTransforms(dt) --Procedural animation local bodyTransform = GetBodyTransform(body) local ang = animAngle local jump = math.sin(jumpTimer/JUMP_TIME*math.pi) for i=1,4 do local ua = (math.sin(ang+legs[i].offset+0)*40-20)*animScale local la = (math.sin(ang+legs[i].offset+180)*40+30)*animScale if jump > 0 then if i==1 or i==2 then ua = mix(ua, -80, jump) la = mix(la, 30, jump) else ua = mix(ua, 20, jump) la = mix(la, 30, jump) end end local u = Transform(legs[i][1].lt.pos, QuatRotateQuat(QuatEuler(ua, 0, 0), legs[i][1].lt.rot)) local l = Transform(legs[i][2].lt.pos, QuatRotateQuat(QuatEuler(la, 0, 0), legs[i][2].lt.rot)) SetBodyTransform(legs[i][1].body, TransformToParentTransform(bodyTransform, u)) SetBodyTransform(legs[i][2].body, TransformToParentTransform(GetBodyTransform(legs[i][1].body), l)) end local ha = (math.sin(ang)*10+0)*animScale local h = Transform(head.lt.pos, QuatRotateQuat(QuatEuler(ha, 0, 0), head.lt.rot)) SetBodyTransform(head.body, TransformToParentTransform(bodyTransform, h)) --Jump, should be implemented better of course if isDrive and InputPressed(input.keys[input.layout].jump) and not isJump and contact then local v = GetBodyVelocity(body) v[2] = speed*0.55 SetBodyVelocity(body, v) isJump = true PlaySound(jumpSnd, GetBodyTransform(body).pos) PlayHaptic(jumpEffect, 1.0) end if isJump then jumpTimer = jumpTimer +dt if jumpTimer > JUMP_TIME then isJump = false jumpTimer = 0 end end end function update(dt) if not isDrive then idletimer = idletimer + dt if idletimer > idletimermax then PlaySound(idleSnd, GetBodyTransform(body).pos) idletimer = 0 idletimermax = math.random(2,8) end end local bodyTransform = GetBodyTransform(body) local disttoPlayer = VecLength(VecSub(GetPlayerPos(), GetBodyTransform(body).pos)) if isDrive then if GetFloat("game.map.fade") == 0 then local t = GetBodyTransform(body) t.rot = QuatEuler(lookingDirection[1],lookingDirection[2],0) t.pos[2] = t.pos[2] + 1 SetPlayerTransform(t) if disttoPlayer > 4 then RemoveTag(body, "autobreak") local t = GetBodyTransform(body) t.pos[2] = t.pos[2] + 1 SetPlayerTransform(t) PlaySound(playerSitSound, GetBodyTransform(body).pos) SetString("game.player.tool", playerTool) isDrive = false end end end if disttoPlayer > 15 then if IsBodyDynamic(body) then SetBodyDynamic(body, false) end return else if not IsBodyDynamic(body) then SetBodyDynamic(body, true) end end local fwd = TransformToParentVec(bodyTransform, Vec(0, 0, -1)) local right = TransformToParentVec(bodyTransform, Vec(1, 0, 0)) local targetAngVel = 0 local targetLinVel = 0 local r = clamp(linVel/5, -2, 2) local isGamepad = LastInputDevice() == UI_DEVICE_GAMEPAD if isDrive then if InputDown(input.keys[input.layout].left) or InputDown(input.keys[input.layout].right) then targetAngVel = InputDown(input.keys[input.layout].left) and r or -r turning = true end if InputDown(input.keys[input.layout].up) then targetLinVel = FORWARD_SPEED if isGamepad then direction = DIRECTION_FORWARD end end if InputDown(input.keys[input.layout].down) then targetLinVel = -BACKWARD_SPEED if isGamepad then direction = DIRECTION_BACKWARD end end if not InputDown(input.keys[input.layout].left) and not InputDown(input.keys[input.layout].right) and not InputDown(input.keys[input.layout].up) and not InputDown(input.keys[input.layout].down) then direction = DIRECTION_NONE end if direction ~= DIRECTION_NONE and isGamepad then targetLinVel = direction == DIRECTION_BACKWARD and -BACKWARD_SPEED or FORWARD_SPEED end if targetLinVel > 0 and trotEffectTimer >= 0.3 then PlayHaptic(trotEffect, 0.5) end if trotEffectTimer >= 0.4 then trotEffectTimer = 0 end trotEffectTimer = trotEffectTimer + dt end linVel = linVel + clamp(targetLinVel - linVel, -0.3, 0.3) angVel = angVel + clamp(targetAngVel - angVel, -0.2, 0.2) --Determine animation speed from the biggest of actual speed and desired speed local actualSpeed = -VecDot(fwd, GetBodyVelocity(body), fwd) if math.abs(linVel) > math.abs(actualSpeed) then speed = linVel else speed = actualSpeed end --Update animation cycle local d = clamp(speed/10, -1.0, 1.0) if d < 0 then d = -math.sqrt(-d) else d = math.sqrt(d) end animAngle = animAngle + d*15*dt animScale = math.abs(d) local ang = animAngle local height = 0.8 local targetHeight = height + (math.sin(ang+0)*0.05+0.05)*animScale local origin = TransformToParentPoint(bodyTransform, Vec(0, height, 0)) local down = Vec(0, -1, 0) --Measure how far above ground local rej = GetJointedBodies(body) for i=1, #rej do QueryRejectBody(rej[i]) end QueryRequire("physical large") local radius = 0.2 local hit, dist, n, s = QueryRaycast(origin, down, targetHeight+0.5, radius) dist = dist + radius contact = hit local mass = GetBodyMass(body) if hit then --Move horse relative what we're standing on local b = GetShapeBody(s) --Maintain height above ground local yv = clamp((targetHeight-dist)/dt*0.3, -3, 3) ConstrainVelocity(b, body, origin, down, yv, 0.0, mass*2) --Forward speed local f = mass * STRENGTH * 0.5 ConstrainVelocity(b, body, origin, fwd, linVel, -f, f) --Friction to not slide sideways ConstrainVelocity(b, body, origin, right, 0, -f, f) --Some particles if rnd(0, 1) < velocity/50 then ParticleColor(1, 0.9, 0.8) ParticleAlpha(0.4, 0) ParticleCollide(0, 1) ParticleRadius(0.5*velocity/10) SpawnParticle(VecAdd(origin, VecScale(down, dist)), VecAdd(rndVec(1), Vec(0, 1, 0)), velocity/5) end end local ba = math.sin(ang)*0.14*animScale local jump = math.sin(jumpTimer/JUMP_TIME*math.pi) local vy = GetBodyVelocity(body)[2]*0.2 vy = clamp(vy, -0.5, 0.5) ba = mix(ba, vy, jump) if not lean then lean = 0 end local leanTarget = -angVel*velocity/60 lean = lean + clamp((leanTarget-lean)*0.9, -0.04, 0.04) lean = clamp(lean, -0.5, 0.5) local f = mass * STRENGTH local up, d --Upright around z up = VecCross(TransformToParentVec(bodyTransform, Vec(math.sin(lean), math.cos(lean), 0)), Vec(0, 1, 0)) d = VecDot(up, fwd) ConstrainAngularVelocity(body, 0, fwd, d*10, -f , f) --Upright around x up = VecCross(TransformToParentVec(bodyTransform, Vec(0, math.cos(ba), math.sin(ba))), Vec(0, 1, 0)) d = VecDot(up, right) ConstrainAngularVelocity(body, 0, right, d*10, -f , f) --Rotational ConstrainAngularVelocity(body, 0, Vec(0, 1, 0), angVel, -f , f) end function draw() if canDrawHints and isDrive then drawHints() end end function drawHints() info = {} for key,val in ipairs(input.keys.orderrobot) do getBind = GetString("game.input."..input.keys[input.layout][val]) if getBind == "" then getBind = input.keys[input.layout][val] end local inputKey = string.upper(getBind) local desc = GetTranslatedStringByKey(input.keys.descrobot[val]) key = val if input.layout == "gamepad" then inputKey = "[[" .. input.keys[input.layout][key] .. ";iconsize=42,42]]" end info[#info+1] = {inputKey, desc} end UiPush() UiAlign("top left") local offsetTargets = 0 if GetBool("game.hud.heistStatsVisible") then offsetTargets = GetInt("game.map.tgH") + 50 end local kw = 0 local vw = 0 for i=1, #info do UiFont("arial.ttf", 35) local w0 = 75 w0 = UiGetTextSize(info[i][1]) UiFont("arial.ttf", 30) local w1 = UiGetTextSize(info[i][2]) kw = math.max(kw,w0) vw = math.max(vw,w1) end local iconSize = 42 local w = kw + vw + 60 local h = #info * (iconSize + 6) + 40 SetInt("game.hud.hintH", h + GetInt("game.hud.hintH")) UiTranslate(UiWidth()-w-20, UiHeight()-h-offsetTargets-2) UiColor(0,0,0,0.5) UiImageBox("ui/common/box-solid-6.png", w, h-15, 6, 6) UiTranslate(kw + 30, 10) UiColor(1,1,1) for i=1, #info do local key = info[i][1] local func = info[i][2] UiPush() UiTranslate(0, iconSize / 2) UiFont("arial.ttf", 34) UiAlign("right middle") UiText(key) UiPop() UiPush() UiTranslate(10, iconSize / 2) UiColor(0.75, 0.75, 0.75) UiFont("arial.ttf", 30) UiAlign("left middle") UiText(func) UiPop() UiTranslate(0, iconSize+6) end UiPop() end