Freedom Force – Scripting A Mugging

A reasonably common secondary objective or just side action is to have the heroes defend some civilians from a group of bad guys. In the standard Freedom Force campaign for example raptors were threatening civilians on one mission. On another mission there was a mugging in an alley to prevent.

If we make scenes like that easy to create then we can easily drop them into a corner of a map, as a reward for players who explore a little or as a distraction from the real mission.

In this tutorial we will create some code to handle such a scene. The code won’t actually be especially good, but it will be improved upon later.

A Very Simple Map

To start with create a new mod with FFEdit (I do that by creating a directory in the Freedom Force install directory (I called this one ‘Mug’), and unzipping data.ff (I keep a copy called data.zip) into the directory. I then create a mission directory in that, and run FFEdit set up the ‘Options’ tab to reflect the new directory, and click on the ‘Mission’ tab. Then click New, and use the Open File dialog to find the ‘mission’ directory I just created. Then I click the create new directory button and create a directory for the mission, in this case I called it ‘mug_test’. Open the directory and click ‘Open’.

Now pick a Layout File, for the purposes of his tutorial the simplest one to use is the blank_city layout – since it is flat and has no objects in it, which means it is fast and simple (we won’t lose things behind buildings, for example).

Give it a name, say ‘Mugging Test’, and click the ‘Edit In-Game’ button to add out characters.

Add Minute Man and four thugs with bats and a couple of civilians. Group the thugs and civs together a little away from Minute Man. Finally add a positional marker and make the radius (right click + drag should work but doesn’t, you have to enter a value in the first Numeric Parameters box – 25 is good for this tutorial) large enough to cover the thugs and civs. The radius of the positional marker will be used to activate the scene, so when a hero enters it the mugging will be activated. It should look something like this screen shot: screen shot

I’m assuming that your characters have the same name as mine (I used the defaults, so the thugs are called ‘thug_with_bat’, ‘thug_with_bat_002’, ‘thug_with_bat_003’, and ‘thug_with_bat_004’, the civilians are called ‘civilian_male’ and ‘civilian_female’, and the positional marker is called ‘positional_001’. If your names are different you’ll have to make sure the code uses your names…

A Minimal Script

Now edit the script. We’ll start out with an empty file. Note things will be much easier if you use an editor that understands python syntax – it will save you making indentation errors. Personally I use PythonWin but only because I already had Python installed and it came with it.

First off enter the following basic code that shows us when things are called and helps in debugging the code later:


from cshelper import *
from js       import *
from ai       import *

def OnPostInit():
	print 'postinit'
	setMission(1) #we'll use mission 1 - it has a nice benefit later

def OnPrecacheObjects():
	print 'precache'


def OnMissionWon():
	print 'Mission Won'
	
		
def OnMissionLose():
	print 'Mission Lost'

Now is a good point to click the ‘Run’ button and check that things work how they should. Currently the thugs should attack you when they see you, and the civilians should wander around aimlessly. If you want to make sure your setup is close to mine you can download mine at this point here: mug.zip (186KB).

Time for a Mugging

Now the idea of the mugging scene is that the thugs will attack the civilians, meaning the hero will have to attack the thugs in order to protect the innocent bystanders. Well assuming the hero is a classic comic book hero anyway.

The basic idea is that we keep count of how many thugs are KOd, and when that count reaches 4 the objective is complete. Unless a civilian is KOd first of course, in which case he objective is failed. We turn the AI for the thugs and the civilians off until a hero approaches (that’s what the marker is for). That way they stay where we put them instead of wandering off, and also don’t use up CPU that can be better used elsewhere. When a hero gets close enough we wake up the thugs and civilians and set the thugs AI to attack the civilians instead of the hero.

Event Handler Functions

So first off we’ll write a function to keep track of how many thugs have been KOd. Freedom Force makes that easy because it will call a function when an object ‘dies’, so we just write that function:


muggers_KOd = 0

def On_Mugger_KO(event):
	# increment the count of KOd muggers
	ff.muggers_KOd = ff.muggers_KOd + 1
	# if the count is four that means all the muggers are KOd
	if ff.muggers_KOd >= 4:
		#check if the objective is already done
		if not(Objective_IsComplete('secondary_1') or Objective_IsFailed('secondary_1')):
			# if not then set it to complete
			Objective_Complete('secondary_1')

muggers_KOd is a global variable which is initially set to zero. Every time our function gets called we add one to muggers_KOd. When it reaches 4 all the muggers are KOd. We check >= 4 instead of just 4, purely to be paranoid just in case something strange happens – in programming it is best to be paranoid.

We check if the objective is already complete or failed. It would have failed if one of the civilians had been KOd. It shouldn’t be complete but again it’s best to be paranoid and make sure – maybe we accidentally created 5 muggers for example. And if the objective isn’t done, we set it to complete.

Reasonably simple. Now for the victim KO handler:


def On_Victim_KO(event):
	# if the mission isn't over it is failed now
	if not(Objective_IsComplete('secondary_1') or Objective_IsFailed('secondary_1')):
		# if not then set it to failed
		Objective_Fail('secondary_1')

Much simpler I hope you agree. If this function is called then a civilian has been KOd and the objective has thus not been met. We don’t care if the civilian was KOd by the hero, or by the thugs, or was hit by a car. However, if the objective is already complete we leave it as it is – so if the civilian is KOd after all the thugs are gone it doesn’t matter, that’s a good thing to do since civilians get hit by cars all too often. Note we also first check if the objective has already failed. That can happen because there are two civilians.

We also need an event function that will be called when the hero enters the marker area. It should wake up the thugs and civilians and set up the encounter:


def On_Enter_Marker(event):
	# a hero has entered the marker
	# if the scene is already activated do nothing
	if Objective_Exists('secondary_1'):
		print "already active"
		return
	# create the objective
	newObjective('secondary',1)
	# wake up the muggers and victims
	for mugger in ('thug_with_bat','thug_with_bat_002','thug_with_bat_003','thug_with_bat_004'):
		AI_SetEnabled(mugger,1)
		#make the muggers attack the victims
		addGoalToAI(mugger,KillObjectsTuple(('civilian_female', 'civilian_male')))
	for victim in ('civilian_female', 'civilian_male'):
		AI_SetEnabled(victim,1)

In that code we first check to see if the action is already under way and if so we finish (return exits the function instantly). Otherwise we set up a secondary objective. Then we wake up the AI for each of the thugs, and also tell the thug AI to attack the two civilians. Lastly we wake up the civilians – they will soon find the thugs attacking them and will run away without any work on our part.

Those three functions will handle the mugging and hopeful rescue. All we need to do now is tell Freedom Force when to call them. We do that by registering them as event handlers, and to do that we write yet another function:


def Setup_Mugging():
	# setup to catch the muggers' deaths
	for mugger in ('thug_with_bat','thug_with_bat_002','thug_with_bat_003','thug_with_bat_004'):
		AI_SetEnabled(mugger,0)
		regDeath(mugger,'On_Mugger_KO')
	# setup to catch the victims' deaths
	for victim in ('civilian_female', 'civilian_male'):
		AI_SetEnabled(victim,0)
		regDeath(victim,'On_Victim_KO')
	# setup to catch the victims' deaths
	#setup to catch the hero entering the marker
	regHeroMarkerEnter('positional_001','On_Enter_Marker')

That function is a little bit similar to the last one, it turns off the AI for all the thugs, and also tells Freedom Force to call On_Mugger_KO upon the ‘death’ of each thug. Then it does a similar thing for the two civilians, this time telling Freedom Force to call On_Victim_KO. And finally it tells Freedom Force to call On_Enter_Marker when any hero enters the marker range.

All we have to do now is call the Setup_Mugging function from OnPostInit, so change OnPostInit to look like this:


def OnPostInit():
	print 'postinit'
	setMission(1)
	Setup_Mugging()

And that’s it. You should be able to click the ‘Run’ button in FFEdit. The thugs and civilians should just stand motionless until you walk close enough to be inside the marker range at which point the thugs should attack the civilians. Quickly now save them. You only need to hit a thug once and he will stop attacking the civilians and start attacking you.

If you are getting syntax errors and can’t work out why (you can get errors in python from invisible things – for example a tab and some spaces are very different to python), or just can’t be bothered typing (cutting and pasting Python code from a web site is bound to produce whitespace errors), you can download my mission.py file at this point in the process: mission_1.py (4KB), you’ll need to copy-n-paste or save it over the top of the existing mission.py.

A Minor Problem…

There is a major problem with the code above. If you KO a thug and then save the game, and load that save game you won’t be able to complete the objective. That’s because the muggers_KOd counter is lost when you load.

Luckily the problem is easily solved. Freedom Force allows us to store some data in the save game, by using mission attributes. We can change our code to do so by changing just two of the functions (and by removing the global variable):


def On_Mugger_KO(event):
	# increment the count of KOd muggers
	count = Mission_GetAttr('attr_muggers_dead') + 1.0
	# if the count is four that means all the muggers are KOd
	if count >= 4.0:
		#check if the objective is already done
		if not(Objective_IsComplete('secondary_1') or Objective_IsFailed('secondary_1')):
			# if not then set it to complete
			Objective_Complete('secondary_1')
	Mission_SetAttr('attr_muggers_dead',count)


def Setup_Mugging():
	# setup to catch the muggers' deaths
	for mugger in ('thug_with_bat','thug_with_bat_002','thug_with_bat_003','thug_with_bat_004'):
		AI_SetEnabled(mugger,0)
		regDeath(mugger,'On_Mugger_KO')
	# setup to catch the victims' deaths
	for victim in ('civilian_female', 'civilian_male'):
		AI_SetEnabled(victim,0)
		regDeath(victim,'On_Victim_KO')
	# setup to catch the victims' deaths
	#setup to catch the hero entering the marker
	regHeroMarkerEnter('positional_001','On_Enter_Marker')
	Mission_SetAttr('attr_muggers_dead',0.1)

Now the code stores the count of thugs that have been disposed of in an attribute which means it will be saved in the save game file.

You may notice we are initialising the count to 0.1 this time. That’s because attributes are floating point numbers, and floating point numbers often have rounding errors. It’s best not to say x == 4.0 when x is a floating point because x might actually be 3.99999999 or 4.0000001 when it should be 4.0. The 0.1 means when we test >=4.0 we can be certain the value isn’t 3.999999 but will be 4.099999999 and hence the test will succeed.

Time to make sure it still works… Again you can grab my version of mission.py: mission_2.py (4KB).

A Minor Makeover

We still aren’t done though. If we decide to add a thug to make the objective more difficult we have to change all the functions except for On_Victim_KO. In programming this is considered bad, chances are you will miss one of them and you’ll have a hard to find bug. There is a simple way to make modifications easier though – we replace the duplicated elements with a variable or function. In this case we replace the muggers and civilians with a list.

Instead of having ‘thug_with_bat’ appear multiple times we put it in a variable, that way we can change it easier later. So we add the following code variables (near the top before all the functions):


thug_list = ('thug_with_bat', 'thug_with_bat_002', 'thug_with_bat_003', 'thug_with_bat_004')
civilian_list = ('civilian_female', 'civilian_male')

We then change the code to use those variables, out final code becomes:


from cshelper import *
from js       import *
from ai       import *

thug_list=('thug_with_bat','thug_with_bat_002','thug_with_bat_003','thug_with_bat_004')
civilian_list=('civilian_female','civilian_male')


# This will get called whenever a mugger gets KOd		
def On_Mugger_KO(event):
	# increment the count of KOd muggers
	count = Mission_GetAttr('attr_muggers_dead') + 1.0
	# if the count is >= number of thugs then all the muggers are KOd
	if count >= len(thug_list):
		#check if the objective is already done
		if not(Objective_IsComplete('secondary_1') or Objective_IsFailed('secondary_1')):
			# if not then set it to complete
			Objective_Complete('secondary_1')
	Mission_SetAttr('attr_muggers_dead',count)


def On_Victim_KO(event):
	# if the mission isn't over it is failed now
	if not(Objective_IsComplete('secondary_1') or Objective_IsFailed('secondary_1')):
		# if not then set it to failed
		Objective_Fail('secondary_1')


def On_Enter_Marker(event):
	# a hero has entered the marker
	# if the scene is already activated do nothing
	if Objective_Exists('secondary_1'):
		print "already active"
		return
	# create the objective
	newObjective('secondary',1)
	# wake up the muggers and victims
	for mugger in thug_list:
		AI_SetEnabled(mugger,1)
		#make the muggers attack the victims
		addGoalToAI(mugger,KillObjectsTuple(civilian_list))
	for victim in civilian_list:
		AI_SetEnabled(victim,1)


def Setup_Mugging():
	# setup to catch the muggers' deaths
	for mugger in thug_list:
		AI_SetEnabled(mugger,0)
		regDeath(mugger,'On_Mugger_KO')
	# setup to catch the victims' deaths
	for victim in civilian_list:
		AI_SetEnabled(victim,0)
		regDeath(victim,'On_Victim_KO')
	#setup to catch the hero entering the marker
	regHeroMarkerEnter('positional_001','On_Enter_Marker')
	Mission_SetAttr('attr_muggers_dead',0.1)


def OnPostInit():
	print 'postinit'
	setMission(1)
	Setup_Mugging()


def OnPrecacheObjects():
	print 'precache'


def OnMissionWon():
	print 'Mission Won'


def OnMissionLose():
	print 'Mission Lost'

Hopefully, you agree that the code becomes simpler when we move the thugs and civilian names into lists. It also means we can add an extra thug by adding the appropriate character to the mission and then adding the name to the end of thug_list. There’s not need to actually modify any of the code.

That’s all for now, the next item will show are more generic way of achieving this. But of course in case something has gone wrong you can download my mission.py file at this point in the job: mission_3.py (4KB), you’ll have to rename it and put it in the right directory of course. You can also have a look at an explanation of the actual code.

The End

Oh yes, why did we choose mission 1 for the setMission()? It’s because mission 1 in Freedom Force has a mugging as the first secondary objective so we get the right text in the Objectives list 🙂

Obviously the setup above is not meant to be even similar to what you would use on a real map. You would trap the civilians in an alley so they can’t run to the corner of the map. The marker radius should be wider. Also there is no code to handle the player killing the thugs with projectiles without entering the area 🙂