#include "script/common.lua" min_cooldown = GetIntParam("MinTornadoDelay", 15) max_cooldown = GetIntParam("MaxTornadoDelay", 35) local MIN_RADIUS = 0.3 local MAX_RADIUS = 8 local ROT_SPEED = 5 local HEIGHT = 30 local MIN_SPAWN_DIST_TO_PLAYER = 10 local SUCK_RADIUS = 20 local randomtry = 0 local randomtime = 0 local allowtornadospawn = true local cutsceneworks = true local destructiontimer = 3 local destructioncount = 1 local clockpart = nil local fallenpart = nil function rnd(mi, ma) return math.random(1000)/1000*(ma-mi) + mi end function rndVec(t) return Vec(rnd(-t, t), rnd(-t, t), rnd(-t, t)) end tornado = {} tornado.pos = Vec() tornado.age = 0 tornado.lifetime = 0 tornado.spawnAngle = 0 tornado.active = false local tornadostartpos = nil local tornadoendpos = nil local destructionpoints = nil starttornado = GetBoolParam("StartTornado", true) function init() tornadoSnd = LoadLoop("tornado.ogg") --- Tornado triggers and cooldowns----- triggers = FindTriggers() trigger_cooldown = {} for i = 1, #triggers do SetTag(triggers[i], "id", tostring(i)) trigger_cooldown[i] = 0 end --------------------------------------- tornadostartpos = FindLocation("start") tornadoendpos = FindLocation("finish") destructionpoints = FindLocations("destruction") clockpart = FindBodies("clockpart", true) fallenpart = FindLocations("fallenpart", true) clocks = FindShapes("clock", true) cutsceneclock = FindBodies("cutsceneclock", true) cutsceneimpulsedir = FindLocations("cutsceneimpulsedir", true) for i = 1, #clockpart do for j = 1, #fallenpart do if GetTagValue(clockpart[i], "clockpart") == GetTagValue(fallenpart[j],"fallenpart") then local dir = VecScale(VecNormalize(VecSub(GetLocationTransform(fallenpart[j]).pos, GetBodyTransform(clockpart[i]).pos)), tonumber(GetTagValue(fallenpart[j],"impulse"))) ApplyBodyImpulse(clockpart[i], Vec(0,0,0), dir) end end end end function spawn(startPos, endPos, lifetime) tornado.startPos = startPos tornado.endPos = endPos tornado.age = 0 tornado.lifetime = lifetime tornado.spawnAngle = 0 tornado.pos = startPos tornado.active = true end function mix(lo, hi, t) return lo+(hi-lo)*t end function getOffset(relativeHeight) local bigAng = tornado.age * 1 + relativeHeight*8 local bend = math.sin(relativeHeight*math.pi) * 3 -- Bend only in the middle return Vec(math.sin(bigAng)*bend, 0, math.sin(bigAng*0.7)*bend) end function emitParticles() tornado.spawnAngle = tornado.spawnAngle + 2.39996 TornadoSpawnParticlesCPP(tornado.spawnAngle, HEIGHT, MIN_RADIUS, MAX_RADIUS, ROT_SPEED, tornado.pos, tornado.amount, tornado.age, false) end function destroy() local h = rnd(0.0, 8.0) local pos = VecAdd(tornado.pos, getOffset(h/HEIGHT)) pos[2] = pos[2] + h MakeHole(pos, 1.0, 0.5, 0, true) end function getVelForBody(pos, mass, vel, angVel) --Closest point on tornado local tornadoPoint = VecCopy(tornado.pos) tornadoPoint[2] = pos[2] local tornadoHeight = clamp((tornadoPoint[2] - tornado.pos[2])/HEIGHT, 0.0, 1.0) tornadoPoint = VecAdd(tornadoPoint, getOffset(tornadoHeight)) local toTornado = VecSub(tornadoPoint, pos) local dist = VecLength(toTornado) if dist > 0.0 then toTornado = VecScale(toTornado, 1.0 / dist) end if dist < SUCK_RADIUS then local strength = 1.0 - dist/SUCK_RADIUS strength = strength * (1-tornadoHeight) strength = strength * strength strength = strength * 3.0 * tornado.amount strength = strength * 1000 / (1000+mass) local addVel = VecScale(toTornado, strength) addVel[2] = strength * 0.5 local perp = Vec(toTornado[3], 0.0, -toTornado[1]) addVel = VecAdd(addVel, VecScale(perp, strength*0.2)) local maxVel = 8 for i=1, 3 do if addVel[i] < 0 and vel[i] < -maxVel then addVel[i]=0 end if addVel[i] > 0 and vel[i] > maxVel then addVel[i]=0 end end local maxAngVel = 5 local addAngVel = -ROT_SPEED * strength * 0.05 if angVel[2] < -maxAngVel then addAngVel = 0 end return VecAdd(vel, addVel), VecAdd(angVel, Vec(0, addAngVel, 0)) else return vel, angVel end end function suction() local lo = VecAdd(tornado.pos, Vec(-SUCK_RADIUS, 0, -SUCK_RADIUS)) local hi = VecAdd(tornado.pos, Vec(SUCK_RADIUS, HEIGHT, SUCK_RADIUS)) --TornadoBodiesSuctionLUA(lo, hi) TornadoBodiesSuctionCPP(tornado.pos, tornado.amount, tornado.age, HEIGHT,SUCK_RADIUS, ROT_SPEED, lo, hi) --Affect player if GetPlayerVehicle() == 0 and not GetBool("game.map.enabled") then local ppos = GetPlayerTransform().pos ppos[2] = ppos[2] + 1.0 -- Chest height local tpos = VecCopy(tornado.pos) tpos[2] = ppos[2] -- Same height on tornado local diff = VecSub(tpos, ppos) local length = VecLength(diff) if length > 0 and length < SUCK_RADIUS then local toTornado = VecScale(diff, 1.0/length) QueryRequire("static") local hit = QueryRaycast(ppos, toTornado, length) if not hit then if length < 3 then ReleasePlayerGrab() end ShakeCamera(1.0 - length/SUCK_RADIUS) --Make player approach tornado when on ground local strength = math.pow(1.0 - length / SUCK_RADIUS, 0.5) * 8.0 * tornado.amount; SetPlayerGroundVelocity(VecScale(toTornado, strength)) --Modify player velocity local vel = getVelForBody(ppos, 2000, GetPlayerVelocity(), Vec()) SetPlayerVelocity(vel) end end end end function spawnTornadoInTrigger(trigger) local mi, ma = GetTriggerBounds(trigger) local p1, p2 local minDist = math.max(ma[1]-mi[1], ma[3]-mi[3])*0.25 minDist = math.min(minDist, 5.0) for i=1, 100 do local probe = Vec() for j=1, 3 do probe[j] = mi[j] + (ma[j]-mi[j])*rnd(0,1) end if IsPointInTrigger(trigger, probe) then local ppos = GetPlayerTransform().pos ppos[2] = probe[2] if not p1 and VecLength(VecSub(probe, ppos)) > MIN_SPAWN_DIST_TO_PLAYER then p1 = probe p1[2] = mi[2] else if VecLength(VecSub(probe, p1)) > minDist then p2 = probe p2[2] = mi[2] break end end end end if p1 and p2 then spawn(p1, p2, 15) end end function spawnTornado() local ppos = TransformToParentPoint(GetPlayerTransform(), Vec(0, 0, -16)) DebugCross(ppos) ppos[2] = 0 local bestLength = 1000000 local bestTrigger = nil for i=1, #triggers do local mi, ma = GetTriggerBounds(triggers[i]) local pos = VecLerp(mi, ma, 0.5) pos[2] = 0 local l = VecLength(VecSub(ppos, pos)) if l < bestLength and trigger_cooldown[tonumber(GetTagValue(triggers[i],"id"))] <= 0 then bestLength = l bestTrigger = triggers[i] end end if bestTrigger then local setcooldown = math.random(min_cooldown, max_cooldown) local besttrigger_id = tonumber(GetTagValue(bestTrigger, "id")) trigger_cooldown[besttrigger_id] = setcooldown spawnTornadoInTrigger(bestTrigger) end end function update(dt) for i = 1, #trigger_cooldown do if trigger_cooldown[i] > 0 then trigger_cooldown[i] = trigger_cooldown[i] - dt end end if starttornado then spawn(GetLocationTransform(tornadostartpos).pos, GetLocationTransform(tornadoendpos).pos, 10) starttornado = false end if allowtornadospawn then if tornado.active then tornado.pos = VecLerp(tornado.startPos, tornado.endPos, tornado.age/tornado.lifetime) local fadeIn = 3 local fadeOut = 3 if tornado.age < fadeIn then tornado.amount = tornado.age/fadeIn elseif tornado.lifetime-tornado.age < fadeOut then tornado.amount = (tornado.lifetime-tornado.age)/fadeOut else tornado.amount = 1.0 end tornado.amount = tornado.amount * tornado.amount PlayLoop(tornadoSnd, tornado.pos, tornado.amount * 3) tornado.pos[1] = tornado.pos[1] + 0.03 tornado.pos[3] = tornado.pos[3] + 0.03 --destroy() suction() emitParticles() tornado.age = tornado.age + dt if cutsceneworks and destructioncount < #destructionpoints and tornado.age > 2 then if destructiontimer + 0.15 < GetTime() then MakeHole(GetLocationTransform(destructionpoints[destructioncount]).pos, 6.5, 6.5, 6.5, true) destructioncount = destructioncount + 1 destructiontimer = GetTime() end end CutsceneGears() if tornado.age > tornado.lifetime then tornado.active = false allowtornadospawn = false randomtry = 0 randomtime = 0 if cutsceneworks then cutsceneworks = false SUCK_RADIUS = 12 MAX_RADIUS = 8 MIN_RADIUS = 0.3 ROT_SPEED = 5 for i = 1, #clocks do Delete(clocks[i]) end end end else spawnTornado() end else randomtime = randomtime + dt if randomtime > 1 then --math.randomseed(GetScriptId()) local rollthedice = math.random(3) if rollthedice > 2 then allowtornadospawn = true else randomtry = randomtry + 1 randomtime = 0 end if randomtry > 2 then allowtornadospawn = true end end end end function CutsceneGears() if cutsceneworks then for i = 1, #cutsceneclock do if IsHandleValid(cutsceneclock[i]) and not IsBodyDynamic(cutsceneclock[i]) then if tonumber(GetTagValue(cutsceneclock[i], "time")) < GetTime() then RemoveTag(GetBodyShapes(cutsceneclock[i])[1], "invisible") SetBodyDynamic(cutsceneclock[i], true) SetShapeCollisionFilter(GetBodyShapes(cutsceneclock[i])[1], 2, 2) for j = 1, #cutsceneimpulsedir do if GetTagValue(cutsceneclock[i], "cutsceneclock") == GetTagValue(cutsceneimpulsedir[j], "cutsceneimpulsedir") then local dir = VecScale(VecNormalize(VecSub(GetLocationTransform(cutsceneimpulsedir[j]).pos, GetBodyTransform(cutsceneclock[i]).pos)), tonumber(GetTagValue(cutsceneimpulsedir[j],"impulse"))) ApplyBodyImpulse(cutsceneclock[i], Vec(0,0,0), dir) end end end elseif IsHandleValid(cutsceneclock[i]) and IsBodyDynamic(cutsceneclock[i]) then if tonumber(GetTagValue(cutsceneclock[i], "deleteafter")) < GetTime() then Delete(cutsceneclock[i]) end end end end end function handleCommand(cmd) if cmd == "quickload" then --from scene init tornadostartpos = FindLocation("start") tornadoendpos = FindLocation("finish") destructionpoints = FindLocations("destruction") clockpart = FindBodies("clockpart", true) fallenpart = FindLocations("fallenpart", true) clocks = FindShapes("clock", true) cutsceneclock = FindBodies("cutsceneclock", true) cutsceneimpulsedir = FindLocations("cutsceneimpulsedir", true) end end