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
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
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).
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
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\
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.
Click to show .dll files, and select CreateScript.dll.
When it appears, make sure the check box beside it is checked.
Still show .dll files, and select ScriptObjectWizardAddin.dll.
When it appears, make sure the check box beside it is checked.
That completes the API initialization, you are now set up to create as many scripts as you like.
The next set of steps are needed to remove some elements that were eliminated from the API in an earlier patch
#include "Draw.h"and
void tyour_script_name::mInitializeDraw(.....)
{
.....
}
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:
1 = Earth-like planet 5 = Dark Earth-like 8 = Fire planet 2 = Ringed-earth 6 = Saturn-like 9 = Ice planet 3 = Mars-like 7 = Night city planet 0 = Gas planet 4 = Jupiter-like
@ = Moon & = Nebula1 : = Small black hole [ = Small asteroid # = Corpse ( = Sun1 ? = Nebula2 , = Large black hole ] = Large asteroid ^ = Organian ) = Sun2 < = Light dust cloud % = Wormhole * = Ice asteroid ! = Sun3 > = Heavy dust cloud $ = Fissure ~ = Pulsar
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
};
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[]
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)
};
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.
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
.
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
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 );
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);
}
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
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:
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:
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
.
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;
}
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:
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
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
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
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 idWe 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 );
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:
#includeThis also requires declarations in CommonDefs.h of course: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; }
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.
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?".
We will need to:
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.
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);
}
}
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!
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:
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.)
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.
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 );
}
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
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 );
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);
}
}
}
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.
// 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));
// set two teams as mutually hostile but ignoring ... fMissionInfo->mSetTeamRelations(kPlayerTeam, kEnemyTeam, kTeamHostile, kGoalIgnore, 1.0, kTrue);
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.
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 .
// 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);
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.
// 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;
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, .
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:
kTimerEndGamePause, // use this to adjust the amount of seconds to pause before leaving the tactical game kTimerCountdownSet, // use this to display a countdown timer on the tactical screen kTimerCountdownAdjust, // use this to modify the countdown timer kTimerTurnsSet, // use thses to setup a timer callback using game turn time kTimerSecondsSet, // use these to setup a timer callback using seconds kTimerKillTimer, // used to stop a timer from going off again in the future
The setup process is as follows:
Similarly we can register states, generate a change of state, and handle the change of state using mRegisterState, mGoToState, and mOnStateChange respectively. The states themselves, however, are classes - thus the standard approach is to create a .h and .cpp file for each state, and include all the relevant code. For example, in the PlayerTeam.cpp file we might create the states and pick an initial one:
void tPlayerTeam::mSetupState()
{
mRegisterState( new tPlayerTeamInitialState( ) );
mRegisterState( new tPlayerTeamAnotherState( ) );
mRegisterState( new tPlayerTeamThirdState( ) );
mGotoState( typeid( tPlayerTeamInitialState ) );
}
Then that state could be set up in its own class in PlayerTeamInitialState.h and .cpp,
and in the .cpp file we could initialize things in mBeginState method and include handlers for
all the events relevant to that state.