Freedom Force – Scripting A Mugging III

This will be an extension of the Scripting A Mugging II tutorial, so make sure you’ve done it first.

In this tutorial we will make some final tweaks to the mugging.py module to make it support cut scenes and in the process make it a little more flexible.

To start with you’ll need the mod created in the first and second parts. To make things simpler, we will be modifying the mission from part II, so make sure your version is vaguely similar to mine. If you want you can download a zip file containing the mod directory as mine was at the end of part II (you can just construct it from the previous scripts of course): Mug.zip (252KB).

The Plan

The mugging.py module from the previous tutorial uses the callbacks provided by Freedom Force. We call functions like regHeroMarkerEnter passing them the name of a function that Freedom Force will call when a certain event occurs. Callbacks are a very useful technique in programming, in this tutorial we will create a callback mechanism of our own.

Why a call back? We could just play a cut-scene specified by the script, but that would be less flexible. If all that is wanted is for a cut-scene to play, then the author of the mission script can do that by writing a one line function of the form:


def play_the_scene(hero, mugging_index):
	play(the_scene)

The advantage of using a function is that the author can do more complicated things. For example, they might check to see which hero activated the mugging and play a different cut-scene for a certain hero. Or they might not play a cut-scene at all but do something entirely different. Using a callback mechanism gives the author the freedom to do things that the module author didn’t think of.

We’ll add three callbacks to our mugging module. One that will be called when the mugging is activated by a hero entering the marker area. One that will be called when the mugging is completed successfully by the KOing of the last mugger. And one that will be called when the mugging is failed by the KOing of a victim.

The New mugging.py Code


# A mugging managing module.
# Sets up scenes in which one group of objects attacks another when a hero
# comes within range. If all the attacking objects are KOd before any of the
# attacked objects are KOd the scene is considered won by the player.
#
#Usage:
# import * from mugging
# # ...
# mugging_data =(('objective_name',('mugger1','mugger2'),('victim1','victim2'),
#                       'marker',start_cb,win_cb,lose_cb),
#                ('objective_name',('mugger1','mugger2'),('victim1','victim2'),
#                       'marker',start_cb,win_cb,lose_cb)
#               )
# # ...mugging_data
# mugging.Init_Muggings()
#
#Notes:
# mugging_data must be in the ff module
# The objectives must already be registered (the module will activate and complete
# or fail them).

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


def mugging_all_kod(objects):
    for object in objects:
        if Object_IsAlive(object):
            return 0
    return 1

        
# This will get called whenever a mugger gets KOd		
def mugging_on_mugger_ko(event):
    mugging_index = int(event.user)
    objective = ff.mugging_data[mugging_index][0]
    muggers = ff.mugging_data[mugging_index][1]
    success_func = ff.mugging_data[mugging_index][5]
    if mugging_all_kod(muggers):
		#check if the objective is defined
        if objective != None:
            #check if the objective is already done
            if not(Objective_IsComplete(objective) or Objective_IsFailed(objective)):
                # if not then set it to complete
                Objective_Complete(objective)
                if success_func != None:
                    success_func(event.object,mugging_index)
        else:
            if Mission_GetAttr('attr_mugging_status_'+str(mugging_index))==0.0:
                Mission_SetAttr('attr_mugging_status_'+str(mugging_index),1.0)
                if success_func != None:
                    success_func(event.object,mugging_index)

                

def mugging_on_victim_ko(event):
    # if the mission isn't over it is failed now
    mugging_index = int(event.user)
    objective = ff.mugging_data[mugging_index][0]
    failure_func = ff.mugging_data[mugging_index][6]
    if objective != None:
        #check if the objective is already done
        if not(Objective_IsComplete(objective) or Objective_IsFailed(objective)):
            Objective_Fail(objective)
            if failure_func != None:
                failure_func(event.object,mugging_index)
    else:
        if Mission_GetAttr('attr_mugging_status_'+str(mugging_index))==0.0:
            Mission_SetAttr('attr_mugging_status_'+str(mugging_index),1.0)
            if failure_func != None:
                failure_func(event.object,mugging_index)


def mugging_on_enter_marker(event):
    # a hero has entered the marker
    # if the scene is already activated do nothing
    mugging_index = int(event.user)
    objective = ff.mugging_data[mugging_index][0]
    muggers = ff.mugging_data[mugging_index][1]
    victims = ff.mugging_data[mugging_index][2]
    enter_func = ff.mugging_data[mugging_index][4]
    if objective != None:
        Objective_Activate(objective)
    else:
        Mission_SetAttr('attr_mugging_status_'+str(mugging_index),0.0)
    # wake up the muggers and victims
    for mugger in muggers:
        AI_SetEnabled(mugger,1)
        #make the muggers attack the victims
        if victims != None:
            addGoalToAI(mugger,KillObjectsTuple(tuple(victims)))
    if victims != None:
        for victim in victims:
            AI_SetEnabled(victim,1)
    if enter_func != None:
        enter_func(event.object, mugging_index)
        

def Init_Muggings():
    for mugging_index in range(0,len(ff.mugging_data)):
        objective = ff.mugging_data[mugging_index][0]
        muggers = ff.mugging_data[mugging_index][1]
        victims = ff.mugging_data[mugging_index][2]
        marker = ff.mugging_data[mugging_index][3]
        # setup to catch the muggers' deaths
        for mugger in muggers:
            AI_SetEnabled(mugger,0)
            regDeath(mugger,'mugging_on_mugger_ko',float(mugging_index))
        # setup to catch the victims' deaths
        for victim in victims:
            AI_SetEnabled(victim,0)
            regDeath(victim,'mugging_on_victim_ko',float(mugging_index))
        #setup to catch the hero entering the marker
        regHeroMarkerEnter(marker,'mugging_on_enter_marker',float(mugging_index))

There are only a few changes from the original code from the previous tutorial.

The code now uses a mission attribute for each mugging that doesn’t have an objective associated with it. This is needed because now whether the player succeeds or fails matters even if there is no objective, since we have to call either the win_cb, or lose_cb callback function. In the previous tutorial if there was no objective it didn’t matter how the scene ended and hence we didn’t need an attribute to keep track of the state. We use ‘attr_mugging_state_’+str(mission_index) as the attribute name, which means the first mugging will use ‘attr_mugging_state_0’ and the second ‘attr_mugging_state_1’, and so on.

‘mugging_on_enter_marker’ is changed by adding two lines of code to call the appropriate callback function (unless it is None in which case we don’t call it). The callback is passed the name of the marker associated with the mugging and the index of this mugging.

‘mugging_on_victim_ko’ and ‘mugging_on_mugger_ko’ are also modified to call the appropriate callback functions. If the objective is set then we simply add the call after failing/completing the mission. If the objective is None then we use the mission attribute and call the callback if the mugging hasn’t already been finished. These callbacks are passed the name of the victim/mugger who was KOd to end the mugging and the index of the mugging in mugging_data.

To use the new code we need to modify the mission.py as well, the following will work for testing purposes:


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

from mugging import *

def callback(object, index) :
    output = "Callback called with object='"+object+"' and index="+str(index)
    print output
    Mission_StatusText(output)

mugging_data = ( ('secondary_1',
                   ('mugger_a_1','mugger_a_2', 'mugger_a_3', 'mugger_a_4'),
                   ('victim_a',), 'mugging_a_area', callback, callback, callback),
                 ('secondary_2',
                   ('mugger_b_1','mugger_b_2','mugger_b_3','mugger_b_4'),
                   ('victim_b',), 'mugging_b_area', callback, callback, callback),
               )


def OnPostInit():
	print 'postinit'
	setMission(1)
	registerObjective('secondary',1)
	registerObjective('secondary',2)
	Init_Muggings()

def OnPrecacheObjects():
	print 'precache'


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

When you run the mission some text should now be output (to the console and the status text area) stating which object and index the callback function was passed. Try it out to make sure, you can download the code if you want: mugging.py, mission.py.

A Cut-Scene Example

Creating a good cut-scene requires a lot of work. You need to write the dialog, record the dialog, work out what you want the camera to do, etc, etc. For the purposes of this tutorial, we will just have some of the characters say a few lines and not bother with any camera movement, or with making the characters move or look at each other. In fact we will just reuse one of the mugging dialogs from the original game.

The code is simple enough, assuming you have a vague idea about how the cut-scene scripting interface works. I’ll just show you the new mission.py without explaining it:


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

from mugging import *

def start_callback(object, index) :
	play(mugging_cs)
	
mugging_data = ( ('secondary_1',
                   ('mugger_a_1','mugger_a_2', 'mugger_a_3', 'mugger_a_4'),
                   ('victim_a',), 'mugging_a_area', None, None, None),
                 ('secondary_2',
                   ('mugger_b_1','mugger_b_2','mugger_b_3','mugger_b_4'),
                   ('victim_b',), 'mugging_b_area', start_callback, None, None),
               )



