-- LUA Script - precede every function and global member with lowercase name of script + '_main' -- -- pickuppable script -- ================== -- -- Assign script to all items to be manipulated by the player -- -------------------------- -- user editable values -- -------------------------- local maxWeight = 800 -- default maximum weight of pickuppable -- or stack of pickuppables local maxSize = 400 -- default maximum size of pickuppable local minSpeed = 0.05 -- speed of movement at max weight local carryOfst = 0 -- x offset of carrying position \ local carryHght = 27 -- y offset of carrying position | relative to player local carryDist = 40 -- z offset of carrying position / local handSize = 15 -- size of hand icon (screen %) local throwEnabled = true -- Picked up items can be thrown local pushEnabled = true -- Also Pull -------------------------- local U = require "scriptbank\\utillib" local Q = require "scriptbank\\quatlib" local P = require "scriptbank\\physlib" local PU = require "scriptbank\\pickuplib" local modf = math.modf local deg = math.deg local rad = math.rad local max = math.max local min = math.min local abs = math.abs local sin = math.sin local cos = math.cos local atan = math.atan2 local sqrt = math.sqrt local log = math.log local rnd = math.random local find = string.find -- return a copy of a table/list (move to utils when tested) local function deepcopy( orig ) local orig_type = type( orig ) local copy if orig_type == 'table' then copy = {} for orig_key, orig_value in next, orig, nil do copy[ deepcopy( orig_key ) ] = deepcopy( orig_value ) end setmetatable( copy, deepcopy( getmetatable( orig ) ) ) else -- number, string, boolean, etc copy = orig end return copy end -- ============================== -- -- global variables for save/load -- -- ============================== -- g_pickuppables_list = g_pickuppables_list or {} -- fill g_pickuppables_list with saved version for situation -- where saving at a checkpoint if g_pickuppables_list == {} and g_pickuppables_snapshot ~= nil then g_pickuppables_list = deepcopy( g_pickuppables_snapshot ) end -- ============================== -- -- control lists local carryingEnt = nil -- 'pickuppable' actually in hand (i.e. bottom of any 'Stack' local droppedEnt = nil -- 'pickuppable' last to be in hand (i.e. last to be dropped) local promptsOn = true -- whether to show Prompts to player or not local thrownObj = nil -- details of thrown objects for collision purposes -- ================================= -- -- start of global functions section -- -- ================================= -- function PU_EnableThrowing() throwEnabled = true g_pickuppables_list.throwEnabled = throwEnabled end function PU_CanThrow() return throwEnabled end function PU_DisableThrowing() throwEnabled = false g_pickuppables_list.throwEnabled = throwEnabled end function PU_EnablePushPull() pushEnabled = true g_pickuppables_list.pushEnabled = pushEnabled end function PU_CanPush() return pushEnabled end function PU_DisablePushPull() pushEnabled = false g_pickuppables_list.pushEnabled = pushEnabled end -- global functions to read/set max weight we can lift function PU_GetMaxCarryWeight() return maxWeight end function PU_SetMaxCarryWeight( value ) maxWeight = value g_pickuppables_list.maxWeight = maxWeight end -- global functions to read/set max size of item we can lift function PU_GetMaxCarrySize() return maxSize end function PU_SetMaxCarrySize( value ) maxSize = value g_pickuppables_list.maxSize = maxSize end function PU_DisablePrompts() promptsOn = false end -- stops entity specified from being 'pickuppable' function PU_RemoveEntity( e ) local remList = g_pickuppables_list.removed if remList == nil then g_pickuppables_list.removed = {} remList = g_pickuppables_list.removed end remList[ e ] = true PU.setdimensions( e, nil ) PU.setpEnt( e, nil ) end -- returns nil or entity id of item being carried function PU_GetEntityCarried() return carryingEnt end -- returns nil or entity id of last dropped item function PU_LastDropped() return droppedEnt end -- returns empty list or list of all carried items function PU_GetCarriedList() local tempList = {} if carryingEnt == nil then return tempList end tempList[ #tempList + 1 ] = carryingEnt local function getStackItems( p ) if p.stackList == nil then return end for k, v in pairs( p.stackItems ) do tempList[ #tempList + 1 ] = k getStackItems( PU.getpEnt( k ) ) end end getStackItems( PU.getpEnt( carryingEnt ) ) return tempList end -- returns a list of carried items by name function PU_CarriedNames() local tempList = {} for _, v in pairs( PU_GetCarriedList() ) do tempList[ #tempList + 1 ] = PU.getName( v ) end return tempList end -- creates a copy of the current state for save/load purposes function PU_SnapshotGlobalList() g_pickuppables_snapshot = deepcopy( g_pickuppables_list ) end -- End of global functions -- -- ======================= -- local mouse1Clicked = false local mouse2Clicked = false local mouseTimer = math.huge -- sprite 'pointers' local Sprites = {} local handShowing = 'none' local function HideHand( hand ) SetSpritePosition( Sprites[ hand ], 200, 200) handShowing = 'none' end local function ShowHand( hand ) SetSpritePosition( Sprites[ hand ], 50, 50) handShowing = hand end ------------------------------------------- -- Init routine - called once per entity -- -- at spawn time -- ------------------------------------------- function pickuppable_init_name( e, name ) -- These includes ensure that the 'make standalone' process -- copies the libararies to the standalone folder Include( "utillib.lua" ) Include( "quatlib.lua" ) Include( "physlib.lua" ) Include( "pickuplib.lua" ) -- save name PU.setName( e, name ) -- null the list entries for this entity so that when -- main routine called for the first time they will be -- correctly initialised PU.setdimensions( g_Entity[e].obj, nil ) PU.setpEnt( e, nil ) -- load sprites if not already loaded if Sprites[ 'hand1' ] == nil then Sprites[ 'hand1' ] = CreateSprite(LoadImage ( "scriptbank\\pickuppable\\hand1.png" ) ) Sprites[ 'hand2' ] = CreateSprite(LoadImage ( "scriptbank\\pickuppable\\hand2.png" ) ) local s = Sprites[ 'hand1' ] SetSpriteOffset( s, -1 , handSize / 2 ) SetSpriteSize ( s, -1 , handSize ) SetSpriteDepth ( s, 0 ) HideHand( 'hand1' ) s = Sprites[ 'hand2' ] SetSpriteOffset( s, -1 , handSize / 2 ) SetSpriteSize ( s, -1 , handSize ) SetSpriteDepth ( s, 0 ) HideHand( 'hand2' ) end -- if there is global maximum weight/size use it to initialise the -- local copy if g_pickuppables_list.maxWeight ~= nil then maxWeight = g_pickuppables_list.maxWeight end if g_pickuppables_list.maxSize ~= nil then maxSize = g_pickuppables_list.maxSize end if g_pickuppables_list.pushEnabled ~= nil then pushEnabled = g_pickuppables_list.pushEnabled end if g_pickuppables_list.throwEnabled ~= nil then throwEnabled = g_pickuppables_list.throwEnabled end end local function processRotation( e, p, plQ ) local yaw = -( g_MouseX - 50 ) / 1000 local pitch = -( g_MouseY - 50 ) / 1000 if abs( yaw ) < 0.01 then yaw = 0 end if p.stackList ~= nil or abs( pitch ) < 0.01 then pitch = 0 end if promptsOn then Prompt( "Use mouse to rotate " .. PU.getName( e ) ) end if roty ~= 0 or rotx ~= 0 then local quat = Q.FromEuler( pitch, yaw, 0 ) p.quat = Q.Mul( quat, p.quat ) end end local topSpeed = 0 local dropTimer = math.huge local function dropObject( p, e ) droppedEnt = e mouse1Clicked = true carryingEnt = nil mouseTimer = g_Time + 200 -- reset quat ready for next time we pick this entity up -- stacking will use its own calculations p.quat = nil -- clear stackList(s) PU.clearStacks( p ) p.stackList = nil SetGamePlayerControlTopspeed( topSpeed ) dropTimer = g_Time + 1000 end local corners = {} local function adjustForTerrainCollision( nx, ny, nz, dims, ax, ay, az ) -- We need to work out the coordinates of each 'corner' and check if -- any of them would be under the terrain, if so we need to raise y -- by the difference plus a little bit -- ( could be expanded to add a raycast from corner to corner to check -- for a terrain collision, but that's difficult ;-) ) -- TODO - replace this with the new terrain collision detection commands -- at some point local hw, hh, hl = dims.w / 2, dims.h / 2, dims.l / 2 -- rotate offsets local ofx, ofy, ofz = U.Rotate3D( dims.cxs, dims.cys,dims.czs, ax, ay, az ); -- create our own collision box corners = { { xo = hw, yo = hh, zo = hl }, { xo = hw, yo = hh, zo = -hl }, { xo = hw, yo = -hh, zo = hl }, { xo = hw, yo = -hh, zo = -hl }, { xo = -hw, yo = hh, zo = hl }, { xo = -hw, yo = hh, zo = -hl }, { xo = -hw, yo = -hh, zo = hl }, { xo = -hw, yo = -hh, zo = -hl } }; local heightDiff = 0 -- now check all the corners to see if any are below -- the terrain for k, v in pairs( corners ) do -- rotate offsets to entity angle local ox, oy, oz = U.Rotate3D( v.xo, v.yo, v.zo, ax, ay, az ); local th = GetTerrainHeight( nx + ox + ofx , nz + oz + ofz ) local yh = ny + oy + ofy if th > yh and th - yh > heightDiff then heightDiff = th - yh end end return ny + heightDiff end -- synchromesh additions : check if any pickuppable is sitting on a platform function PU_EntityOnPlatform( x, z, y, Ang, L, W, H ) return PU.EntityOnPlatform( x, z, y, Ang, L, W, H ) end function PU_MoveIfOn( x, z, y, Ang, L, W, H, xi, yi, zi, rQuat ) PU.MoveIfOn( x, z, y, Ang, L, W, H, xi, yi, zi, rQuat ) end ----------------------------------------------------------------------------- local throwTimer = 0 local hitObject = {} local grenades = {} local function handleGrenades( timeNow ) for k, v in pairs( grenades ) do if timeNow > v.timer then SetEntityHealth( k, 0) grenades[ k ] = nil end end end local function CheckThrowCollision( timeNow ) if throwTimer == 0 then throwTimer = g_Time + 3000 end local colList = P.GetObjectCollisionDetails( thrownObj ) if colList ~= nil then for i, v in ipairs( colList ) do if v.obj ~= 0 then local force = v.f * 1000 local e = P.ObjectToEntity( v.obj ) local Ent = g_Entity[ e ] if Ent ~= nil and ( ai_bot_state[ e ] ~= nil or ( PU.getName( e ) ~= nil and find( PU.getName( e ), "Explosive" ) ~= nil ) ) then SetEntityHealth( e, Ent.health - force ) end if PU_ShowDecal ~= nil then PU_ShowDecal( "PU Decal", v.x, v.y, v.z, force * 5 ) end end end -- remove health from thrown object causing it to -- explode ( if explodable ) local e = P.ObjectToEntity( thrownObj ) local name = PU.getName( e ) if find( name, "Explosive" ) ~= nil or find( name, "Grenade" ) ~= nil then SetEntityHealth( e, 0) elseif find( name, "GrenadeDelayed" ) ~= nil then grenades[ e ] = { timer = timeNow + 2000 } end elseif g_Time < throwTimer then -- didn't hit anything yet so give it more time return end -- done with this object so remove from checking list RemoveObjectCollisionCheck( thrownObj ) thrownObj = nil throwTimer = 0 end local function HandleTerrainCollisions( timeNow ) local colList = P.GetTerrainCollisionDetails( thrownObj ) local hitTerrain = false if colList == nil then return end for _, v in ipairs( colList ) do if v.x ~= 0 then if PU_ShowDecal ~= nil then PU_ShowDecal( "PU Decal2", v.x, v.y, v.z ) end hitTerrain = true end end if hitTerrain then local e = P.ObjectToEntity( thrownObj ) local name = PU.getName( e ) if find( name, "GrenadeDelayed" ) ~= nil then grenades[ e ] = { timer = timeNow + 2000 } elseif find( name, "Grenade" ) ~= nil then SetEntityHealth( e, 0) end end end local currSpeed = 0 local colCheckTimer = 0 ----------------------------------------------------- -- Start of main entry routine - called very frame -- ----------------------------------------------------- function pickuppable_main(e) local timeNow = g_Time handleGrenades( timeNow ) if throwEnabled and P.ObjectToEntity( thrownObj ) == e then if timeNow > colCheckTimer then HandleTerrainCollisions( timeNow ) CheckThrowCollision( timeNow ) colCheckTimer = timeNow + 100 -- 1/10 second- end end if g_pickuppables_list.removed ~= nil and g_pickuppables_list.removed[ e ] then return end local Ent = g_Entity[ e ] if Ent == nil then return end PushObject( Ent.obj, 0, 0, 0 ) -- first time though calculate dimensions of entity local dims = PU.getdimensions( Ent.obj ) if dims == nil then PU.setdimensions( Ent.obj, P.GetObjectDimensions( Ent.obj ) ) return end local p = PU.getpEnt( e ) if p == nil then -- store the object id for future use PU.setpEnt( e, { obj = Ent.obj } ) return end if carryingEnt == e or droppedEnt == e then PU.stopEntsPunchingThroughTerrain( carryingEnt ) -- delayed re-enabling of jump mode if timeNow > dropTimer and carryingEnt == nil then SetGamePlayerControlJumpMode( 0 ) dropTimer = math.huge end end local pObj, objX, objY, objZ = 0, 0, 0, 0 if carryingEnt == nil then if not U.PlayerCloserThan( e, 200 ) then return end pObj, objX, objY, objZ = P.ObjectPlayerLookingAt( 100 ) if pObj == p.obj then if handShowing == 'none' then ShowHand( 'hand1' ) end elseif not PU.ObjectPickuppable( pObj ) then if handShowing ~= 'none' then HideHand( handShowing ) end return else return end elseif carryingEnt == e then if g_PlrKeySPACE == 1 then SetGamePlayerControlTopspeed( minSpeed ) else SetGamePlayerControlTopspeed( currSpeed ) end -- get player position and angles local ppX, ppY, ppZ = g_PlayerPosX, g_PlayerPosY, g_PlayerPosZ local paX, paY, paZ = rad( g_PlayerAngX ), rad( g_PlayerAngY ), rad( g_PlayerAngZ ); -- player quaternion local plQ = Q.FromEuler( 0, paY, paZ ) local Obj = P.ObjectPlayerLookingAt( 100 ) if Obj == p.obj then -- we are carrying this entity if handShowing ~= 'none' then HideHand( handShowing ) end if promptsOn then if not throwEnabled then Prompt( "LMB to drop " .. PU.getNames( e ) .. ", Hold RMB to rotate") else Prompt( "LMB(+E) to drop(throw) " .. PU.getName( e ) .. ", Hold RMB to rotate") end end if g_MouseClick == 1 or GetPlayerWeaponID() ~= 0 then if not mouse1Clicked then dropObject( p, e ) if not throwEnabled then return end -- handle throwing if g_KeyPressE == 1 and g_MouseClick == 1 then local force = min( maxWeight / 6, dims.m * 20000 * ( 100 / GetEntityWeight( e ) ) ) vx, vy, vz = U.Rotate3D ( 0, 0, 1, paX, paY, paZ ) if PU.getName( e ) == 'saw blade' then force = force * 20 PushObject( p.obj, vx * force, vy * force, vz * force, rnd()/10 , 0, rnd()/10 ) else PushObject( p.obj, vx * force, vy * force, vz * force, rnd()/100, rnd()/100, rnd()/100 ) end -- test new collision testing command thrownObj = p.obj AddObjectCollisionCheck( p.obj ) -- do initial check in case enemy really close! CheckThrowCollision() end return end elseif g_MouseClick == 2 then if not mouse2Clicked then mouse2Clicked = true ActivateMouse() end processRotation( e, p, plQ ) elseif mouse2Clicked or (mouse1Clicked and timeNow > mouseTimer) then DeactivateMouse() mouse1Clicked = false mouse2Clicked = false mouseTimer = math.huge end end -- move entity with player -- (may need to handle collision as entity will probably be -- outside player capsule) if p.quat == nil then -- we've just picked it up so initialise: -- work out offset position of entity when held by player local size = max( dims.w, dims.h, dims.l ) -- need to ensure entity is offset far enough to rotate p.xo, p.yo, p.zo = carryOfst, carryHght, carryDist + size / 2 -- get entity angles local _,_,_, eaX, eaY, eaZ = GetObjectPosAng( p.obj ) -- need to work out real world angle of entity rwY = PU.getRealWorldYangle( eaX, eaY, eaZ ) -- store angle as quaternion p.quat = Q.FromEuler( 0, rwY - paY, 0 ) end -- work out offset position of entity when held by player -- first taking account of player angles local xo, yo, zo = U.Rotate3D( p.xo, p.yo, p.zo, paX, paY, paZ ) -- work out rotation quaternion of entity local enQ = Q.Mul( plQ, p.quat ) -- then object offsets, i.e to centre it properly local oAx, oAy, oAz = Q.ToEuler( enQ ) local oxo, oyo, ozo = U.Rotate3D( dims.cx, dims.cy, dims.cz, oAx, oAy, oAz ) -- calculate new position for entity p.rx, p.ry, p.rz = ppX + xo - oxo, ppY + yo - oyo, ppZ + zo - ozo -- make sure we don't sink the entity into the terrain p.ry = adjustForTerrainCollision( p.rx, p.ry, p.rz, dims, oAx, oAy, oAz ) PU.positionHandler( e, p, enQ, 7 ) if p.dropObject then dropObject( p, e ) end return else -- something being carried and it isn't us so leave return end if GetPlayerWeaponID() ~= 0 then if promptsOn then Prompt( "Can't manipulate " .. PU.getName( e ) .. " without your hands free!" ) end if handShowing == 'hand1' then HideHand( handShowing ) ShowHand( 'hand2' ) end mouseTimer = timeNow + 200 return end -- if we've got here we can potentially pick up the item -- check how heavy it is if max( dims.w, dims.h, dims.l ) > maxSize then if promptsOn then Prompt( PU.getName( e ) .. " is too large to carry!" ) end if handShowing == 'hand1' then HideHand( handShowing ) ShowHand( 'hand2' ) end elseif dims.m > maxWeight then if promptsOn then Prompt( PU.getName( e ) .. " is too heavy to lift!" ) end if handShowing == 'hand1' then HideHand( handShowing ) ShowHand( 'hand2' ) end else if handShowing == 'hand2' and timeNow > mouseTimer then HideHand( handShowing ) ShowHand( 'hand1' ) end if mouse1Clicked then if timeNow > mouseTimer then mouse1Clicked = false mouseTimer = math.huge end return end if promptsOn then if pushEnabled then Prompt( "LMB to pick up " .. PU.getName( e ) .. ", E/R + LMB to Push/Pull." ) else Prompt( "LMB to pick up " .. PU.getName( e ) ) end end local pushX, pushY, pushZ = P.GetViewVector() --PromptLocal( e, pushX .. "," .. pushY .. "," .. pushZ .. " : " .. -- objX .. "," .. objY .. "," .. objZ ) if g_MouseClick == 1 then if pushEnabled then -- handle pushing if g_KeyPressE == 1 or g_KeyPressR == 1 then -- local pushX, pushY, pushZ = P.GetViewVector() -- get object position and angle local ex, ey, ez, xa, ya, za = GetObjectPosAng( p.obj ) -- work out centre of object local ox, oy, oz = U.Rotate3D( dims.cx, dims.cy, dims.cz, xa, ya, za ); local cx, cy, cz = ex + ox, ey + oy, ez + oz -- finally work out contact point offsets from centre ox, oy, oz = objX - cx, objY - cy, objZ - cz -- push force less than throw force local force if g_KeyPressE == 1 then local force = min( maxWeight / 20, dims.m / 8 * 20000 ) -- and push! PushObject( p.obj, pushX*force, pushY*force, pushZ*force, ox, oy, oz ) else -- pull force less than push force local force = min( maxWeight / 60, dims.m / 16 * 20000 ) -- and pull! PushObject( p.obj, -pushX*force, -pushY*force, -pushZ*force, ox, oy, oz ) end return end end mouse1Clicked = true -- build stack list PU.buildStackList( p, p.obj ) -- check how heavy the stack is local currWeight = PU.stackWeight( p ) + dims.m if currWeight > maxWeight then if promptsOn then PromptDuration( "Stack is too heavy, remove some items or get stronger!", 2000 ) end if handShowing == 'hand1' then HideHand( handShowing ) ShowHand( 'hand2' ) end mouseTimer = timeNow + 2000 else carryingEnt = e mouseTimer = timeNow + 200 topSpeed = GetGamePlayerControlTopspeed() currSpeed = minSpeed + ( ( maxWeight - currWeight ) / ---------------------- maxWeight ) * ( topSpeed - minSpeed ) SetGamePlayerControlTopspeed( currSpeed ) SetGamePlayerControlJumpMode( 3 ) end end end end