Mission scripting information

The structure of this document is given below, along with a % marker indicating how far along that particular section is. I'm adding to the documentation pretty much as I learn the stuff myself, so FEEDBACK IS IMPORTANT! Anything you see that is wrong or just plain dumb please point it out, fire an email to dwessels@telus.net and I'll fix it as quick as I can. Thank you thank you thank you in advance!

If you're a newcomer to mission scripting, then I'd advise going through sections 1, 2, 3, 12, and 16 in order, following the steps as closely as possible. This will give you a simple skirmish mission including a couple of events. Once you've done that, then the other sections should make more sense and you can start devising and customizing your own missions.

As I get the opportunity, I'll be adding more examples in sections 8 and 11 especially, along with the step-by-step instructions for the additional mission scripts. If you looked at early versions you might notice that I've re-organized the document a bit as I got a better feel for what kind of detail was going to wind up in different sections.

It's been damn interesting putting this document together - I'm sure it's got lots of mistakes, those are my fault! For mission code, examples, corrections, and explanations I owe many many many thanks to cam78, Jim, clintk, sfcvixen, RogueJedi, Kziti Kat, Khoros, Magnumman, Dave Ferrell, Dan Suleski, Scott Bruno, everyone at Taldren, and all the other mission scripters who have posted problems and solutions. And, for keeping the community alive and active, a big round of thanks HAS to go to Articfires, Rook and the new RT group, BBJones and the SFC2.net gang, all the other indie server admins, and everyone out there who plays the game!

Dave (Nuclear) Wessels


Important: Starfleet Command, Empires at War, and Orion Pirates are copyrights of Interplay Productions, while Star Trek and all related material are copyright of Paramount Pictures. The API code itself is a copyright of Taldren.