def OnPostInit():
	print 'postinit'
	setMission(1)
	registerObjective('secondary',1)
	registerObjective('secondary',2)
	Init_Muggings()



def OnPrecacheObjects():
	print 'precache'


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


mugging_cs = [
(
	"speak('mugger_b_1', 'MISSPCH_1_TB_10')",
),
(
	"speak('victim_b', 'MISSPCH_1_F1_01')",
),
(
	"speakNoCB('mugger_b_2', 'MISSPCH_1_TB_11')",
),
]

One thing to note, is that I’ve placed the mugging_data variable at the top of the file, usually you would place it after the code (like the cut-scenes in the original Freedom Force missions). I’ve put it at the top since it’s what this tutorial is about.

Is That All

The mugging.py module is now feature complete (well for these tutorials anyway). By that I mean that no more features will be added to it. There are some interface issues which we can make a little better though.

The mugging_data variable is a list mugging setups, remembering the order of the data and even just how many items there are is not simple. And making a mistake will result in errors inside mugging.py which will not tell you what the problem is anyway. Making things hard to fix.

Python offers a simple solution which will make it much easier to use the mugging.py module: Dictionaries.

Dictionaries?

A dictionary is like a list or tuple, except that instead of referencing items by their index (eg. var[0] to get the first element of var), you reference items by name (eg. var[‘objective’] to get the element with key ‘objective’). This has the advantage that the mission script can refer to the list of muggers by say key ‘muggers’ instead of remembering it is the second element in the list. It also means that instead of specifying None for elements that aren’t wanted (such as callback functions) they can just be ignored.

Dictionaries in python have a member function (a function associated with the dictionary) called has_key, which tells you if a certain key exists in the dictionary. We’ll use it to replace non-existent elements with None so that the mugging.py doesn’t require too much modification.

We modify the mugging.py module as so:


# A mugging managing module.
# Sets up scenes in which one group of objects attacks another when a hero
# comes within range. If all the attacking objects are KOd before any of the
# attacked objects are KOd the scene is considered won by the player.
#
#Usage:
# import * from mugging
# # ...
# mugging_data =( { 'objective' :'objective_name',
#                   'muggers'   : ('mugger1','mugger2'),
#                   'victims'   : ('victim1','victim2'),
#                   'marker'    : 'marker',
#                   'start_cb'  : start_cb,
#                   'win_cb'    : win_cb,
#                   'lose_cb'   : lose_cb },
#                 { 'objective' :'objective_name',
#                   'muggers'   : ('mugger1','mugger2'),
#                   'victims'   : ('victim1','victim2'),
#                   'marker'    : 'marker',
#                   'start_cb'  : start_cb,
#                   'win_cb'    : win_cb,
#                   'lose_cb' : lose_cb },
#               )
# # ...mugging_data
# mugging.Init_Muggings()
#
#Notes:
# mugging_data must be in the ff module
# The objectives must already be registered (the module will activate and complete
# or fail them).

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


def mugging_all_kod(objects):
    for object in objects:
        if Object_IsAlive(object):
            return 0
    return 1

def mugging_get_item(name, index):
    if ff.mugging_data[index].has_key(name):
        return ff.mugging_data[index][name]
    else:
        return None
        
# This will get called whenever a mugger gets KOd		
def mugging_on_mugger_ko(event):
    mugging_index = int(event.user)
    objective = mugging_get_item('objective',mugging_index)
    muggers = mugging_get_item('muggers',mugging_index)
    success_func = mugging_get_item('win_cb',mugging_index)
    if mugging_all_kod(muggers):
		#check if the objective is defined
        if objective != None:
            #check if the objective is already done
            if not(Objective_IsComplete(objective) or Objective_IsFailed(objective)):
                # if not then set it to complete
                Objective_Complete(objective)
                if success_func != None:
                    success_func(event.object,mugging_index)
        else:
            if Mission_GetAttr('attr_mugging_status_'+str(mugging_index))==0.0:
                Mission_SetAttr('attr_mugging_status_'+str(mugging_index),1.0)
                if success_func != None:
                    success_func(event.object,mugging_index)

                

