@fboes/aerofly-custom-missions 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.editorconfig +1 -5
  2. package/.eslintrc.json +24 -0
  3. package/CHANGELOG.md +59 -21
  4. package/dist/dto/AeroflyLocalizedText.js +44 -0
  5. package/dist/dto/AeroflyMission.js +173 -0
  6. package/dist/dto/AeroflyMissionCheckpoint.js +127 -0
  7. package/dist/dto/AeroflyMissionConditions.js +124 -0
  8. package/dist/dto/AeroflyMissionConditionsCloud.js +90 -0
  9. package/dist/dto/AeroflyMissionTargetPlane.js +35 -0
  10. package/dist/dto/AeroflyMissionsList.js +36 -0
  11. package/dist/index.js +7 -634
  12. package/dist/index.test.js +17 -11
  13. package/dist/node/AeroflyConfigurationNode.js +82 -0
  14. package/docs/custom_missions_user.tmc +3 -6
  15. package/docs/custom_missions_user.xml +100 -0
  16. package/package.json +9 -10
  17. package/src/dto/AeroflyLocalizedText.ts +74 -0
  18. package/src/dto/AeroflyMission.ts +372 -0
  19. package/src/dto/AeroflyMissionCheckpoint.ts +234 -0
  20. package/src/dto/AeroflyMissionConditions.ts +189 -0
  21. package/src/dto/AeroflyMissionConditionsCloud.ts +111 -0
  22. package/src/dto/AeroflyMissionTargetPlane.ts +62 -0
  23. package/src/dto/AeroflyMissionsList.ts +52 -0
  24. package/src/index.test.ts +22 -25
  25. package/src/index.ts +7 -1099
  26. package/src/node/AeroflyConfigurationNode.ts +89 -0
  27. package/types/AeroflyMissionCheckpoint.d.ts +140 -0
  28. package/types/AeroflyMissionTargetPlane.d.ts +40 -0
  29. package/types/dto/AeroflyConfigFileSet.d.ts +10 -0
  30. package/types/dto/AeroflyConfigFileSet.d.ts.map +1 -0
  31. package/types/dto/AeroflyConfiguration.interface.d.ts +4 -0
  32. package/types/dto/AeroflyConfiguration.interface.d.ts.map +1 -0
  33. package/types/dto/AeroflyLocalizedText.d.ts +58 -0
  34. package/types/dto/AeroflyLocalizedText.d.ts.map +1 -0
  35. package/types/dto/AeroflyMission.d.ts +163 -0
  36. package/types/dto/AeroflyMission.d.ts.map +1 -0
  37. package/types/dto/AeroflyMissionCheckpoint.d.ts +126 -0
  38. package/types/dto/AeroflyMissionCheckpoint.d.ts.map +1 -0
  39. package/types/dto/AeroflyMissionConditions.d.ts +102 -0
  40. package/types/dto/AeroflyMissionConditions.d.ts.map +1 -0
  41. package/types/dto/AeroflyMissionConditionsCloud.d.ts +57 -0
  42. package/types/dto/AeroflyMissionConditionsCloud.d.ts.map +1 -0
  43. package/types/dto/AeroflyMissionTargetPlane.d.ts +45 -0
  44. package/types/dto/AeroflyMissionTargetPlane.d.ts.map +1 -0
  45. package/types/dto/AeroflyMissionsList.d.ts +30 -0
  46. package/types/dto/AeroflyMissionsList.d.ts.map +1 -0
  47. package/types/dto/basicTypes.d.ts +1 -0
  48. package/types/index.d.ts +7 -585
  49. package/types/index.d.ts.map +1 -1
  50. package/types/node/AeroflyConfigurationNode.d.ts +14 -0
  51. package/types/node/AeroflyConfigurationNode.d.ts.map +1 -0
  52. package/eslint.config.js +0 -29
