PDA

View Full Version : Script for vector to home base?



No601_Swallow
Jul-21-2014, 15:46
I've searched, but I can't find an example, so I was wondering if anyone has tried to generate a vector to home base via a script. I'm wondering if the tab+4 (for example) menu item might be used for this. There are some great RDF scripts around, so would a vector to certain bases (or beacons perhaps) also be possible?

(This would be a great boon for certain squadron mates who, when confronted with a compass rose, instinctively look for a corner to start from...)

Salmo
Jul-23-2014, 05:03
First off, you could use the in-game Tab-7-4 vector to home base menu system. However, here's another solution using the tab-4 custom menu.

A DETAILED TUTORIAL FOR INTERMEDIATE-LEVEL CODERS IS IN THE POST BELOW ....



using System;
using maddox.game;
using maddox.game.world;
using part;
using System.Collections.Generic;
using maddox.GP;

public class Mission : AMission
{
Random random = new Random();
Dictionary<AiAircraft, AiAirport> HomeBases = new Dictionary<AiAircraft, AiAirport>();

public override void Init(ABattle battle, int missionNumber)
{
base.Init(battle, missionNumber);
this.MissionNumberListener = -1; // listen for all game events
}

public override void OnAircraftTookOff(int missionNumber, string shortName, AiAircraft aircraft)
{
base.OnAircraftTookOff(missionNumber, shortName, aircraft);
if (HomeBases.ContainsKey(aircraft))
{ // we've already recorded a home base for this aircraft
HomeBases.Remove(aircraft); // delete previous home base for this aircraft
}
HomeBases.Add(aircraft, GetNearestAirfield(aircraft.Pos())); // remember the base where this aircraft tookoff from
}

public AiAirport GetNearestAirfield(Point3d location)
{
AiAirport NearestAirfield = null;
AiAirport[] airports = GamePlay.gpAirports();
Point3d StartPos = location;

if (airports != null)
{
foreach (AiAirport airport in airports)
{
if (NearestAirfield != null)
{
if (NearestAirfield.Pos().distance(ref StartPos) > airport.Pos().distance(ref StartPos))
NearestAirfield = airport;
}
else NearestAirfield = airport;
}
}
return NearestAirfield;
}

private double GetHeadingDegrees(Point3d origin, Point3d destination)
{
// Calculate angle from two coordinates
// reference: http://stackoverflow.com/questions/8337239/how-do-i-calculate-angle-from-two-coordinates
float x1 = (float)origin.x; // use float variable type for calculations
float y1 = (float)origin.y; // use float variable type for calculations
float x2 = (float)destination.x; // use float variable type for calculations
float y2 = (float)destination.y; // use float variable type for calculations
float dy = x2 - x1; // change in X coordinate between 2 points
float dx = y2 - y1; // change in Y coordinate between 2 points

double radians = Math.Atan2(dy, dx); // vector from origin to destination in radians
double degree = radians * (180 / Math.PI); // vector from origin to destination in degrees
if (degree < 0) degree += 360; // solve for compass wrap around
double heading = RoundToNearest(degree, 30); // game heading only in 30 degree slices
if (heading == 360) heading = 0; // correct for game speak
return heading; // return the heading in degrees (closest 30 degree slice)
}

private double GetDistanceBetweenTwoPoints(Point3d origin, Point3d destination, string measurementType = "metric")
{
Point2d origin2d = new Point2d(origin.x, origin.y);
Point2d destination2d = new Point2d(destination.x, destination.y);
double distance = origin2d.distance(ref destination2d) / 1000; // distance between 2 points in kilometres
if (measurementType != "metric") distance = distance / 1.609344; // convert kilometres into miles
distance = RoundToNearest(distance, 1); // round to nearest kilometre or miles
return distance;
}

public override void OnPlaceEnter(Player player, AiActor actor, int placeIndex)
{
base.OnPlaceEnter(player, actor, placeIndex);
setMainMenu(player);
}

public override void OnOrderMissionMenuSelected(Player player, int ID, int menuItemIndex)
{
base.OnOrderMissionMenuSelected(player, ID, menuItemIndex);
if (player != null)
{
switch (ID)
#region TAB-4 main menu
{
case 0: // MAIN MENU
switch (menuItemIndex)
{
case 1:
setSubMenu1(player); // ground control
break;
case 0:
setMainMenu(player);
break;
}
break;
#endregion

#region MAIN MENU OPTION 1
case 2: // MAIN MENU OPTION 1 - Ground control
AiAircraft playeraircraft = null;
AiAirport airbase = null;
playeraircraft = player.Place() as AiAircraft; // get player's aircraft object
switch (menuItemIndex)
{
case 1: // degrees to home base
if (HomeBases.ContainsKey(playeraircraft))
{
airbase = HomeBases[playeraircraft]; // get HB from dictionary using the key
}
DegreesToAirbaseSpeak(player, airbase);
setMainMenu(player);
break;
case 2: // degrees to nearest airbase
airbase = GetNearestAirfield(playeraircraft.Pos()); // get nearest friendly base
DegreesToAirbaseSpeak(player, airbase);
setMainMenu(player);
break;
case 0:
setMainMenu(player);
break;
}
break;

#endregion
}
}
}

private void setMainMenu(Player player)
{
GamePlay.gpSetOrderMissionMenu(player, false, 0, new string[] { "Ground control" }, new bool[] { true });
}

private void setSubMenu1(Player player)
{
GamePlay.gpSetOrderMissionMenu(player, true, 2, new string[]
{
"Vector to Home Base",
"Vector to nearest airfield"
}, new bool[] { true, true});
}

private void DegreesToAirbaseSpeak(Player player, AiAirport airbase)
{
if (player != null && HomeBases.Count > 0 && airbase != null)
{
AiAircraft playeraircraft = player.Place() as AiAircraft;
if (playeraircraft != null && playeraircraft.IsAirborne() && HomeBases.ContainsKey(playeraircraft))
{
// construct in-game voice message
// first phrase ingame voice
string Phrase1 = null;
if (random.NextDouble() < 0.5) Phrase1 = "This_is_Home_base";
sayMessageToPlayer(player, Phrase1);

// second phrase ingame voice
string Phrase2 = null;
Phrase2 = "Vector";
//if (random.NextDouble() < 0.5) Phrase2 = "Vector_to_base_is";
sayMessageToPlayer(player, Phrase2);

// third phrase ingame voice
string Phrase3 = null;
double heading = GetHeadingDegrees(playeraircraft.Pos(), airbase.Pos()); // calculate direction to HB in degrees
Phrase3 = heading.ToString().Trim(); // ingame heading voice (30 degree slices)
sayMessageToPlayer(player, Phrase3);

// fourth phrase distance to home base
string Phrase4 = null;
string measurementtype = "metric";
if (player.Army() == 1) measurementtype = "imperial";
double distance = GetDistanceBetweenTwoPoints(playeraircraft.Pos(), airbase.Pos(), measurementtype);

if (player.Army() == 1)
{ // red army
Phrase4 = string.Concat(distance.ToString(), "_miles");
if (distance > 15) Phrase4 = null;
}
else
{ // blue army
Phrase4 = string.Concat(distance.ToString(), "_kilometers");
if (distance > 30) Phrase4 = null;
}
sayMessageToPlayer(player, Phrase4);

// fifth phrase ingame voice
string Phrase5 = null;
if (random.NextDouble() < 0.5) Phrase5 = "Good_luck"; // ingame voice signoff
sayMessageToPlayer(player, Phrase5);
}
else
{ // unable message
string UnableMsg = "Are_you_daft_man";
sayMessageToPlayer(player, UnableMsg);
}
}
else
{ // unable message
string UnableMsg = "Are_you_daft_man";
sayMessageToPlayer(player, UnableMsg);
}
}

public Player[] ToPlayer(Player player)
#region returns an array containing the named player ONLY
{
try
{
List<Player> players = new List<Player>();
players.Add(player);
return players.ToArray();
}
catch (Exception e)
{
Console.WriteLine("Error: [ToPlayer] " + e.Message);
return null;
}
}
#endregion

private double ConvertVectorToDegrees(Vector2d vector)
{
Vector2d matVector = new Vector2d(vector.y, vector.x);
// direction value is in radians so need to multiply by 180/Pi to get value in degrees
return matVector.direction() * 180.0 / Math.PI;
}

public static double RoundToNearest(double Amount, double RoundTo)
#region Rounding off to nearest 10, 100, 1000 and other multiples of 10
// reference: http://codingsense.wordpress.com/2010/02/04/rounding-off-to-nearest-in-c/
{
double ExcessAmount = Amount % RoundTo;
if (ExcessAmount < (RoundTo / 2))
{
Amount -= ExcessAmount;
}
else
{
Amount += (RoundTo - ExcessAmount);
}
return Amount;

}
#endregion

private void sayMessageToPlayer(Player player, string msg)
#region send a speech message to specified player only
{ // send a speech message to specified player only
try
{
foreach (AiAirGroup g in GamePlay.gpAirGroups(player.Army()))
{
bool SaidToGroup = false;
foreach (AiActor a in g.GetItems())
{
AiAircraft aircraft = a as AiAircraft;
if (aircraft.Player(0) == player)
{
if (SaidToGroup == true) break; // speech already said to group
SaidToGroup = true;
aircraft.SayToGroup(aircraft.AirGroup(), msg);
}
}
}
}
catch { }
}
#endregion
}

Salmo
Jul-23-2014, 05:54
I've searched, but I can't find an example, so I was wondering if anyone has tried to generate a vector to home base via a script. I'm wondering if the tab+4 (for example) menu item might be used for this. There are some great RDF scripts around, so would a vector to certain bases (or beacons perhaps) also be possible? (This would be a great boon for certain squadron mates who, when confronted with a compass rose, instinctively look for a corner to start from...)

There are many seperate elements that need to be brought together to achieve what you ask. This an excellent opportunity to present a solution as MODERATELY_DIFFICULT SCRIPT TUTORIAL. So here goes ....

Let's think for a moment before we start any coding. What sort of information do we need to know to get a vector to home base, and how are we going to get what we're after?

3D/2D Spacial Coordinates
We'll need to have the postion the player is at when he calls for a vector (ie the player's aircraft position in 3D or 2D space). Next, we'll need to know the player's home base postion, but how are we to know the player's home base? We'll have to record this somewhere when the player takes off in his aircraft. But wait, there's more ..... what if the player doesn't make it back to base & spawns in a new aircraft? We'll have to 'remember' their new home base, and account for each base-change so we've always got the player's current home base as the players plays the game. Also, what if the player wants vector to another base, rather than home base. Say vector to nearest airfield. Would it be useful to also include a distance to the base in the information provided to the player? If we want distance, we'll have to account for different measurement system for each army. ie imperial for red, metric for blue.

Thus we need ...
1. Player's current postion.
2. Players current home base postion.
3. Nearest airfield position.
4. Player army.
5. What base they was vector to.
6. How far to the base they want vectoring to.

Maths is fun ....
Now let's think about what sort of mathematics that's involved in getting a heading from the player's postion to their home base? We need to calculate an angle in degrees from the players position (XY coordinate) to the players home base postion (XY coordinate) and then relate this to a compass heading. We need to do a little algebra to get the result we want. Here's the problem in a diagram (figure 1). Let's image that point P is the player's aircraft position & point Q is the postion of the player's home base. We need to calcuate the angle between points NQ&P.
http://labspace.open.ac.uk/file.php/5396/M208_1_I007i.jpg
Figure 1: Diagram of line between 2 points in 2D space

The formula to calculate an angle between 2 points this is ...
http://www.mathisfunforum.com/latex/8/4/e860ce27de3d419529c76193b2ad911.gif

So we need to to find the difference between the X coordinates & Y coordinates of the two points, then take the arctangent of the difference in the Y values divided by the X values. Fortunately, C# provides use with some built-in code that makes this operation easy (we'll get that later). We've got a small snag however, in that our algebraic calculation will give us the angle NPQ, but this is an abgle from the horizontal line PN. So we'll need to make some sort of conversion of our angle result to get it into compass 360 degree format.

How do we want to present the result to the player?
We could present the vector to home base or nearest airfield as a written message on the screen or perhaps even as a voice message similar to the in-game radar voice-overs. For this tutorial I'll choose to make a voice announcement to players since this adds an extra element to the script coding (... and I think it's a bit more immersive). I also decided to add a distance to the base in the voice announcement (just because I could).

BASE CODE FRAMEWORK
So now let's just start with some skeleton code that we can buildup as we go. Here's a generic start to a script, most COD scripts will have this type of format. Note that we start by refering to several of the game files & C# library files (the USING statements), and we have Mission class. The Mission class starts with an open-curly bracket & ends with a closed-curly bracket. Open & close curly brackets is the method C# uses to define where some sort of code section starts & ends. So we'll put our code 'inside' (between the curly brackets) the Mision class as we go.



using System;
using maddox.game;
using maddox.game.world;
using part;
using System.Collections.Generic;
using maddox.GP;

public class Mission : AMission
{

}

Now we need to put some code inside the MISSION section to do what we want. First, we'll want to tell the game to "listen" for all events that happen during the battle. So we'll add a "mission listener". Without this in the script, the script just won't detect the various events we want to use for our solution.


using System;
using maddox.game;
using maddox.game.world;
using part;
using System.Collections.Generic;
using maddox.GP;

public class Mission : AMission
{
public override void Init(ABattle battle, int missionNumber)
{
base.Init(battle, missionNumber);
this.MissionNumberListener = -1; // listen for all game events
}
}

Let's now introduce a way to 'remember' the player's home base. There are several ways we could do this, but probably the most useful is to use a C# dictionary (http://www.dotnetperls.com/dictionary). A dictionary is just a list of stuff. Each dictionary record contains two entries a "key" & a "value". The key MUST always be unique, so you can't have a dictionary entry with two keys that are the same. The 'value' associated with the 'key' can be any value. Values can be the same for different dictionary keys. Keys & values in a dictionary can be any data type, they could be strings, or integers or decimals, or even specific COD game object types. We'll use COD game object types in our dictionary. Dictionaries provide fast lookups, based on keys, to get values. So first, we'll define an empty dictionary called "HomeBases". I also initiated a random object so we can do some randomisation of radar voice-over announcements when we get the voice-over part of the script.


using System;
using maddox.game;
using maddox.game.world;
using part;
using System.Collections.Generic;
using maddox.GP;

public class Mission : AMission
{

Random random = new Random(); // we want to randomise some things in the script
// game aircraft is unique key, airport is value associated with the key
Dictionary<AiAircraft, AiAirport> HomeBases = new Dictionary<AiAircraft, AiAirport>();

public override void Init(ABattle battle, int missionNumber)
{
base.Init(battle, missionNumber);
this.MissionNumberListener = -1; // listen for all game events
}
}

Note the HomeBases dictionary has an COD AiAircraft object as it's 'key' and a COD AiAirport object as a value for their data types. This way we can use the player's aircraft as the dictionary key (because at any given point of time a COD aircraft objects is 'unique') & the players home airport as the dictionary value (because we can have multiple values that are the same for any unique key). Thus, we can have mutiple player aircrafts taking off from the same air base. Next, we'll need to add to the Homebases dictionary every time a player takesoff in an aircraft. We'll use the OnAircraftTookOff game event for this. Remembering we'll also have to account for if a player has aready taken off from another base in the same sortie (& thus there will already be a dictionary entry for that player's aircraft). We'll also use a special custom-built function called GetNearestAirfield to work out what airfield the player tookoff form. So when a player takesoff, we'll add the player's aircraft object (the unique dictionary key) & the base the player tookoff from (the value associated with the dictionary key) to the dictionary.



using System;
using maddox.game;
using maddox.game.world;
using part;
using System.Collections.Generic;
using maddox.GP;

public class Mission : AMission
{
Random random = new Random(); // we want to randomise some things in the script
// game aircraft is unique key, airport is value associated with the key
Dictionary<AiAircraft, AiAirport> HomeBases = new Dictionary<AiAircraft, AiAirport>();

public override void Init(ABattle battle, int missionNumber)
{
base.Init(battle, missionNumber);
this.MissionNumberListener = -1; // listen for all game events
}

public override void OnAircraftTookOff(int missionNumber, string shortName, AiAircraft aircraft)
{
base.OnAircraftTookOff(missionNumber, shortName, aircraft);
if (HomeBases.ContainsKey(aircraft))
{ // we've already recorded a home base for this aircraft
HomeBases.Remove(aircraft); // delete previous home base for this aircraft
}
HomeBases.Add(aircraft, GetNearestAirfield(aircraft.Pos())); // remember the base where this aircraft tookoff from
}

public AiAirport GetNearestAirfield(Point3d location)
{
AiAirport NearestAirfield = null;
AiAirport[] airports = GamePlay.gpAirports();
Point3d StartPos = location;

if (airports != null)
{
foreach (AiAirport airport in airports)
{
if (NearestAirfield != null)
{
if (NearestAirfield.Pos().distance(ref StartPos) > airport.Pos().distance(ref StartPos))
NearestAirfield = airport;
}
else NearestAirfield = airport;
}
}
return NearestAirfield;
}
}

Now we've recorded the airfield the player last tookoff from. This record will be dynamically updated every time a player takesoff in an aircraft, and we can recall the dictionary entry any time by refering to it's unique 'key' which will be the players own aircraft. Let's move on to putting in some code to use the TAB-4 custom menu. I won't explain this code here as this tutorial is aimed at the moderately-difficult level of coder, but suffice to say we're just setting up a TAB-4 menu system with a four functions. One fuction called setMainMenu which sets/resets the TAB-4 main menu & another function called setSubMenu1 which sets a TAB-4 submenu with two items "Vector to HB" & "Vector to nearest base", the DegreesToAirbaseSpeak function that describes what we need to do when the player presses the "Vector to HB" menu item, or prersses the "Vector to nearest base" menu item.

Note that the DegreesToAirbaseSpeak function takes two arguements. It recieves the player to speak to, and it recieves the airbase to make announcements about. So in the Case 1 section (the vector to HB menu item), we set the airbase to be the player's home base by refering to the "key" in the directory. The key being the player's aircraft object. In the Case 2 section we set the airbase variable to be the nearest airfield to the player's plane's location.


using System;
using maddox.game;
using maddox.game.world;
using part;
using System.Collections.Generic;
using maddox.GP;

public class Mission : AMission
{
Random random = new Random(); // we want to randomise some things in the script
// game aircraft is unique key, airport is value associated with the key
Dictionary<AiAircraft, AiAirport> HomeBases = new Dictionary<AiAircraft, AiAirport>();

public override void Init(ABattle battle, int missionNumber)
{
base.Init(battle, missionNumber);
this.MissionNumberListener = -1; // listen for all game events
}

public override void OnAircraftTookOff(int missionNumber, string shortName, AiAircraft aircraft)
{
base.OnAircraftTookOff(missionNumber, shortName, aircraft);
if (HomeBases.ContainsKey(aircraft))
{ // we've already recorded a home base for this aircraft
HomeBases.Remove(aircraft); // delete previous home base for this aircraft
}
HomeBases.Add(aircraft, GetNearestAirfield(aircraft.Pos())); // remember the base where this aircraft tookoff from
}


public override void OnOrderMissionMenuSelected(Player player, int ID, int menuItemIndex)
{
base.OnOrderMissionMenuSelected(player, ID, menuItemIndex);
if (player != null)
{
switch (ID)
#region TAB-4 main menu
{
case 0: // MAIN MENU
switch (menuItemIndex)
{
case 1:
setSubMenu1(player); // ground control
break;
case 0:
setMainMenu(player);
break;
}
break;
#endregion

#region MAIN MENU OPTION 1
case 2: // MAIN MENU OPTION 1 - Ground control
AiAircraft playeraircraft = null;
AiAirport airbase = null;
playeraircraft = player.Place() as AiAircraft; // get player's aircraft object
switch (menuItemIndex)
{
case 1: // degrees to home base
if (HomeBases.ContainsKey(playeraircraft))
{
airbase = HomeBases[playeraircraft]; // get HB from dictionary using the key
}
DegreesToAirbaseSpeak(player, airbase);
setMainMenu(player);
break;
case 2: // degrees to nearest airbase
airbase = GetNearestAirfield(playeraircraft.Pos()); // get nearest friendly base
DegreesToAirbaseSpeak(player, airbase);
setMainMenu(player);
break;
case 0:
setMainMenu(player);
break;
}
break;

#endregion
}
}
}

private void setMainMenu(Player player)
{
GamePlay.gpSetOrderMissionMenu(player, false, 0, new string[] { "Ground control" }, new bool[] { true });
}

private void setSubMenu1(Player player)
{
GamePlay.gpSetOrderMissionMenu(player, true, 2, new string[]
{
"Vector to Home Base",
"Vector to nearest airfield"
}, new bool[] { true, true});
}

public AiAirport GetNearestAirfield(Point3d location)
{
AiAirport NearestAirfield = null;
AiAirport[] airports = GamePlay.gpAirports();
Point3d StartPos = location;

if (airports != null)
{
foreach (AiAirport airport in airports)
{
if (NearestAirfield != null)
{
if (NearestAirfield.Pos().distance(ref StartPos) > airport.Pos().distance(ref StartPos))
NearestAirfield = airport;
}
else NearestAirfield = airport;
}
}
return NearestAirfield;
}
}

So we've got our TAB-4 menu, now let's add the DegreesToAirbaseSpeak fuction. This is where we do the radar in-game voice stuff. It calls some more custom-built functions; sayMessageToPlayer is a function that does the voice-overs in-game. It does what is says; it will cause a named voice output to play ingame to the named player only. The code also calls the GetHeadingDegrees function. This function is the guts of what we want to do. It's where we will actually do our calcuations. Note also, I've set-up a series of phrases & used the random fuction to vary slighly how phrases might be announced to the player. That way the player will not always hear exactly the same words every time they call for a vector to base.


using System;
using maddox.game;
using maddox.game.world;
using part;
using System.Collections.Generic;
using maddox.GP;

public class Mission : AMission
{
Random random = new Random(); // we want to randomise some things in the script
// game aircraft is unique key, airport is value associated with the key
Dictionary<AiAircraft, AiAirport> HomeBases = new Dictionary<AiAircraft, AiAirport>();

public override void Init(ABattle battle, int missionNumber)
{
base.Init(battle, missionNumber);
this.MissionNumberListener = -1; // listen for all game events
}

public override void OnAircraftTookOff(int missionNumber, string shortName, AiAircraft aircraft)
{
base.OnAircraftTookOff(missionNumber, shortName, aircraft);
if (HomeBases.ContainsKey(aircraft))
{ // we've already recorded a home base for this aircraft
HomeBases.Remove(aircraft); // delete previous home base for this aircraft
}
HomeBases.Add(aircraft, GetNearestAirfield(aircraft.Pos())); // remember the base where this aircraft tookoff from
}

public override void OnOrderMissionMenuSelected(Player player, int ID, int menuItemIndex)
{
base.OnOrderMissionMenuSelected(player, ID, menuItemIndex);
if (player != null)
{
switch (ID)
#region TAB-4 main menu
{
case 0: // MAIN MENU
switch (menuItemIndex)
{
case 1:
setSubMenu1(player); // ground control
break;
case 0:
setMainMenu(player);
break;
}
break;
#endregion

#region MAIN MENU OPTION 1
case 2: // MAIN MENU OPTION 1 - Ground control
AiAircraft playeraircraft = null;
AiAirport airbase = null;
playeraircraft = player.Place() as AiAircraft; // get player's aircraft object
switch (menuItemIndex)
{
case 1: // degrees to home base
if (HomeBases.ContainsKey(playeraircraft))
{
airbase = HomeBases[playeraircraft]; // get HB from dictionary using the key
}
DegreesToAirbaseSpeak(player, airbase);
setMainMenu(player);
break;
case 2: // degrees to nearest airbase
airbase = GetNearestAirfield(playeraircraft.Pos()); // get nearest friendly base
DegreesToAirbaseSpeak(player, airbase);
setMainMenu(player);
break;
case 0:
setMainMenu(player);
break;
}
break;

#endregion
}
}
}

private void setMainMenu(Player player)
{
GamePlay.gpSetOrderMissionMenu(player, false, 0, new string[] { "Ground control" }, new bool[] { true });
}

private void setSubMenu1(Player player)
{
GamePlay.gpSetOrderMissionMenu(player, true, 2, new string[]
{
"Vector to Home Base",
"Vector to nearest airfield"
}, new bool[] { true, true});
}

private void DegreesToAirbaseSpeak(Player player, AiAirport airbase)
{
if (player != null && HomeBases.Count > 0 && airbase != null)
{
AiAircraft playeraircraft = player.Place() as AiAircraft;
if (playeraircraft != null && playeraircraft.IsAirborne() && HomeBases.ContainsKey(playeraircraft))
{
// construct in-game voice message
// first phrase ingame voice
string Phrase1 = null;
if (random.NextDouble() < 0.5) Phrase1 = "This_is_Home_base";
sayMessageToPlayer(player, Phrase1);

// second phrase ingame voice
string Phrase2 = null;
Phrase2 = "Vector";
//if (random.NextDouble() < 0.5) Phrase2 = "Vector_to_base_is";
sayMessageToPlayer(player, Phrase2);

// third phrase ingame voice
string Phrase3 = null;
double heading = GetHeadingDegrees(playeraircraft.Pos(), airbase.Pos()); // calculate direction to HB in degrees
Phrase3 = heading.ToString().Trim(); // ingame heading voice (30 degree slices)
sayMessageToPlayer(player, Phrase3);

// fourth phrase distance to home base
string Phrase4 = null;
string measurementtype = "metric";
if (player.Army() == 1) measurementtype = "imperial";
double distance = GetDistanceBetweenTwoPoints(playeraircraft.Pos(), airbase.Pos(), measurementtype);

if (player.Army() == 1)
{ // red army
Phrase4 = string.Concat(distance.ToString(), "_miles");
if (distance > 15) Phrase4 = null;
}
else
{ // blue army
Phrase4 = string.Concat(distance.ToString(), "_kilometers");
if (distance > 30) Phrase4 = null;
}
sayMessageToPlayer(player, Phrase4);

// fifth phrase ingame voice
string Phrase5 = null;
if (random.NextDouble() < 0.5) Phrase5 = "Good_luck"; // ingame voice signoff
sayMessageToPlayer(player, Phrase5);
}
else
{ // unable message
string UnableMsg = "Are_you_daft_man";
sayMessageToPlayer(player, UnableMsg);
}
}
else
{ // unable message
string UnableMsg = "Are_you_daft_man";
sayMessageToPlayer(player, UnableMsg);
}
}

private void sayMessageToPlayer(Player player, string msg)
#region send a speech message to specified player only
{ // send a speech message to specified player only
try
{
foreach (AiAirGroup g in GamePlay.gpAirGroups(player.Army()))
{
bool SaidToGroup = false;
foreach (AiActor a in g.GetItems())
{
AiAircraft aircraft = a as AiAircraft;
if (aircraft.Player(0) == player)
{
if (SaidToGroup == true) break; // speech already said to group
SaidToGroup = true;
aircraft.SayToGroup(aircraft.AirGroup(), msg);
}
}
}
}
catch { }
}
#endregion

public AiAirport GetNearestAirfield(Point3d location)
{
AiAirport NearestAirfield = null;
AiAirport[] airports = GamePlay.gpAirports();
Point3d StartPos = location;

if (airports != null)
{
foreach (AiAirport airport in airports)
{
if (NearestAirfield != null)
{
if (NearestAirfield.Pos().distance(ref StartPos) > airport.Pos().distance(ref StartPos))
NearestAirfield = airport;
}
else NearestAirfield = airport;
}
}
return NearestAirfield;
}
}

Now to put in the code that actually does the vector & distance calculations. The GetHeadingDegrees function will calculate the vector to the requested base, and the GetDistanceBetweenTwoPoints function will calculate the distance from the player's aircraft to the requested base.

GetHeadingDegrees
It worth explaining in detail here exactly that we're doing when we call the GetHeadingDegrees function. Firstly, note that we get the two values that we need to get our HB vector. ie The players aircraft, & the players home base. You'll notice the first thing we do is convert the coordinates for the player's aircraft position & the players HB postion into float values for further calculations. Then we calculate the difference in X & Y coordinate values. Then we use C#'s built-in arctangent maths function to get the angle (in radians), & then convert the radians to degrees.

So far, so good. But we're not done yet. We then convert the degrees into a meaning full compass setting (remember the anglewas from the horizaontal X axis & not a true compass bearing). Then finally, we have to do some rounding to the nearest 30 degrees using the custom-built RoundToNearest function call. This is becuase the game only makes radar announcements in 30 degree slices & the angle has to be a multiple of 30 for the in-game voice-over to work properly.

