Tutorial - FSOpen and LUA 3 - Conditional Actions and Hooks
So we have the basics on how the scripts in FreeSpace work. A script gets called, and we modify things, commonly with handles of objects within FreeSpace. So far we've only really called our scripts through the script-eval*. But there are so many other ways to call a script. Probably the best way will be through conditional hooks.
(Technically we run part of our script in On Game Init where we define the actual functions)
As I alluded to in the first tutorial, conditional hooks only occur when a certain action happens. You can look at the page on the wiki for a complete list of conditional hooks and any hook variables and are associated with it. So we can use that fact to introduce some real fun with the game. Before these would require actual code requests, but with the magic of Lua, we can just script it!
Instead of this tutorial being overbearing with me rambling on for paragraphs, I'm just going to post some excessively documented scripts and hopefully it should be enough for you to see how and why certain things are done.
The two scripts I'm going to cover is a healing beam and remote activated warheads (think like the Infyrno/Piranha, only the detonation would happen after impact).
First, we will need some new weapons...
Weapons Table
(These two entries require assets from the 2014 mediavps)
#Primary Weapons $Name: HealBeam#Tutorial $Model File: none @Laser Bitmap: laserglow01 @Laser Color: 0, 255, 54 @Laser Length: 0.0 @Laser Head Radius: 0.3 @Laser Tail Radius: 0.3 $Mass: 100.0 $Velocity: 1600.0 $Fire Wait: 30.0 $Damage: 50 $Armor Factor: 0.0 $Shield Factor: 0.0 $Subsystem Factor: 0.0 $Lifetime: 25.0 $Energy Consumed: 0.30 $Cargo Size: 0.0 $Homing: NO $LaunchSnd: 124 $ImpactSnd: 88 $Flags: ("Big Ship" "huge" "beam") $Impact Explosion: ExpMissileHit1 $Impact Explosion Radius: 60.0 $BeamInfo: +Type: 0 +Life: 7.5 +Warmup: 3500 +Warmdown: 3500 +Radius: 120.0 +PCount: 20 +PRadius: 5.0 +PAngle: 60.0 +PAni: particle_green +Miss Factor: 1.0 1.1 1.1 1.7 1.4 +BeamSound: 115 ;; the looping beam-firing sound +WarmupSound: 154 ;; associated warmup sound +WarmdownSound: 158 ;; associated warmdown sound +Muzzleglow: beamglow3 +Shots: 0 +ShrinkFactor: 0.0 +ShrinkPct: 0.0 $Section: +Width: 45.0 +Texture: TerBeam1Core +RGBA Inner: 255 255 255 255 +RGBA Outer: 150 150 150 10 +Flicker: 0.0 +Zadd: 4.0 +Translation: 10 $Section: +Width: 70.0 +Texture: TerBeam2Core +RGBA Inner: 160 160 0 255 +RGBA Outer: 60 60 0 10 +Flicker: 0.45 +Zadd: 3.0 +Translation: 7 $Section: +Width: 80.0 +Texture: TerBeam2Glow +RGBA Inner: 255 255 255 255 +RGBA Outer: 150 150 150 10 +Flicker: 0.4 +Zadd: 2.0 +Translation: 5 $Section: +Width: 100.0 +Texture: TerBeam2GlowHaze +RGBA Inner: 255 0 0 255 +RGBA Outer: 60 0 0 10 +Flicker: 0.5 +Zadd: 0.0 +Translation: 2 #End #Secondary Weapons $Name: Harpoon-RC#Tutorial +Title: XSTR("GTM-19c Harpoon-Claymore", -1) +Description: XSTR( "Standard Issue Fast Target Lock Remote Control Detonate", -1) $end_multi_text +Tech Title: XSTR("GTM-19c Harpoon-Claymore", -1) +Tech Anim: Tech_Harpoon +Tech Description: XSTR( "A modified version of the Harpoon missile. This variant has an armor piercing nosecose that embeds itself in the hull of an enemy ship, allowing the pilot to detonate the warhead at their discretion. While it has a limited role in an active combat zone, it is a favorite for cloak and dagger types in Special Operations.", -1) $end_multi_text $Model File: crossbow.pof $Mass: 15.0 $Velocity: 250.0 $Fire Wait: 2.0 $Damage: 100 ;; damage applied when within inner radius $Blast Force: 60.0 $Inner Radius: 20.0 ;; radius at which damage is full (0 for impact only) $Outer Radius: 40.0 ;; max radius for attenuated damage $Shockwave Speed: 0 ;; velocity of shockwave. 0 for none. $Armor Factor: 0.0 $Shield Factor: 0.0 $Subsystem Factor: 0.0 $Lifetime: 5.0 $Energy Consumed: 0.0 ;; Energy used when fired $Cargo Size: 2.5 ;; Amount of space taken up in weapon cargo $Homing: YES ;; the following indented fields are only required when $Homing is YES +Type: ASPECT ;; Legal: HEAT, ASPECT +Turn Time: 1.0 +Min Lock Time: 2.0 ;; Minimum lock time (in seconds) +Lock Pixels/Sec: 70 ;; Pixels moved per sec while locking +Catch-up Pixels/Sec: 100 ;; Pixels moved per sec while catching up +Catch-up Penalty: 30 ;; Extra pixels to move after catching up $LaunchSnd: 91 ;; The sound it makes when fired $ImpactSnd: 88 ;; The sound it makes when it hits something $FlyBySnd: -1 $Rearm Rate: 2.0 ;; number of missiles/sec that are rearmed $Flags: ( "player allowed") $Trail: +Start Width: 0.3 +End Width: 0 +Start Alpha: 1.0 +End Alpha: 0.0 +Max Life: 2.5 +Bitmap: missiletrail06 $Icon: iconCrossbow $Anim: cross $Impact Explosion: ExpMissileHit1 $Impact Explosion Radius: 9.92 $Piercing Impact Explosion: exp06 $Piercing Impact Radius: 4.96 $Piercing Impact Velocity: 20 $Piercing Impact Splash Velocity: -5 $Piercing Impact Variance: 0.1 $Piercing Impact Particles: 5 $Thruster Flame Effect: missilethruster06 $Thruster Glow Effect: missileglow06 #End
Healing Beam Script
#Conditional Hooks $Application: FS2_Open $On Game Init: [ function BeamHeal(ship) --We're expecting a ship type handle to be passed through if ship:isValid() then --Always check to see if these things are valid to be used! local healStep = ship.HitpointsMax * 0.002 --The heal rate is proportional to max health, so all ships heal to the max within the same amount of time. Alternatively healStep could be the damage of the weapon. --I'll leave that as a fun excercise for you to try! (Hint: you'll need an additional argument to get passed through in the function) if ship.HitpointsLeft <= ship.HitpointsMax then --It would be a good idea to only try to heal ships that have less than max health! --The ship in question will get its HP healed by healStep, so we should take what the end result would be after healing and make sure it won't go over the maximum amount of HP. If it doesn't we can just heal like normal, but if it does go over, we should just make it cap at max HP. --Mind you we could just heal and check afterwards if HP is over MaxHP, but it's probably better to ensure we touch ship properties as little as possible. if ship.HitpointsLeft + healStep <= ship.HitpointsMax then ship.HitpointsLeft = ship.HitpointsLeft + healStep else ship.HitpointsLeft = ship.HitpointsMax end end --In a ship's handle, there are all the normal attributes, but if we use [] indices on the ship handle, we get the subsystem handles. --So ship["communications"] should give us the communications subsystem. But does our ship have engine or engine01 and engine02? --Instead of explicitly going after the names of subsystem, we can just go through the whole thing with numerical indices. --for you people with some lua knowledge already, "for k,v in pairs()" won't work for these "virtual indexed" type of tables (they are slow for real time actions, so don't use them if you can avoid it!) --the best way is to go through these type of tables is to get the size of the table, assign it to a variable, then do a for loop through everything --real time looping should always be done with a numerical loop! local numSubsys = #ship --#ship is the number of subsystems on the ship that we should heal for i=1, numSubsys do --We'll do the same type of checks on HP here as we did with the main health. if ship[i].HitpointsLeft <= ship[i].HitpointsMax then if ship[i].HitpointsLeft + healStep <= ship[i].HitpointsMax then ship[i].HitpointsLeft = ship[i].HitpointsLeft + healStep else ship[i].HitpointsLeft = ship[i].HitpointsMax end end end end end ] $Weapon class: HealBeam#Tutorial $On Ship Collision: [ --[[ Here the the conditional hook is Weapon Class, and we've defined it to only look at the HealBeam#Tutorial class. We change the $Weapon class: to weapons to make other weapons heal. Our action is On Ship Collision, and one of the hook variables for that action is hv.Object, which is the Object that has been collided with. So this bit of code will only run with HealBeam#Tutorial collides with a ship! ]]-- BeamHeal(hv.Object) ] #End
Remote Activated Warhead Detonation Script
#Conditional Hooks $Application: FS2_Open $On Game Init: [ RCD = {} --Empty table object to get us started. Read up on Lua's Object Oriented flavored methods for more info on this sort of thing. function RCD:Init() --But just a quick FYI, when we're within a function using that special colon operator, like we are right now, "self" is a special local variable that fills in for the parent object. So if we do "self.Enabled = true" this is the same as "RCD.Enabled = true" --The colon operator also inserts self into the first argument of the function. Again, check out the lua documentation for more on that! --We don't need to worry about defining variables as local if we're defining variables that are members of a larger table object. These variables are already local to just this table/object/class/whatever anyway! self.Enabled = true --We may wish to stop our script, so let's have an enabled variable self.List = {} --List will contain the list of ships that we hit and the local offsets of the hit. self.ExecuteKey = "D" --When we hit the ExecuteKey, we will detonate all charges self.CheatKey = "5" --Cheating to give ourselves the weapon... for test purposes of course! end function RCD:AddToList(weapon, object) --We're expected to get a weapon and the object that the weapon has hit local t = {} --t is going to be our temporary table before we officially insert it into self.List t.Damage = weapon.Class.Damage --Damage taken from the weapon, note that in the table the hull/shield/subsystem factors are all zero, so the initial hit will do no damage, but the actual damage will still get stored in here --Time for some 3d math, folks! --Small 3d math function reference --To go from a local relative offset to a global world vector: --globalVector = referenceOrientation:unrotateVector(localVector) --To go from a global world vector to a local relative offset: --localVector = referenceOrientation:rotateVector(globalVector) --weapon.Position is a global world vector, and because our target is going to be flying around a lot, when we choose to detonate, that position will be too far away to damage our target. So we will use those functions to discover the local relative position to the hit object, and when we want to detonate we'll run the function in reverse to get the global world coordinates again. t.Offset = object.Orientation:rotateVector(weapon.Position) - object.Position t.Object = object:getSignature() --Rather than saving an entire ship handle, which can get big and clumsy, we'll just grab the object signature self.List[#self.List+1] = t --table[#table+1] is a easy build up a continous numerical table end function RCD:CauseDamage() --There is no createExplosion() function, so we'll have to use the SEXP with mn.runSEXP() local listLength = #self.List --First find out how many charges we've got to blow up for i = 1, listLength do local boom = self.List[i] --self.List[i] is so tedious to type local target = mn.getObjectFromSignature(boom.Object) --We need this to get the global position local pos = target.Position + target.Orientation:unrotateVector(boom.Offset) --And now we unrotate the offset from last time --Boom! mn.runSEXP("( explosion-effect " .. pos.x .. " " .. pos.y .. " " .. pos.z .. " " .. boom.Damage .. " 10 100 10 100 0 0 7 )") end --After we've gone through the entire list, we will revert the list back to an empty object self.List = {} end ] $On Gameplay Start: [ --On Gameplay Start will only occur right after the briefing is done, as gameplay begins. RCD:Init() ] $Weapon class: Harpoon-RC#Tutorial $On Ship Collision: [ --For the Harpoon-RC#Tutorial, upon hitting a ship we should... --Add it to a list! if RCD.Enabled then RCD:AddToList(hv.Weapon, hv.Object) end ] $State: GS_STATE_GAME_PLAY $On Key Released: [ --During gameplay, when any key is released, this block of code will be run. The important hook variable here is hv.Key, so we check that to see if it matches any keys and if so, we'll do run some functions. if RCD.Enabled then if hv.Key == RCD.ExecuteKey then RCD:CauseDamage() end --Comment out this part if we want to not allow cheating, this is only for testing anyway... if hv.Key == RCD.CheatKey then hv.Player.SecondaryBanks[1].WeaponClass = tb.WeaponClasses["Harpoon-RC#Tutorial"] end end ] #End
Here's a link to a complete sample mod with a test mission.
If you're unfamiliar with anything I did, check out the Lua Documentation website or shoot me (Axem) a PM on the hard-light forums.
If you're curious about the fastest methods to do certain things in Lua during time-critical events (such as during On HUD Draw or On Frame), check out these benchmarks done on some various methods.
Hopefully this gets you on your way to crafting fun and amazing scripts for FreeSpace!