@@ -0,0 +1,189 @@
1
+ import { AeroflyConfigurationNode } from "../node/AeroflyConfigurationNode.js";
2
+ import { meterPerStatuteMile } from "./AeroflyMission.js";
3
+ import { AeroflyMissionConditionsCloud } from "./AeroflyMissionConditionsCloud.js";
4
+
5
+ /**
6
+ * Weather data for wind
7
+ * @property {number} direction in degree
8
+ * @property {number} speed in kts
9
+ * @property {number} gusts in kts
10
+ */
11
+ export type AeroflyMissionConditionsWind = {
12
+ direction: number;
13
+ speed: number;
14
+ gusts: number;
15
+ };
16
+
17
+ /**
18
+ * @class
19
+ * Time and weather data for the given flight plan
20
+ *
21
+ * The purpose of this class is to collect data needed for Aerofly FS4's
22
+ * `custom_missions_user.tmc` flight plan file format, and export the structure
23
+ * for this file via the `toString()` method.
24
+ */
25
+ export class AeroflyMissionConditions {
26
+ /**
27
+ * @property {Date} time of flight plan. Relevant is the UTC part, so
28
+ * consider setting this date in UTC.
29
+ */
30
+ time: Date;
31
+
32
+ /**
33
+ * @property {object} wind state
34
+ */
35
+ wind: AeroflyMissionConditionsWind;
36
+
37
+ /**
38
+ * @property {number} turbulenceStrength 0..1, percentage
39
+ */
40
+ turbulenceStrength: number;
41
+
42
+ /**
43
+ * @property {number} thermalStrength 0..1, percentage
44
+ */
45
+ thermalStrength: number;
46
+
47
+ /**
48
+ * @property {number} visibility in meters
49
+ */
50
+ visibility: number;
51
+
52
+ /**
53
+ * @property {AeroflyMissionConditionsCloud[]} clouds for the whole flight
54
+ */
55
+ clouds: AeroflyMissionConditionsCloud[] = [];
56
+
57
+ /**
58
+ * @param {object} additionalAttributes allows to set additional attributes on creation
59
+ * @param {Date} [additionalAttributes.time] of flight plan. Relevant is the UTC part, so
60
+ * consider setting this date in UTC.
61
+ * @param {{direction: number, speed: number, gusts: number}} [additionalAttributes.wind] state
62
+ * @param {number} [additionalAttributes.turbulenceStrength] 0..1, percentage
63
+ * @param {number} [additionalAttributes.thermalStrength] 0..1, percentage
64
+ * @param {number} [additionalAttributes.visibility] in meters
65
+ * @param {?number} [additionalAttributes.visibility_sm] in statute miles, will overwrite visibility
66
+ * @param {?number} [additionalAttributes.temperature] in °C, will overwrite thermalStrength
67
+ * @param {AeroflyMissionConditionsCloud[]} [additionalAttributes.clouds] for the whole flight
68
+ */
69
+ constructor({
70
+ time = new Date(),
71
+ wind = {
72
+ direction: 0,
73
+ speed: 0,
74
+ gusts: 0,
75
+ },
76
+ turbulenceStrength = 0,
77
+ thermalStrength = 0,
78
+ visibility = 25_000,
79
+ visibility_sm = 0,
80
+ temperature = 0,
81
+ clouds = [],
82
+ }: Partial<AeroflyMissionConditions> & {
83
+ visibility_sm?: number;
84
+ temperature?: number;
85
+ } = {}) {
86
+ this.time = time;
87
+ this.wind = wind;
88
+ this.turbulenceStrength = turbulenceStrength;
89
+ this.thermalStrength = thermalStrength;
90
+ this.visibility = visibility;
91
+ this.clouds = clouds;
92
+
93
+ if (visibility_sm) {
94
+ this.visibility_sm = visibility_sm;
95
+ }
96
+ if (temperature) {
97
+ this.temperature = temperature;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * @returns {number} the Aerofly value for UTC hours + minutes/60 + seconds/3600. Ignores milliseconds ;)
103
+ */
104
+ get time_hours(): number {
105
+ return this.time.getUTCHours() + this.time.getUTCMinutes() / 60 + this.time.getUTCSeconds() / 3600;
106
+ }
107
+
108
+ /**
109
+ * @returns {string} Time representation like "20:15:00"
110
+ */
111
+ get time_presentational(): string {
112
+ return [this.time.getUTCHours(), this.time.getUTCMinutes(), this.time.getUTCSeconds()]
113
+ .map((t) => {
114
+ return String(t).padStart(2, "0");
115
+ })
116
+ .join(":");
117
+ }
118
+
119
+ /**
120
+ * @param {number} visibility_sm `this.visibility` in statute miles instead of meters
121
+ */
122
+ set visibility_sm(visibility_sm: number) {
123
+ this.visibility = visibility_sm * meterPerStatuteMile;
124
+ }
125
+
126
+ /**
127
+ * @returns {number} `this.visibility` in statute miles instead of meters
128
+ */
129
+ get visibility_sm(): number {
130
+ return this.visibility / meterPerStatuteMile;
131
+ }
132
+
133
+ /**
134
+ * Will set `this.thermalStrength`
135
+ * @param {number} temperature in °C
136
+ */
137
+ set temperature(temperature: number) {
138
+ // Range from -15°C to 35°C
139
+ this.thermalStrength = Math.max(0, (temperature + 15) / 50) ** 2;
140
+ }
141
+
142
+ /**
143
+ * @returns {number} in °C
144
+ */
145
+ get temperature(): number {
146
+ return Math.sqrt(this.thermalStrength) * 50 - 15;
147
+ }
148
+
149
+ /**
150
+ * @returns {AeroflyConfigurationNode[]} cloud elements
151
+ */
152
+ getCloudElements(): AeroflyConfigurationNode[] {
153
+ return this.clouds
154
+ .slice(0, 2) // Aerofly FS4 supports max 2 cloud layers
155
+ .flatMap((c: AeroflyMissionConditionsCloud, index: number) => c.getElements(index));
156
+ }
157
+
158
+ /**
159
+ * @returns {string} to use in Aerofly FS4's `custom_missions_user.tmc`
160
+ */
161
+ getElement(): AeroflyConfigurationNode {
162
+ if (this.clouds.length < 1) {
163
+ this.clouds = [new AeroflyMissionConditionsCloud(0, 0)];
164
+ }
165
+
166
+ return new AeroflyConfigurationNode("tmmission_conditions", "conditions")
167
+ .append(
168
+ new AeroflyConfigurationNode("tm_time_utc", "time")
169
+ .appendChild("int32", "time_year", this.time.getUTCFullYear())
170
+ .appendChild("int32", "time_month", this.time.getUTCMonth() + 1)
171
+ .appendChild("int32", "time_day", this.time.getUTCDate())
172
+ .appendChild("float64", "time_hours", this.time_hours, `${this.time_presentational} UTC`),
173
+ )
174
+ .appendChild("float64", "wind_direction", this.wind.direction)
175
+ .appendChild("float64", "wind_speed", this.wind.speed, "kts")
176
+ .appendChild("float64", "wind_gusts", this.wind.gusts, "kts")
177
+ .appendChild("float64", "turbulence_strength", this.turbulenceStrength)
178
+ .appendChild("float64", "thermal_strength", this.thermalStrength, `${this.temperature} °C`)
179
+ .appendChild("float64", "visibility", this.visibility, `${this.visibility_sm} SM`)
180
+ .append(...this.getCloudElements());
181
+ }
182
+
183
+ /**
184
+ * @returns {string} to use in Aerofly FS4's `custom_missions_user.tmc`
185
+ */
186
+ toString(): string {
187
+ return this.getElement().toString();
188
+ }
189
+ }
@@ -0,0 +1,111 @@
1
+ import { AeroflyConfigurationNode } from "../node/AeroflyConfigurationNode.js";
2
+ import { feetPerMeter } from "./AeroflyMission.js";
3
+
4
+ /**
5
+ * Cloud coverage codes
6
+ */
7
+ export type AeroflyMissionConditionsCloudCoverCode = "CLR" | "FEW" | "SCT" | "BKN" | "OVC";
8
+
9
+ /**
10
+ * @class
11
+ * A cloud layer for the current flight plan's weather data
12
+ *
13
+ * The purpose of this class is to collect data needed for Aerofly FS4's
14
+ * `custom_missions_user.tmc` flight plan file format, and export the structure
15
+ * for this file via the `toString()` method.
16
+ */
17
+ export class AeroflyMissionConditionsCloud {
18
+ /**
19
+ * @property {number} cover 0..1, percentage
20
+ */
21
+ cover: number;
22
+
23
+ /**
24
+ * @property {number} base altitude in meters AGL
25
+ */
26
+ base: number;
27
+
28
+ /**
29
+ * @param {number} cover 0..1, percentage
30
+ * @param {number} base altitude in meters AGL
31
+ */
32
+ constructor(cover: number, base: number) {
33
+ this.cover = cover;
34
+ this.base = base;
35
+ }
36
+
37
+ /**
38
+ * @param {number} cover 0..1, percentage
39
+ * @param {number} base_feet altitude, but in feet AGL instead of meters AGL
40
+ * @returns {AeroflyMissionConditionsCloud} self
41
+ */
42
+ static createInFeet(cover: number, base_feet: number): AeroflyMissionConditionsCloud {
43
+ return new AeroflyMissionConditionsCloud(cover, base_feet / feetPerMeter);
44
+ }
45
+
46
+ /**
47
+ * @param {number} base_feet `this.base` in feet instead of meters
48
+ */
49
+ set base_feet(base_feet: number) {
50
+ this.base = base_feet / feetPerMeter;
51
+ }
52
+
53
+ /**
54
+ * @returns {number} `this.base` in feet instead of meters
55
+ */
56
+ get base_feet(): number {
57
+ return this.base * feetPerMeter;
58
+ }
59
+
60
+ /**
61
+ * @returns {string} Cloud coverage as text representation like "OVC" for `this.cover`
62
+ */
63
+ get cover_code(): AeroflyMissionConditionsCloudCoverCode {
64
+ if (this.cover < 1 / 8) {
65
+ return "CLR";
66
+ } else if (this.cover <= 2 / 8) {
67
+ return "FEW";
68
+ } else if (this.cover <= 4 / 8) {
69
+ return "SCT";
70
+ } else if (this.cover <= 7 / 8) {
71
+ return "BKN";
72
+ }
73
+ return "OVC";
74
+ }
75
+
76
+ /**
77
+ * @param {number} index if used in an array will set the array index
78
+ * @returns {AeroflyConfigurationNode[]} to use in Aerofly FS4's `custom_missions_user.tmc`
79
+ */
80
+ getElements(index: number = 0): AeroflyConfigurationNode[] {
81
+ const getIndexString = (index: number) => {
82
+ switch (index) {
83
+ case 0:
84
+ return "cloud";
85
+ case 1:
86
+ return "cirrus";
87
+ case 2:
88
+ return "cumulus_mediocris";
89
+ default:
90
+ return "more_clouds";
91
+ }
92
+ };
93
+
94
+ const indexString = getIndexString(index);
95
+
96
+ return [
97
+ new AeroflyConfigurationNode("float64", `${indexString}_cover`, this.cover ?? 0, this.cover_code),
98
+ new AeroflyConfigurationNode("float64", `${indexString}_base`, this.base, `${this.base_feet} ft AGL`),
99
+ ];
100
+ }
101
+
102
+ /**
103
+ * @param {number} index if used in an array will set the array index
104
+ * @returns {string} to use in Aerofly FS4's `custom_missions_user.tmc`
105
+ */
106
+ toString(index: number = 0): string {
107
+ return this.getElements()
108
+ .map((element) => element.toString(index))
109
+ .join("\n");
110
+ }
111
+ }
@@ -0,0 +1,62 @@
1
+ import { AeroflyConfigurationNode } from "../node/AeroflyConfigurationNode.js";
2
+
3
+ /**
4
+ * @class
5
+ * A target plane which the aircraft needs to cross.
6
+ */
7
+ export class AeroflyMissionTargetPlane {
8
+ /**
9
+ * @property {number} longitude easting, using the World Geodetic
10
+ * System 1984 (WGS 84) [WGS84] datum, with longitude and latitude units
11
+ * of decimal degrees; -180..180
12
+ */
13
+ longitude: number;
14
+
15
+ /**
16
+ * @property {number} latitude northing, using the World Geodetic
17
+ * System 1984 (WGS 84) [WGS84] datum, with longitude and latitude units
18
+ * of decimal degrees; -90..90
19
+ */
20
+ latitude: number;
21
+
22
+ /**
23
+ * @property {number} dir in degree
24
+ */
25
+ dir: number;
26
+
27
+ /**
28
+ * @property {string} name of property
29
+ */
30
+ name: string;
31
+
32
+ /**
33
+ *
34
+ * @param {number} longitude easting, using the World Geodetic
35
+ * System 1984 (WGS 84) [WGS84] datum, with longitude and latitude units
36
+ * of decimal degrees; -180..180
37
+ * @param {number}latitude northing, using the World Geodetic
38
+ * System 1984 (WGS 84) [WGS84] datum, with longitude and latitude units
39
+ * of decimal degrees; -90..90
40
+ * @param {number} dir in degree
41
+ * @param {string} name of property
42
+ */
43
+ constructor(longitude: number, latitude: number, dir: number, name: string = "finish") {
44
+ this.longitude = longitude;
45
+ this.latitude = latitude;
46
+ this.dir = dir;
47
+ this.name = name;
48
+ }
49
+
50
+ getElement(): AeroflyConfigurationNode {
51
+ return new AeroflyConfigurationNode("tmmission_target_plane", this.name)
52
+ .appendChild("vector2_float64", "lon_lat", [this.longitude, this.latitude])
53
+ .appendChild("float64", "direction", this.dir);
54
+ }
55
+
56
+ /**
57
+ * @returns {string} to use in Aerofly FS4's `custom_missions_user.tmc`
58
+ */
59
+ toString(): string {
60
+ return this.getElement().toString();
61
+ }
62
+ }
@@ -0,0 +1,52 @@
1
+ import { AeroflyConfigurationNode } from "../node/AeroflyConfigurationNode.js";
2
+ import { AeroflyMission } from "./AeroflyMission.js";
3
+
4
+ /**
5
+ * @class
6
+ * A list of flight plans.
7
+ *
8
+ * The purpose of this class is to collect data needed for Aerofly FS4's
9
+ * `custom_missions_user.tmc` flight plan file format, and export the structure
10
+ * for this file via the `toString()` method.
11
+ */
12
+ export class AeroflyMissionsList {
13
+ /**
14
+ * @property {AeroflyMission[]} missions in this mission list
15
+ */
16
+ missions: AeroflyMission[];
17
+
18
+ /**
19
+ * @param {AeroflyMission[]} missions in this mission list
20
+ */
21
+ constructor(missions: AeroflyMission[] = []) {
22
+ this.missions = missions;
23
+ }
24
+
25
+ getElement(): AeroflyConfigurationNode {
26
+ return new AeroflyConfigurationNode("file", "").append(
27
+ new AeroflyConfigurationNode("tmmissions_list", "").append(
28
+ new AeroflyConfigurationNode("list_tmmission_definition", "missions").append(
29
+ ...this.missions.map((m): AeroflyConfigurationNode =>{
30
+ const mission = m.getElement();
31
+ mission._comment = `End of ${mission.name}`;
32
+ return mission;
33
+ }),
34
+ ),
35
+ ),
36
+ );
37
+ }
38
+
39
+ /**
40
+ * @returns {string} to use in Aerofly FS4's `custom_missions_user.tmc`
41
+ */
42
+ toString(): string {
43
+ return this.getElement().toString();
44
+ }
45
+
46
+ /**
47
+ * @returns {string} XML represenation of this mission list
48
+ */
49
+ toXmlString(): string {
50
+ return this.getElement().toXmlString();
51
+ }
52
+ }
package/src/index.test.ts CHANGED
@@ -1,14 +1,12 @@
1
- import {
2
- AeroflyConfigFileSet,
3
- AeroflyMissionsList,
4
- AeroflyMission,
5
- AeroflyMissionConditions,
6
- AeroflyMissionConditionsCloud,
7
- AeroflyMissionCheckpoint,
8
- AeroflyLocalizedText,
9
- AeroflyMissionTargetPlane,
10
- } from "./index.js";
11
1
  import { strict as assert } from "node:assert";
2
+ import { AeroflyLocalizedText } from "./index.js";
3
+ import { AeroflyMission } from "./index.js";
4
+ import { AeroflyMissionCheckpoint } from "./index.js";
5
+ import { AeroflyMissionConditions } from "./index.js";
6
+ import { AeroflyMissionConditionsCloud } from "./index.js";
7
+ import { AeroflyMissionsList } from "./index.js";
8
+ import { AeroflyMissionTargetPlane } from "./index.js";
9
+ import { AeroflyConfigurationNode } from "./node/AeroflyConfigurationNode.js";
12
10
 
13
11
  const assertValidAeroflyStructure = (aeroflyString: string): void => {
14
12
  const openingBrackets = aeroflyString.match(/</g);
@@ -16,6 +14,7 @@ const assertValidAeroflyStructure = (aeroflyString: string): void => {
16
14
  const openingBrackets2 = aeroflyString.match(/\[/g);
17
15
  const closingBrackets2 = aeroflyString.match(/\]/g);
18
16
 
17
+ assert.ok(openingBrackets?.length ?? 0 > 0, "Has opening <");
19
18
  assert.strictEqual(openingBrackets?.length, closingBrackets?.length, "Number of <> matches");
20
19
  assert.strictEqual(openingBrackets2?.length, closingBrackets2?.length, "Number of [] matches");
21
20
  };
@@ -25,21 +24,17 @@ const assertIncludes = (string: string, includes: string): void => {
25
24
  };
26
25
 
27
26
  {
28
- const file = new AeroflyConfigFileSet(0, "file", "");
29
- file.pushRaw(
30
- new AeroflyConfigFileSet(1, "tmmissions_list", "")
31
- .pushRaw(
32
- new AeroflyConfigFileSet(2, "list_tmmission_definition", "missions")
33
- .pushRaw(
34
- new AeroflyConfigFileSet(3, "tmmission_definition", "mission")
35
- .push("string8", "title", "KCCR #1: Concord / Buchanan Field")
36
- .push("float64", "origin_alt", 1066.799965862401, "3500 ft MSL")
37
- .toString(),
38
- )
39
- .pushRaw(new AeroflyConfigFileSet(3, "tmmission_definition", "mission").toString())
40
- .toString(),
41
- )
42
- .toString(),
27
+ const file = new AeroflyConfigurationNode("file", "");
28
+ file.append(
29
+ new AeroflyConfigurationNode("tmmissions_list", "").append(
30
+ new AeroflyConfigurationNode("list_tmmission_definition", "missions")
31
+ .append(
32
+ new AeroflyConfigurationNode("tmmission_definition", "mission")
33
+ .appendChild("string8", "title", "KCCR #1: Concord / Buchanan Field")
34
+ .appendChild("float64", "origin_alt", 1066.799965862401, "3500 ft MSL"),
35
+ )
36
+ .append(new AeroflyConfigurationNode("tmmission_definition", "mission")),
37
+ ),
43
38
  );
44
39
  assertValidAeroflyStructure(file.toString());
45
40
  console.log("✅ AeroflyMission test successful");
@@ -245,6 +240,7 @@ const assertIncludes = (string: string, includes: string): void => {
245
240
 
246
241
  let missionListString = missionList.toString();
247
242
 
243
+ assert.strictEqual(missionListString, missionList.toString());
248
244
  assertIncludes(missionListString, "[origin]");
249
245
  assertIncludes(missionListString, "[tmmission_definition]");
250
246
  assertIncludes(missionListString, "[list_tmmission_checkpoint]");
@@ -295,6 +291,7 @@ const assertIncludes = (string: string, includes: string): void => {
295
291
 
296
292
  //console.dir(missionList.missions[0], { depth: null });
297
293
  //console.log(missionListString);
294
+ //console.log(missionList.getElement().toXmlString());
298
295
 
299
296
  console.log("✅ AeroflyMissionsList test successful");
300
297
  }