def mugging_on_victim_ko(event):
    # if the mission isn't over it is failed now
    mugging_index = int(event.user)
    objective = mugging_get_item('objective',mugging_index)
    failure_func = mugging_get_item('lose_cb',mugging_index)
    if objective != None:
        #check if the objective is already done
        if not(Objective_IsComplete(objective) or Objective_IsFailed(objective)):
            Objective_Fail(objective)
            if failure_func != None:
                failure_func(event.object,mugging_index)
    else:
        if Mission_GetAttr('attr_mugging_status_'+str(mugging_index))==0.0:
            Mission_SetAttr('attr_mugging_status_'+str(mugging_index),1.0)
            if failure_func != None:
                failure_func(event.object,mugging_index)


def mugging_on_enter_marker(event):
    # a hero has entered the marker
    # if the scene is already activated do nothing
    mugging_index = int(event.user)
    objective = mugging_get_item('objective',mugging_index)
    muggers = mugging_get_item('muggers',mugging_index)
    victims = mugging_get_item('victims',mugging_index)
    enter_func = mugging_get_item('start_cb',mugging_index)
    if objective != None:
        Objective_Activate(objective)
    else:
        Mission_SetAttr('attr_mugging_status_'+str(mugging_index),0.0)
    # wake up the muggers and victims
    for mugger in muggers:
        AI_SetEnabled(mugger,1)
        #make the muggers attack the victims
        if victims != None:
            addGoalToAI(mugger,KillObjectsTuple(tuple(victims)))
    if victims != None:
        for victim in victims:
            AI_SetEnabled(victim,1)
    if enter_func != None:
        enter_func(event.object, mugging_index)
        

def Init_Muggings():
    for mugging_index in range(0,len(ff.mugging_data)):
        objective = mugging_get_item('objective',mugging_index)
        muggers = mugging_get_item('muggers',mugging_index)
        victims = mugging_get_item('victims',mugging_index)
        marker = mugging_get_item('marker',mugging_index)
        # setup to catch the muggers' deaths
        for mugger in muggers:
            AI_SetEnabled(mugger,0)
            regDeath(mugger,'mugging_on_mugger_ko',float(mugging_index))
        # setup to catch the victims' deaths
        for victim in victims:
            AI_SetEnabled(victim,0)
            regDeath(victim,'mugging_on_victim_ko',float(mugging_index))
        #setup to catch the hero entering the marker
        regHeroMarkerEnter(marker,'mugging_on_enter_marker',float(mugging_index))

The only changes from the previous version is the creation of the mugging_get_item function and replacing all the accesses to mugging_data with calls to that function.

This change makes the mugging.py code simpler (in my opinion anyway) because now instead of ff.mugging_data[mugging_index][0] we have mugging_get_item(‘objective’,mugging_index), which is much more clear in what it is actually doing (retrieving the objective for the mugging).

We need to modify mission.py as well, but all that changes in it is the definition of mugging_data. Hopefully, you will agree that it is much clearer what is what in this new version:


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

from mugging import *

def start_callback(object, index) :
	play(mugging_cs)
	
mugging_data = ( { 'objective' : 'secondary_1',
                   'muggers'   : ('mugger_a_1','mugger_a_2', 'mugger_a_3', 'mugger_a_4'),
                   'victims'   : ('victim_a',),
                   'marker'    : 'mugging_a_area' },
				 
                 { 'objective' : 'secondary_2',
                   'muggers'   : ('mugger_b_1','mugger_b_2','mugger_b_3','mugger_b_4'),
                   'victims'   : ('victim_b',),
                   'marker'    : 'mugging_b_area',
                   'start_cb'  : start_callback },
               )



def OnPostInit():
	print 'postinit'
	setMission(1)
	registerObjective('secondary',1)
	registerObjective('secondary',2)
	Init_Muggings()


def OnPrecacheObjects():
	print 'precache'


def OnMissionWon():
	print 'Mission Won'


def OnMissionLose():
	print 'Mission Lost'


mugging_cs = [
(
	"speak('mugger_b_1', 'MISSPCH_1_TB_10')",
),
(
	"speak('victim_b', 'MISSPCH_1_F1_01')",
),
(
	"speakNoCB('mugger_b_2', 'MISSPCH_1_TB_11')",
),
]

And that’s all. The mugging.py and mission.py above can be downloaded: mugging_3.py and mission_3.py.

In Conclusion

Hopefully in these three tutorials you have learnt a little bit about scripting Freedom Force with python. There is of course a lot more to it then the very little we have touched in these tutorials, but I hope this has helped a little and look forward to playing the mods I’m sure you’ll create.