@fboes/aerofly-custom-missions
Version:
Builder for Aerofly FS4 Custom Missions Files
217 lines (194 loc) • 7.64 kB
text/typescript
import { AeroflyConfigurationNode } from "../node/AeroflyConfigurationNode.js";
import { meterPerStatuteMile } from "./AeroflyMission.js";
import { AeroflyMissionConditionsCloud } from "./AeroflyMissionConditionsCloud.js";
/**
* Weather data for wind
* @property {number} direction in degree
* @property {number} speed in kts
* @property {number} gusts in kts
*/
export type AeroflyMissionConditionsWind = {
direction: number;
speed: number;
gusts: number;
};
/**
* @class
* Time and weather data for the given flight plan
*
* The purpose of this class is to collect data needed for Aerofly FS4's
* `custom_missions_user.tmc` flight plan file format, and export the structure
* for this file via the `toString()` method.
*/
export class AeroflyMissionConditions {
/**
* @property {Date} time of flight plan. Relevant is the UTC part, so
* consider setting this date in UTC.
*/
time: Date;
/**
* @property {object} wind state
*/
wind: AeroflyMissionConditionsWind;
/**
* @property {number} turbulenceStrength normalized value [0,1]
*/
turbulenceStrength: number;
/**
* @property {number} thermalStrength normalized value [0,1]
*/
thermalStrength: number;
/**
* @property {number} visibility in meters
*/
visibility: number;
/**
* @property {AeroflyMissionConditionsCloud[]} clouds for the whole flight
*/
clouds: AeroflyMissionConditionsCloud[] = [];
/**
* @param {object} additionalAttributes allows to set additional attributes on creation
* @param {Date} [additionalAttributes.time] of flight plan. Relevant is the UTC part, so
* consider setting this date in UTC.
* @param {{direction: number, speed: number, gusts: number}} [additionalAttributes.wind] state
* @param {number} [additionalAttributes.turbulenceStrength] normalized value [0,1]
* @param {number} [additionalAttributes.thermalStrength] normalized value [0,1]
* @param {number} [additionalAttributes.visibility] in meters
* @param {?number} [additionalAttributes.visibility_sm] in statute miles, will overwrite visibility
* @param {?number} [additionalAttributes.temperature] in °C, will overwrite thermalStrength
* @param {AeroflyMissionConditionsCloud[]} [additionalAttributes.clouds] for the whole flight
*/
constructor({
time = new Date(),
wind = {
direction: 0,
speed: 0,
gusts: 0,
},
turbulenceStrength = 0,
thermalStrength = 0,
visibility = 25_000,
visibility_sm = 0,
temperature = 0,
clouds = [],
}: Partial<AeroflyMissionConditions> & {
visibility_sm?: number;
temperature?: number;
} = {}) {
this.time = time;
this.wind = wind;
this.turbulenceStrength = turbulenceStrength;
this.thermalStrength = thermalStrength;
this.visibility = visibility;
this.clouds = clouds;
if (visibility_sm) {
this.visibility_sm = visibility_sm;
}
if (temperature) {
this.temperature = temperature;
}
}
/**
* @returns {number} the Aerofly value for UTC hours + minutes/60 + seconds/3600. Ignores milliseconds ;)
*/
get time_hours(): number {
return this.time.getUTCHours() + this.time.getUTCMinutes() / 60 + this.time.getUTCSeconds() / 3600;
}
/**
* @returns {string} Time representation like "20:15:00"
*/
get time_presentational(): string {
return [this.time.getUTCHours(), this.time.getUTCMinutes(), this.time.getUTCSeconds()]
.map((t) => {
return String(t).padStart(2, "0");
})
.join(":");
}
/**
* @param {number} visibility_sm `this.visibility` in statute miles instead of meters
*/
set visibility_sm(visibility_sm: number) {
this.visibility = visibility_sm * meterPerStatuteMile;
}
/**
* @returns {number} `this.visibility` in statute miles instead of meters
*/
get visibility_sm(): number {
return this.visibility / meterPerStatuteMile;
}
/**
* Will set `this.thermalStrength`
* @param {number} temperature in °C
*/
set temperature(temperature: number) {
// Range from -15°C to 35°C
this.thermalStrength = Math.max(0, (temperature + 15) / 50) ** 2;
}
/**
* @returns {number} in °C
*/
get temperature(): number {
return Math.sqrt(this.thermalStrength) * 50 - 15;
}
/**
* @returns {AeroflyConfigurationNode[]} cloud elements
*/
getCloudElements(): AeroflyConfigurationNode[] {
return this.clouds.flatMap((c: AeroflyMissionConditionsCloud, index: number) => c.getElements(index));
}
/**
* @returns {string} to use in Aerofly FS4's `custom_missions_user.tmc`
*/
getElement(): AeroflyConfigurationNode {
if (this.clouds.length < 1) {
this.clouds = [new AeroflyMissionConditionsCloud(0, 0)];
}
return new AeroflyConfigurationNode("tmmission_conditions", "conditions")
.append(
new AeroflyConfigurationNode("tm_time_utc", "time")
.appendChild("int32", "time_year", this.time.getUTCFullYear())
.appendChild("int32", "time_month", this.time.getUTCMonth() + 1)
.appendChild("int32", "time_day", this.time.getUTCDate())
.appendChild("float64", "time_hours", this.time_hours, `${this.time_presentational} UTC`),
)
.appendChild("float64", "wind_direction", this.wind.direction)
.appendChild("float64", "wind_speed", this.wind.speed, "kts")
.appendChild("float64", "wind_gusts", this.wind.gusts, "kts")
.appendChild("float64", "turbulence_strength", this.turbulenceStrength)
.appendChild("float64", "thermal_strength", this.thermalStrength, `${this.temperature} °C`)
.appendChild("float64", "visibility", this.visibility, `${this.visibility_sm} SM`)
.append(...this.getCloudElements());
}
/**
* @returns {string} to use in Aerofly FS4's `custom_missions_user.tmc`
*/
toString(): string {
return this.getElement().toString();
}
static fromJSON(json: unknown): AeroflyMissionConditions {
if (typeof json !== "object" || json === null) {
throw new Error("Invalid mission condition data");
}
const data = json as Record<string, unknown>;
if (typeof data.wind !== "object" || data.wind === null) {
throw new Error("Invalid wind data");
}
const wind = data.wind as Record<string, unknown>;
return new AeroflyMissionConditions({
time: data.time ? new Date(String(data.time)) : new Date(),
wind: {
direction: Number(wind.direction ?? 0),
speed: Number(wind.speed ?? 0),
gusts: Number(wind.gusts ?? 0),
},
turbulenceStrength: Number(data.turbulenceStrength ?? 0),
thermalStrength: Number(data.thermalStrength ?? 0),
visibility: Number(data.visibility ?? 25_000),
visibility_sm: Number(data.visibility_sm ?? 0),
temperature: Number(data.temperature ?? 0),
clouds: (Array.isArray(data.checkpoints) ? data.checkpoints : []).map((c) =>
AeroflyMissionConditionsCloud.fromJSON(c),
),
});
}
}