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.
We will start out creating just the basic skirmish part - setting up teams, relations, ships, victory conditions, and briefing/debriefing messages.
Once that works, we will add the overall game timer and messages to taunt the player if they are too slow working through the mission.
Once that works, we will add the periodic navigator updates - figuring out if we are getting closer to the pirate ship or farther away from it.
By this point, you should have a good idea of how to manipulate a basic mission, and determine/set up some of the information you need for event handling.
We'll go through exact instructions to set up the basic teams, ships, victory conditions, and messages to complete a working script. Once that is done, we'll go back and revise the mission to add some simple timers and in-game messages to torture the player, producing a new complete script. Finally, we'll go back and revise it yet again to add the ability to track our distances from the enemy ship and give the player updates on our progress, producing our final complete version of the script.
enum
{
kPlayerTeam = kTeam1,
kPlayerNPCTeam,
kEnemyTeam,
kNPC1Team,
};
This lists all the teams, and we want to get rid of the ones that aren't player or enemy.
While we're at it, we'll throw in a name for the enumerated type as well, may save some grief later:
enum eTeamTypes
{
kPlayerTeam = kTeam1,
kEnemyTeam,
};
const tTeamSpec& team2 = mCreateTeamSpec("PlayerNPCTeam",
static_cast<eTeamID>(kPlayerNPCTeam),
kMaxChance,
kTrue,
kNPCTeam,
kMaxChance,
kSpecPlayableRace,
kAnyRace,
typeid(tPlayerNPCTeam));
and,
const tTeamSpec& team4 = mCreateTeamSpec("NPC1Team",
static_cast<eTeamID>(kNPC1Team),
kMaxChance,
kTrue,
kNPCTeam,
kMaxChance,
kSpecPlayableRace,
kAnyRace,
typeid(tNPC1Team));
Don't worry about what this stuff does right now, just delete it.
Specs->mSetupTeam(team1); Specs->mSetupTeam(team2); Specs->mSetupTeam(team3); Specs->mSetupTeam(team4);Well, we've only got two teams left, so delete those last two lines.
int32 teamList[kRelationsCount] =
{
kPlayerTeam,
kEnemyTeam,
};
Now down below that is a great ugly declaration for a two-dimensional array of strings,
where these strings represent the relationships and goals between each possible pair of teams.
We've eliminated two of the four teams, so we need to completely rewrite this beastie:
std::string relationships[kRelationsCount][kRelationsCount] =
{
//Plyr = kPlayerTeam,
//Nme = kEnemyTeam,
// Plyr Nme
/*Plyr*/ "--", "WD",
/*Nme */ "HI", "--",
};
The WD means the player is at war with the pirate and the goal is to destroy the pirate (although of course
what the player actually does is completely up to them).
For each ship, we need to specify where it is, and which way it is facing.
To specify a position, we use an uppercase letter in the range G-Z. Let's use G for the player and
H for the pirate.
To specify facing, we use the matching lowercase letter. E.g. if we put a small g on the map, the player's
ship will face from the uppercase G towards the lowercase g (and similarly for the pirate with H/h).
Put the two ships wherever you like, and maybe put a small asteroid (character [ beside the pirate.
In my mission map, I put the two ships a long way
apart, but more or less facing each other.
Open the MissionText.cpp file, and examine the list of default strings it contains. You can customize these messages by altering the string to be whatever you want, but for now don't add any new strings or delete any existing ones - simply change the contents of the strings that are there. (We'll go into the why's in the creation of the next version of the mission.)
The string names are reasonably self explanatory:
All these types of messages will (as far as we're concerned) automatically turn up in the right place at the right time.
Here is the version we want for that routine, I've included lots of comments in it to explain what I think is going on:
void tHis_TestSkirmish::mScriptSpecs( tMissionSpec* Specs )
{
Specs->fMissionTitle = Messages[kMissionTitle_msg]; // sets mission title from MissionText.h
Specs->fMissionDescription = Messages[kDescription_msg]; // sets mission description as above
Specs->fMissionType = kHistoricalMission; // see below for the mission types I'm interested in
// Tutorial means tutorial, Historical means skirmish, Campaign means D2
Specs->fMetaLocationSpec = tMetaLocationSpec( 1, kAnyRaceHex, kAnyTerrainHex );
// I *think* this determines where in the dynaverse this can occur
// - the number is the D2 hex range for the mission
// - the second parameter is which racial territory the mission can appear in
// (kPlayerRaceHex, kEnemyRaceHex, kAlliedRaceHex, kNeutralRaceHex, kAnyRaceHex,
// kFederationRaceHex, kKlingonRaceHex, kRomulanRaceHex, kGornRaceHex, kLyranRaceHex,
// kHydranRaceHex, kMirakRaceHex, kISCRaceHex)
// - the final parameter is the required terrain to get the mission to appear
// (any combination of kAsteroidHex, kNebulaHex, kBlackHoleHex, kShippingLaneHex,
// kDustCloudsHex, kEmptySpaceHex, kAnyTerrainHex, kBaseStationHex, kBattleStationHex,
// kStarbaseHex, kBaseWeaponsPlatformHex, kBaseListeningPostHex, kAnyBaseHex,
// kHomeWorldHex, kCoreWorldHex, kAsteroidBaseHex, kColonyHex, kAnyPlanetHex)
Specs->fMinDate = kMinDate; // earliest campaign year the mission can occur in (D2)
Specs->fMaxDate = kMaxDate; // latest campaign year the mission can occur in (D2)
Specs->fBandwidthPart = kAllBand;
Specs->fPlayerRace = kSpecLyran; // makes the mission available to Lyrans only (my preference, deal with it!)
// we can choose from Fed, Kling, Romulan, Lyran, Gorn, Hydran, ISC, or Mirak
// and join them together with the or operator | (fPlayerRace is evaluated bitwise)
// e.g. = kSpecLyran | kSpecFed;
Specs->fMenuStates = kDisableCustomSkirmish; // disables player customization buttons
// The buttons still LOOK like they're available to the player, but whatever they select
// is actually ignored
// Having done this, we will need to explicitly choose and assign player and enemy ships
// An alternative would be to selectively enable some buttons, using values such as kEnableSupplyDock
// create specs for each and every team
const tTeamSpec& team1 = mCreateTeamSpec( "Player Team", // team name
static_cast< eTeamID >( kPlayerTeam ), // eTeamID
kMaxChance, // float, chance team is available
kTrue, // true if the team is required, false otherwise
kPrimaryTeam, // eTeamType (kNPCTeam, kPlayableTeam, kPrimaryTeam, kPrimaryOpponentTeam)
kMinChance, // float, chance team can be playable by AI
kSpecPlayableRace, // iMissSpecRace, racial (bitmask) combination selected from
// kSpecFed, kSpecKling, kSpecLyran, kSpecHydran, kSpecGorn, kSpecRomulan, kSpecISC, kSpecMirak,
// kSpecNoRace, kSpecTholian, kSpecLDR, and a bunch of others
kAnyRace, // iRaceCriteria: bitmask, the MissSpecRace list using any combination of
// kSameAs, kAnyEnemyOf, kWorstEnemyOf, kAnyAllyOf, kBestAllyOf, kAnyRace, kDifferentThan
// e.g. if the iMisSpecRace was kSpecFed and the iRaceCriteria was kAnyEnemyOf then the
// combination of the two would describe any race that was an enemy of the Federation
typeid( tPlayerTeam ),
kNoTeamTag, // eTeamTag - the letter associated with the team in multiplayer
Messages[kMissionTitle_msg] ); // team mission title
const tTeamSpec& team2 = mCreateTeamSpec( "Enemy Team",
static_cast< eTeamID >( kEnemyTeam ),
kMaxChance,
kTrue,
kNPCTeam,
kMaxChance,
kSpecPlayableRace,
kAnyRace,
typeid( tEnemyTeam ) );
// given the specs, set up each team
Specs->mSetupTeam( team1 );
Specs->mSetupTeam( team2 );
}
First let's create the player ship, open up PlayerTeam.cpp and find mCreateShipsForTeam. Let's replace the entire body of this routine so that it looks like:
void tPlayerTeam::mCreateShipsForTeam()
{
// Here we will explicitly create the player ship as a Lyran CC
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 ); // available eShipOptions (bitmask combination) are:
// kNoShipOptions, kDefaultShipOptions, kStartPositionCanBeOffset,
// kCanAddToFleetUponCapture, kDeathUponCapture, kDeathUponDisengage,
// kCanBeRestricted, kCanBeCarrier, kCanBeEscort, kCanBeSublight,
// kCanBeScout, kCanBeMarine, kCanBeTournament, kCanBeSmallQShip
}
The comments are, of course, optional and justmeant to explain what the choices are
Following a similar path, let's create the enemy ship. Open up EnemyTeam.cpp and find mCreateShipsForTeam and replace it with:
void tEnemyTeam::mCreateShipsForTeam()
{
mCreateShip( typeid( tShipInfo ),
"O-CA+1",
kStartPosition_H,
kNoMetaShipID,
kEnemyShip1,
0, 0, -1, -1,
"One poor dead pirate",
kNoShipOptions );
}
Note, this requires the addition of kEnemyShip1 to our EnemyTeam.h file, just as we must ensure
kPlayerShip1 is defined in the PlayerTeam.h file. Add the appropriate types within the
tEnemyInfo and tPlayerInfo classes:
enum eCustomIDs
{
kEnemyShip1 = 200, // similarly, kPlayerShip1 in the PlayerTeam.h file
};
We're going to set it up so that all these things are tracked in the BaseVictory and BaseVictoryState files (.cpp and .h). The way we'll do that is to set up a variable that records how well/poorly the player is doing, and when different game events occur we'll update that variable. When it comes time to end the mission, we'll check the current value of that variable to decide what kind of a result to award.
enum ePlayerWinStatus
// define the possible player win/loss states,
// to be tracked during the mission
{
kPlayerAstVictory,
kPlayerVictory,
kPlayerDraw,
kPlayerDefeat,
kPlayerDevDefeat,
};
class tBaseVictory : public tVictoryCondition
{
public:
tBaseVictory( );
tBaseVictory( tTeamInfo* team );
void mSetupState();
//
// Accessors.
//
// returns current player win/no-win status
ePlayerWinStatus mGetPlayerResults(){return fPlayerResults;}
//
// Modifiers.
//
// sets current player win/no-win status
void mSetPlayerResults(ePlayerWinStatus winstatus) {fPlayerResults = winstatus;}
private:
//
// Victory attributes.
//
// records current player win/loss status
ePlayerWinStatus fPlayerResults;
};
eVictoryLevels tBaseVictoryState::mCalcVictoryStatus( eTeamID ID )
{
// pretty basic, the variable in this version tells us exactly what kind of victory to return
switch(fChildActor->mGetPlayerResults())
{
case kPlayerAstVictory:
return kAstoundingVictory;
case kPlayerVictory:
return kVictory;
case kPlayerDefeat:
return kDefeat;
case kPlayerDevDefeat:
return kDevastatingDefeat;
default:
return kDraw;
}
}
void tBaseVictoryState::mOnDeath( tShipInfo* ship )
{
if ( !ship )
return;
// determine which team's ship died
eTeamID team = ship->mGetTeam();
// in this case, there are only two teams and one ship per team
// if the player ship died, set the mission result to a devastating defeat
if(team == static_cast< eTeamID >( kPlayerTeam ) )
{
//Set this before calling mSetGameStatus
fChildActor->mSetPlayerResults(kPlayerDevDefeat);
gScriptInterface.mSetGameStatus( static_cast< eTeamID >( kPlayerTeam ), kPlayerLoses );
}
// otherwise the enemy ship died, set the mission result to victory
else
{
fChildActor->mSetPlayerResults(kPlayerVictory);
gScriptInterface.mSetGameStatus( static_cast< eTeamID >( kPlayerTeam ), kPlayerWins );
}
}
void tBaseVictoryState::mOnDisengaged( tShipInfo* ship )
{
if ( !ship )
return;
// determine which team's ship disengaged
eTeamID team = ship->mGetTeam();
// in this case, there are only two teams and one ship per team
// if the player ship disengaged, set the mission result to a defeat
if(team == static_cast< eTeamID >( kPlayerTeam ) )
{
//Set this before calling mSetGameStatus
fChildActor->mSetPlayerResults(kPlayerDefeat);
gScriptInterface.mSetGameStatus( static_cast< eTeamID >( kPlayerTeam ), kPlayerLoses );
}
// otherwise the enemy ship disengaged, set the mission result to a draw
else
{
fChildActor->mSetPlayerResults(kPlayerDraw);
gScriptInterface.mSetGameStatus( static_cast< eTeamID >( kPlayerTeam ), kPlayerWins );
}
}
void tBaseVictoryState::mOnCapture( tShipInfo* ship )
{
if ( !ship )
return;
// determine which team's ship got captured, note we look for the original team owners of the ship,
// because it has just changed hands
eTeamID team = ship->mGetOriginalTeam();
// in this case, there are only two teams and one ship per team
// if the player ship got captured, set the mission result to a devastating defeat
if(team == static_cast< eTeamID >( kPlayerTeam ) )
{
//Set this before calling mSetGameStatus
fChildActor->mSetPlayerResults(kPlayerDevDefeat);
gScriptInterface.mSetGameStatus( static_cast< eTeamID >( kPlayerTeam ), kPlayerLoses );
}
// otherwise the enemy ship got captured, set the mission result to astounding victory
else
{
fChildActor->mSetPlayerResults(kPlayerAstVictory);
gScriptInterface.mSetGameStatus( static_cast< eTeamID >( kPlayerTeam ), kPlayerWins );
}
}
// create a timer that will be used in tracking total mission time
enum eTimeIDs
{
kMissionTimeID,
};
// create classifiers to define the player's mission speed as fast, medium, or slow
enum eMissionSpeed
{
kFast, // player completes mission in less than 12 turns
kMedium, // player completes mission in less than 22 turns
kSlow, // player takes at least 22 turns to complete mission
};
In the public section of the class, add the following:
//
// Routines to get and set the overall mission time
//
eMissionSpeed mGetMissionSpeed() { return fMissionSpeed; }
void mSetMissionSpeed(eMissionSpeed speed) { fMissionSpeed = speed; }
In the private section of the class, add the following:
// // Variable to track overall mission time // eMissionSpeed fMissionSpeed;
To add a new kind of message to the mission, we need to create an identifier for it in MissionText.h, and supply the text of the message in MissionText.cpp. There is a catch, however. The order in which kinds of messages are listed in MissionText.h must EXACTLY match the order in which the associated text is supplied in MissionText.cpp. Similarly, the number of messages described in both files must be identical.
In MissionText.h, look for the section on Event dialog, and add two new message identifiers:
// // Event dialog // kPlayerNotFast_msg, // this message gets sent after about 12 turns kPlayerSlow_msg, // this message gets sent 10 turns laterNow in MissionText.cpp we'll set up the matching text:
// // Event dialog // "Hey [PLAYER_NAME], a fast captain would be done by now!", "Geez Captain, can't we pick up the pace a bit? \n The pirate is going to die of boredom at this rate",The [PLAYER_NAME] will actually insert the player's game name into the message (handy when you feel like taunting someone).
// first we'll set the player's current completion rating to fast, // then set a timer to go off after 10 turns, // a fast player will finish the mission before this goes off, // if they don't manage that then their rating will be downgraded // to medium and we'll set things up so that 10 turns later their // rating gets downgraded to slow mSetMissionSpeed(kFast); mSetTimedInterval(kTimerTurnsSet, kMissionTimeID, 11.8f);
Within this routine, we must identify which kind of timer went off (actually we only have one kind right now) and include code to respond to it.
Our code will send the kPlayerNotFast_msg to the player, and set another timer. If that timer goes off later we'll send yet another taunt to the player (but won't bother setting things up for a third go-round).
Set up the mOnTimedInterval routine to look as follows:
void tPlayerShipBaseState::mOnTimedInterval( int32 timerID )
{
// when the timer goes off we'll figure out if we've moved closer to
// or farther from the pirate during the last turn, and display an
// appropriate message
switch(timerID)
{
case kMissionTimeID:
// downgrade the classification of mission speed
if (mGetMissionSpeed() == kFast)
{
mSetMissionSpeed(kMedium);
mSetTimedInterval(kTimerTurnsSet, kMissionTimeID, 10.0f);
fMissionInfo->mDisplayMessage(static_cast<eTeamID>(kPlayerTeam),kPlayerNotFast_msg);
} else
{
fMissionInfo->mDisplayMessage(static_cast<eTeamID>(kPlayerTeam),kPlayerSlow_msg);
mSetMissionSpeed(kSlow);
}
break;
}
}
Observe that the mDisplayMessage routine is used to send a specific message to the player team.
You can use this routine to send any messages you like, as long as they are properly set up with identifiers
(in MissionText.h) and matching text (in MissionText.cpp).
Just for the heck of it, let's set up another message to taunt the player if they leave the map, and see if we can determine when it occurs.
We'll add an identifier in MissionText.h
// // Event dialog // kRunaway_msg, kPlayerNotFast_msg, kPlayerSlow_msg,And we'll add the text in MissionText.cpp
// // Event dialog // "When danger reared its ugly head, the Brave Sir Robin turned and fled... \n Way to be a coward [PLAYER_NAME]", "Hey [PLAYER_NAME], a fast captain would be done by now!", "Geez Captain, can't we pick up the pace a bit? \n The pirate is going to die of boredom at this rate",Now we want to know if the player ship leaves the map. When this happens, the player ship's state is affected, i.e. another event is generated. So let's open up PlayerShipBaseState.cpp again, and look for routine mOnDisengaged. Within that routine we'll add the call to display our new taunt:
void tPlayerShipBaseState::mOnDisengaged( tShipInfo* ship )
{
// display a taunt if the player disengages
fMissionInfo->mDisplayMessage(static_cast<eTeamID>(kPlayerTeam),kRunaway_msg);
}
// // Event dialog // kRunaway_msg, kWarmer_msg, kColder_msg, kPlayerNotFast_msg, kPlayerSlow_msg,Then open up MissionText.cpp and add the message text:
// // Event dialog // "When danger reared its ugly head, the Brave Sir Robin turned and fled... \n Way to be a coward [PLAYER_NAME]", "I believe we have moved somewhat closer to the other ship", "I believe we have moved somewhat away from the other ship", "Hey [PLAYER_NAME], a fast captain would be done by now!", "Geez Captain, can't we pick up the pace a bit? \n The pirate is going to die of boredom at this rate",
//
// Routines to get and set the recorded distance from the pirate ship
//
float mGetLastDistance() { return fLastDistance; }
void mSetLastDistance(float distance) { fLastDistance = distance; }
And add the following to the private section of the same class:
// // Variable to track last recorded distance from the pirate ship // float fLastDistance;
// create a timer that will be used to track total mission time
// and another that will track time for periodic status updates
enum eTimeIDs
{
kMissionTimeID,
kDistanceUpdateID,
};
To calculate the distance between the two ships, we use the mGetTacticalDistance routine, but to do so we need a pointer to each ship.
To get this, we use a mission info routine to get pointers to the two teams, then through the teams we get pointers to the first (in this case only) ship on each team.
In PlayerShipBaseState.cpp our resulting mBeginState routine now looks like:
void tPlayerShipBaseState::mBeginState()
{
// here we'll create two times, one used to track overall mission time,
// the other used to create periodic updates on the distance between the
// pirate ship and the player ship
// first we'll set the player's current completion rating to fast,
// then set a timer to go off after 10 turns,
// a fast player will finish the mission before this goes off,
// if they don't manage that then their rating will be downgraded
// to medium and we'll set things up so that 10 turns later their
// rating gets downgraded to slow
mSetMissionSpeed(kFast);
mSetTimedInterval(kTimerTurnsSet, kMissionTimeID, 11.8f);
// then we'll set the timer for the periodic distance updates,
// this one will involve a bunch of steps
// first we'll establish pointers to the two teams
tTeamInfo *pirateTeam, *playerTeam;
pirateTeam = fMissionInfo->mGetTeamHandle(static_cast<eTeamID>(kEnemyTeam));
playerTeam = fMissionInfo->mGetTeamHandle(static_cast<eTeamID>(kPlayerTeam));
// then we'll establish pointers to the two ships
tShipInfo *pirateShip, *playerShip;
tShipIterator ship = pirateTeam->mGetFirstShip();
pirateShip = *ship;
ship = playerTeam->mGetFirstShip();
playerShip = *ship;
// then find out how far apart the two ships are
float separation = playerShip->mGetTacticalDistance(pirateShip);
// then we'll record that distance in the class variable we set up
// in the .h file (using our access routines)
mSetLastDistance(separation);
// then we'll turn on the timer we declared in the .h file
// setting it to go off after one turn
// (every time it goes off we'll let the player know if they're
// getting closer to the pirate or farther away)
mSetTimedInterval(kTimerTurnsSet, kDistanceUpdateID, 1.0f);
}
void tPlayerShipBaseState::mOnTimedInterval( int32 timerID )
{
// when the timer goes off we'll figure out if we've moved closer to
// or farther from the pirate during the last turn, and display an
// appropriate message
switch(timerID)
{
case kMissionTimeID:
// downgrade the classification of mission speed
if (mGetMissionSpeed() == kFast)
{
mSetMissionSpeed(kMedium);
mSetTimedInterval(kTimerTurnsSet, kMissionTimeID, 10.0f);
fMissionInfo->mDisplayMessage(static_cast<eTeamID>(kPlayerTeam),kPlayerNotFast_msg);
} else
{
fMissionInfo->mDisplayMessage(static_cast<eTeamID>(kPlayerTeam),kPlayerSlow_msg);
mSetMissionSpeed(kSlow);
}
break;
case kDistanceUpdateID:
// first we'll establish pointers to the two teams
tTeamInfo *pirateTeam, *playerTeam;
pirateTeam = fMissionInfo->mGetTeamHandle(static_cast<eTeamID>(kEnemyTeam));
playerTeam = fMissionInfo->mGetTeamHandle(static_cast<eTeamID>(kPlayerTeam));
// then we'll establish pointers to the two ships
tShipInfo *pirateShip, *playerShip;
tShipIterator ship = pirateTeam->mGetFirstShip();
pirateShip = *ship;
ship = playerTeam->mGetFirstShip();
playerShip = *ship;
// remember how far apart the two ships were last turn
float oldseparation = mGetLastDistance();
// then find out how far apart the two ships are now
float newseparation = playerShip->mGetTacticalDistance(pirateShip);
// then we'll record that distance in the class variable we set up
// in the .h file (using our access routines)
mSetLastDistance(newseparation);
// send the warmer/colder message to the player ship
if (oldseparation > newseparation)
fMissionInfo->mDisplayMessage(static_cast<eTeamID>(kPlayerTeam),kWarmer_msg);
else
fMissionInfo->mDisplayMessage(static_cast<eTeamID>(kPlayerTeam),kColder_msg);
// if we're not within normal scanner range
// reset the timer to repeat the process next turn
if (newseparation > 100.0)
mSetTimedInterval(kTimerTurnsSet, kDistanceUpdateID, 1.0f);
break;
}
}
First, go to MissionMaps.cpp and add 4 other random starting positions, i.e. add i, I, j, J, k, K, l, L somewhere on the map.
Now open EnemyTeam.cpp to randomly select one of the five positions when the mission starts.
At the beginning of the mCreateShipsForTeam function we will use the mRandomInt32 call with two range parameters, then based on whatever value pops up we select a different starting position.
int32 position = fMissionInfo->mRandomInt32(1, 5);
switch (position)
{
case 1: position = kStartPosition_H;
break;
case 2: position = kStartPosition_I;
break;
case 3: position = kStartPosition_J;
break;
case 4: position = kStartPosition_K;
break;
default: position = kStartPosition_L;
break;
}
Then in the mCreateShip call we replace the kStartPosition_H we were using earlier
with the variable that holds our randomly selected value, i.e. position.
VOILA!
For our simple skirmish, here are the final versions of each of the files we created, the only changes from the code you should have created by following the steps above are in the comments, I've tried to add/clarify explanations in a number of places:
One of the things that helps in having a deeper understanding of the interaction of the mission components is knowing how the various classes are based on one another.
For the basic skirmish mission we've just discussed, here is the layout of the key classes:
tState
/ \
/ tVictoryState
/ \
tChildActorState tChildActorVictoryState
/ | \ \
/ | \ tBaseVictoryState
/ | \
/ tEnemyTeamBaseState \
/ \
tPlayerTeamBaseState tPlayerShipBaseState
tAllocatedClass
|
tActor----+--------------+
/ | \ \
/ | \ \
/ | \ tVictoryCondition
tTeamInfo tScript \ |
/ | | \ |
tEnemyTeam | tYourScriptName \ tBaseVictory
| \
tPlayerTeam tShipInfo
/ |
/ |
tPlayerShip tEnemyShip
For our simple campaign mission, here are the final versions of each of the files we created, the only changes from the code you should have created by following the steps above are in the comments, I've tried to add/clarify explanations in a number of places:
You and an enemy (both pulled from the dynaverse) are each out to capture a pirate who is somewhere between you. It takes a couple of seconds before your navigator nails down the pirate location, then the race is on! A few seconds later, the pirate (realizing how bad the situation is) will try to run to the border to disengage. About 5 turns into the mission an ally of the pirate will appear on the map (near the edge of the border the pirate was running towards) and both pirates will turn to fight. Whoever captures the pirate wins, but anyone who gets destroyed loses.
(Disclaimer about D2 hex value updates still applies)
For our second campaign mission, here are the final versions of each of the files created, the only changes from the code you should have created by following the steps above are in the comments, I've tried to add/clarify explanations in a number of places:
For example, if the script is to be added to the Lyran vs the ISC campaign, then edit ISC-Lyran.mct, either adding your new mission to the list that file contains, or replacing an existing mission with it. (For instance, the monster missions and plethora of pirates both bug me, so I tend to replace them with other missions in the .mct file.)
Ok, here's the way I understand things working in the Dynaverse. It's highly likely I've got some of that wrong, so please take it all with a grain of salt. Anything here that's actually correct should be credited to clintk, who has done a great job of trying to research this stuff.
The best I can do so far is make up our own information for a variety of pseudo-D2 information: go to your main script .cpp file, e.g. Met_YourScriptName.cpp, and look for the method int32 tMet_YourScriptName::mInitializeStart(tDynaverseInfo& Info). Within this routine, the Info parameter has already been stocked with most of the relevant D2 information.
(As an aside: this routine returns an integer value indicating which map you want used. It appears that if you return 1 then map 0 is used, if you return 2 then map 1 is used, etc.)
Below I provide a list of all the accessible fields and their data types, in the later sections we'll discuss how to use that information.
int32u Info.fMissionDate.fDay; // turn within the current year
int32u Info.fMissionDate.fYear; // current year
std::string Info.fMissionName;
eSectorName Info.fMissionLocation; // things like Romulan Core hex
int32 Info.fCampaignYear; // years since start of campaign
eTeamID Info.fPlayerTeam;
eRaceName Info.fPlayerRace;
eRaceName Info.fAlliedRaces[kNumberOfRaces + 1]; // list of who's your allies (races)
eRaceName Info.fEnemyRaces[kNumberOfRaces + 1]; // list of who's your enemies
eRaceName Info.fPreviousAlliedRace; // ally in last mission
eRaceName Info.fPreviousEnemyRace; // enemy in last mission
eVictoryLevels Info.fPreviousVictoryLevel; // how you did in last mission
int32 Info.fPlayerFleetSize;
tShipInGame Info.fPlayerFleet[3]; // tShipInGame can record nearly everything known about the ship,
// - position, type, name, BPV, human-controlled, meta-id,
// and also has some interesting fields I haven't played with,
// particurly fDisplayOnMap (a Boolean variable...)
iMapPosition Info.fPlayerFleetPosOffset;
int32 Info.fPreferedMap; // the value D2 sets describing the current hex terrain
tTeamScriptDescription Info.fTeams[kMaxTeams];
iTruth Info.fHosting;
eGameStatus Info.fCampaignStatus;
Here's the scoop:
PART I In the mission script, the scripter gets to set restrictions on where/when a mission should be offered, including:
These were the options covered in 6.1.1 Setting up the mission options, in mScriptSpecs
As a mission scripter I thought that was all worthless or buggy, because the missions would appear in all sorts of invalid places anyway. This brings us to:
PART II
The server only treats those rules as advice, not as rules!
The settings in MissionMatching.gf control how much weight the server attaches to each different "area" of script recommendations.
Of course, the .gf settings don't break down into exactly the same categories as the scripter provides, but they're pretty close.
Furthermore, the default settings in that file are utter rubbish - leading to the rules laid down in the mission script being largely bypassed. It looks like they were set up to give a maximum randomness of missions encountered, without real concern as to the implications.
After a helluva lot of fiddling, here are the relevant MissionMatching.gf settings I changed to get missions to appear exactly where their scripts say they should. The original settings appear in comments.
//weight to missions matching based on terrain [TerrainScoring] PlanetTypeScoreForMatching=12000 // 5000 BaseTypeScoreForMatching=8000 // 2500 TerrainTypeScoreForMatching=4000 // 100 //weight to missions matching based on political tensions [PoliticsScoring] // NOTE THE NEGATIVE SIGNS IN THE -10000's // It's important! ;) BonusForExactPoliticalMatch=2000 // 1000 LookingForOwnHexInOwn=2000 // 1000 LookingForOwnHexInAlly=-10000 // 500 LookingForOwnHexInNeutral=-10000 // 0 LookingForOwnHexInEnemy=-10000 LookingForEnemyHexInOwn=-10000 LookingForEnemyHexInAlly=-10000 LookingForEnemyHexInNeutral=-10000 // 0 LookingForEnemyHexInEnemy=1000 LookingForAllyHexInOwn=-10000 // 0 LookingForAllyHexInAlly=1000 LookingForAllyHexInNeutral=-10000 // 0 LookingForAllyHexInEnemy=-10000 //weight to missions matching based on ships available [FleetScoring] GoodBPVScore=1000 TooWeakBPVScore=0 // 300 TooStrongBPV=0 GoodShipCountScore=1000 TooFewShipCountScore=0 // 300 TooManyShipCountScore=0 PlaceBaseMissionScore=50000 BaseScoreBonus=10000 // weights of categories [ScoringWeights] WeightMissionsLastPlayed=0.2 WeightPoliticsScore=1.0 WeightTerrainScore=1.0 WeightFleetScore=1.0 // 0.5 [RelationshipScoring] MaxRelationShipScore=1000 PoorEnemyOfScore=0 // 100 PoorAllyOfScore=0 // 200 DecentWorstEnemyScore=0 // 300 DecentAllyOfScore=0 // 200What it all means
At the very least this means we can now prepare custom missions and get them to appear where they are needed and expected.
It does, however, mean that if you apply this to a stock server then somewhat fewer missions are offered (since it doesn't give all the "wrong" mission opportunities it used to).
This may also turn out to have an important side effect, in that I suspect some of our weird-hex-effects come in as a result of poor misson offering (e.g. a script intended to run in a player's own territory instead gets run in their allies territory, and updates the DV incorrectly as a result). That's still pretty speculative however.
It appears that the relative weighting of planets, bases, terrain, and politics is critical. If bases and planets, or terrain and bases, are too closely weighted then you start getting base or planet missions cropping up in empty space.