-- LUA Script - precede every function and global member with lowercase name of script + '_main' -- Default script - does nothing. local PlayerActivationRange = 1000 -- change value to suit local HeliTargetHeight = 10000 -- change value to suit local Q = require "scriptbank\\quatlib" math.randomseed(os.time()) local deg = math.deg local rad = math.rad local sin = math.sin local cos = math.cos local atan = math.atan2 local tan = math.tan local pi = math.pi local abs = math.abs local random = math.random -- make sure random number generator is seeded random(); random(); random() planes = {} function helicopter_init(e) Include("quatlib.lua") SetAnimationSpeed(e, 0) end local function CloserThan(Ent, dist) local dX, dZ = Ent.x - g_PlayerPosX, Ent.z - g_PlayerPosZ return (dX*dX + dZ*dZ) < dist*dist end local function TimerExpired(p) return g_Time > p.timer end local function SetTimerSecs(p, val) if TimerExpired(p) then p.timer = g_Time + val * 1000 end end local function PlayerAngle(Ent) -- gives angle to player from entity return 180 + deg(atan(Ent.x - g_PlayerPosX, Ent.z - g_PlayerPosZ)) end local function CloseEnough(p1, p2) local dX, dY, dZ = p1.x - p2.x, p1.y - p2.y, p1.z - p2.z return (dX*dX + dY*dY + dZ * dZ) < (2000 * 2000) end local function SetCollisionFlag(e, plane) plane.checkCols = false for k,v in pairs(planes) do if k ~= e then if CloseEnough (plane, v) then plane.checkCols = true break end end end end local function WillCollide(x, y, z, x2, y2, z2) local dX, dY, dZ = x - x2, y - y2, z - z2 local coll = (dX*dX + dY*dY + dZ*dZ) < (300*300) local angle, ydiff = 0, 0 if coll then angle = 180 + deg(atan(dX, dZ)) ydiff = dY end return angle, ydiff end local function turnAngle(tA, cA) -- degrees local dA = tA - cA if dA > 0 then if dA < 180 then return dA end return -(360 - dA) else if dA > 180 then return -(360 + dA) end return dA end end local function Rotate3D (x, y, z, xrot, yrot, zrot) function RotatePoint2D (x, y, Ang) -- Ang in radians local Sa, Ca = sin(Ang), cos(Ang) return x*Ca - y*Sa, x*Sa + y*Ca end local NX, NY, NZ = x, y, z -- X NZ, NY = RotatePoint2D (NZ, NY, -xrot) -- Y NX, NZ = RotatePoint2D (NX, NZ, -yrot) -- Z NY, NX = RotatePoint2D (NY, NX, -zrot) return NX, NY, NZ end local function WrapAng(Ang) local RAng = Ang while RAng >= 360 do RAng = RAng - 360 end while RAng < 0 do RAng = RAng + 360 end return RAng end function HeliAddRocket(e) for k,v in pairs(planes) do for i = 1,4 do if v.rockets[i] == nil then planes[k].rockets[i] = e return true end end end return false end local function fireRocket(p) local fireAngle = rad(p.currPitch + 90) local fireDist = tan(fireAngle) * (p.y - g_PlayerPosY) local dX, dZ = p.x - g_PlayerPosX, p.z - g_PlayerPosZ local pdist = (dX * dX + dZ * dZ) fireDist = fireDist * fireDist -- be careful here, all distance values are squared! if fireDist < (pdist - 150000) or fireDist + p.stepSize > (pdist + 150000) then return end for i = 1,4 do if p.rockets[i] ~= nil and random (1,3) == 1 then if hrFire ~= nil then hrFire(p.rockets[i], p.currQuat) p.rockets[i] = nil end end end end local function doRocket(e, x, y, z, xo, yo, zo, xA, yA, zA) local XO, YO, ZO = Rotate3D (xo, yo, zo, xA, yA, zA) ResetPosition(e, x + XO, y + YO, z + ZO) ResetRotation(e, deg(xA), deg(yA), deg(zA)) end local function displayRockets(p, xA, yA, zA) if p.rockets[1] ~= nil then doRocket(p.rockets[1], p.x, p.y, p.z, -88, 32, 25, xA, yA, zA) end if p.rockets[2] ~= nil then doRocket(p.rockets[2], p.x, p.y, p.z, -58, 32, 25, xA, yA, zA) end if p.rockets[3] ~= nil then doRocket(p.rockets[3], p.x, p.y, p.z, 58, 32, 25, xA, yA, zA) end if p.rockets[4] ~= nil then doRocket(p.rockets[4], p.x, p.y, p.z, 88, 32, 25, xA, yA, zA) end end local pitchQuat = {} local bankQuat = {} local function updatePosition(e, Ent, plane) local FPSrel = 1 / g_scheduler[e].frames_per_second plane.angleLeft = turnAngle(plane.targAngle, plane.currAngle) -- do rotation here if abs(plane.angleLeft) > 0.5 then local turnAmount = plane.turnSpeed * FPSrel if turnAmount > abs(plane.angleLeft) then turnAmount = abs(plane.angleLeft) end local turnQuat if plane.angleLeft > 0 then turnQuat = Q.FromEuler(0, rad(turnAmount), 0) plane.currAngle = plane.currAngle + turnAmount plane.targBank = 30 else turnQuat = Q.FromEuler(0, -rad(turnAmount), 0) plane.currAngle = plane.currAngle - turnAmount plane.targBank = -30 end plane.quat = Q.Mul(plane.quat, turnQuat) else plane.angleLeft = 0 if plane.state ~= 'fall' and plane.state ~= 'startfall' then plane.targBank = 0 plane.currAngle = WrapAng(plane.currAngle) end end local quat = plane.quat -------------------------------------------------------------- -- do bank here if plane.targBank ~= plane.currBank then local bA = (plane.targBank - plane.currBank) * FPSrel plane.currBank = bA + plane.currBank bankQuat[e] = Q.FromEuler(0, 0, rad(plane.currBank)) end if plane.currBank ~= 0 then quat = Q.Mul(quat, bankQuat[e]) end -------------------------------------------------------------- -- do pitch here if plane.targPitch ~= plane.currPitch then local pA = (plane.targPitch - plane.currPitch) * FPSrel plane.currPitch = pA + plane.currPitch pitchQuat[e] = Q.FromEuler(rad(plane.currPitch), 0, 0) end if plane.currPitch ~= 0 then quat = Q.Mul(quat, pitchQuat[e]) end plane.currQuat = quat -------------------------------------------------------------- local xA, yA, zA = Q.ToEuler(quat) -- change the vector to match 'forward' for the model, i.e. pointing up is 0,0,1 left would be 1,0,0 etc local vX, vY, vZ = Rotate3D (0, 0, 1, xA, yA, zA) local cX, cZ = vX * plane.speed * FPSrel, vZ * plane.speed * FPSrel -- store step size for rocket targetting code plane.stepSize = cX * cX + cZ * cZ -- climb code local cY = plane.climbSpeed * FPSrel local nX, nY, nZ = plane.x + cX, plane.y + cY, plane.z + cZ local ccX1, ccZ1 = plane.x + cX * 60, plane.z + cZ * 60 local ccX2, ccZ2 = plane.x + cX * 120, plane.z + cZ * 120 local colAng, colY, colType = 0,0,'close' if plane.checkCols then -- check for collision with other plane for k,v in pairs(planes) do if k ~= e and v.checkCols then colAng, colY = WillCollide(nX, nY, nZ, v.x, v.y, v.z) if colAng == 0 and colY == 0 then colAng, colY = WillCollide(ccX1, nY, ccZ1, v.x, v.y, v.z) end if colAng == 0 and colY == 0 then colAng, colY = WillCollide(ccX2, nY, ccZ2, v.x, v.y, v.z) if colAng ~= 0 or colY ~= 0 then colType = 'far' end else colType = 'near' end if colAng ~= 0 or colY ~= 0 then break end end end end if colAng ~= 0 then if colType == 'close' then plane.targAngle = WrapAng(colAng + 180) elseif colType == 'near' then plane.targAngle = WrapAng(colAng + 90) else plane.targAngle = WrapAng(colAng + 45) end end if plane.state ~= 'fall' and colY ~= 0 and abs(colY) < 300 then -- too close to another plane so climb or decend if colY < 0 then plane.climbSpeed = plane.climbSpeed + 20 else plane.climbSpeed = plane.climbSpeed - 20 end end -- now check terrain if plane.height ~= 0 then if plane.state == 'fly' or plane.state == 'turn' or plane.state == 'turning' then local tD = GetTerrainHeight(ccX2, ccZ2) + plane.height - nY if tD > 0 then -- climb, climb, climb !!! if tD > 200 then tD = 200 end plane.climbSpeed = tD elseif colY == 0 then -- maintain height tD = GetTerrainHeight(nX,nZ) + plane.height - nY plane.climbSpeed = tD end end end plane.x, plane.y, plane.z = nX, nY, nZ CollisionOff(e) ResetPosition(e, plane.x, plane.y, plane.z) ResetRotation(e, deg(xA), deg(yA), deg(zA)) displayRockets(plane, xA, yA, zA) CollisionOn(e) end function helicopter_main(e) local Ent = g_Entity[e] if Ent == nil then return end local plane = planes[e] if plane == nil then local Ang = WrapAng(Ent.angley) if Ang == 90 or Ang == 270 then Ang = Ang + 0.0001 end planes[e] = {x = Ent.x, y = Ent.y, z = Ent.z, quat = Q.FromEuler(0, rad(Ang), 0), currQuat = Q.FromEuler(0, rad(Ang), 0), state = 'init', timer = 0, lasttime = 0, speed = 0, -- units per second turnSpeed = 0, -- degrees per second climbSpeed = 0, -- units per second targAngle = Ang, currAngle = Ang, angleLeft = 0, -- radians targPitch = 0, currPitch = 0, targBank = 0, currBank = 0, height = 0, -- units above terrain hasBlade = false, checkCols = false, rockets = {}, stepSize = 0, soundPlaying = false }; if Ent.health < 100 then SetEntityHealth(e, 250) end return end if plane.state ~= 'idle' and plane.state ~= 'init' and plane.state ~= 'spinup' and plane.state ~= 'down' then updatePosition(e, Ent, plane) end if plane.state == 'down' or not scheduler(e) then return end -- initialization if plane.state == 'init' then plane.state = 'idle' GravityOff(e) local xA, yA, zA = Q.ToEuler(plane.quat) displayRockets(plane, xA, yA, zA) elseif plane.state == 'idle' and CloserThan(Ent, PlayerActivationRange + random(-400,400)) then plane.state = 'spinup' SetAnimationSpeed(e, 1) SetTimerSecs(plane, random(6,8)) elseif plane.state == 'spinup' and TimerExpired(plane) then plane.climbSpeed = random(80,120) SetTimerSecs(plane, random(3,4)) plane.state = 'takeoff' SetAnimationSpeed(e, 2) -- taking off elseif plane.state == 'takeoff' then if TimerExpired(plane) then plane.state = 'takeoff2' SetTimerSecs(plane, random(3,4)) plane.targPitch = random(-45,-35) end elseif plane.state == 'takeoff2' then if not TimerExpired(plane) then plane.climbSpeed = plane.climbSpeed + random(5,7) plane.speed = plane.speed + random(18,22) else plane.state = 'fly' plane.targPitch = random(-20,-15) end -- flying straight elseif plane.state == 'fly' then if plane.climbSpeed > 0 then plane.climbSpeed = plane.climbSpeed - random(6,9) else plane.climbSpeed = 0 if plane.height == 0 then plane.height = plane.y - GetTerrainHeight(plane.x, plane.z) if abs(plane.height - HeliTargetHeight) > 500 then plane.height = HeliTargetHeight + random(-400, 400) end end end if not CloserThan(Ent, random(8000,10000)) then plane.state = 'turn' plane.turnSpeed = random(20,30) plane.speed = plane.speed / 1.5 else if not plane.soundPlaying then LoopSound(e, 0) SetSoundVolume(80) plane.soundPlaying = true end end -- turning elseif plane.state == 'turn' then plane.targAngle = PlayerAngle(Ent) plane.state = 'turning' elseif plane.state == 'turning' then if plane.angleLeft == 0 and CloserThan(Ent, random(3000, 4000)) then plane.state = 'fly' plane.speed = plane.speed * 1.5 else local pA = PlayerAngle(Ent) if abs(plane.currAngle - pA) < 1 then if mfHeliShoot ~= nil then if random(1,3) == 1 then mfHeliShoot(e) if mfHeliSmoke ~= nil then mfHeliSmoke(e) end end -- if random(1,5) == 5 then if CloserThan(Ent, 9000) and not CloserThan(Ent, 2000) then fireRocket(plane) end -- end end else plane.targAngle = pA end end -- falling down elseif plane.state == 'startfall' then plane.targBank = random(-60,60) plane.targPitch = random(-60,60) plane.targAngle = random(0,359) plane.state = 'fall' elseif plane.state == 'fall' then if plane.speed > 10 then plane.speed = plane.speed - 7 end plane.climbSpeed = plane.climbSpeed - 5 if TimerExpired(plane) then SetTimerSecs(plane, 3) plane.targBank = random(-60,60) plane.targPitch = random(-60,60) plane.targAngle = random(0,359) SetAnimationSpeed(e, 0.25) end if plane.y <= GetTerrainHeight(plane.x, plane.z) + 100 then -- if hit the ground plane.state = 'down' -- change plane.state to down plane.Timer = math.huge StopParticleEmitter(e) -- stop smoke SetEntityHealth(e, 0) end end -- if flying and is shot to death (while keeping entity alive), set falling plane.state and start smoke if Ent.health < 100 then SetEntityHealth(e, 1000) plane.state = 'startfall' SetAnimationSpeed(e, 0.5) StartParticleEmitter(e) StopSound(e, 0) SetTimerSecs(plane, 4) GravityOn(e) else SetCollisionFlag(e, plane) end end ---------------------------------------------------------------- -- Scheduler, this nifty little function keeps track of time. -- -- The main routine should be called every frame and returns -- -- a flag indicating whether the passed in time has expired -- -- since the last time it was called. -- -- If no time period is specified 100ms is used by default, -- -- i.e. 1/10th of a second. -- -- The caller can either ignore the return flag or use it to -- -- trigger time sensitive functionality. -- -- A global value giving the frames per second count is also -- -- generated, this can be used in script to make animations -- -- independent of frames. -- -- The reason this function is tied to an entity rather than -- -- being global is so that each entity can be triggered at a -- -- different time rather than all being triggered in the same -- -- frame. ---------------------------------------------------------------- g_scheduler = {} function scheduler(e, period) period = period or 100 -- defaults to tenths of a second (100ms) if g_Time == nil then return false end if g_scheduler[e] == nil then g_scheduler[e] = {frames_per_second = 60, period_accumulated = math.random(0, period), accumulated_time = 0, timer_value_last_frame = g_Time, frame_counter = 0}; -- 'test' mode sometimes doesn't clear Lua globals so if -- this appears to be the case initialise everything elseif g_scheduler[e].accumulated_time > 2000 then g_scheduler[e].frames_per_second = 60 g_scheduler[e].period_accumulated = math.random(0, period) g_scheduler[e].accumulated_time = 0 g_scheduler[e].timer_value_last_frame = g_Time g_scheduler[e].frame_counter = 0 end local entry = g_scheduler[e] local do_this_frame_flag = false entry.frame_counter = entry.frame_counter + 1 local time_since_last_frame = g_Time - entry.timer_value_last_frame if (entry.period_accumulated + time_since_last_frame) > period then entry.period_accumulated = (entry.period_accumulated + time_since_last_frame) - period; do_this_frame_flag = true else entry.period_accumulated = entry.period_accumulated + time_since_last_frame end if (entry.accumulated_time + time_since_last_frame) > 1000 then -- more than a second passed? entry.accumulated_time = (entry.accumulated_time + time_since_last_frame) - 1000; entry.frames_per_second = entry.frame_counter entry.frame_counter = 0 else entry.accumulated_time = entry.accumulated_time + time_since_last_frame end entry.timer_value_last_frame = entry.timer_value_last_frame + time_since_last_frame return do_this_frame_flag end