I have no objections to people using any of my material as long as it is not for profit (though I'd certainly appreciate due credit) but you must respect the rights of the game developers, owners, and publishers.


Document structure

1.0 Mission scripting overview
1.1 Code conventions
2.0 Setting up the API
3.0 Setting up a new script
4.0 Creating the map
4.1 Creating multiple maps, and letting D2 pick the most appropriate one
5.0 Setting up communication
6.0 Setting up teams and behaviour
6.1 Setting up the mission and teams
6.1.1 Setting up the mission options, in mScriptSpecs
6.1.2 Setting up the teams in mScriptSpecs
6.1.3 Setting up team goals and relations in mStartMission
6.1.4 Setting up the team strengths in mDefineTeamShipStrengths
6.2 Setting up the team file structure and classes
6.3 Setting up the team briefing messages
6.4 Scheduling follow-on missions for the team
7.0 Setting up ships
7.1 Hardcoding ship selection
7.2 Drawing ships from the dyna/metaverse
7.3 Adding player-controllable ships
7.4 Generating random ships and fleets
7.5 Bringing the player ship into a mission late
8.0 Setting up team and ship behaviour
8.1 Sabotage (Example)
8.2 A smarter AI (Example)
8.3 Late arrivals (Example)
9.0 Mission Design: philosophies and guidelines
10.0 Determining who won, and the results
10.1 Victory conditions
10.2 Tracking mission results
10.3 Campaign mission results
10.4 Prestige and bonus prestige
10.5 Balancing prestige based on relative team and ship strengths
11.0 Events and information
11.1 Obtaining mission, team, and ship information
11.1.1 Getting/setting mission information via fMissionInfo
11.1.1.1: Team information/settings
11.1.1.2: Ship information/settings
11.1.1.3: Hex information/settings
11.1.1.4: Map information/settings
11.1.1.5: Message information/settings
11.1.1.6: Other misc. information/settings
11.1.2 Getting/setting mission information via tTeamInfo pointers
11.1.2.1: Team information/settings
11.1.2.2: Ship information/settings
11.1.2.3: Team command and control information/settings
11.1.2.4: Team/game status information/settings
11.1.3 Getting/setting mission information via tShipInfo pointers
11.1.3.1: Team information/settings
11.1.3.2: Ship information/settings
11.1.3.3: Control/command information/settings
11.1.3.4: Map information/settings
11.1.3.5: Crew and supply information/settings
11.2 Creating and handling events
12.0 Example 1: simple skirmish mission and basic events
12.1 Step-by-step, creating the skirmish
12.2 Files created:
12.3 Class structure:
13.0 Example 2: simple single-player campaign mission and events
13.1 Step-by-step mission creation: (NOT DONE YET)
13.2 Files created:
14.0 Example 3: more complex campaign mission and events
14.1 Step-by-step mission creation: (NOT DONE YET)
14.2 Files created:
15.0 Example 4: metaverse campaign mission (NOT DONE YET)
16.0 Building and incorporating the mission
17.0 The Mysteries of the Metaverse
General sequence of events
WHERE TO GET DYNAVERSE INFO INSIDE THE MISSION SCRIPT?
The remaining steps not done yet, except as covered in the various segments above
17.1 Specifying valid mission locations
17.2 Specifying valid mission participants
17.3 Setting up the teams
17.4 Team prestige determination
17.5 Telling the metaverse what happened
17.6 Handling player disconnects/drops


EAW/OP MISSION SCRIPTING


1.0 Mission scripting overview
The sections below outline the sequence of steps one follows to create and use new mission scripts.

The level of detail included here will increase as I get time - many of the actual scripting sections are just outlines at this point. What I'll likely do is fill in the examples of step-by-step instructions to build a complete working mission, then go back and add to the general guidelines. If you see errors or omissions, or if I'm just plain doing something stupid somewhere, please let me know: my email address is dwessels@telus.net

Keep in mind that, as we're simply talking about C++ programming, the number of ways to achieve the same effect is essentially infinite. The descriptions I have provided simply follow some of the informal standards the mission scripting community has adopted, plus (naturally) a few of my own habits and styles.

I've tried to get the path names correct, but typos are likely so please notify me if you spot any errors. I've focused this on EAW, but have tried to include comparable file and folder information for OP as well.

Note on MS Visual C++:
To make full use of this requires MS VC++ 6.0, preferably running service pack 4. (In fact, I'm running pack 5 and haven't encountered problems yet.) The (free) introductory versions of VC++ 6.0 will allow you to create and compile the scripts as described below, but will not allow you to actually run them with the game. I'm guessing the academic version would allow you full use, but I haven't tried it. The versions from "Standard" up all fully support the script use.

Throughout this document I assume you have a basic understanding of C++ and object oriented programming. If you don't, there are a ton of good books and websites, but I might as well include a shameless plug for my own material on the topic (don't worry, it's all free). Here are links to my introductory C++ course and followup C++ course.

If you don't have VC++, all is not lost! There is a good mission script editor produced by the folks at EagleEye. It's called FMSE and there is a patch that goes along with it. This editor will allow you to select teams, ships, terrain, victory conditions, and briefing messages (pretty much the only thing it doesn't let you do is scheduling sequences of actions or events).

1.1 Code conventions
The mission scripters have adopted a few conventions with regards to naming and data types that are worth mentioning here:

2.0 Setting up the API
When you first set up the API and Visual C++ 6.0 to interact with one another, there is some customization that must be performed.

The instructions below assume that you are working on EAW - if you are working on OP then replace
Taldren\Starfleet Command II\API\SFC2-API Release 2\ with the OP version:
Taldren Software Inc\Starfleet Command Orion Pirates\OP API - R2.1

  1. copy all files from the API include folder:
    C:\Program Files\Taldren\Starfleet Command II\API\SFC2-API Release 2\Stl\include
    to the VC++ include folder:
    C:\Program Files\Microsoft Visual Studio\VC98\include
    (This may overwrite one or more older VC++ files.)

  2. copy all files from the API lib folder:
    C:\Program Files\Taldren\Starfleet Command II\API\SFC2-API Release 2\Stl\lib
    to the VC++ lib folder:
    C:\Program Files\Microsoft Visual Studio\VC98\lib (again, this may overwrite one or more older VC++ files.)

  3. You need a working directory (i.e. a folder) to store your new VC++ script projects, below I'm assuming this working directory is C:\SFCDEV, but you can replace this with whatever you like.

    Into your working directory, copy the folder (and all its contents)
    C:\Program Files\Taldren\Starfleet Command II\API\SFC2 - API Release 1.2
    i.e., it should now appear in your working folder:
    C:SFCDEV\SFC2 - API Release 1.2 or (for OP) C:\SFCDEV\OP API R2.1\

  4. You must perform some editing of your registry, so from start/run, fire up REGEDIT, then:
    • select HKEY_LOCAL_MACHINE/SOFTWARE/Taldren
    • right click Taldren and create a new key: VCScriptAddins
    • right click this new key, and create a new key: Settings
    • right click this new key, and create a string variable: ScriptRootPath
    • double-click this variable (right window) and set its value to C:\SFCDEV\SFC2 - API Release 1.2\Scripts\ or to C:\SFCDEV\OP API - R2.1\Scripts\ (for OP)

  5. Now we will start up VC++ and customize the working environment for compatibility with our mission scripting. First, go to folder C:\SFCDEV\SFC2 - API Release 1.2\Scripts and double-click SFC2_Script_API.dsw

    Another exception for the OP user: the specified file wasn't included in the OP API release. You can download it here courtesy of RogueJedi.

  6. Next, select Tools|Customize|Addins and Macro Files|Browse.

    Click to show .dll files, and select CreateScript.dll.

    When it appears, make sure the check box beside it is checked.

  7. Next, select Tools|Customize|Addins and Macro Files|Browse again.

    Still show .dll files, and select ScriptObjectWizardAddin.dll.

    When it appears, make sure the check box beside it is checked.

  8. Drag the two new icons (#1 is the object wizard, #2 is the CreateScript wizard) to one of the top toolbars and remember these - you'll be using these two icons every time you create a new script.
      As Kziti Kat points out, you must do this from Tools|Customize|Commands|Category Drop Down:Addins

That completes the API initialization, you are now set up to create as many scripts as you like.


3.0 Setting up a new script
The following steps are done once for each new script.

  1. Go to C:\SFCDEV\SFC2 - API Release 1.2\Scripts and double click SFC2_Script_API.dsw

  2. Click on the CreateScript icon (#2 on the toolbar)

  3. Select a race (for campaign) or type of mission

  4. Fill in the name field

  5. Choose an option (the default option uses the bare bones Tem_Standard template, the single player option uses the Tem_Single template, and the multiplayer option uses the Tem_Multi template).

  6. Click the Create box

  7. Close VC++

  8. Go to the appropriate directory (based on race or mission type of your script) in the Scripts folder in your working space.

  9. Go to the folder with your new mission's name

  10. Remove the file your_script_name.dsp and rename your_script_name.dsp.new to your_script_name.dsp

  11. Reopen VC++ with your_script_name.dsw (it seems to find it/create it even if it isn't originally visible)
      Actually, as RogueJedi points out, all you have to do is close the workspace (File | Close workspace, click yes when it asks you to save changes to the workspace), rename the project files, and then re-open the workspace.

  12. Click on the File View tab on left side, click your_script_name and in the Project menu select Set As Active Project

  13. Go to Projects | Settings | C/C++
    Select C++ Language in the drop down menu and make sure the RTTI checkbox is checked
    Close the display box

  14. Go to Build | Set Active Configuration and choose your_script_name - Win 32 Release

    The next set of steps are needed to remove some elements that were eliminated from the API in an earlier patch

  15. Go to your project, and using the file view (on left-hand side), expand to view files, and remove the files Draw.h and Draw.cpp

  16. In the left hand window, double-click your_script_name.h, and remove the declaration
    virtual void mIniitializeDraw(const int32 MapPicked);

  17. Double-click your_script_name.cpp, and remove the following two code segments:
    #include "Draw.h"
    
    and
    void tyour_script_name::mInitializeDraw(.....)
    {
       .....
    }
    
You are now set to start manipulating your new script!

4.0 Creating the map
To create a custom map you need to edit the MissionMap.cpp and MissionMap.h files.
Basically there is a simple ascii representation of the map, including initial terrain and ship positions and facing.

Actually, unless you are using multiple maps within a single mission, the MissionMap.h file can probably be left unchanged.

The MissionMap.cpp file contains an array of strings, the first two of which are titles/comments for the map and the rest of which actually define the map. E.g.:

const char* MapLines[]=
{
  "YOUR_MISSION_NAME",
  "// YOUR_MISSION_NAME Map",
  " +____1____2____3+",
  " |...............|",
  " |...............|",
  " |...............|",
  " |........*......|",
  "1|...............|",
  " |...............|",
  " |....G..........|",
  " |...............|",
  " |...............|",
  "3|........g......|",
  " |...............|",
  " |...............|",
  " |...@...........|",
  " |...............|",
  "3|...............|",
  " +---------------+",
  "///",
  0
};
Each character represents a 10-unit block on the game map, so the above map is 30 by 30 hexes (a region of space 30000k by 30000k).

Each hex (i.e. each character in the string representation of the map) can start of with one feature, and different kinds of features are indicated using different characters:

In the map above, there is one ship facing more-or-less towards the lower right corner, a moon in the lower left quadrant, and an ice-asteroid in the upper right quadrant.

We can also decide which parts of the map will be displayed for the player, this is covered in section 6.3.

4.1 Creating multiple maps, and letting D2 pick the most appropriate one

We can set up multiple maps for a mission, each with different terrain and ship starting points, and allow the dynaverse to automatically pick the most appropriate one.

To do this, we must create all the maps (in the MissionMap.cpp file) and add a terrain selection method in the Met_ScriptName.cpp file (and an appropriate method description in the Met_ScriptName.h file).

The selection routine must be named mAssignMapTerrainTypes, taking one integer parameter, and this will automatically be called by the dynaverse to select the most appropriate of your maps.

Here is an example of the routine:

tMapTerrain tMet_ScriptName::mAssignMapTerrainTypes(int32 Map)
{
	/*
	 *	For each map, describe which kinds of terrain it is appropriate for
	 *		This is done using calls to the tMapTerrain constructor,
	 *		   supplying it with three parameters:
	 *			 iTerrainTypes TerrainType = kTerrainAnyTerrain,
	 *			 iPlanetTypes PlanetType = kPlanetAnyPlanet,
	 *			 iBaseTypes BaseType = kBaseAnyBase 
	 *
	 *	The valid types for iTerrainTypes are
	 *		kTerrainNone
	 *		kTerrainSpace	= kTerrainSpace1 | kTerrainSpace2 | kTerrainSpace3 |
	 *						  kTerrainSpace4 | kTerrainSpace5 | kTerrainSpace6,
	 *		kTerrainAsteroids	= kTerrainAsteroids1 | kTerrainAsteroids2 | kTerrainAsteroids3 |
	 *						  kTerrainAsteroids4 | kTerrainAsteroids5 | kTerrainAsteroids6,	
	 *		kTerrainNebula		= kTerrainNebula1 | kTerrainNebula2 | kTerrainNebula3 |
	 *						  kTerrainNebula4 | kTerrainNebula5 | kTerrainNebula6,
	 *		kTerrainBlackholes	= kTerrainBlackHole1 | kTerrainBlackHole2 | 
	 *						  kTerrainBlackHole3 | kTerrainBlackHole4 | 
	 *						  kTerrainBlackHole5 | kTerrainBlackHole6,
	 *		kTerrainAnyTerrain	= kTerrainSpace | kTerrainAsteroids | kTerrainNebula | kTerrainBlackholes |
	 *						  kTerrainDustclouds | kTerrainShippingLane,
	 *		
	 *	The valid types for iPlanetTypes are
	 *		kPlanetNone
	 *		kPlanetHomeWorld	= kPlanetHomeWorld1 | kPlanetHomeWorld2 | kPlanetHomeWorld3,
	 *		kPlanetCoreWorld	= kPlanetCoreWorld1 | kPlanetCoreWorld2 | kPlanetCoreWorld3,
	 *		kPlanetColony		= kPlanetColony1 | kPlanetColony2 | kPlanetColony3,
	 *		kPlanetAsteroidBase	= kPlanetAsteroidBase1 | kPlanetAsteroidBase2 | kPlanetAsteroidBase3,
	 *		kPlanetAnyPlanet	= kPlanetHomeWorld | kPlanetCoreWorld | kPlanetColony | kPlanetAsteroidBase,
	 *
	 *	The valid types for iBaseTypes are
	 *		kBaseNone
	 *		kBaseAnyBase	= kBaseStarbase | kBaseBattleStation | kBaseBaseStation |
	 *						  kBaseWeaponsPlatform | kBaseListeningPost,
	 */
	switch ( Map )
	{
	case 0: return tMapTerrain (kTerrainSpace, kPlanetNone, kBaseNone);
	case 1:	return tMapTerrain (kTerrainAnyTerrain, kPlanetNone, kBaseAnyBase);
	case 2: return tMapTerrain (kTerrainAnyTerrain, kPlanetAnyPlanet, kBaseNone);
	case 3: return tMapTerrain (kTerrainAnyTerrain, kPlanetAnyPlanet, kBaseAnyBase);
	case 4: return tMapTerrain (kTerrainSpace, kPlanetNone, kBaseNone);
	case 5: return tMapTerrain (kTerrainAsteroids, kPlanetNone, kBaseNone);
	case 6: return tMapTerrain (kTerrainNebula, kPlanetNone, kBaseNone);
	case 7: return tMapTerrain (kTerrainBlackholes, kPlanetNone, kBaseNone);
	default: break;
	}
	return tMapTerrain();
}
Now, in our MissionMap.cpp file we need to have a map which corresponds to each of the cases which could get selected above. I've abbreviated the maps themselves below, otherwise here is an example of the file setup:
int32 TotalMaps = 8;
const char* MapTypes[] = { "Met_ScriptName Map",
					"Met_ScriptNameBase Map",
					"Met_ScriptNamePlanet Map",
					"Met_ScriptNameBoth Map",
					"Met_ScriptNameEmpty Map",
					"Met_ScriptNameAsteroid Map",
					"Met_ScriptNameNebula Map" 
					"Met_ScriptNameBlackHole Map",
			};
const char* MapLines[]=
{
   "Met_ScriptName",
   "// Met_ScriptName Map",
   " +____1____2____3____4____5____6____7____8+",
   " |........................................|",
   " |........................................|",
   // most of the map itself deleted here
   " |........................................|",
   "8|........................................|",
   " +----------------------------------------+",
   "      1    2    3    4    5    6    7    8",
   "///",
   "// Met_ScriptNameBase Map",
   " +____1____2____3____4____5____6____7____8+",
   " |........................................|",
   " |........................................|",
   // again, deleted most of the map
   "8|........................................|",
   " +----------------------------------------+",
   "      1    2    3    4    5    6    7    8",
   "///",
   "// Met_ScriptNamePlanet Map",
   " +____1____2____3____4____5____6____7____8+",
   " |........................................|",
   " |........................................|",
//
// etc etc etc for all 8 maps
//
   " |........................................|",
   "8|........................................|",
   " +----------------------------------------+",
   "      1    2    3    4    5    6    7    8",
   "///",
   0
};

5.0 Setting up communication
All messages for all players are established in the MissionText.cpp and MissionText.h files.

In MissionText.h we establish an enumerated type, TextMessages, with a unique value for each different kind of message that can be displayed. E.g. kMissionTitle_msg, kDescription_msg, ....

In MissionText.cpp we find the actual strings for these text messages, inside std::string Messages[] =.... The order of the messages in MissionText.cpp must exactly match the order of the ids in MissionText.h, otherwise you'll get the wrong messages popping up in the wrong places.

There are a handful of predefined message types:

You can also add as many other messages as you like, just give each a unique identifier in the enumerated type in MissionText.h and (in the matching spot) put the string of text in MissionText.cpp.

To display the added messages during the mission, add the following code within one of the event handling routines:
fMissionInfo->mDisplayMessage(static_cast<eTeamID>(kPlayerTeam),kTHEMESSAGEID_msg);
This sends the message with enumerated value kTHEMESSAGEID_msg to the player team (kPlayerTeam is an enumerated value specifying the player team, declared in the CommonDefs.h file).

When creating the strings, there are a number of variables we can insert such as:

For example, "[STARDATE]: [PLAYER_RANK] [PLAYER_NAME] commanding. At coordinates [SYSTEM_NAME] ..." might appear on the screen as "2269.3: Captain Nuclear Wessels commanding. At coordinates 32x,17y ...".

There are also several other forms of communication discussed in other segments of the document:

Just as an aside, there are a handful of arrays declared in Race.h that come in handy if you want to grab a string representing the race name, based on a race type. Two of the handy ones are: gRaceNames[] and gFullRaceNames[]


6.0 Setting up teams and behaviour
In this step we'll look at establishing each desired team in the mission, including how the team and mission relate to the dyna/metaverse for campaign games. We will defer discussion of information gathering and event handling until sections 11.1 and 11.2 respectively.

I'M BASING THIS ON THE ASSUMPTION YOU USE THE SINGLE PLAYER TEMPLATE IN THE SETUP WIZARD,
if you use anything else then you'll have to make appropriate translations.

6.1 Setting up the mission and teams

Perhaps the first most advisable thing to do is establish identifiers for each team - I generally put this in CommonDefs.h. The single player wizard will assume four teams, and assign the ids you'll see below. You may well want to add or eliminate teams, or change the names and identifiers for some teams. (This has some implications we'll address in the section on class/file structure further below, but can lead to more readable and maintainable code if you think you may be modifying your mission a few weeks or months down the road.) Using the default team ids, here's the way your enumerated type might look in CommonDefs.h
enum eTeamTypes
{
	kPlayerTeam = kTeam1,     // this will be the primary (drafting) player
      kPlayerNPCTeam = kTeam2,  // this will be their AI ally team
	kEnemyTeam = kTeam3,      // this will be the primary opponent (drafted) player
	kNPC1Team = kTeam4,       // this will be some additional AI team (pirates, monsters, another race, whatever)
};
In your_mission_name.cpp we have a lot of work to do setting up the mission options and team options.

6.1.1 Setting up the mission options, in mScriptSpecs Each of these options is established by assigning a value to a field in the Specs object, e.g. Specs->fPlayerRace = ... or Specs->fMinDate = .... Below are a list of the fields, what they're for, and what options are available.

Mission title: this is the title displayed when selecting the mission, we can set this from one of the enumerated messages in the MissionText.h and .cpp files, for example:
Specs->fMissionTitle = Messages[kMissionTitle_msg];

Mission description: similarly, this sets the description of the mission, and is also set using the enumerated messages in the MissionText files, e.g.:
Specs->fMissionDescription = Messages[kDescription_msg];

Mission type: we can establish a mission by assigning either kTutorialMission for a tutorial mission, kHistoricalMission for skirmish missions, kCampaignMission for dyna/metaverse missions, or kMultiplayerMission for (surprise) multiplayer missions. Whoops, there is also kUniqueCampaignMission for the scripted one-time-only campaign missions. Example:
Specs->fMissionType = kCampaignMission;

Mission location: for campaign missions, we can describe where a mission can be offered in the dyna/metaverse. We can supply a list of categories of "whose territory" the mission can be offered in, and also a list of what kind of nearby terrain is required. Furthermore, we can specify how far away a valid hex can be located for us to run a mission in the current hex (range 0 means the current hex must match the criteria, range 1 means the current hex or any adjacent one, etc). For example, if the mission can be offered in enemy or neutral territory, and the player must actually be in an asteroid or dust cloud hex, we could specify:
Specs->fMetaLocationSpec(0, kEnemyRace|kNeutralRace, kAsteroidHex|kDustCloudHex);
The race and terrain options are controlled by bitmasks, so you can use logical or (|) to create combinations like the above.

Note that bases, planets, and terrain are considered separate categories: i.e. in a hex with a base and a planet you could get base missions, planet missions, or empty space missions.

    NOTE: These settings are really treated as recommendations by the dynaverse - there are settings in the server .gf file that determine how much weight it really assigns to the specifications you make here. For more details on those file settings see section 17.1.

The available race options are: kPlayerRaceHex, kEnemyRaceHex, kAlliedRaceHex, kNeutralRaceHex, kAnyRaceHex, kFederationRaceHex, kKlingonRaceHex, kISCRaceHex, kRomulanRaceHex, kGornRaceHex, kLyranRaceHex, kHydranRaceHex, kMirakRaceHex
The available terrain options are: kAnyTerrainHex, kEmptySpaceHex, kAnyBaseHex, kAnyPlanetHex, kAsteroidHex, kDustCloudHex, kBlackHoleHex, kShippingLaneHex, kCoreWorldHex, kHomeWorldHex, kColonyHex, kAsteroidBaseHex, kBaseStationHex, kBattleStationHex, kStarbaseHex, kBaseListeningPostHex, kBaseWeaponsPlatformHex .

Mission date: we can specify that a mission can only be offered in a specific time period in the dyna/metaverse, to do so we specify the earliest and latest date. For example, to specify that the mission could only be offered in the earliest 10 years of a campaign, we would use:
Specs->fMinDate = kMinDate; Specs->fMaxDate = kMinDate + 10;
A value of 0 corresponds to 2263, 10 to 2273, etc. kMinDate = 0, kMaxDate = 60.

BPV range: theoreticall we can specify a mission can only be offered to a player whose ships falls in a specific BPV range, e.g.
Specs->fMinBPV = 80; Specs->fMaxBPV = 140; Later, in section 6.1.4 we can set restrictions on the strengths of each individual ship in each team.

Player race (primary, or drafting, player): we can specify that the mission can only be offered to players from certain races, again with a bitmask set up to allow selection of multiple races. The options are kSpecFed, kSpecLyran, kSpecKling, kSpecRomulan, kSpecGorn, kSpecISC, kSpecHydran, kSpecMirak, kSpecNonPirateRace, e.g.:
Specs->fPlayerRace = kSpecLyran | kSpecKling;

Ship numbers: we can specify that the mission only be offered to fleets with certain numbers of ships: kMissSpecNoShip, kMissSpecOneShip, kMissSpecTwoShip, kMissSpecThreeShip, kMissSpecAllShips, for example to specify that the player needs either two or three ships:
Specs->fNumShips = kMissSpecTwoShip | kMissSpecThreeShip;

Menu options: for skirmish and multiplayer missions we can enable or disable some of the menu buttons the player can use: kNoMenuButtons, kEnableSupplyDock, kDisableCustomSkirmish, kDisableAI, kDisableTagA, ..., kDisableTagF, kDisableAllTags, kDisableAll. An example preventing custom selection of ships in a skirmish mission would be
Specs->fMenuStates = kDisableCustomSkirmish;

Bandwidth options: to be honest, I don't know what the hell this does, but the options are kNumClientsMask, kForgotClientInitialization, k0Client, ..., k5Client, kHostBandwidthMask, kForgotHostInitialization, k1Band, ..., k6Band, kThirdBand, kHalfBand, kAllBand and an example is
Specs->fBandwidthPart = kAllBand;

6.1.2 Setting up the teams in mScriptSpecs
For each team we will use the mCreateTeamSpec method to set the basic parameters/options for the team. This method expects to receive the value for each option, in the correct order. The options are as follows:

  • The team name: a string used to name the team, e.g. "Player Team", "Pirate Team", "Rescue Team", whatever.

  • The team ID, e.g. kPlayerTeam, kEnemyTeam, etc. This is usually cast to type eTeamID.

  • The chance the team should be used (a value in the range 0-1). For example, the primary player team is required, so would be set to 1.0, whereas you may have an NPC team that only comes into play half the time, so this may be set to 0.5.

  • Is the team required or not?, if the mission requires that this particular team be present, then set this to kTrue, otherwise it can be set to kFalse

  • The type of team - there are four types:
       kNPCTeam, //Only run by the AI
       kPlayableTeam, // Can be played by player or AI 
       kPrimaryTeam, //e.g. the drafter
       kPrimaryOpponentTeam, // e.g. an enemy draftee
    
  • The chance the team can be played by the AI instead of a human player, again this can be any value between 0 and 1.

  • The valid races for the team, chosen from a combination of kSpecFed, kSpecKling, kSpecRomulan, kSpecLyran, kSpecHydran, kSpecGorn, kSpecTholian, kSpecISC, kSpecMirak, kSpecLDR, kSpecWYN, kSpecJindarian, kSpecOrion, = 1 << kOrion, kSpecMonster, kSpecAnyRace, kSpecNonPirateRace, kSpecPlayableRace

  • Race modifying criteria, chosen from a combination of kSameAs, kAnyEnemyOf, kWorstEnemyOf, kAnyAllyOf, kBestAllyOf, kAnyRace, kDifferentThan

  • The type of team allocation used, used by applying typeid to the team's class, e.g. typeid(tPlayerTeam) or typeid(tEnemyTeam), etc. (Discussion of team classes takes place in section 6.2.)

  • The tag for the team, use kNoTeamTag unless this is a multiplayer mission. For multiplayer, the other options are: kTagA, kTagB, ... , kTagZ (though only A through F are valid for human players).

  • The mission title used for that team, e.g. "Data Recovery" versus "Holding Action", depending on which side of the mission you're on.
Examples:
	const tTeamSpec& team1 = mCreateTeamSpec(	"Player Team",
								static_cast< eTeamID >( kPlayerTeam ),
								kMaxChance,
								kTrue,
								kPrimaryTeam,
								kMinChance,
								kSpecPlayableRace,
								kAnyRace,
								typeid( tPlayerTeam ),
								kNoTeamTag,
								Messages[kMissionTitle_msg] );

	const tTeamSpec& team2 = mCreateTeamSpec(	"Enemy Team",
								static_cast< eTeamID >( kEnemyTeam ),
								kMaxChance,
								kTrue,
								kPrimaryOpponentTeam,
								kMaxChance,
								kSpecPlayableRace,
								kAnyEnemyOf | kPlayerTeam,
								typeid( tEnemyTeam ),
								kNoTeamTag,
								Messages[kMissionTitle_msg] );

	const tTeamSpec& team3 = mCreateTeamSpec(	"Pirate Team",
								static_cast< eTeamID >( kNPC1Team ),
								kMaxChance,
								kTrue,
								kNPCTeam,
								kMaxChance,
								kSpecOrion,
								kAnyRace,
								typeid( tNPC1Team ) );
Once we've established the teams, we must set them up, so follow our team creation code with the setup calls, e.g.:
	Specs->mSetupTeam( team1 );
	Specs->mSetupTeam( team2 );
	Specs->mSetupTeam( team3 );
6.1.3 Setting up team goals and relations in mStartMission
Here we will create a twodimensional character array specifying how each team relates to one another, and what each team's goal is with respect to each other team. The possible goals, relationships, and representative characters are shown below:
Goals              Relationships       Notes
- Ignore (self)    - Self              Cannot fire on allies
I Ignore (other)   A Allied            Hostile AI ships will not fire until fired upon
C Cripple          F Friendly          At war AI ships will actively seek out enemies
X Capture          N Neutral
D Destroy          H Hostile
                   W War
                   P Passive
As noted above, the relations have an impact on how teams can interact, but they also have an impact on how AI teams will respond to commands. For example, an AI team that is set to HI will stop dead in space until given a command to follow, defend, or whatever. Sometimes to get an AI command to work it is necessary to change the relationships between teams, but doing so runs the risk of also changing the behaviour of other ships on the team.

To set up the relationships, we need to set the number of teams in kRelationsCount, create the 2D character array, and then call mSetTeamRelations to actually set the relationships. For example:

void tMet_PigInTheMiddle::mStartMission()
{
	tScript::mStartMission();

	const int kRelationsCount = 3;
	int32 teamList[kRelationsCount] =
	{
		kPlayerTeam,
		kEnemyTeam,
		kNPC1Team,
	};

	std::string relationships[kRelationsCount][kRelationsCount] = 
	{
		//Plyr = kPlayerTeam,
		//Nme  = kEnemyTeam,
		//NPC1 = kNPC1Team,

		//	     Plyr   Nme   NPC1
		/*Plyr*/ "--",  "WI", "WX",
		/*Nme */ "WI",  "--", "WX",
		/*NPC1*/ "HI",  "HI", "--",
	};
	fMissionInfo->mSetTeamRelations( teamList, (std::string*)relationships, kRelationsCount);
}
6.1.4 Setting up the team strengths in mDefineTeamShipStrengths
Earlier we defined the overall BPV restrictions for mission to be offered to the primary player. Here we set up specific strength limitations for each of the individual ships in a team (up to 3 ships per team) for example:
void tMet_PigInTheMiddle::mDefineTeamShipStrengths( void )
{
	mSetTeamShipStrength( kPlayerTeam, kClassFrigate, kClassHeavyBattlecruiser, kMinBPV, 250 );
	mSetTeamShipStrength( kPlayerTeam, kClassFrigate, kClassNewHeavyCruiser, kMinBPV, 180 );
	mSetTeamShipStrength( kPlayerTeam, kClassFrigate, kClassHeavyCruiser, kMinBPV, 120 );

	mSetTeamShipStrength( kEnemyTeam, kClassDestroyer, kClassNewHeavyCruiser, 80, 180 );
	mSetTeamShipStrength( kEnemyTeam, kClassFrigate, kClassHeavyCruiser, 50, 140 );
	mSetTeamShipStrength( kEnemyTeam, kClassFrigate, kClassLightCruiser, kMinBPV, 120 );
}
If I understand things correctly, in D2 the mission matcher actually goes through your TeamShipStrength declarations to identify the appropriate team slots - this takes place *before* a list of missions is actually offered to the player, then actual drafting etc takes place if and when the player actually chooses that mission.

The available ship categories are kClassShuttle, kClassPseudoFighter, kClassFreighter, kClassFrigate, kClassDestroyer, kClassWarDestroyer, kClassLightCruiser, kClassHeavyCruiser, kClassNewHeavyCruiser, kClassHeavyBattlecruiser, kClassCarrier, kClassDreadnought, kClassBattleship

6.2 Setting up the team/ship file structure and classes
The single player template sets up four teams, the PlayerTeam, EnemyTeam, PlayerNPCTeam, and the NPC1Team. For each team, it will create a Team.h and Team.cpp file, plus a TeamBaseState.h and TeamBaseState.cpp file. In addition, it will create a PlayerShip.h and PlayerShip.cpp file, and a PlayerShipBaseState.h and PlayerShipBaseState.cpp file.

The Team.h and Team.cpp files are used to set up team ships, victory conditions, and mission scheduling.

The Ship.h and Ship.cpp files are used to control a specific team's ships.

The TeamBaseState.h and TeamBaseState.cpp files are used for event handling for events relevant to that specific team.

Similarly, the ShipBaseState.h and ShipBaseState.cpp files are used for event handling for events relevant to a particular team's ships.

Each of these files creates and defines a particular class, so at the outset we have the following classes by default:

If we want to change the name of a team, e.g. use Pirate instead of NPC1, then we should change the name of the class for that team, and the name of the files, and the name of the identifier for the team, and (here's the ugly part) all the references to the class/file/identifier. This means going through the .cpp files and changing all instances of tNPC1Team to tPirateTeam, for instance. It improves readability, but it's a pain in the butt if you go to do it manually. When you do this, be sure you update the #include statements at the tops of the files as well.

Now, suppose we want to add specialized event handling for the enemy team's ship ... we don't have tEnemyShip, tEnemyShipBaseState, or any files for them. Here's a solution:

  1. Make copies of PlayerShip.h, PlayerShip.cpp, PlayerShipBaseState.h, and PlayerShipBaseState.cpp, and name them EnemyShip.h, EnemyShip.cpp, etc.

  2. Go through the new copies, changing each instance of Player to Enemy, again make sure you catch the #include statements as well.

  3. Add the new files to the C++ project. (Go to
    Project | Add to Project | Files, and select the file you want to add. It should appear in the list of files in the left hand window, then make sure you drag it to the desired directory - use the structure for the player team and ships as an example if you have any doubts.)
Now you should be able to work on the new ship/states the same as you would with the player ship/states.

Fortunately in VC++ you can also automate this process: details to be posted shortly.

Example include layout: if you want to lay out the files and includes from scratch, here is what it looks like:

6.3 Setting up the team briefing messages
For each team, we can specify a different briefing message based on which race they are. This is done in the your_mission_name.cpp file, in the mDefineBriefings routine, for example:
void tMet_YourScriptName::mDefineBriefings( void )
{
      // the enemy player gets the same briefing no matter what
	for ( int32 i = kFederation; i <= kMirak; i++ )
	{
		fBriefingIndex[kEnemyTeam][i] = kEnemyBriefing_msg;
	}

      // the main player gets a different message if they're playing
      //     Mirak than if they're playing anything else:
	for ( int32 i = kFederation; i < kMirak; i++ )
	{
		fBriefingIndex[kPlayerTeam][i] = kBriefing_msg;
	}
      fBriefingIndex[kPlayerTeam][kMirak] = kMirakBriefing_msg;

}
We can also dictate what is shown on the briefing map for the player, in the mASCIIOverrides method (also in your_mission_name.cpp. For example:
void tMet_YourScriptName::mASCIIOverrides( tMapOverrides* Map )
{
	tScript::mASCIIOverrides( Map );

	Map->fTacticalMapIcon['G'] = kObjectPlayerShip; // show the player ship at map location G
	Map->fTacticalMapIcon['H'] = kObjectEnemyShip;  // show the enemy ship at map location H
	Map->fTacticalMapIcon['I'] = kObjectEnemyShip;
}
Valid display objects include kObjectEmptySpace, kObjectNebula, kObjectBlackHole, kObjectFissure, kObjectOrganian, kObjectPulsar, kObjectWormHole, kObjectSun, kObjectPlanet, kObjectAsteroid, kObjectDustCloud, kObjectCorpse, kObjectEnemyBase, kObjectPlayerBase, kObjectEnemyShip, kObjectAlliedShip, kObjectPlayerShip .

6.4 Scheduling follow-on missions for the team
In the PlayerTeam.cpp file, in the mScheduleNextMission method, we will include instructions to the dyna/metaverse on what it should do with the results of this mission.

This is done by setting a number of values in fMissionScheduler, I'll try to outline the values and settings below:

void tPlayerTeam::mScheduleNextMission( tVictoryCondition* VictoryCondition )
{
	fMissionScheduler.fVictoryLevel = VictoryCondition->mCalcVictoryStatus( mGetTeam() );
	fMissionScheduler.fPrestige = VictoryCondition->mGetPrestigePoints( fMissionScheduler.fVictoryLevel );
	fMissionScheduler.fBonusPrestige = VictoryCondition->mGetBonusPrestige(); // player added bonus calculation routine

	fMissionScheduler.fNextMissionScore = 0;
	fMissionScheduler.fNextMissionTitle = "";
	fMissionScheduler.fMedal = kNoMedals;
	fMissionScheduler.fCampaignEvent = tTeamInfo::tMissionScheduler::kNone;
}

7.0 Setting up ships
Here we look at the different ways of establishing the ships associated with a specific team in different game settings (campaigns, multiplayer, skirmish, etc) and we consider the class and file structure commonly used for each team's ships. Once again, I assume you used the single player template in the setup wizard

In the appropriate Team.h file (e.g. PlayerTeam.h) we establish identifiers for the possible ships on that team, e.g.

// in file PlayerTeam.h            in file EnemyTeam.h
enum eCustomPlayerIDs              enum eCustomEnemyIDs
{                                  {
  kPlayerShip1 = 100,                  kEnemyShip1 = 200,
  kPlayerShip2 = 101,                  kEnemyShip2 = 201,
  kPlayerShip3 = 103,                  kEnemyShip3 = 202,
};                                 };
Ships for a team are created in the appropriate Team.cpp file, e.g. PlayerTeam.cpp for primary player ships, EnemyTeam.cpp for primary opponent ships, etc.

The actual creation is carried out in mCreateShipsForTeam (options discussed below), and if the ship is human controlled we need to set that up in mTeamSetupComplete:

void tPlayerTeam::mTeamSetupComplete()
{
	tTeamInfo::mTeamSetupComplete();

	// this sets the first ship in the list to be the one the player
	// starts out controlling
	tShipIterator ship = mGetFirstShip();
	Assert( *ship != NULL );
	if ( *ship != NULL )
	{
		if ( mIsTeamHumanControlled() )
		{
			ship->mSetShipPlayerControlled();
		}
	}
}
The mCreateShip method expects the following fields:

7.1 Hardcoding ship selection For example, the following hard-codes a Lyran command cruiser as the player ship
	mCreateShip(typeid( tPlayerShip ),
			"L-CC",
			kStartPosition_G, // which map letter matches this ship
			kNoMetaShipID,  // tells D2 not to leave this as a persistent ship
			kPlayerShip1,  // id for the ship
			0, 0, -1, -1,   //x0, y0, are coordinates of ship
						// x1, y1 are coordinates it is pointing towards
			"Tikka Terror", // ship name
			kNoShipOptions );  // no ship options available

7.2 Drawing ships from the dyna/metaverse
Rather than creating a ship just for this mission, in campaigns we usually want to use whatever ship or ships a player is currently flying in the campaign.

For example, the following extracts an opposing ship from the dyna/metaverse:

	mCreateShip( typeid( tEnemyShip ), // draft the enemy team's ships from the dyna/metaverse
			shipScriptDescription.fClassName.c_str(), // use whatever kind of ship is specified there
			kStartPosition_H, // starting position
			shipScriptDescription.fMetaDatabaseID, // dyna/metaverse ID for the ship
			kEnemyShip1, // mission id for the ship
			0, 0, // coordinates relative to the start position above
                  -1, -1,	// facing relative to the start position above
			shipScriptDescription.fCustomName.c_str(), // use the player's name for the ship
			static_cast< eShipOptions >( kDefaultShipOptions | kCanBeCarrier ) ); // restrictions on what can be drafted

7.3 Adding player-controllable ships
As mentioned, the mCreateShip method is invoked within the mCreateShipsForTeam method in the appropriate XTeam.cpp file.

If we want to give the player extra ships (either hardcoding a fleet, or temporarily adding to the player's current dyna/metaverse fleet) then we simply use multiple calls to mCreateShip.

For example, the following gives the player his dyna/metaverse fleet, plus control of a neutral base station:

	mCreateShip( typeid( tPlayerShip ), 
			shipScriptDescription.fClassName.c_str(), 
			kStartPosition_H,
			shipScriptDescription.fMetaDatabaseID,
			kPlayerShip1, 
			0, 0, 
                  -1, -1,	
			shipScriptDescription.fCustomName.c_str(),
			static_cast< eShipOptions >( kDefaultShipOptions | kCanBeCarrier ) ); 
	mCreateShip(typeid( tPlayerShip ),
			"N-BS",
			kStartPosition_G, // which map letter matches this ship
			kNoMetaShipID,  // tells D2 not to leave this as a persistent ship
			kPlayerShip2,  // id for the ship
			0, 0, -1, -1,   //x0, y0, are coordinates of ship relative to position G
						// x1, y1 are coordinates it is pointing towards
			"R and R stop", // base name
			kNoShipOptions );  // no ship options available

7.4 Generating random ships and fleets
We also have the ability to randomly generate a ship of a specific BPV, from a range of ship sizes. However THIS IS NOT TO BE USED FOR A METAVERSE CAMPAIGN. (I suspect this is because of the lack of a metaverse ship id ... this might wind up leaving lots of extra persistant ships on the map that can't be drafted.)
	mCreateShip(typeid( tPlayerNPC1 ), // class type
			85,                    // ship BPV
			kClassFrigate,         // smallest hull size
			kClassDestroyer,       // largest hull size
			0.2,                   // allowable % variation from the specified BPV
			kStartPosition_Z,      // start position
			kPlayerNPC1 );         // team id
We can even generate a ship later in the mission. To do so, when the appropriate event occurs (e.g. a timer goes off N turns into the mission) use the call fChildActor->mCreateShip (plus the usual collection of parameters) to spawn the new ship at whatever location you like. To go along with this, we should be able to make use of methods to manipulate which ships are on which teams:
	iTruth	mAddShipToTeamList( tCreateShipInfo& Info );
	void		mRemoveShipFromTeamList( tShipInfo* Ship );

It is also possible to duplicate an existing ship, with a call to mDuplicateShip. I've outlined the parameter list below

mDuplicateShip(PtrToShipToBeCopied, 
                MapPosition, 
                StartX, StartY, 
                FacingX, FacingY, 
                ShipName, 
                ShipOptions );
Finally, we can randomly generate a fleet in much the same way as we would randomly generate a single ship,
mCreateFleet(ShipClass, 
             TotalShips, 
             TotalBPV, 
             SmallestHullSize, 
             LargestHullSize, 
             VariantPercent, 
             MapPosition, 
             CustomID, 
             StartX, StartY, 
             FacingX, FacingY, 
             CustomName, 
             ShipOptions, 
             FleetRace );

7.5 Bringing the player ship into a mission late
OK, after a bit of experimenting it turns out we can bring a metaverse ship into a mission on a "delayed entry" basis. This means that the player can't do anything, and just sees a blank screen until their ship arrives in the mission. (Can't see a good reason for doing this in single player, but it has all sorts of nasty implications in online missions.)

As far as I can tell, the only place we can capture the information for a player's metaverse ship is inside the mCreateNewShip routine, which we can do thusly:

void tPlayerTeam::mCreateNewShip( const tShipScriptDescription& shipScriptDescription )
{
		

	// store the settings from the player's metaverse ship
	//    so we can generate it later
	SetPlayerShipDescription(shipScriptDescription.fMetaDatabaseID,
							 shipScriptDescription.fClassName.c_str(),
							 shipScriptDescription.fCustomName.c_str());

}
You'll note this involves the use of a new function, SetPlayerShipDescription. This is because we need to store the player ship information until we decide to use it. In this example, I placed the access routines in CommonDefs.cpp:
#include 

int32 gPlayerShipMetaID;
char *gPlayerShipType;
char *gPlayerShipName;

void SetPlayerShipDescription(int32 id, const char *type, const char *name)
{
	// store the player ship's metaverse id
	gPlayerShipMetaID = id;

	// store the type of the player's metaverse ship
	int len = strlen(type) + 1;
	gPlayerShipType = new char[len];
	strcpy(gPlayerShipType, type);

	// store the name of the player's metaverse ship
	len = strlen(name) + 1;
	gPlayerShipName = new char[len];
	strcpy(gPlayerShipName, name);
}

int32 GetPlayerShipMetaID()
{
	return gPlayerShipMetaID;
}

char *GetPlayerShipType()
{
	return gPlayerShipType;
}

char *GetPlayerShipName()
{
	return gPlayerShipName;
}
This also requires declarations in CommonDefs.h of course:
void SetPlayerShipDescription(int32 id, const char *type, const char *name);
int32 GetPlayerShipMetaID();
char *GetPlayerShipType();
char *GetPlayerShipName();
 
Finally, somewhere along the line we need to actually bring the player's ship into the mission. In this example, it's done with a timer event:
void tPlayerTeamBaseState::mOnTimedInterval( int32 timerID )
{
	static int count = 0;

	if (timerID == kPlayerShipArrives) {
		if (count > 0) return;
		count = 1;
		mGeneratePlayerShip(GetPlayerShipMetaID(), GetPlayerShipType(), GetPlayerShipName());
	}
}

void tPlayerTeamBaseState::mGeneratePlayerShip(int32 id, const char *type, const char *name)
{
		// get a handle for the team
		tTeamInfo* playerteam = fMissionInfo->mGetTeamHandle(static_cast<eTeamID>(kPlayerTeam));
    

		// get the player's latest ship
		playerteam->mCreateShip(	typeid( tPlayerShip ),
						type,
						kStartPosition_I,
						id,
						tPlayerTeam::kPlayerShip2,
						0, 0,
						-1, -1,
						name,
						static_cast<eShipOptions>(kDefaultShipOptions|kCanBeCarrier));
								 
}
Remember to add void mGeneratePlayerShip(int32 id, const char *type, const char *name) into the PlayerTeamBaseState.h for this, and of course set up the timer itself.

8.0 Setting up team and ship behaviour
This section is basically a preliminary view of the material covered in much greater detail in the events and information section. Here I assume that you have set up the mission, team, and ship files and classes as described in sections 6 and 7 above.

One of the big strengths of custom-built missions is that, for any ship that isn't player controlled, you can decide what it does and when. Even for the player ships you can do all sorts of interesting things (damaging or repairing systems, adding or removing parts, limiting movement, etc).

All of this relies on setting up variables to remember what (of interest) has happened so far, telling the system to watch for specific kinds of events, and when those events occur adjust the appropriate variable values and set up other kinds of events.

What you can do is really only limited by your imagination and the amount of time you are willing to spend coding and debugging.

The specific events that are available, how to set them up, and how to handle them when they occur are listed in section 11.2, while the routines available to help you observe or change the conditions of specific teams or ships are listed in section 11.1.

I will, hopefully shortly, use this section to include a number of different examples along the lines of "I want this to happen, how do I make it so?".

8.1 Sabotage (Example)
Here is the subplot idea: some turns into the mission (long enough for the player to get good and close to the enemy) a saboteur (sp?) aboard chooses the moment to strike. We'll choose the number of turns randomly, and we'll choose the impact randomly. Some possible sabotage effects could include: dropping all shields, crashing the shuttle bay, stunning some weapons systems, stunning the sensors/scanners, etc. We'll actually hit them with two acts of sabotage (if the mission goes long enough) at different times. To be nice, each time we do this we'll give the player some bonus prestige.

We will need to:

  1. In a setup state, randomly generate the number of turns before the sabotage takes effect and start a timer for each case of sabotage (to go off in said number of turns). To do this, in the mBeginState method we can use the mRandomInt32 and mSetTimedInterval routines.
  2. When the timer goes off, we need to randomly select the effect and apply the event. Again we'll be using mRandomInt32, plus mSetSystemHealthFromOrig or mSetSystemHealthFromCurr, plus we'll set up a message to tell the player why their ship just deteriorated.

How to do it? I'll post the explanation soon, or you can look at the code in BaseVictoryState.h and BaseVictoryState.cpp for the simple campaign mission shown in section 13.

8.2 A smarter AI (Example)
Here the goal is to get the AI to adapt, at least a little bit, to changing conditions.

Here's the rundown:

To achieve this, in the enemy team's base state we'll check and see if it's an AI team. If so, we'll set up the timer that indicates when we should re-evaluate strategy:

void tEnemyTeamBaseState::mBeginState()
{
	// if the team is AI controlled, then every second turn
	//    re-evaluate how the battle is going
	tTeamInfo *enemyteam = fMissionInfo->mGetTeamHandle(static_cast<eTeamID>(kEnemyTeam));
	if (!enemyteam) return;
	if (enemyteam->mIsTeamHumanControlled() == kFalse)
		mSetTimedInterval(kTimerTurnsSet, kEvaluateAIStrategy, 2.0f);
}

To set the timer up, in our .h file we must set up an ID for the timer, i.e.:

enum eCustomTimerIDs
{
kEvaluateAIStrategy,
}
Now, when the timer goes off, we need to get the BPV of the current ships for both teams, and the damage status of the current ships for both teams. We will then use that to create a rating of the team strengths, and use the ratio of the AI team's strength versus the player team's strength to decide what to do next.
void tEnemyTeamBaseState::mOnTimedInterval( int32 timerID )
{
	if (timerID == kEvaluateAIStrategy) {
		// time to reevaluate the AI's strategy for the team
		//    decide whether to destroy, capture, or disengage,
		// and decide which ship is the key target for now

		// first, get the BPV for the team,
		//    multiplied by (1 minus the damage percent) for the team
		tTeamInfo *enemyteam = fMissionInfo->mGetTeamHandle(static_cast<eTeamID>(kEnemyTeam));
		if (!enemyteam) return;

		// Note: both team->mGetCombatBPV and team->mGetTotalDamagePercent are based
		//       only on the ships currently in play

		float EnemyStrength = enemyteam->mGetCombatBPV() * (100.0 - (enemyteam->mGetTotalDamagePercent()));
		int32 enemyshipcount = enemyteam->mShipsCurrentlyControlled();
		tShipInfo *ship;

		// next, do the same thing for the player team
		tTeamInfo *playerteam = fMissionInfo->mGetTeamHandle(static_cast<eTeamID>(kPlayerTeam));
		if (!playerteam) return;
		float PlayerStrength = playerteam->mGetCombatBPV() * (100.0 - (playerteam->mGetTotalDamagePercent()));

		// assess the relative strengths
		float StrengthAssessment = EnemyStrength / PlayerStrength;

		// if the player team is in much worse state than this team,
		//    then consider trying a capture

		// if this team is in much worse shape than the player team
		//    then consider disengaging

		// otherwise just treat is as normal combat

		if (StrengthAssessment > 2.0) { // go for capture
			fMissionInfo->mSetTeamRelations(static_cast<eTeamID>(kEnemyTeam), static_cast<eTeamID>(kPlayerTeam), kTeamAtWar, kGoalCapture, 1.0, kFalse);

		} else if (StrengthAssessment < 0.75) { // run away, everyone scatter for the nearest border
			for (int32 i = 0; i < enemyshipcount; i++) {
				ship = enemyteam->mGetThisShip(i);		
				if (ship != NULL) ship->mDisengage(kByMapEdge, kVeryHighPriority);
			}

		} else { // continue normal play (aim to destroy the player ships)
			fMissionInfo->mSetTeamRelations(static_cast<eTeamID>(kEnemyTeam), static_cast<eTeamID>(kPlayerTeam), kTeamAtWar, kGoalDestroy, 1.0, kFalse);
		}

		// set the timer to go off again in another two turns
		mSetTimedInterval(kTimerTurnsSet, kEvaluateAIStrategy, 2.0f);
	}
}

8.3 Late arrivals (Example)
Whoops, change of plan - for the moment I've moved this into section 7.5 above.
9.0 Mission Design: philosophies and guidelines
This is where I get to get on my soapbox for a little bit, and talk about the missions themselves - what's good, what's not. Of course, this is entirely my own opinion, but what the hell - I'm writing the document, so I get to babble if I like - so there!

There are lots of points to consider. First, ask yourself

The answers to those three questions tell you a lot about what your mission should address.

If the mission is once per campaign then you can afford to throw in as many specialized plot twists as you like - they'll play it rarely enough that the twists are still interesting. If it comes up repeatedly, however, or if it is a skirmish mission, then having a narrowly-focused plotline is only interesting until the player figures out the right sequence of choices. It might still be challenging from a technical point of view, but the plot is no longer the interesting part.

If the mission is something that occurs repeatedly then you need some way to keep the player off balance. The random ship generation can do that to a degree in skirmish missions, as can the semi-random process of drawing in other ships from the dyna/metaverse. However, it is rarely enough to keep the long term interest of an avid player (look at the online campaigns, many players routinely complete fifty or a hundred missions every week ...).

Pseudo-random ship placement can help somewhat, as can the inclusion of some random events (e.g. sometimes there's a third or fourth team generated, sometimes there isn't ... sometimes that team is your ally, sometimes it isn't).

Beefing up the AI teams can also help. If left to its own devices, the AI in most missions is quite predictable. However, you can set timers to explicitly check conditions, and if it seems appropriate change the AI orders and priorities. Oh, let's say have the player's AI allies suddenly panic and disengage at a critical time, or suddenly have several enemy ships suddenly ordered to try a high-priority capture on a crippled player ship. Dig through the AI commands and priority system, you CAN create a better AI!

Keep the difficulty settings in mind - if someone is flying on the beginner levels they shouldn't be hammered with an impossible mission, the game is hard enough on beginners as it is. On the other hand, if someone is flying at admiral level, then why not make 'em work for it? (evil grin)

On a similar note, the mission should be challenging but not insane - if the player has to read your mind to have any chance of surviving then it isn't a good mission (IMO). If there is something particularly unusual the player must do, then there should be some reasonable hint about it.

Getting the victory conditions and prestige awards right is also a biggie - overly large prestige awards or penalties can dramatically unbalance a campaign, but overly small ones can discourage the player. (I'd lean more to the latter than the former, but then I like long campaigns.) On the victory conditions - organize your conditions carefully and comprehensively ... there's nothing worse than coming up with an innovative solution to a difficult mission and then getting hammered by incorrect victory conditions because it was something the mission designer hadn't thought of or tested.

Test your campaigns before you turn them loose on the public!
Many scripters are pretty good at this, but there are a few who release missions buggy enough to be Microsoft products.

Get ideas anyplace you can - look at all the missions posted for SFC, EAW, and OP, as well as any from other games that look relevant. "Borrow" ideas from TV shows, movies, real life, your latest dream/nightmare, whatever works!

Other than that, HAVE FUN!!!! Let the creative juices flow, and have a helluva good time with it. If the debugging gets you down, then walk away from it for a bit and go play! Hey - go get on SFC2.net's server, see what BBJones, Skull, Articfires, and Tantalus have dreamed up for us this week!

And, on behalf of everyone who plays this game, thanks for trying to come up with new ideas and new challenges!


10.0 Determining who won, and the results
Here we will go over the steps involved in tracking victory conditions and generating the correct set of game results for both single and multiplayer missions.

I tend to do all my storage and computation of victory conditions in BaseVictoryState.h and BaseVictoryState.cpp, but I note that a more common practice seems to be to carry this out in states associated with the individual teams. Again, it's programming - you can do it any way that works and makes sense to you when you come back to modify (fix) it later.

There are two big issues in calculating victory conditions:

  1. Identifying all the possible combinations of results, and assigning a reasonable victory condition based on the specific combination.
  2. Accurately updating the combinations as each relevant event takes place.
These get more and more difficult as you add more and more conditions, more and more teams, and multiple ships per team.

The simplest case is just a "last team standing" scenario - if your team runs away, gets captured, or gets destroyed then you lose, otherwise you win.

This is a little dull for a custom scripted mission however.

You may want to alter the level of victory based on the relative damage between the teams, the number of ships which were captured, as opposed to destroyed, and the number which were destroyed as opposed to disengaging.

You may also want to alter the victory levels based on how long it took the player to achieve them.

There may also be very mission specific goals, e.g. delivering something to a planet, stealing something from another ship, successfully negotiating a deal with some other team, etc etc. These may far outweigh the combat considerations in the calculation of victory.

In terms of rewarding (or penalizing) a player, there are three primary means:

The victory levels available are kAstoundingVictory, kVictory, kDraw, kDefeat, kDevastatingDefeat, while the prestige awards can be almost any integer value. (If it is a campaign mission, however, keep in mind how unbalancing huge awards or penalties can be.)

10.1 Victory conditions

The routine used to compute victory status is mCalcVictoryStatus, and I tend to write a custom routine which it in turn calls, e.g.:

eVictoryLevels tBaseVictoryState::mCalcVictoryStatus( eTeamID ID )
// Assumes there are two human teams, the player team and the enemy team
// The victory status for the enemy team is simply assigned as the
//     opposite of the status for the player team
{
	switch (fChildActor->mGetPlayerResults(kPlayerTeam))
	{
	case kPlayerAstVictory:
		if (ID == kPlayerTeam) return kAstoundingVictory;
		else return kDevastatingDefeat;
		break;
	case kPlayerVictory:
		if (ID == kPlayerTeam) return kVictory;
		else return kDefeat;
		break;
	case kPlayerDefeat:
		if (ID == kPlayerTeam) return kDefeat;
		else return kVictory;
		break;
	case kPlayerDevDefeat:
		if (ID == kPlayerTeam) return kDevastatingDefeat;
		else return kAstoundingVictory;
		break;
	default:
		return kDraw;
	}
}
This is a fairly simple case - if there are more than two teams then the results will be more complicated to compute.

The mGetPlayerResults routine examines variables I store in BaseVictoryState.h, e.g. counting the number of ships on each team which disengage, or are destroyed or captured, tracking which specific objectives were met or not, etc. That is the topic of the next section.

Tracking mission results
The biggest trick is making sure that during the mission you track all the events that change the victory status for each team in the mission.

In the event handling routines I update these conditions. The absolutely crucial events to consider are the capture, destruction, and departure of ships on each team. The event handlers for each of these must identify changes in the overall victory status and must figure out if it is time for a mission to end. (Otherwise, one winds up in one of those endless Alt-F4 missions.)

Here are examples of handlers for those three events, also kept in BaseVictoryState.cpp:

void tBaseVictoryState::mOnDeath( tShipInfo* ship )
{
	// check for null pointer
	if ( !ship )	return;

	// get a handle for each team
	tTeamInfo* playerTeam = fMissionInfo->mGetTeamHandle(static_cast<eTeamID>(kPlayerTeam));
	tTeamInfo* enemyTeam = fMissionInfo->mGetTeamHandle(static_cast<eTeamID>(kEnemyTeam));
	tTeamInfo* pirateTeam = fMissionInfo->mGetTeamHandle(static_cast<eTeamID>(kNPC1Team));

	// see which team originally owned the dead ship and which one
	//      owned it just before it died
	eTeamID NewOwners = static_cast<eTeamID>(ship->mGetTeam());
	eTeamID FirstOwners = static_cast<eTeamID>(ship->mGetOriginalTeam());

	// see how many ships each team has left
	int32 playershipcount = playerTeam->mShipsCurrentlyControlled();
	int32 enemyshipcount = enemyTeam->mShipsCurrentlyControlled();
	int32 pirateshipcount = pirateTeam->mShipsCurrentlyControlled();

	if (FirstOwners == kNPC1Team) { // the pirate ship was destroyed
			fChildActor->mSetTeamStatus(kNPC1Team, kTeamDestroyed);
	} else if (FirstOwners == kPlayerTeam) { // one of the player team's ships was destroyed
		if (playershipcount == 0) 
			fChildActor->mSetTeamStatus(kPlayerTeam, kTeamDestroyed);
	} else { // one of the enemy team's ships was destroyed
		if (enemyshipcount == 0)
			fChildActor->mSetTeamStatus(kEnemyTeam, kTeamDestroyed);
	}

	// figure out if the scenario should end now
	if (playershipcount == 0)
		gScriptInterface.mSetGameStatus( static_cast< eTeamID >( kPlayerTeam ), kPlayerLoses );
	else if ((pirateshipcount + enemyshipcount) == 0)
		gScriptInterface.mSetGameStatus( static_cast< eTeamID >( kPlayerTeam ), kPlayerWins );

}

void tBaseVictoryState::mOnCapture( tShipInfo* ship )
{

	// check for null pointer
	if ( !ship )	return;

	// get a handle for each team
	tTeamInfo* playerTeam = fMissionInfo->mGetTeamHandle(static_cast<eTeamID>(kPlayerTeam));
	tTeamInfo* enemyTeam = fMissionInfo->mGetTeamHandle(static_cast<eTeamID>(kEnemyTeam));
	tTeamInfo* pirateTeam = fMissionInfo->mGetTeamHandle(static_cast<eTeamID>(kNPC1Team));

	// see which team originally owned the captured ship,
	// and which team owns it now
	eTeamID NewOwners = static_cast<eTeamID>(ship->mGetTeam());
	eTeamID FirstOwners = static_cast<eTeamID>(ship->mGetOriginalTeam());

	// see how many ships each team has left
	int32 playershipcount = playerTeam->mShipsCurrentlyControlled();
	int32 enemyshipcount = enemyTeam->mShipsCurrentlyControlled();
	int32 pirateshipcount = pirateTeam->mShipsCurrentlyControlled();

	if (FirstOwners == kNPC1Team) { // the pirate ship was captured
		if (NewOwners == kPlayerTeam)
			fChildActor->mSetTeamStatus(kNPC1Team, kCapturedByPlayer);
		else if (NewOwners == kEnemyTeam)
			fChildActor->mSetTeamStatus(kNPC1Team, kCapturedByEnemy);
	} else if (FirstOwners == kPlayerTeam) { // one of the player team's ships was captured
		if ((NewOwners != kPlayerTeam) && (playershipcount == 0)) 
			fChildActor->mSetTeamStatus(kPlayerTeam, kCapturedByEnemy);
	} else { // one of the enemy team's ships was captured
		if ((NewOwners != kEnemyTeam) && (enemyshipcount == 0))
			fChildActor->mSetTeamStatus(kEnemyTeam, kCapturedByPlayer);
	}

	if (FirstOwners == NewOwners)
		fChildActor->mSetTeamStatus(static_cast<eTeamTypes>(NewOwners), kSurviving);

	// figure out if the scenario should end now,
	// i.e. has all of one side or the other left
	if (playershipcount == 0)
		gScriptInterface.mSetGameStatus( static_cast< eTeamID >( kPlayerTeam ), kPlayerLoses );
	else if ((pirateshipcount + enemyshipcount) == 0)
		gScriptInterface.mSetGameStatus( static_cast< eTeamID >( kPlayerTeam ), kPlayerWins );

}

void tBaseVictoryState::mOnDisengaged( tShipInfo* ship )
{
	// check for null pointer
	if ( !ship )	return;

	// see which team originally owned the departed ship and which one
	//      owned it just before it left
	eTeamID NewOwners = static_cast<eTeamID>(ship->mGetTeam());
	eTeamID FirstOwners = static_cast<eTeamID>(ship->mGetOriginalTeam());

	// get a handle for each team
	tTeamInfo* playerTeam = fMissionInfo->mGetTeamHandle(static_cast<eTeamID>(kPlayerTeam));
	tTeamInfo* enemyTeam = fMissionInfo->mGetTeamHandle(static_cast<eTeamID>(kEnemyTeam));
	tTeamInfo* pirateTeam = fMissionInfo->mGetTeamHandle(static_cast<eTeamID>(kNPC1Team));

	// see how many ships each team has left
	int32 playershipcount = playerTeam->mShipsCurrentlyControlled();
	int32 enemyshipcount = enemyTeam->mShipsCurrentlyControlled();
	int32 pirateshipcount = pirateTeam->mShipsCurrentlyControlled();


	// update the status of the team that left, 
	//    if that was the last ship on the team
	switch (NewOwners)
	{
	case kPlayerTeam:
		if (playershipcount == 0)
			fChildActor->mSetTeamStatus(kPlayerTeam, kTeamDisengaged);
		break;
	case kEnemyTeam:
		if (enemyshipcount == 0)
			fChildActor->mSetTeamStatus(kEnemyTeam, kTeamDisengaged);
		break;
	default:
		if (pirateshipcount == 0)
			fChildActor->mSetTeamStatus(kNPC1Team, kTeamDisengaged);
		break;
	}

	// if all the player ships are gone, or all the other ships are gone,
	// then whether the player won or lost depends purely on whether or
	// not the player got the pirate
	if (fChildActor->mGetTeamStatus(kNPC1Team) == kCapturedByPlayer)
		gScriptInterface.mSetGameStatus( static_cast< eTeamID >( kPlayerTeam ), kPlayerWins );
	else
		gScriptInterface.mSetGameStatus( static_cast< eTeamID >( kPlayerTeam ), kPlayerLoses );

}

Campaign mission results This brings up the issue of mSetGameStatus, which effectively communicates the game results to the dyna/metaverse once we've decided the game should end.

This routine sends the team id and the team results back, with the following bitmask (i.e. any combination) options for the results: kGameInProgress, kEnemyCaptured, kEnemyDestroyed, kEnemyDisengaged kPlayerWins, kPlayerDisengaged, kAllyDestroyed, kAllyCaptured, kAllyDisengaged, kPlayerLoses, kMissionFailed, kPlayerDestroyed, kPlayerCaptured, kLeftEarly, kExpellFromElite, kCampaignCompleted, kWinMission, kLoseMission

Prestige and bonus prestige
OK, that's victory conditions, what about prestige and bonus prestige?

The easiest way to set prestige is simply to tie it to the victory level, this is done in file BaseVictory.cpp in method mSetupState, e.g.:

void tBaseVictory::mSetupState()
{
	tBaseVictoryState* baseVictoryState = new tBaseVictoryState( 300, 200, 50, -50, -100 );
	mRegisterState( baseVictoryState );
	mGotoState( typeid( tBaseVictoryState ) );
}
The numbers reflect the base prestige award/penalty for astounding victory down through devastating defeat.

When we're examining various events, we can also manipulate team prestige and bonus awards through various victory state access methods:

mGetPrestigePoints( eVictoryLevels Level );
mGetBonusPrestige();
mLookupPrestigeValue( eVictoryLevels Level );
mAddToPrestige( int32 X );
mAddToPrestige( eVictoryLevels Level, int32 X );
mAddToBonusPrestige( int32 X );

Balancing prestige based on relative team and ship strengths
Now, suppose we decide that we want to scale the prestige awards based on the relative strengths of the teams involved. Let's try the following: We'll create a method to compute the relative strengths of the two teams, and keep it in BaseVictoryState.cpp:
float tBaseVictoryState::mGetStrengthRatio()
{
	// get the current relative strength ratios of the two teams
	tTeamInfo* enemyTeam = fMissionInfo->mGetTeamHandle( static_cast< eTeamID >( kEnemyTeam ) );
	tTeamInfo* playerTeam = fMissionInfo->mGetTeamHandle( static_cast< eTeamID >( kPlayerTeam ) );
	return (((float)(playerTeam->mGetCombatBPV())) / ((float)(enemyTeam->mGetCombatBPV())));
}
Now we'll update each of the death, destruction, and capture events in BaseVictoryState.cpp to compute the relevant bonus prestige:
void tBaseVictoryState::mOnDeath( tShipInfo* ship )
{
	// check for null pointer
	if ( !ship )	return;

	// see which team owned the dead ship
	eTeamID Owners = static_cast<eTeamID>(ship->mGetTeam());

	if (Owners == kPlayerTeam) { //  player ship was destroyed
		tTeamInfo* playerTeam = fMissionInfo->mGetTeamHandle(static_cast<eTeamID>(kPlayerTeam));
		int32 playershipcount = playerTeam->mShipsCurrentlyControlled();
		mAddToBonusPrestige(0 - (ship->mGetShipBPV() * (mGetStrengthRatio())));
		if (playershipcount == 0) {
			mSetVictoryStatus(kPlayerDestroyed);
			gScriptInterface.mSetGameStatus( static_cast< eTeamID >( kPlayerTeam ), kLoseMission );
			fMissionInfo->mInvokeGameStatus(kTrue, kNoTeam);
		}
	} else //  enemy ship was destroyed
	{
		tTeamInfo* enemyTeam = fMissionInfo->mGetTeamHandle(static_cast<eTeamID>(kEnemyTeam));
		int32 enemyshipcount = enemyTeam->mShipsCurrentlyControlled();
		mAddToBonusPrestige(ship->mGetShipBPV() * (mGetStrengthRatio()));
		if (enemyshipcount == 0) {
			mSetVictoryStatus(kEnemyDestroyed);
			gScriptInterface.mSetGameStatus( static_cast< eTeamID >( kPlayerTeam ), kWinMission );
			fMissionInfo->mInvokeGameStatus(kTrue, kNoTeam);
		}
	}

}

void tBaseVictoryState::mOnCapture( tShipInfo* ship )
{
	// check for null pointer
	if ( !ship )	return;

	// see which team owned the captured ship
	eTeamID Owners = static_cast<eTeamID>(ship->mGetOriginalTeam());

	if (Owners == kPlayerTeam) { //  player ship was captured
		// give the player a penalty for being captured
		mAddToBonusPrestige(0 - (1.5 * ship->mGetShipBPV() * (mGetStrengthRatio())));
		tTeamInfo* playerTeam = fMissionInfo->mGetTeamHandle(static_cast<eTeamID>(kPlayerTeam));
		int32 playershipcount = playerTeam->mShipsCurrentlyControlled();
		if (playershipcount == 0) {
			mSetVictoryStatus(kPlayerCaptured);
			gScriptInterface.mSetGameStatus( static_cast< eTeamID >( kPlayerTeam ), kLoseMission );
			fMissionInfo->mInvokeGameStatus(kTrue, kNoTeam);
		}
	} else if (Owners == kEnemyTeam)  //  enemy ship was captured
	{
		// give the player a 75 point bonus for capturing the enemy
		mAddToBonusPrestige(1.5 * ship->mGetShipBPV() * (mGetStrengthRatio()));
		tTeamInfo* enemyTeam = fMissionInfo->mGetTeamHandle(static_cast<eTeamID>(kEnemyTeam));
		int32 enemyshipcount = enemyTeam->mShipsCurrentlyControlled();
		if (enemyshipcount == 0) {
			mSetVictoryStatus(kEnemyCaptured);
			gScriptInterface.mSetGameStatus( static_cast< eTeamID >( kPlayerTeam ),  kWinMission );
			fMissionInfo->mInvokeGameStatus(kTrue, kNoTeam);
		}
	}

}

void tBaseVictoryState::mOnDisengaged( tShipInfo* ship )
{
	// check for null pointer
	if ( !ship )	return;

	// see which team owned the deaparted ship
	eTeamID Owners = static_cast<eTeamID>(ship->mGetTeam());

	if (Owners == kPlayerTeam) { //  player ship disengaged
		tTeamInfo* playerTeam = fMissionInfo->mGetTeamHandle(static_cast<eTeamID>(kPlayerTeam));
		int32 playershipcount = playerTeam->mShipsCurrentlyControlled();
		mAddToBonusPrestige(0 - (0.25 * ship->mGetShipBPV() * (mGetStrengthRatio())));
		if (playershipcount == 0) {
			mSetVictoryStatus(kPlayerDisengaged);
			gScriptInterface.mSetGameStatus( static_cast< eTeamID >( kPlayerTeam ), kLoseMission );
			fMissionInfo->mInvokeGameStatus(kTrue, kNoTeam);
		}
	} else if (Owners == kEnemyTeam) //  enemy ship disengaged
	{
		tTeamInfo* enemyTeam = fMissionInfo->mGetTeamHandle(static_cast<eTeamID>(kEnemyTeam));
		int32 enemyshipcount = enemyTeam->mShipsCurrentlyControlled();
		mAddToBonusPrestige(0.25 * ship->mGetShipBPV() * (mGetStrengthRatio()));
		if (enemyshipcount == 0) {
			mSetVictoryStatus(kEnemyDisengaged);
			gScriptInterface.mSetGameStatus( static_cast< eTeamID >( kPlayerTeam ), kWinMission );
			fMissionInfo->mInvokeGameStatus(kTrue, kNoTeam);
		}
	}


}

11.0 Events and information
There are many many routines for getting information, setting values, and setting up and processing events. It's not always intuitive whether something is handled as an event or just a regular setting, so you might have to dig around a bit in this section. Most of the information getting/setting takes place vi either the fMissionInfo pointer, or pointers to the relevant team (tTeamInfo * ptrs) or the relevant ship (tShipInfo * ptrs). Event handling and the results are generally controlled through the various xxxState.h and xxxState.cpp files.

In this section I have, for now, generally ignored routines used in initially setting up the mission, teams, and ships, since those are covered elsewhere. This section is mostly to catch everything not covered by sections 4 through 10.

11.1 Getting and setting mission, team, and ship information
During the mission, we may be interested in getting and/or setting information values relevant to the mission overall, a specific team, or a specific ship. In this section we'll give an overview to all three parts.

11.1.1 Getting/setting mission information via fMissionInfo

11.1.1.1: Team information/settings

  • The first information we will look at (since it is needed for many other methods) is how to get pointers to the teams in a mission. We use the mGetTeamHandle method, and as a parameter we supply the team identifier (from the list created in CommonDefs.h). An example is shown below.
    	// first we'll establish pointers to the two teams
          TeamInfo *pirateTeam, *playerTeam;
    	pirateTeam = fMissionInfo->mGetTeamHandle(static_cast<eTeamID>(kEnemyTeam));
    	playerTeam = fMissionInfo->mGetTeamHandle(static_cast<eTeamID>(kPlayerTeam));
    

  • We can get the relations between two teams:
    fMissionInfo->mGetTeamRelations(kPlayerTeam, kEnemyTeam);

  • We can alter the relations between one team and another using mSetTeamRelations and providing the two teams, the new relationship, the new goal, an EPVfactor, and a boolean variable declaring whether or not the relationships are supposed to be mutual (otherwise you are only altering one team's views towards the other). The most common relationships are kTeamAllied, kTeamFriendly, kTeamNeutral, kTeamHostile, kTeamAtWar and the goals are kGoalIgnore, kGoalCripple, kGoalCapture, kGoalDestroy. I'm not sure what the EPVfactor is for, but using a value of 1.0 seems to work ok! E.g.
    // set two teams as mutually hostile but ignoring ...
    fMissionInfo->mSetTeamRelations(kPlayerTeam, kEnemyTeam, kTeamHostile, kGoalIgnore, 1.0, kTrue); 
    

11.1.1.2: Ship information/settings

  • Get the target of a ship: mGetShipTarget: we pass the pointer to one ship as a parameter and get a pointer to the target back as the return value

11.1.1.3: Hex/D2 information/settings

  • We can get the current difficulty settings, I assume 0 = lowest, 2 = highest as with the .ini and .gf settings
    int32 setting = fMissionInfo->mGetDifficultyLevel()

  • Get the type of D2 hex the mission is located in: mGetHexType: I believe we create a (class) variable of type tMetaLocationSpec and for this variable we create an expression describing the type of hex we're interested in. We then pass the variable as a parameter to mGetHexType, and we get back a boolean (iTruth) value saying yay or nay, is the current hex of that type. The details on the tMetaLocationSpec class and related types are in MissionMatchLocation.h. Here's a hypothetical example, I haven't tried it yet:
    tMetaLocationSpec location( 5, kPlayerRaceHex, kAnyBase|kAnyPlanet );
    if (fMissionInfo->mGetHexType(location)) 
    {
       ....
    }
    
    The tMetalocationSpec constructor is used in campaign missions to specify what D2 hexes the mission can be generated in, e.g.
    tMetaLocationSpec(1, kEnemyRaceHex, kDustCloudHex|kAsteroidHex) indicates the mission can be located in any hex where you are within 1 hex of an enemy hex and within 1 hex of either a dust cloud hex or an asteroid hex.

    The available options for specifying the race restrictions are:kPlayerRaceHex, kEnemyRaceHex, kAlliedRaceHex, kNeutralRaceHex, kAnyRaceHex, kFederationRaceHex, kKlingonRaceHex, kISCRaceHex, kRomulanRaceHex, kGornRaceHex, kLyranRaceHex, kHydranRaceHex, kMirakRaceHex, while the available terrain options are:kAnyTerrainHex, kEmptySpaceHex, kAnyBaseHex, kAnyPlanetHex, kAsteroidHex, kDustCloudHex, kBlackHoleHex, kShippingLaneHex, kCoreWorldHex, kHomeWorldHex, kColonyHex, kAsteroidBaseHex, kBaseStationHex, kBattleStationHex, kStarbaseHex, kBaseListeningPostHex, kBaseWeaponsPlatformHex .

11.1.1.4: Map information/settings

  • We can get the map dimensions: mGetMapDimensions: we pass pointers to ints for the maximum X and Y coordinates, the values are set by the method

  • We can set a goal indicator for teams or ships, and the indicator can be either to a map location or another ship. Examples:
    // here we assume playership and enemyship have type tShipInfo*, 
    //                toggle has value either kTrue or kFalse (toggle the indicator on/off)
    //                x/ycoord are coordinates at which the indicator should appear
    //                whichteam has team id's, such as kPlayerTeam, kEnemyTeam, etc
    fMissionInfo->mSetGoalIndicator(whichteam, enemyship);
    fMissionInfo->mSetGoalIndicator(whichteam, toggle, xcoord, ycoord);
    fMissionInfo->mSetGoalIndicator(playership, enemyship);
    fMissionInfo->mSetGoalIndicator(playership, toggle, xcoord, ycoord);
    

  • We can generate a new map object, using its coordinates and the corresponding character symbol from MissionMaps.cpp, e.g. creating a moon at coordinates 32,17:
    fMissionInfo->mCreateMapObject('@', 32, 17);
    Similarly we can add an icon to the map, e.g. fMissionInfo->mAddMapIcon(objecttype, xcoord, ycoord, subtype); where the object types are enumerated in MapObject.h and include things such as kObjectSun, kObjectPlanet, etc. I'm not sure where the subtypes are enumerated, will try to remember to look this up later.

11.1.1.5: Message information/settings

  • We can display a message for a particular team/ship. The team id is from CommonDefs.h, the message id is from MissionText.h, the ship ptr is NULL by default but you can specify an individual ship, and the message type defaults to kMissionScriptMessage but can be set to any of the following: kEngineerScriptMessage, kMissionScriptMessage, kScienceScriptMessage, kFleetScriptMessage, kNonCommScriptMessage. E.g.:
    fMissionInfo->mDisplayMessage(static_cast<eTeamID>(teamid), msgid, msgtype, shipptr);

  • We can add specific messages to the debriefings, e.g.
    fMissionInfo->mAddToDebriefingList(kPlayerTeam, "you really sucked that time");

  • We can add debugging messages to appear during the mission, e.g.:
    fMissionInfo->mDebugMessage("This is what seems to be going on...");

11.1.1.6: Other misc. information/settings

  • We can generate a random integer in a specified range:
    int32 newnumber = fMissionInfo->mRandomInt32(lowvalue, highvalue);

  • We can make a button start or stop flashing on the player control panel using mStartFlashingButton and mStopFlashingButton, supplying the appropriate string for the button identifier.

    Note that if the player actually pushes the flashing button, we can detect that and respond to it, see the events section 11.2.7 for details.

11.1.2 Getting/setting mission information via tTeamInfo pointers
11.1.2.1: Team information/settings

  • We can get the id value for the team using mGetTeam, which returns a value which can be matched against the ids from the CommonDefs.h file

  • We can get the race of a team using mGetRace, the possible races are kFederation, kKlingon, kRomulan, kLyran, kHydran, kGorn, kISC, kMirak, kOrion, kMonster, kTholian, kLDR, kWYN, kJindarian, kAndro, kNeutralRace, kMirror,

  • We can determine if a team is human controlled using mIsTeamHumanControlled (returns kTrue or kFalse) or get a pointer to the ship which is currently controlled using mCurrentlyControlledShip

  • We can get the combat BPV of the ships currently in play for a team, mGetCombatBPV()

  • We can get the damage rating for the ships currently in play for the team: mGetTotalDamagePercent
    This is given as a floating point value in the range 0.0 to 100.0, representing the appropriate percent.

  • We can assess how much damage was done to another team (hit count I presume) using mAssessDamages(targetteamptr)

  • We can get the relationship with another team, e.g. mGetRelations(kEnemyTeam) and we can get the goal with respect to another team, e.g. mGetGoal(kEnemyTeam)

  • We can set relations with another team using mSetRelations(teamid, relations, goals, 1.0)

11.1.2.2: Ship information/settings

  • We can get the number of ships the team currently has in play using mGetShipsInPlayCount

  • We can get also get ship counts for the team using:
    - number in play: mGetShipsInPlayCount
    - number remaining from original team: mShipsRemainingInPlayFromOriginalTeam
    - number currently controlled: mShipsCurrentlyControlled

  • We can get pointers to the ships on a team. An example is shown below.
    	// establish pointers to the first ship on each of two teams
    	tShipInfo *pirateShip, *playerShip;
    	tShipIterator ship = pirateTeam->mGetFirstShip();
    	pirateShip = *ship;
    	ship = playerTeam->mGetFirstShip();
    	playerShip = *ship;
    

  • We can, I think, determine which is the team's flagship using mGetTeamFlag, this should return an index number (e.g. is it ship 1, 2, or 3)

  • We can set the currently controlled ship using mSetCurrentlyControlledShip(shipptr), bet this could be used to really drive a player nuts!

  • We can get a specific ship from the team by index, e.g. mGetThisShip(1) returns a tShipInfo pointer for the first ship in the list.

11.1.2.3: Team command and control information/settings

  • We can remove a ship as the team's target using mRemoveShipAsTarget(shipptr)

  • We can clear the current list of commands to the AI using mClearCommands

  • We can tell a team to defend a particular ship, what to treat as a threat radius, what range to follow a threat out to, and what defense priority to use via:
    mDefendEntity(shipptr, threatradius, followradius, priority).
    The AI priority levels are: kNeverPriority, kVeryLowPriority, kLowPriority, kMediumPriority, kHighPriority, kVeryHighPriority, kAbsolutePriority

11.1.2.4: Team/game status information/settings

  • We can get the current game status using mGetGameStatus, returning values such as kGameInProgress, kEnemyCaptured, kEnemyDestroyed, kEnemyDisengaged, kPlayerWins, kPlayerDisengaged, kAllyDestroyed, kAllyCaptured, kAllyDisengaged, kPlayerLoses, kMissionFailed, kPlayerDestroyed, kPlayerCaptured, kLoseMission, kWinMission, kLeftEarly, kExpellFromElite, kCampaignCompleted. These are set as bitmasks, so combinations are possible.

  • We can get the current event status of the team, mGetTeamStatus, this will return a value of type iNotifyEvents, i.e. a list of all the events being monitored for that team

  • We can set the game status using mSetGameStatus (see the first list item under "learning things" above for a status list)

11.1.3 Getting/setting mission information via tShipInfo pointers
11.1.3.1: Team/game information/settings

  • We can get the race of a ship: mGetRace

  • We can get the current and original teams for the ship: mGetTeam, mGetOriginalTeam

11.1.3.2: Ship information/settings

  • We can get the class of a ship by type using mGetClassType or by name using mGetClassName

  • We can get the percent of damage to the ship so far, mGetDamagePercent

  • We can get the BPV of the ship: mGetShipBPV

  • We can get the current ship options list using mGetShipOptions. The options are set up in the scriptsdata.h file, and basically cover what special categories of ship are allowable, whether the ship can be captured or not, and where the ship placement can be relative to it's listed map starting position.

  • Destroy the ship mDestroyShip or destroy it silently mDestroyShipSilently

  • Reset the name of the ship mChangeName(msgid)

  • Set the health of a system as a percent (float value between 0 and 1) of its original health, with a true/false flag as to whether they're allowed to repair it or not:
    mSetSystemHealthFromOrig(system, percent, flag).

  • Set the health of a system as a percent (float value between 0 and 1) of its current health, with a flag to indicate if damage should be rounded up or not
    mSetSystemHealthFromCurr(system, percent, flag)

    Note: setting the health to 0 destroys the weapon, setting the health to 0.5 stuns it, and setting the health to 1.0 repairs it. For example, ship->mSetSystemHealthFromOrig(kAllShieldsHardPoint, 0.5) destroys all the ship's shields.

    Note: damage assigned in this way does NOT appear to be effective once the ship leaves the mission. I.e. if you destroy half the engines using SetSystemHealth, and the enemy destroys another 25%, when the ship leaves the mission only the 25% damage done by the enemy needs to be repaired.

    The systems are specified by hardpoints, which is an extremely lengthy list, enumerated in sysbaseenums.h. Oh what the hell, here are most of them: kRandomInternalsHardPoint, kAllEnginesHardPoint, kAllShieldsHardPoint, kAllWeaponsHardPoint, kBridgeHardPoint, kFlagBridgeHardPoint, kEmergencyBridgeHardPoint, kAuxBridgeHardPoint, kSensorGroupHardPoint, kSensorHardPoint, kScannerHardPoint, kHullGroupHardPoint, kForwardHullHardPoint, kAftHullHardPoint, kCenterHullHardPoint, kExcessDamageHardPoint, kTransporterHardPoint, kTractorHardPoint, kMechTractorHardPoint, kShuttleBayHardPoint, kFighterBayHardPoint, kEngineGroupHardPoint, kRightWarpHardPoint, kRightWarpHardPoint, kLeftWarpHardPoint, kCenterWarpHardPoint, kAPRPowerHardPoint, kImpulsePowerHardPoint, kTotalEngineHardPoints, kBatteryHardPoint, kLabHardPoint, kBarracksHardPoint, kCargoHardPoint, kShieldGroupHardPoint, kShieldOneHardPoint, kShieldOneHardPoint, kShieldTwoHardPoint, kShieldThreeHardPoint, kShieldFourHardPoint, kShieldFiveHardPoint, kShieldSixHardPoint, kShieldSixHardPoint, kTotalShieldHardPoints, kShipSystemADD6HardPoint, kShipSystemADD12HardPoint, kShipSystemADD30HardPoint, kArmorHardPoint, kCloakHardPoint, kDamageControlGroupHardPoint, kDamageControlHardPoint, kInternalRepairHardPoint, kOfficerGroupHardPoint, kHelmOfficerHardPoint, kEngineerOfficerHardPoint, kScienceOfficerHardPoint, kWeaponsOfficerHardPoint, kSecurityOfficerHardPoint, kCommOfficerHardPoint, kCaptOfficerHardPoint, kProbeHardPoint, kHeavyWeapon1HardPoint, kNewFirstHeavyWeaponHardPoint=kHeavyWeapon1HardPoint, kHeavyWeapon1HardPoint,..., kHeavyWeapon10HardPoint, kWeapon11HardPoint,..., kWeapon25HardPoint, kNumberOfShipSystemHardPoints, kNewTotalWeaponHardPoints, .

11.1.3.3: Control/command information/settings

  • We can get the current alert status of a ship, mGetAlertStatus

  • Set the current ship alert status, mSetAlertStatus

  • Set the deepscan status: mSetDeepScanStatus(OnOffFlag)

  • Set the (AI) ship target and priority mSetShipTarget(shipptr, priority)

  • Clear the (AI) ship's target list: mRemoveTargets

  • Clear the (AI) ship's command list: mClearCommands

  • Order an AI ship to disengage, how to disengage, and its priority mDisengage(method, priority): methods are kByAcceleration, kByMapEdge

  • Order the AI ship to patrol, supplying a list of patrol points, a detection radius, and a priority: mPatrol(radius, points, priority). The patrol points are described in tmissionmap.h

  • Order an AI ship to turn cloak on/off mCloak(flag)

  • Order an AI ship to probe another ship mFireProbe(targetship, priority)

  • Order an AI ship to defend another ship mDefendEntity(shipptr, threatradius, followradius, priority)

  • Order an AI ship to follow another ship mFollowEntity(shipptr, priority)

  • Order an AI ship to tractor another ship mTractorEntity(shipptr, xcoord, ycoord, priority) (to a specific x,y coord?)

  • Setting the ship thrust and maximum speed mSetShipThrust(power), mSetShipMaxSpeed(speed),

  • Set the ship to be player or AI controlled: mSetShipPlayerControlled, mSetShipAIControlled

11.1.3.4: Map information/settings

  • We can get the start and end coordinates of a ship using mGetStartX, mGetStartY, mGetEndX, and mGetEndY

  • We can get the distance between a ship and another ship, map object, or map location using mGetTacticalDistance, giving it as parameters either the X,Y coordinates, or a pointer to a ship, or a map object

11.1.3.5: Crew and supply information/settings

  • We can get the rank of any of the ship officers using mGetOfficerRank(officer), officer types include kWeaponsOfficer, kEngineeringOfficer, kScienceOfficer, kCommOfficer, kHelmOfficer, kSecurityOfficer, kCaptainOfficer, kMaxOfficers while the ranks include kRookie, kJunior, kSenior, kVeteran, kLegendary,

  • Add an item to stores or set the amount of an item to stores
    mAddItemToStores(storestype, storesubtype, amount)
    mAddItemToStores(storestype, transportertype, amount)
    mSetItemInStores(storestype, storesubtype, amount)
    mSetItemInStores(storestype, transportertype, amount)

  • Remove an item from stores, specified by either its ShipStores type or its Transporter item type: mRemoveItemFromStores(type, amount). The ship stores types cover kMissilesTypeI, kMissilesTypeIV, kSpareParts, kMarines, kSmall_Mines, kLarge_Mines, kTransporterItem, whereas the transporter items cover kTransGornEgg, kTransSpareParts, kTransCaptain, kTransBlueStar, kTransDilithiumCrystals, kTransGravTank, kTransMarineDude, kTransHarryMudd, kTransInfiltrator, kTransNovaMine, kTransRomulanAle, kTransTribbles, kTransAlienArtifact, kTransCaptainDude, kTransWeaponSchematic, kTransMedicalSupplies, kTransAwayTeamItem, kTransDiplomat, kTransInjuredDude, kTransMedicineJar, kTransScientists, kTransLifePodDude, kTransDeathPlague, kTransBlackBox, kTransPsionicDisruptor, kTransIonicProjector, kTransEngineers, kTransPrisoner,

  • Set an officer name or rank, mSetOfficerRank(rankid), mSetOfficerName(msgid)

11.2 Creating and handling events
Once the team and ship creation has taken place, most of the action is controlled by watching for specific events and invoking special routines when the event takes place. For instance, if a ship reaches a certain area on the map we might want something to occur (get a message from a planet, enemy ships warp into the battle, whatever). Similary if the ship's mission is to deliver medical supplies to the planet, then when the supplies are delivered we might want to end the mission.

Generally speaking, we have to keep track of what has (and what has not) happened so far - i.e. the current state of the game. We can track this on several levels simultaneously: keeping track of the state of a team, the state of a specific ship, and the state of the mission with respect to victory conditions. The code for tracking and modifying the states is kept in various xxxState.h and xxxState.cpp files, along with code for setting up and dealing with (handling) the various events that effect the state. I.e., we want to place the handling in PlayerShipBaseState.h for handling events relevant to player ships, in BaseVictoryState.cpp for events relevant to victory conditions, etc.

For most events, the game does not automatically watch to see if they are taking place (this would create unnecessary processing demands, since many events are not relevant in most missions). As a result, we often need to invoke one routine to tell the system to begin watching for a particular kind of event, and another routine that actually deals with the event if/when it actually occurs.

There are many possible events and results, including things such as: