347 lines
12 KiB
Text
347 lines
12 KiB
Text
if (globals["tanker"] != nil) {
|
|
# reload with io.load_nasal(getprop("/sim/fg-root") ~ "/Nasal/tanker.nas");
|
|
print("reloading " ~ caller(0)[2]);
|
|
var _setlistener = reinit;
|
|
reinit();
|
|
}
|
|
#--------------------------------------------------------------------------------------------------
|
|
|
|
|
|
var boom_tanker = "Models/Geometry/KC135/KC135.xml";
|
|
var probe_tanker = "Models/Geometry/KA6-D/KA6-D.xml";
|
|
|
|
|
|
var oclock = func(bearing) int(0.5 + geo.normdeg(bearing) / 30) or 12;
|
|
|
|
|
|
var tanker_msg = func setprop("sim/messages/ai-plane", call(sprintf, arg));
|
|
var pilot_msg = func setprop("/sim/messages/pilot", call(sprintf, arg));
|
|
var atc_msg = func setprop("sim/messages/atc", call(sprintf, arg));
|
|
|
|
|
|
var skip_cloud_layer = func(alt) {
|
|
var c = [];
|
|
foreach (var layer; props.globals.getNode("/environment/clouds").getChildren("layer")) {
|
|
var elev = (layer.getNode("elevation-ft", 1).getValue() or -9999) * FT2M;
|
|
var thck = (layer.getNode("thickness-ft", 1).getValue() or 0) * FT2M;
|
|
if (elev > -1000)
|
|
append(c, { bottom: elev - thck * 0.5 - 100, top: elev + thck * 0.5 + 100 });
|
|
}
|
|
while (check; 1) {
|
|
foreach (var layer; c) {
|
|
if (alt > layer.bottom and alt < layer.top) {
|
|
alt += 1000;
|
|
continue check;
|
|
}
|
|
}
|
|
return alt;
|
|
}
|
|
}
|
|
|
|
|
|
var identity = {
|
|
get: func {
|
|
# return free AI id number and least used, free callsign/channel pair
|
|
var data = {}; # copy of me.pool
|
|
var revdata = {}; # channel->callsign
|
|
foreach (var k; keys(me.pool)) {
|
|
data[k] = me.pool[k];
|
|
revdata[me.pool[k][0]] = k;
|
|
}
|
|
var id_used = {};
|
|
foreach (var t; props.globals.getNode("ai/models", 1).getChildren()) {
|
|
if ((var c = t.getNode("callsign")) != nil)
|
|
delete(data, c.getValue() or "");
|
|
if ((var c = t.getNode("navaids/tacan/channel-ID")) != nil)
|
|
delete(data, revdata[c.getValue() or ""]);
|
|
if ((var c = t.getNode("id")) != nil)
|
|
id_used[c.getValue()] = 1;
|
|
}
|
|
for (var aiid = -2; aiid; aiid -= 1)
|
|
if (!id_used[aiid])
|
|
break;
|
|
if (!size(data))
|
|
return [aiid, "MOBIL3", "062X"];
|
|
var d = sort(keys(data), func(a, b) data[a][1] - data[b][1])[0];
|
|
me.pool[d][1] += 1;
|
|
return [aiid, d, data[d][0]];
|
|
},
|
|
pool: {
|
|
ESSO1: ["040X", rand()], ESSO2: ["041X", rand()], ESSO3: ["042X", rand()],
|
|
TEXACO1: ["050X", rand()], TEXACO2: ["051X", rand()], TEXACO3: ["052X", rand()],
|
|
MOBIL1: ["060X", rand()], MOBIL2: ["061X", rand()], MOBIL3: ["062X", rand()],
|
|
},
|
|
};
|
|
|
|
# functions to indicate the course [deg] (heading/ground) for a given heading [deg] (true heading) and ktas [kt]
|
|
# just adding wind.
|
|
|
|
var heading_to_course_dev = func(ktas, heading) {
|
|
var alpha = (getprop("/environment/wind-from-heading-deg") - heading - 180) * D2R;
|
|
var wind_speed = getprop("/environment/wind-speed-kt");
|
|
return math.atan2(math.sin(alpha) * wind_speed, ktas + math.cos(alpha) * wind_speed) * R2D;
|
|
}
|
|
|
|
var heading_to_course = func(ktas, heading) {
|
|
return geo.normdeg(heading + heading_to_course_dev(ktas, heading));
|
|
}
|
|
|
|
# functions to calculate the needed heading [deg] (true heading) to have a desired course on ground [deg]
|
|
# for a given ktas [kt] the speed is speed on the heading, not on the course.
|
|
# need some improvement to indicate that it's impossible if the wind is too strong, but don't know what to do for this :)
|
|
|
|
var course_to_heading_dev = func(ktas, course) {
|
|
var alpha = (getprop("/environment/wind-from-heading-deg") - course) * D2R;
|
|
var rapport = math.sin(alpha) * getprop("/environment/wind-speed-kt") / ktas;
|
|
if (rapport > 1) return 1000; # not sure what to return here to indicate wind will be too strong to achieve course
|
|
else return math.asin(rapport) * R2D;
|
|
}
|
|
|
|
var course_to_heading = func(ktas, course) {
|
|
return geo.normdeg(course + course_to_heading_dev(ktas, course));
|
|
}
|
|
|
|
var Tanker = {
|
|
new: func(aiid, callsign, tacan, type, kias, heading, coord) {
|
|
var m = { parents: [Tanker] };
|
|
m.callsign = callsign;
|
|
m.tacan = tacan;
|
|
m.kias = kias;
|
|
m.heading = m.course = m.track_course = heading;
|
|
m.out_of_range_time = 0;
|
|
m.interval = 10;
|
|
m.length = (getprop("tanker/pattern-length-nm") or 50) * NM2M;
|
|
m.roll = 0;
|
|
m.coord = geo.Coord.new(coord);
|
|
m.anchor = geo.Coord.new(coord).apply_course_distance(m.track_course, m.length); # ARCP
|
|
m.goal = [nil, m.anchor];
|
|
m.lastmode = "none";
|
|
m.mode = "leg";
|
|
m.rollrate = 2; # deg/s
|
|
m.maxbank = 25;
|
|
m.drift_angle = 0;
|
|
|
|
var n = props.globals.getNode("models", 1);
|
|
for (var i = 0; 1; i += 1)
|
|
if (n.getChild("model", i, 0) == nil)
|
|
break;
|
|
m.model = n.getChild("model", i, 1);
|
|
|
|
var n = props.globals.getNode("ai/models", 1);
|
|
for (var i = 0; 1; i += 1)
|
|
if (n.getChild("tanker", i, 0) == nil)
|
|
break;
|
|
m.ai = n.getChild("tanker", i, 1);
|
|
|
|
m.ai.getNode("id", 1).setIntValue(aiid);
|
|
m.ai.getNode("callsign", 1).setValue(m.callsign ~ "");
|
|
m.ai.getNode("tanker", 1).setBoolValue(1);
|
|
m.ai.getNode("valid", 1).setBoolValue(1);
|
|
m.ai.getNode("navaids/tacan/channel-ID", 1).setValue(m.tacan);
|
|
m.ai.getNode("refuel/type", 1).setValue(type);
|
|
m.ai.getNode("refuel/contact", 1).setBoolValue(0);
|
|
|
|
m.latN = m.ai.getNode("position/latitude-deg", 1);
|
|
m.lonN = m.ai.getNode("position/longitude-deg", 1);
|
|
m.altN = m.ai.getNode("position/altitude-ft", 1);
|
|
m.hdgN = m.ai.getNode("orientation/true-heading-deg", 1);
|
|
m.pitchN = m.ai.getNode("orientation/pitch-deg", 1);
|
|
m.rollN = m.ai.getNode("orientation/roll-deg", 1);
|
|
m.ktasN = m.ai.getNode("velocities/true-airspeed-kt", 1);
|
|
m.vertN = m.ai.getNode("velocities/vertical-speed-fps", 1);
|
|
m.rangeN = m.ai.getNode("radar/range-nm", 1);
|
|
m.brgN = m.ai.getNode("radar/bearing-deg", 1);
|
|
m.elevN = m.ai.getNode("radar/elevation-deg", 1);
|
|
m.contactN = m.ai.getNode("refuel/contact", 1);
|
|
|
|
m.update();
|
|
m.model.getNode("path", 1).setValue(type == "boom" ? boom_tanker : probe_tanker);
|
|
m.model.getNode("latitude-deg-prop", 1).setValue(m.latN.getPath());
|
|
m.model.getNode("longitude-deg-prop", 1).setValue(m.lonN.getPath());
|
|
m.model.getNode("elevation-ft-prop", 1).setValue(m.altN.getPath());
|
|
m.model.getNode("heading-deg-prop", 1).setValue(m.hdgN.getPath());
|
|
m.model.getNode("pitch-deg-prop", 1).setValue(m.pitchN.getPath());
|
|
m.model.getNode("roll-deg-prop", 1).setValue(m.rollN.getPath());
|
|
m.model.getNode("load", 1).remove();
|
|
m.identify();
|
|
return Tanker.active[m.callsign] = m;
|
|
},
|
|
del: func {
|
|
tanker_msg(me.callsign ~ " returns to base");
|
|
me.model.remove();
|
|
me.ai.remove();
|
|
delete(Tanker.active, me.callsign);
|
|
},
|
|
update: func {
|
|
var dt = getprop("sim/time/delta-sec");
|
|
var alt = me.coord.alt();
|
|
|
|
if ((me.interval += dt) >= 5) {
|
|
me.interval -= 5;
|
|
if (me.mode == "leg")
|
|
me.headwind = aircraft.wind_speed_from(me.course);
|
|
me.ktas = aircraft.kias_to_ktas(me.kias, alt);
|
|
}
|
|
|
|
if (me.roll) {
|
|
me.headwind = aircraft.wind_speed_from(me.course);
|
|
me.heading = me.heading - dt * 1085.941 * math.tan(me.roll * D2R) / me.ktas;
|
|
me.drift_angle = me.heading - me.course;
|
|
} else {
|
|
me.drift_angle = course_to_heading_dev(me.ktas, me.course);
|
|
me.heading = me.course + me.drift_angle;
|
|
}
|
|
var deviation = me.roll ? me.course - heading_to_course(me.ktas, me.heading) : 0;
|
|
var distance = dt * (me.ktas * math.cos(me.drift_angle * D2R) - me.headwind) * NM2M / 3600;
|
|
|
|
if (me.mode == "leg") {
|
|
if (me.lastmode != "leg") {
|
|
me.lastmode = "leg";
|
|
# swap ARCP anchor and tanker exit point as leg end points
|
|
var g = me.goal[0];
|
|
me.goal[0] = me.goal[1];
|
|
me.goal[1] = g;
|
|
|
|
me.course = me.coord.course_to(me.goal[0]);
|
|
me.leg_remaining = me.coord.distance_to(me.goal[0]);
|
|
me.roll_target = 0;
|
|
me.leg_warning = 0;
|
|
}
|
|
if ((me.leg_remaining -= distance) > 0)
|
|
me.mode = "turn";
|
|
|
|
} else { # me.mode == "turn"
|
|
if (me.lastmode != "turn") {
|
|
me.lastmode = "turn";
|
|
me.full_bank_turn_angle = 0;
|
|
me.turn_remaining = 180;
|
|
me.roll_target = 25;
|
|
}
|
|
if (!me.full_bank_turn_angle and me.roll >= me.roll_target)
|
|
me.full_bank_turn_angle = geo.normdeg(180 - me.turn_remaining);
|
|
|
|
#if (me.turn_remaining < me.full_bank_turn_angle)
|
|
# me.roll_target = 0;
|
|
|
|
#if ((me.turn_remaining -= deviation) < 0) {
|
|
# if (me.goal[1] == nil) # define tanker exit point (opposite of anchor point/ARCP)
|
|
# me.goal[1] = geo.Coord.new(me.coord).apply_course_distance(me.track_course - 180,
|
|
# me.length);
|
|
# me.mode = "leg";
|
|
#}
|
|
me.headwind = aircraft.wind_speed_from(me.course);
|
|
|
|
}
|
|
|
|
|
|
me.coord.apply_course_distance(me.course -= deviation, distance);
|
|
|
|
me.ac = geo.aircraft_position();
|
|
me.distance = me.ac.distance_to(me.coord);
|
|
me.bearing = me.ac.course_to(me.coord);
|
|
|
|
var dalt = alt - me.ac.alt();
|
|
var ac_hdg = getprop("/orientation/heading-deg");
|
|
|
|
me.latN.setDoubleValue(me.coord.lat());
|
|
me.lonN.setDoubleValue(me.coord.lon());
|
|
me.altN.setDoubleValue(alt * M2FT);
|
|
me.hdgN.setDoubleValue(me.heading);
|
|
me.pitchN.setDoubleValue(0);
|
|
me.rollN.setDoubleValue(-me.roll);
|
|
me.ktasN.setDoubleValue(me.ktas);
|
|
me.vertN.setDoubleValue(0);
|
|
me.rangeN.setDoubleValue(me.distance * M2NM);
|
|
me.brgN.setDoubleValue(me.bearing);
|
|
me.elevN.setDoubleValue(math.atan2(dalt, me.distance) * R2D);
|
|
me.contactN.setBoolValue(me.distance < 76 and dalt > 0 # 250 ft
|
|
and abs(view.normdeg(me.bearing - ac_hdg)) < 20);
|
|
|
|
var droll = me.roll_target - me.roll;
|
|
if (droll > 0) {
|
|
me.roll += me.rollrate * dt;
|
|
if (me.roll > me.roll_target)
|
|
me.roll = me.roll_target;
|
|
} elsif (droll < 0) {
|
|
me.roll -= me.rollrate * dt;
|
|
if (me.roll < me.roll_target)
|
|
me.roll = me.roll_target;
|
|
}
|
|
|
|
if (!me.leg_warning and me.leg_remaining < NM2M) {
|
|
tanker_msg(me.callsign ~ ", turn in one mile");
|
|
me.leg_warning = 1;
|
|
}
|
|
|
|
me.now = getprop("/sim/time/elapsed-sec");
|
|
if (me.distance < 90000)
|
|
me.out_of_range_time = me.now;
|
|
elsif (me.now - me.out_of_range_time > 600)
|
|
return me.del();
|
|
settimer(func me.update(), 0);
|
|
},
|
|
identify: func {
|
|
me.out_of_range_time = me.now;
|
|
var alt = int((me.coord.alt() * M2FT + 50) / 100) * 100;
|
|
tanker_msg("%s at %.0f, heading %.0f with %.0f knots, TACAN %s",
|
|
me.callsign, alt, me.course, me.kias, me.tacan);
|
|
},
|
|
report: func {
|
|
me.out_of_range_time = me.now;
|
|
var dist = int(me.distance * M2NM);
|
|
var hdg = getprop("orientation/heading-deg");
|
|
var diff = (me.coord.alt() - me.ac.alt()) * M2FT;
|
|
var qual = diff > 3000 ? " well" : abs(diff) > 1000 ? " slightly" : "";
|
|
var rel = diff > 1000 ? " above" : diff < -1000 ? " below" : "";
|
|
atc_msg("Tanker %s is at %s o'clock%s",
|
|
me.callsign, oclock(me.ac.course_to(me.coord) - hdg),
|
|
qual ~ rel);
|
|
},
|
|
active: {},
|
|
};
|
|
|
|
|
|
var request = func {
|
|
var tanker = values(Tanker.active);
|
|
if (size(tanker))
|
|
return tanker[0].identify();
|
|
|
|
var type = props.globals.getNode("systems/refuel", 1).getChildren("type");
|
|
if (!size(type))
|
|
return;
|
|
type = type[rand() * size(type)].getValue();
|
|
|
|
var (aiid, callsign, tacanid) =_= identity.get();
|
|
var hdg = getprop("orientation/heading-deg");
|
|
var course = hdg + (rand() - 0.5) * 60;
|
|
var dist = 6000 + rand() * 4000;
|
|
var alt = int(10 + rand() * 22) * 1000; # FL100--FL320
|
|
alt = skip_cloud_layer(alt * FT2M);
|
|
var coord = geo.aircraft_position().apply_course_distance(course, dist).set_alt(alt);
|
|
Tanker.new(aiid, callsign, tacanid, type, 250, hdg, coord);
|
|
}
|
|
|
|
|
|
var report = func {
|
|
var tanker = values(Tanker.active);
|
|
if (size(tanker))
|
|
tanker[0].report();
|
|
}
|
|
|
|
|
|
var reinit = func {
|
|
foreach (var t; values(Tanker.active))
|
|
t.del();
|
|
}
|
|
|
|
|
|
_setlistener("/sim/signals/nasal-dir-initialized", func {
|
|
var aar_capable = size(props.globals.getNode("systems/refuel", 1).getChildren("type"));
|
|
gui.menuEnable("tanker", aar_capable);
|
|
if (!aar_capable)
|
|
request = func { atc_msg("no tanker in range") }; # braces mandatory
|
|
|
|
setlistener("/sim/signals/reinit", reinit, 1);
|
|
});
|
|
|
|
|