GetDistanceBetweenTwoPoints
This function recies two arguemets; the player's aircraft & the airbase we are interested in. You can see how C# & the game code work to get the distance between the two points. I've also included a check for the player's aircraft ary to the return value will be in metres for blue army and in miles for the red army.


using System;
using maddox.game;
using maddox.game.world;
using part;
using System.Collections.Generic;
using maddox.GP;

public class Mission : AMission
{
Random random = new Random();
Dictionary<AiAircraft, AiAirport> HomeBases = new Dictionary<AiAircraft, AiAirport>();

public override void Init(ABattle battle, int missionNumber)
{
base.Init(battle, missionNumber);
this.MissionNumberListener = -1; // listen for all game events
}

public override void OnAircraftTookOff(int missionNumber, string shortName, AiAircraft aircraft)
{
base.OnAircraftTookOff(missionNumber, shortName, aircraft);
if (HomeBases.ContainsKey(aircraft))
{ // we've already recorded a home base for this aircraft
HomeBases.Remove(aircraft); // delete previous home base for this aircraft
}
HomeBases.Add(aircraft, GetNearestAirfield(aircraft.Pos())); // remember the base where this aircraft tookoff from
}

public AiAirport GetNearestAirfield(Point3d location)
{
AiAirport NearestAirfield = null;
AiAirport[] airports = GamePlay.gpAirports();
Point3d StartPos = location;

if (airports != null)
{
foreach (AiAirport airport in airports)
{
if (NearestAirfield != null)
{
if (NearestAirfield.Pos().distance(ref StartPos) > airport.Pos().distance(ref StartPos))
NearestAirfield = airport;
}
else NearestAirfield = airport;
}
}
return NearestAirfield;
}

private double GetHeadingDegrees(Point3d origin, Point3d destination)
{
// Calculate angle from two coordinates
// reference: http://stackoverflow.com/questions/8337239/how-do-i-calculate-angle-from-two-coordinates
float x1 = (float)origin.x; // use float variable type for calculations
float y1 = (float)origin.y; // use float variable type for calculations
float x2 = (float)destination.x; // use float variable type for calculations
float y2 = (float)destination.y; // use float variable type for calculations
float dy = x2 - x1; // change in X coordinate between 2 points
float dx = y2 - y1; // change in Y coordinate between 2 points

double radians = Math.Atan2(dy, dx); // vector from origin to destination in radians
double degree = radians * (180 / Math.PI); // vector from origin to destination in degrees
if (degree < 0) degree += 360; // solve for compass wrap around
double heading = RoundToNearest(degree, 30); // game heading only in 30 degree slices
if (heading == 360) heading = 0; // correct for game speak
return heading; // return the heading in degrees (closest 30 degree slice)
}

private double GetDistanceBetweenTwoPoints(Point3d origin, Point3d destination, string measurementType = "metric")
{
Point2d origin2d = new Point2d(origin.x, origin.y);
Point2d destination2d = new Point2d(destination.x, destination.y);
double distance = origin2d.distance(ref destination2d) / 1000; // distance between 2 points in kilometres
if (measurementType != "metric") distance = distance / 1.609344; // convert kilometres into miles
distance = RoundToNearest(distance, 1); // round to nearest kilometre or miles
return distance;
}

public override void OnPlaceEnter(Player player, AiActor actor, int placeIndex)
{
base.OnPlaceEnter(player, actor, placeIndex);
setMainMenu(player);
}

public override void OnOrderMissionMenuSelected(Player player, int ID, int menuItemIndex)
{
base.OnOrderMissionMenuSelected(player, ID, menuItemIndex);
if (player != null)
{
switch (ID)
#region TAB-4 main menu
{
case 0: // MAIN MENU
switch (menuItemIndex)
{
case 1:
setSubMenu1(player); // ground control
break;
case 0:
setMainMenu(player);
break;
}
break;
#endregion

#region MAIN MENU OPTION 1
case 2: // MAIN MENU OPTION 1 - Ground control
AiAircraft playeraircraft = null;
AiAirport airbase = null;
playeraircraft = player.Place() as AiAircraft; // get player's aircraft object
switch (menuItemIndex)
{
case 1: // degrees to home base
if (HomeBases.ContainsKey(playeraircraft))
{
airbase = HomeBases[playeraircraft]; // get HB from dictionary using the key
}
DegreesToAirbaseSpeak(player, airbase);
setMainMenu(player);
break;
case 2: // degrees to nearest airbase
airbase = GetNearestAirfield(playeraircraft.Pos()); // get nearest friendly base
DegreesToAirbaseSpeak(player, airbase);
setMainMenu(player);
break;
case 0:
setMainMenu(player);
break;
}
break;

#endregion
}
}
}

private void setMainMenu(Player player)
{
GamePlay.gpSetOrderMissionMenu(player, false, 0, new string[] { "Ground control" }, new bool[] { true });
}

private void setSubMenu1(Player player)
{
GamePlay.gpSetOrderMissionMenu(player, true, 2, new string[]
{
"Vector to Home Base",
"Vector to nearest airfield"
}, new bool[] { true, true});
}

private void DegreesToAirbaseSpeak(Player player, AiAirport airbase)
{
if (player != null && HomeBases.Count > 0 && airbase != null)
{
AiAircraft playeraircraft = player.Place() as AiAircraft;
if (playeraircraft != null && playeraircraft.IsAirborne() && HomeBases.ContainsKey(playeraircraft))
{
// construct in-game voice message
// first phrase ingame voice
string Phrase1 = null;
if (random.NextDouble() < 0.5) Phrase1 = "This_is_Home_base";
sayMessageToPlayer(player, Phrase1);

// second phrase ingame voice
string Phrase2 = null;
Phrase2 = "Vector";
//if (random.NextDouble() < 0.5) Phrase2 = "Vector_to_base_is";
sayMessageToPlayer(player, Phrase2);

// third phrase ingame voice
string Phrase3 = null;
double heading = GetHeadingDegrees(playeraircraft.Pos(), airbase.Pos()); // calculate direction to HB in degrees
Phrase3 = heading.ToString().Trim(); // ingame heading voice (30 degree slices)
sayMessageToPlayer(player, Phrase3);

// fourth phrase distance to home base
string Phrase4 = null;
string measurementtype = "metric";
if (player.Army() == 1) measurementtype = "imperial";
double distance = GetDistanceBetweenTwoPoints(playeraircraft.Pos(), airbase.Pos(), measurementtype);

if (player.Army() == 1)
{ // red army
Phrase4 = string.Concat(distance.ToString(), "_miles");
if (distance > 15) Phrase4 = null;
}
else
{ // blue army
Phrase4 = string.Concat(distance.ToString(), "_kilometers");
if (distance > 30) Phrase4 = null;
}
sayMessageToPlayer(player, Phrase4);

// fifth phrase ingame voice
string Phrase5 = null;
if (random.NextDouble() < 0.5) Phrase5 = "Good_luck"; // ingame voice signoff
sayMessageToPlayer(player, Phrase5);
}
else
{ // unable message
string UnableMsg = "Are_you_daft_man";
sayMessageToPlayer(player, UnableMsg);
}
}
else
{ // unable message
string UnableMsg = "Are_you_daft_man";
sayMessageToPlayer(player, UnableMsg);
}
}

public Player[] ToPlayer(Player player)
#region returns an array containing the named player ONLY
{
try
{
List<Player> players = new List<Player>();
players.Add(player);
return players.ToArray();
}
catch (Exception e)
{
Console.WriteLine("Error: [ToPlayer] " + e.Message);
return null;
}
}
#endregion

private double ConvertVectorToDegrees(Vector2d vector)
{
Vector2d matVector = new Vector2d(vector.y, vector.x);
// direction value is in radians so need to multiply by 180/Pi to get value in degrees
return matVector.direction() * 180.0 / Math.PI;
}

public static double RoundToNearest(double Amount, double RoundTo)
#region Rounding off to nearest 10, 100, 1000 and other multiples of 10
// reference: http://codingsense.wordpress.com/2010/02/04/rounding-off-to-nearest-in-c/
{
double ExcessAmount = Amount % RoundTo;
if (ExcessAmount < (RoundTo / 2))
{
Amount -= ExcessAmount;
}
else
{
Amount += (RoundTo - ExcessAmount);
}
return Amount;

}
#endregion

private void sayMessageToPlayer(Player player, string msg)
#region send a speech message to specified player only
{ // send a speech message to specified player only
try
{
foreach (AiAirGroup g in GamePlay.gpAirGroups(player.Army()))
{
bool SaidToGroup = false;
foreach (AiActor a in g.GetItems())
{
AiAircraft aircraft = a as AiAircraft;
if (aircraft.Player(0) == player)
{
if (SaidToGroup == true) break; // speech already said to group
SaidToGroup = true;
aircraft.SayToGroup(aircraft.AirGroup(), msg);
}
}
}
}
catch { }
}
#endregion
}


I hope non-coders can now get a small understanding of what is involved in even the simplest of requests when it comes to game coding. Quite frankly, I've enjoyed presenting this tutorial & hope it's helpful for intermediate coders. Regards to you al ....
Sal

Salmo
Jul-23-2014, 06:00
reserved

No601_Swallow
Jul-23-2014, 12:53
Brilliant astonishing stuff Salmo! This'll give me plenty to chew over for the next few days/weeks/months! I'll add this to my list of scripting bookmarks. Many many thanks. :salute:

ATAG_Freya
Jul-24-2014, 00:26
Thank you Salmo! As a "non-coder" I found it very interesting and very helpful to see a script presented this way. I'm hoping to learn some basic scripting during the winter months, so tutorials like this are a treasure.