g_ezentitymover_sequence = -- 1 2 3 4 5 6 7 {["Trigger1"] = {"h","sm500","my-5","h","my5"}, ["DeathPad"] = {"h"}, -- 1 2 3 4 5 6 7 8 9 10 ["Door1"] = {"h","sm6000","mx-70","h","pm1000","mx+70","r"}, ["Door2"] = {"h","sm6000","mx+70","h","pm1000","mx-70","r"}, ["Moonlander"]= {"ps5","SM1000;SL100","lb10","si-50;my50","le0","lb12","si-50;mf70;tx2;ty10;my20","le0","h"}, ["Ship1"] = {"ps8","SM500","my50","mf500;tx20","mf700;tz20","mf800","h"} }; g_ezentitymover_dimensions = -- circular platforms only need radius and height {["Trigger1"] = {radius = 100, length = 70, width = 50, height = 20, forward = 0}, ["DeathPad"] = {radius = 100, length = 30, width = 30, height = 20, forward = 0, trigger_only = true}, ["Door1"] = {radius = 0, forward = 0}, ["Door2"] = {radius = 0, forward = 0}, ["Moonlander"]= {radius = 0, forward = 270}, -- denotes no 'player-on-platform' detection ["Ship1"] = {radius = 0, forward = 180} }; g_ezplatform_animations = {{s=3710, e=3744}, -- sit on chair {s=3828, e=3862}, -- get up from chair {s=3870, e=3900} -- walk unarmed }; g_player_on_platform = nil g_ezentitymover_list = {} g_ezentitymover_name = {} g_movement_keys_pressed = false g_jump_pressed_only = false g_Player_Angle = 0 g_player_id = 399 -- for 3rd person !! need to set manually !! g_default_spd = 10 -- seconds per sequence function ezentitymover_init_name(e, name) g_ezentitymover_name[e] = {name = string.match(name, "(%w+)_"), seqn = string.match(name, "_(%d+)"), multi = true} if g_ezentitymover_name[e].name == nil then g_ezentitymover_name[e] = {name = name, seqn = 1, multi = false} end end function ezentitymover_exit (e) g_ezentitymover_list[e] = nil g_ezentitymover_name[e] = nil end function RotatePoint2D (x, y, Ang) -- Ang in radians local Sa, Ca = math.sin(Ang), math.cos(Ang) return x*Ca - y*Sa, x*Sa + y*Ca end function Rotate3D (x, y, z, xrot, yrot, zrot) -- Angles in degrees local NX, NY, NZ = x, y, z -- X NZ, NY = RotatePoint2D (NZ, NY, math.rad(-xrot)) -- Y NX, NZ = RotatePoint2D (NX, NZ, math.rad(yrot)) -- Z NY, NX = RotatePoint2D (NY, NX, math.rad(zrot)) return NX, NY, NZ end -- this function checks if given x,z coordinates are overlapping a particular -- box taking into account rotation function is_overlapping(x1, z1, x2, z2, Ang, L, W, H) local HL, HW = L / 2, W / 2 local DX, DZ = x1 - x2, z1 - z2 local Pang = math.rad(Ang) local NDX, NDZ = RotatePoint2D (DX, DZ, Pang) return (NDZ > -HL and NDZ < HL and NDX > -HW and NDX < HW), NDX, NDZ; end function ezentitymover_parse_sequence (e, Plat, m, a) if m == nil then return end local Mode = string.sub(string.lower(m),1,1) if Mode == 'a' then -- Animation local AnimType = string.sub(string.lower(m),2,2) if AnimType == 'f' then -- Set Frame if a ~= nil then SetAnimationFrame(e, a) StopAnimation(e) end Plat.seq_in_progress = true Plat.sequence_time = g_Time + Plat.speed else -- Anim Sequence if a ~= nil then local anim_index = 0 + a StopAnimation(e) local anims = g_ezentitymover_animations[anim_index] if anims ~= nil then SetAnimationFrames(anims.s, anims.e) if AnimType == 's' then PlayAnimation(e) elseif AnimType == 'l' then LoopAnimation(e) end end end Plat.seq_in_progress = true Plat.sequence_time = g_Time + Plat.speed end elseif Mode == 'l' then -- Looping local BeginEnd = string.sub(string.lower(m),2,2) if BeginEnd == 'b' then Plat.LoopSeq = Plat.sequence Plat.LoopCount = math.abs(a or 2) - 1 elseif BeginEnd == 'e' and Plat.LoopCount > 0 then Plat.LoopCount = Plat.LoopCount - 1 Plat.sequence = Plat.LoopSeq end Plat.sequence_time = g_Time Plat.seq_in_progress = true elseif Mode == 'm' then -- Movement local Direction = string.sub(string.lower(m),2,2) if Direction == 'f' then -- Forward (or backward) if a ~= nil then if (0 + a) < 0 then Plat.Direction = 'B' else Plat.Direction = 'F' end Plat.Dist_Left = math.abs(a) Plat.seq_in_progress = true Plat.sequence_time = g_Time + Plat.speed end elseif Direction == 's' then -- Sideways if a ~= nil then if 0 + a < 0 then Plat.Direction = 'L' else Plat.Direction = 'R' end Plat.Dist_Left = math.abs(a) Plat.seq_in_progress = true Plat.sequence_time = g_Time + Plat.speed end elseif Direction == 'x' then -- X axis if a ~= nil then Plat.TargX = Plat.curr_x + a Plat.seq_in_progress = true Plat.sequence_time = g_Time + Plat.speed end elseif Direction == 'y' then if a ~= nil then Plat.TargY = Plat.curr_y + a Plat.seq_in_progress = true Plat.sequence_time = g_Time + Plat.speed end elseif Direction == 'z' then if a ~= nil then Plat.TargZ = Plat.curr_z + a Plat.seq_in_progress = true Plat.sequence_time = g_Time + Plat.speed end end elseif Mode == 't' then -- Turn local Axis = string.sub(string.lower(m),2,2) if a ~= nil then if Axis == 'y' then Plat.TargYAng = Plat.curr_rotY + a elseif Axis == 'x' then Plat.TargXAng = Plat.curr_rotX + a elseif Axis == 'z' then Plat.TargZAng = Plat.curr_rotZ + a end Plat.seq_in_progress = true Plat.sequence_time = g_Time + Plat.speed end -- TODO: Add Entity relative rotation (using quaternions??) -- p = pitch, r = roll, t = turn (i.e. yaw but we're already using 'y') elseif Mode == 's' then local Units = string.sub(string.lower(m),2,2) if Units == 'i' then -- Increment speed (always milliseconds!!) if a ~= nil then Plat.speed = Plat.speed + a if Plat.speed < Plat.min_spd then Plat.speed = Plat.min_spd elseif Plat.speed > Plat.max_spd then Plat.speed = Plat.max_spd end end elseif Units == 'l' then -- Set minimum speed (always milliseconds!!) Plat.min_spd = math.abs(a or (g_default_spd * 1000)) elseif Units == 'h' then -- Set maximum speed (always milliseconds!!) Plat.max_spd = math.abs(a or (g_default_spd * 1000)) elseif Units == 's' then -- Seconds Plat.speed = math.abs(a or g_default_spd) * 1000 elseif Units == 'm' then -- Milliseconds Plat.speed = math.abs(a or (g_default_spd * 1000)) end Plat.seq_in_progress = true elseif Mode == 'p' then -- Pause local Units = string.sub(string.lower(m),2,2) local pause_time = 0 if Units == 's' then -- Seconds pause_time = math.abs(a or g_default_spd) * 1000 elseif Units == 'm' then -- Milliseconds pause_time = math.abs(a or (g_default_spd * 1000)) end Plat.sequence_time = g_Time + pause_time Plat.seq_in_progress = true end end ------------------------------------------------------------ function ezentitymover_status (e) -- returns Sequence (e.g. 3) and In_Progress flag local Plat = g_ezentitymover_list[e] if Plat ~= nil then return Plat.sequence, Plat.seq_in_progress end return 0, false end ------------------------------------------------------------ function ezentitymover_set_sequence (e, sequence) local Plat = g_ezentitymover_list[e] if Plat ~= nil then if sequence ~= nil then Plat.sequence = sequence else Plat.sequence = Plat.sequence + 1 end Plat.seq_in_progress = false end end ------------------------------------------------------------ function get_ezentity_from_name(name) for k, _ in pairs(g_ezentitymover_name) do if g_ezentitymover_name[k].name == name then return k end end end ------------------------------------------------------------ function ezentitymover_main(e) -- don't do anything until init has been called if g_ezentitymover_name[e] == nil then return end local name = g_ezentitymover_name[e].name -- ... or if name is not in dimensions list if g_ezentitymover_dimensions[name] == nil then return end local params = g_ezentitymover_dimensions[name] local Ent = g_Entity[e] -- initialise list entry if g_ezentitymover_list[e] == nil then g_ezentitymover_list[e] = {init_x = Ent.x, init_y = Ent.y, init_z = Ent.z, init_rotY = Ent.angley, init_rotX = Ent.anglex, init_rotZ = Ent.anglez, curr_x = Ent.x, curr_y = Ent.y, curr_z = Ent.z, curr_rotY = Ent.angley, curr_rotX = Ent.anglex, curr_rotZ = Ent.anglez, speed = g_default_spd * 1000, min_spd = 0, max_spd = math.huge, sequence = (0 + g_ezentitymover_name[e].seqn), seq_in_progress = false, sequence_time = 0, TargX = nil, TargY = nil, TargZ = nil, TargYAng = nil, TargXAng = nil, TargZAng = nil, Dist_Left = nil, Direction = nil, LoopSeq = 0, LoopCount = 0}; return end -- if we have got here the platform is initialised and we are good to go local Plat = g_ezentitymover_list[e] -- Detect ke presses, need arrow keys added if (g_KeyPressW + g_KeyPressS + g_KeyPressA + g_KeyPressD + g_KeyPressSPACE) ~= 0 then g_movement_keys_pressed = true g_jump_pressed_only = g_KeyPressSPACE ~= 0 and (g_KeyPressW + g_KeyPressS + g_KeyPressA + g_KeyPressD) == 0 end local do_this_frame = scheduler(e) if do_this_frame and (g_player_on_platform == nil or g_player_on_platform.entity == e) then -- Time for a bandaid fix, due to physics now being before Lua there is a potential for the -- physics engine to have moved the player relative to the platform since the last check we -- made. This results in the player drifting around in a strange way on the platform. -- To try and improve matters we keep track of whether the movement keys have been pressed -- and only update player position if they have if params.radius > 0 and ((g_movement_keys_pressed and not g_jump_pressed_only) or g_player_on_platform == nil) then local Px, Pz, Py = g_PlayerPosX, g_PlayerPosZ, g_PlayerPosY local player_on_platform = false local xo, zo = 0, 0 if CloserThan (Plat.curr_x, Plat.curr_z, Px, Pz, params.radius) then -- coarse check for squares if Py > (Plat.curr_y + params.height) and Py < (Plat.curr_y + params.height * 5) then -- are we potentially on platform? if params.length ~= nil and params.width ~= nil then player_on_platform, xo, zo = is_overlapping (Px, Pz, Plat.curr_x, Plat.curr_z, Plat.curr_rotY, params.length, params.width, params.height); if player_on_platform then g_player_on_platform = {entity = e, Xoffset = xo, Yoffset = Py - Plat.curr_y, Zoffset = zo}; if g_PlayerThirdPerson == 1 then g_Player_Angle = GetEntityAngleY(g_player_id) else g_Player_Angle = Py end end -- else -- todo: calculate offset for circular case and include rotation end end end if not player_on_platform and g_player_on_platform ~= nil then if g_player_on_platform.entity == e then g_player_on_platform = nil end end end elseif g_PlayerHealth == 0 then g_player_on_platform = nil end local current_seq = g_ezentitymover_sequence[name][Plat.sequence] -- if we've reached the end of the sequence start back at the start if current_seq == nil then Plat.sequence = 1 current_seq = g_ezentitymover_sequence[name][Plat.sequence] end if not Plat.seq_in_progress and string.lower(string.sub(current_seq,1,1)) ~= 'h' then -- Make sure platform angle in 'normal' range while Plat.curr_rotY < -180 or Plat.curr_rotY > 180 do if Plat.curr_rotY < -180 then Plat.curr_rotY = Plat.curr_rotY + 360 elseif Plat.curr_rotY > 180 then Plat.curr_rotY = Plat.curr_rotY - 360 end end Plat.TargXAng = nil Plat.TargYAng = nil Plat.TargZAng = nil if string.lower(string.sub(current_seq,1,1)) == 'r' then if g_ezentitymover_name[e].multi then -- Get initial x,y,z from first platform in sequence for k, _ in pairs(g_ezentitymover_name) do if g_ezentitymover_name[k].name == name and g_ezentitymover_name[k].seqn == '1' then Plat.curr_x = g_ezentitymover_list[k].init_x Plat.curr_y = g_ezentitymover_list[k].init_y Plat.curr_z = g_ezentitymover_list[k].init_z Plat.curr_rotX = g_ezentitymover_list[k].init_rotX Plat.curr_rotY = g_ezentitymover_list[k].init_rotY Plat.curr_rotZ = g_ezentitymover_list[k].init_rotZ break end end else Plat.curr_x = Plat.init_x Plat.curr_y = Plat.init_y Plat.curr_z = Plat.init_z Plat.curr_rotX = Plat.init_rotX Plat.curr_rotY = Plat.init_rotY Plat.curr_rotZ = Plat.init_rotZ end Plat.sequence = 1 if g_player_on_platform ~= nil and g_player_on_platform.entity == e then g_player_on_platform = nil end else local m1, a1 = string.match(current_seq, "(%a+)([+-]?%d+)(;?)") local m2, a2 = string.match(current_seq, ";(%a+)([+-]?%d+)") local m3, a3 = string.match(current_seq, ";%a+[+-]?%d+;(%a+)([+-]?%d+)") local m4, a4 = string.match(current_seq, ";%a+[+-]?%d+;%a+[+-]?%d+;(%a+)([+-]?%d+)") local m5, a5 = string.match(current_seq, ";%a+[+-]?%d+;%a+[+-]?%d+;%a+[+-]?%d+;(%a+)([+-]?%d+)") local m6, a6 = string.match(current_seq, ";%a+[+-]?%d+;;%a+[+-]?%d+;%a+[+-]?%d+;%a+[+-]?%d+;(%a+)([+-]?%d+)") ezentitymover_parse_sequence (e, Plat, m1, a1) ezentitymover_parse_sequence (e, Plat, m2, a2) ezentitymover_parse_sequence (e, Plat, m3, a3) ezentitymover_parse_sequence (e, Plat, m4, a4) ezentitymover_parse_sequence (e, Plat, m5, a5) ezentitymover_parse_sequence (e, Plat, m6, a6) end elseif Plat.seq_in_progress and string.lower(string.sub(current_seq,1,1)) == 'h' then Plat.seq_in_progress = false end local yrot = 0 if Plat.seq_in_progress then local time_left = Plat.sequence_time - g_Time local num_steps = g_scheduler[e].frames_per_second * time_left / 1000 if Plat.Dist_Left ~= nil then local distthisframe = 0 if num_steps < 1 then distthisframe = Plat.Dist_Left else distthisframe = Plat.Dist_Left / num_steps end local NX, NY, NZ = 0, 0, 0 local forwardang = math.rad(params.forward or 0) NX = distthisframe * (math.sin(forwardang)) NZ = distthisframe * (math.cos(forwardang)) if Plat.Direction == 'B' then NX, NZ = -NX, -NZ elseif Plat.Direction == 'L' or Plat.Direction == 'R' then NX = -NX end NX, NY, NZ = Rotate3D (NX, NY, NZ, Plat.curr_rotX, Plat.curr_rotY, Plat.curr_rotZ) -- Angles in degrees Plat.curr_y = Plat.curr_y + NY Plat.curr_x = Plat.curr_x + NX Plat.curr_z = Plat.curr_z + NZ Plat.Dist_Left = Plat.Dist_Left - distthisframe -- make sure 'move' command cancels any x,z movement 'just in case' Plat.TargX = nil Plat.TargZ = nil if Plat.Dist_Left == 0 then Plat.Dist_Left = nil Plat.Direction = nil if Plat.seq_in_progress then Plat.seq_in_progress = false Plat.sequence = Plat.sequence + 1 end end end if Plat.TargX ~= nil then if num_steps < 1 then Plat.curr_x = Plat.TargX Plat.TargX = nil if Plat.seq_in_progress then Plat.seq_in_progress = false Plat.sequence = Plat.sequence + 1 end else Plat.curr_x = Plat.curr_x + ((Plat.TargX - Plat.curr_x) / num_steps) end end if Plat.TargY ~= nil then if num_steps < 1 then Plat.curr_y = Plat.TargY Plat.TargY = nil if Plat.seq_in_progress then Plat.seq_in_progress = false Plat.sequence = Plat.sequence + 1 end else Plat.curr_y = Plat.curr_y + ((Plat.TargY - Plat.curr_y) / num_steps) end end if Plat.TargZ ~= nil then if num_steps < 1 then Plat.curr_z = Plat.TargZ Plat.TargZ = nil if Plat.seq_in_progress then Plat.seq_in_progress = false Plat.sequence = Plat.sequence + 1 end else Plat.curr_z = Plat.curr_z + ((Plat.TargZ - Plat.curr_z) / num_steps) end end if Plat.TargYAng ~= nil then if num_steps < 1 then Plat.curr_rotY = Plat.TargYAng Plat.TargYAng = nil if Plat.seq_in_progress then Plat.seq_in_progress = false Plat.sequence = Plat.sequence + 1 end else yrot = ((Plat.TargYAng - Plat.curr_rotY) / num_steps) Plat.curr_rotY = Plat.curr_rotY + yrot -- Make sure platform angle in 'normal' range Plat.curr_rotY, Plat.TargYAng = canonical_angle(Plat.curr_rotY, Plat.TargYAng) end end if Plat.TargXAng ~= nil then if num_steps < 1 then Plat.curr_rotX = Plat.TargXAng Plat.TargXAng = nil if Plat.seq_in_progress then Plat.seq_in_progress = false Plat.sequence = Plat.sequence + 1 end else local xrot = ((Plat.TargXAng - Plat.curr_rotX) / num_steps) Plat.curr_rotX = Plat.curr_rotX + xrot -- Make sure platform angle in 'normal' range Plat.curr_rotX, Plat.TargXAng = canonical_angle(Plat.curr_rotX, Plat.TargXAng) end end if Plat.TargZAng ~= nil then if num_steps < 1 then Plat.curr_rotZ = Plat.TargZAng Plat.TargZAng = nil if Plat.seq_in_progress then Plat.seq_in_progress = false Plat.sequence = Plat.sequence + 1 end else local zrot = ((Plat.TargZAng - Plat.curr_rotZ) / num_steps) Plat.curr_rotZ = Plat.curr_rotZ + zrot -- Make sure platform angle in 'normal' range Plat.curr_rotZ, Plat.TargZAng = canonical_angle(Plat.curr_rotZ, Plat.TargZAng) end end if g_Time > Plat.sequence_time then Plat.seq_in_progress = false Plat.sequence = Plat.sequence + 1 end end -- PromptLocal(e, Plat.sequence .. ", " .. current_seq .. ", " .. Plat.speed) if g_player_on_platform ~= nil and g_player_on_platform.entity == e and not params.trigger_only and g_PlayerHealth > 0 and not g_movement_keys_pressed then local Pang = math.rad(Plat.curr_rotY) local NDX, NDZ = RotatePoint2D (g_player_on_platform.Xoffset, g_player_on_platform.Zoffset, -Pang) SetFreezePosition ((Plat.curr_x + NDX), (Plat.curr_y + g_player_on_platform.Yoffset), (Plat.curr_z + NDZ)) if g_PlayerThirdPerson == 0 then if yrot ~= 0 then if math.abs(g_PlayerAngY - g_Player_Angle) > 1 then g_Player_Angle = g_PlayerAngY else g_Player_Angle = g_Player_Angle + yrot -- Make sure angle in 'normal' range g_Player_Angle = canonical_angle(g_Player_Angle) SetFreezeAngle(g_PlayerAngX, g_Player_Angle, g_PlayerAngZ) TransportToFreezePosition() end else TransportToFreezePositionOnly() end else if yrot ~= 0 then g_Player_Angle = g_Player_Angle + yrot ResetRotation(g_player_id, 0, g_Player_Angle, 0) end TransportToFreezePositionOnly() end end if do_this_frame and (g_player_on_platform == nil or g_player_on_platform.entity == e) then g_movement_keys_pressed = false end SendMessageI("collisionoff", e) ResetPosition(e, Plat.curr_x, Plat.curr_y, Plat.curr_z) ResetRotation(e, Plat.curr_rotX , Plat.curr_rotY, Plat.curr_rotZ) SendMessageI("collisionon", e) -- PromptLocal(e, math.modf(Plat.curr_rotX) .. ", " .. -- math.modf(Plat.curr_rotY) .. ", " .. -- math.modf(Plat.curr_rotZ)) end ------------------------------------------------------------ function canonical_angle (ang1, ang2) if ang1 < -180 then if ang2 == nil then return (ang1 + 360), nil else return (ang1 + 360), (ang2 + 360) end elseif ang1 > 180 then if ang2 == nil then return (ang1 - 360), nil else return (ang1 - 360), (ang2 - 360) end end return ang1, ang2 end ------------------------------------------------------------ function CloserThan(Ex, Ez, Px, Pz, dist) dist = dist or 100 local DX, DZ = Ex - Px, Ez - Pz return (DX*DX + DZ*DZ) <= (dist * dist) 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