#include "script/common.lua" 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 = 12 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 tornado.trail = false function init() tornadoSnd = LoadLoop("tornado.ogg") triggers = FindTriggers() 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 tornado.trail = true end function tick(dt) 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 TornadSpawnParticlesLUA(tornadoSpawnAngle, height, minRadius, maxRadius, rotSpeed, tornadoPos, tornadoAmount, tornadoAge, tornadoTrail) ParticleReset() ParticleType("plain") ParticleTile(0) ParticleCollide(0) local count = 64 tornado.spawnAngle = tornadoSpawnAngle + 2.39996 local spawnAngle = tornadoSpawnAngle for i=0, count do local hh = i/count hh = hh * hh * hh * hh -- Need to spawn more particles at bottom local q = 1.0-hh local h = height * hh spawnAngle = spawnAngle + 2.39996 --Golden angle local ang = spawnAngle local r = mix(minRadius, maxRadius, hh*hh) * rnd(0.7, 1.3) local v = rotSpeed local lifetime = 2/(v/r) --Position and velocity local pos = VecAdd(tornadoPos, Vec(math.cos(ang)*r, h, math.sin(ang)*r)) local offset = getOffset(hh) pos = VecAdd(pos, offset) local vel = Vec(-math.sin(ang)*v, v*q, math.cos(ang)*v) --Particle properties local col = rnd(0.7, 1.0) ParticleColor(col, 0.9*col, 0.8*col) ParticleRadius(0.4 + r*0.4) ParticleAlpha(q*tornadoAmount, 0, "constant", 0.25, 0.5) --Spawn SpawnParticle(pos, vel, lifetime) end if tornadoTrail then ParticleReset() ParticleCollide(0) ParticleType("smoke") ParticleAlpha(tornadoAmount, 0) ParticleTile(0) ParticleColor(0.9, 0.85, 0.8) ParticleDrag(0.5) for i=1, 4 do ParticleRadius(0.2, rnd(0.3, 0.6)) SpawnParticle(VecAdd(tornado.pos, Vec(rnd(-0.2, 0.2), rnd(0.1, 0.2), rnd(-0.2, 0.2))), Vec(rnd(-10, 10), rnd(-2, 2), rnd(-10, 10)), rnd(1.0, 3.0)) end end ParticleReset() ParticleType("plain") ParticleAlpha(tornadoAmount, 0) ParticleTile(6) ParticleColor(0.4, 0.4, 0.4) ParticleDrag(0.05) ParticleCollide(1.0) for i=1,4 do ParticleGravity(-10) ParticleRadius(rnd(0.01, 0.03)) SpawnParticle(VecAdd(tornadoPos, rndVec(0.1)), Vec(rnd(-5,5), rnd(2, 10), rnd(-5,5)), rnd(0.5, 1.5)) end end function emitParticles() tornado.spawnAngle = tornado.spawnAngle + 2.39996 --TornadSpawnParticlesLUA(tornado.spawnAngle, HEIGHT, MIN_RADIUS, MAX_RADIUS, ROT_SPEED, tornado.pos, tornado.amount, tornado.age, tornado.trail) TornadoSpawnParticlesCPP(tornado.spawnAngle, HEIGHT, MIN_RADIUS, MAX_RADIUS, ROT_SPEED, tornado.pos, tornado.amount, tornado.age, tornado.trail) 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 TornadoBodiesSuctionLUA(lo, hi) QueryRequire("dynamic") local bodies = QueryAabbBodies(lo, hi) for i=1, #bodies do local body = bodies[i] local bodyPos = TransformToParentPoint(GetBodyTransform(body), GetBodyCenterOfMass(body)) local vel, angVel = getVelForBody(bodyPos, GetBodyMass(body), GetBodyVelocity(body), GetBodyAngularVelocity(body)) SetBodyVelocity(body, vel) SetBodyAngularVelocity(body, 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, 10) 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 tornado.lifetime then tornado.active = false end else spawnTornado() end end