Air-to-air refueling enhancements
[fg:toms-fgdata.git] / Nasal / tanker.nas
1 if (globals["tanker"] != nil) {
2         # reload with io.load_nasal(getprop("/sim/fg-root") ~ "/Nasal/tanker.nas");
3         print("reloading " ~ caller(0)[2]);
4         var _setlistener = reinit;
5         reinit();
6 }
7 #--------------------------------------------------------------------------------------------------
8
9 var oclock = func(bearing) int(0.5 + geo.normdeg(bearing) / 30) or 12;
10
11
12 var tanker_msg = func setprop("sim/messages/ai-plane", call(sprintf, arg));
13 var pilot_msg = func setprop("/sim/messages/pilot", call(sprintf, arg));
14 var atc_msg = func setprop("sim/messages/atc", call(sprintf, arg));
15
16
17 var skip_cloud_layer = func(alt) {
18         var c = [];
19         foreach (var layer; props.globals.getNode("/environment/clouds").getChildren("layer")) {
20                 var elev = (layer.getNode("elevation-ft", 1).getValue() or -9999) * FT2M;
21                 var thck = (layer.getNode("thickness-ft", 1).getValue() or 0) * FT2M;
22                 if (elev > -1000)
23                         append(c, { bottom: elev - thck * 0.5 - 100, top: elev + thck * 0.5 + 100 });
24         }
25         while (check; 1) {
26                 foreach (var layer; c) {
27                         if (alt > layer.bottom and alt < layer.top) {
28                                 alt += 1000;
29                                 continue check;
30                         }
31                 }
32                 return alt;
33         }
34 }
35
36
37 var identity = {
38         get: func {
39                 # return free AI id number and least used, free callsign/channel pair
40                 var data = {};     # copy of me.pool
41                 var revdata = {};  # channel->callsign
42                 foreach (var k; keys(me.pool)) {
43                         data[k] = me.pool[k];
44                         revdata[me.pool[k][0]] = k;
45                 }
46                 var id_used = {};
47                 foreach (var t; props.globals.getNode("ai/models", 1).getChildren()) {
48                         if ((var c = t.getNode("callsign")) != nil)
49                                 delete(data, c.getValue() or "");
50                         if ((var c = t.getNode("navaids/tacan/channel-ID")) != nil)
51                                 delete(data, revdata[c.getValue() or ""]);
52                         if ((var c = t.getNode("id")) != nil)
53                                 id_used[c.getValue()] = 1;
54                 }
55                 for (var aiid = -2; aiid; aiid -= 1)
56                         if (!id_used[aiid])
57                                 break;
58                 if (!size(data))
59                         return [aiid, "MOBIL3", "062X"];
60                 var d = sort(keys(data), func(a, b) data[a][1] - data[b][1])[0];
61                 me.pool[d][1] += 1;
62                 return [aiid, d, data[d][0]];
63         },
64         pool: {
65                 ESSO1: ["040X", rand()], ESSO2: ["041X", rand()], ESSO3: ["042X", rand()],
66                 TEXACO1: ["050X", rand()], TEXACO2: ["051X", rand()], TEXACO3: ["052X", rand()],
67                 MOBIL1: ["060X", rand()], MOBIL2: ["061X", rand()], MOBIL3: ["062X", rand()],
68         },
69 };
70
71
72 var Tanker = {
73         new: func(aiid, callsign, tacan, type, model, kias, maxfuel, pattern, heading, coord) {
74                 var m = { parents: [Tanker] };
75                 m.callsign = callsign;
76                 m.tacan = tacan;
77                 m.kias = kias;
78                 m.heading = m.course = m.track_course = heading;
79                 m.out_of_range_time = 0;
80                 m.interval = 10;
81                 m.length = pattern;
82                 m.roll = 0;
83                 m.coord = geo.Coord.new(coord);
84                 m.anchor = geo.Coord.new(coord).apply_course_distance(m.track_course, m.length); # ARCP
85                 m.goal = [nil, m.anchor];
86                 m.lastmode = "none";
87                 m.mode = "leg";
88                 m.rollrate = 2; # deg/s
89                 m.maxbank = 25;
90
91                 var n = props.globals.getNode("models", 1);
92                 for (var i = 0; 1; i += 1)
93                         if (n.getChild("model", i, 0) == nil)
94                                 break;
95                 m.model = n.getChild("model", i, 1);
96
97                 var n = props.globals.getNode("ai/models", 1);
98                 for (var i = 0; 1; i += 1)
99                         if (n.getChild("tanker", i, 0) == nil)
100                                 break;
101                 m.ai = n.getChild("tanker", i, 1);
102
103                 m.ai.getNode("id", 1).setIntValue(aiid);
104                 m.ai.getNode("callsign", 1).setValue(m.callsign ~ "");
105                 m.ai.getNode("tanker", 1).setBoolValue(1);
106                 m.ai.getNode("valid", 1).setBoolValue(1);
107                 m.ai.getNode("navaids/tacan/channel-ID", 1).setValue(m.tacan);
108                 m.ai.getNode("refuel/type", 1).setValue(type);
109                 m.ai.getNode("refuel/max-fuel-transfer-lbs-min", 1).setValue(maxfuel);
110                 m.ai.getNode("refuel/contact", 1).setBoolValue(0);
111                 m.ai.getNode("radar/in-range", 1).setBoolValue(1);
112
113                 m.latN = m.ai.getNode("position/latitude-deg", 1);
114                 m.lonN = m.ai.getNode("position/longitude-deg", 1);
115                 m.altN = m.ai.getNode("position/altitude-ft", 1);
116                 m.hdgN = m.ai.getNode("orientation/true-heading-deg", 1);
117                 m.pitchN = m.ai.getNode("orientation/pitch-deg", 1);
118                 m.rollN = m.ai.getNode("orientation/roll-deg", 1);
119                 m.ktasN = m.ai.getNode("velocities/true-airspeed-kt", 1);
120                 m.vertN = m.ai.getNode("velocities/vertical-speed-fps", 1);
121                 m.rangeN = m.ai.getNode("radar/range-nm", 1);
122                 m.brgN = m.ai.getNode("radar/bearing-deg", 1);
123                 m.elevN = m.ai.getNode("radar/elevation-deg", 1);
124                 m.contactN = m.ai.getNode("refuel/contact", 1);
125                 m.hOffsetN = m.ai.getNode("radar/h-offset", 1);
126                 m.vOffsetN = m.ai.getNode("radar/v-offset", 1);
127
128                 m.update();
129                 
130                 m.model.getNode("path", 1).setValue(model);
131                 m.model.getNode("latitude-deg-prop", 1).setValue(m.latN.getPath());
132                 m.model.getNode("longitude-deg-prop", 1).setValue(m.lonN.getPath());
133                 m.model.getNode("elevation-ft-prop", 1).setValue(m.altN.getPath());
134                 m.model.getNode("heading-deg-prop", 1).setValue(m.hdgN.getPath());
135                 m.model.getNode("pitch-deg-prop", 1).setValue(m.pitchN.getPath());
136                 m.model.getNode("roll-deg-prop", 1).setValue(m.rollN.getPath());
137                 m.model.getNode("load", 1).remove();
138                 m.identify();
139                 return Tanker.active[m.callsign] = m;
140         },
141         del: func {
142                 tanker_msg(me.callsign ~ " returns to base");
143                 me.model.remove();
144                 me.ai.remove();
145                 delete(Tanker.active, me.callsign);
146         },
147         update: func {
148                 var dt = getprop("sim/time/delta-sec");
149                 var alt = me.coord.alt();
150
151                 if ((me.interval += dt) >= 5) {
152                         me.interval -= 5;
153                         me.headwind = aircraft.wind_speed_from(me.course);
154                         me.ktas = aircraft.kias_to_ktas(me.kias, alt);
155                 }
156
157                 var distance = dt * (me.ktas - me.headwind) * NM2M / 3600;
158                 var deviation = me.roll ? 0.5 * dt * 1085.941 * math.tan(me.roll * D2R) / me.ktas : 0;
159
160                 if (me.mode == "leg") {
161                         if (me.lastmode != "leg") {
162                                 me.lastmode = "leg";
163                                 # swap ARCP anchor and tanker exit point as leg end points
164                                 var g = me.goal[0];
165                                 me.goal[0] = me.goal[1];
166                                 me.goal[1] = g;
167
168                                 me.course = me.coord.course_to(me.goal[0]);
169                                 me.leg_remaining = me.coord.distance_to(me.goal[0]);
170                                 me.roll_target = 0;
171                                 me.leg_warning = 0;
172                         }
173                         if ((me.leg_remaining -= distance) < 0)
174                                 me.mode = "turn";
175
176                 } else { # me.mode == "turn"
177                         if (me.lastmode != "turn") {
178                                 me.lastmode = "turn";
179                                 me.full_bank_turn_angle = 0;
180                                 me.turn_remaining = 180;
181                                 me.roll_target = 25;
182                         }
183                         if (!me.full_bank_turn_angle and me.roll >= me.roll_target)
184                                 me.full_bank_turn_angle = geo.normdeg(180 - me.turn_remaining);
185
186                         if (me.turn_remaining < me.full_bank_turn_angle)
187                                 me.roll_target = 0;
188
189                         if ((me.turn_remaining -= deviation) < 0) {
190                                 if (me.goal[1] == nil)  # define tanker exit point (opposite of anchor point/ARCP)
191                                         me.goal[1] = geo.Coord.new(me.coord).apply_course_distance(me.track_course - 180,
192                                                         me.length);
193                                 me.mode = "leg";
194                         }
195                 }
196
197                 me.coord.apply_course_distance(me.course -= deviation, distance);
198
199                 me.ac = geo.aircraft_position();
200                 me.distance = me.ac.distance_to(me.coord);
201                 me.bearing = me.ac.course_to(me.coord);
202
203                 var dalt = alt - me.ac.alt();
204                 var ac_hdg = getprop("/orientation/heading-deg");
205                 var ac_pitch = getprop("/orientation/pitch-deg");
206                 var ac_contact_dist = getprop("/systems/refuel/contact-radius-m");
207                 var elev = math.atan2(dalt, me.distance) * R2D;
208         
209                 me.latN.setDoubleValue(me.coord.lat());
210                 me.lonN.setDoubleValue(me.coord.lon());
211                 me.altN.setDoubleValue(alt * M2FT);
212                 me.hdgN.setDoubleValue(me.heading = me.course);
213                 me.pitchN.setDoubleValue(0);
214                 me.rollN.setDoubleValue(-me.roll);
215                 me.ktasN.setDoubleValue(me.ktas);
216                 me.vertN.setDoubleValue(0);
217                 me.rangeN.setDoubleValue(me.distance * M2NM);
218                 me.brgN.setDoubleValue(me.bearing);
219                 me.elevN.setDoubleValue(elev);
220                 
221                 me.contactN.setBoolValue(me.distance < ac_contact_dist and 
222                                                                                                                 dalt > 0 and 
223                                                                                                                 abs(view.normdeg(me.bearing - ac_hdg)) < 20);
224                                 
225                 me.hOffsetN.setDoubleValue(me.bearing - ac_hdg);
226                 me.vOffsetN.setDoubleValue(elev - ac_pitch);
227
228                 var droll = me.roll_target - me.roll;
229                 if (droll > 0) {
230                         me.roll += me.rollrate * dt;
231                         if (me.roll > me.roll_target)
232                                 me.roll = me.roll_target;
233                 } elsif (droll < 0) {
234                         me.roll -= me.rollrate * dt;
235                         if (me.roll < me.roll_target)
236                                 me.roll = me.roll_target;
237                 }
238
239                 if (!me.leg_warning and me.leg_remaining < NM2M) {
240                         tanker_msg(me.callsign ~ ", turn in one mile");
241                         me.leg_warning = 1;
242                 }
243
244                 me.now = getprop("/sim/time/elapsed-sec");
245                 if (me.distance < 90000)
246                         me.out_of_range_time = me.now;
247                 elsif (me.now - me.out_of_range_time > 600)
248                         return me.del();
249                 settimer(func me.update(), 0);
250         },
251         identify: func {
252                 me.out_of_range_time = me.now;
253                 var alt = int((me.coord.alt() * M2FT + 50) / 100) * 100;
254                 tanker_msg("%s at %.0f, heading %.0f with %.0f knots, TACAN %s",
255                                 me.callsign, alt, me.course, me.kias, me.tacan);
256         },
257         report: func {
258                 me.out_of_range_time = me.now;
259                 var dist = int(me.distance * M2NM);
260                 var hdg = getprop("orientation/heading-deg");
261                 var diff = (me.coord.alt() - me.ac.alt()) * M2FT;
262                 var qual = diff > 3000 ? " well" : abs(diff) > 1000 ? " slightly" : "";
263                 var rel = diff > 1000 ? " above" : diff < -1000 ? " below" : "";
264                 atc_msg("Tanker %s is at %s o'clock%s",
265                                 me.callsign, oclock(me.ac.course_to(me.coord) - hdg),
266                                 qual ~ rel);
267         },
268         active: {},
269 };
270
271 # Factory methods
272
273 # Create a tanker based on a given /sim/ai/tankers/tanker property node
274 var create_tanker = func(tanker_node, course) {
275         var (aiid, callsign, tacanid) =_= identity.get();
276         var model = tanker_node.getNode("model", 1).getValue();
277         var type  = tanker_node.getNode("type", 1).getValue();
278         var spd = tanker_node.getNode("speed-kts", 1).getValue() or 250;
279         var pattern = (tanker_node.getNode("pattern-length-nm", 1).getValue() or 50) * NM2M;
280         var maxfuel = tanker_node.getNode("max-fuel-transfer-lbs-min", 1).getValue() or 6000;
281
282         var alt = int(10 + rand() * 15) * 1000;  # FL100--FL250
283         alt = skip_cloud_layer(alt * FT2M);
284         var dist = 6000 + rand() * 4000;        
285         var coord = geo.aircraft_position().apply_course_distance(course, dist).set_alt(alt);   
286         
287         Tanker.new(aiid, callsign, tacanid, type, model, spd, maxfuel, pattern, course, coord);
288 }
289
290 # Request a new tanker
291 var request_new = func(tanker_node=nil) {
292         var tanker = values(Tanker.active);
293         if (size(tanker)) tanker[0].del();
294         request(tanker_node);
295 }
296
297 var request = func(tanker_node=nil) {
298         var tanker = values(Tanker.active);
299         if (size(tanker))
300                 return tanker[0].identify();
301                 
302         if (tanker_node == nil) {
303                 var type = props.globals.getNode("systems/refuel", 1).getChildren("type");
304                 if (!size(type))
305                         return;
306                 type = type[rand() * size(type)].getValue();
307                 
308                 var tankers = props.globals.getNode("/sim/ai/tankers", 1).getChildren("tanker");
309                 foreach (var tanker; tankers) {
310                   if (tanker.getNode("type", 1).getValue() == type) {
311                     tanker_node = tanker;
312                     break;                
313                   }             
314                 }               
315         }
316
317         var hdg = getprop("orientation/heading-deg");
318         var course = hdg + (rand() - 0.5) * 60;
319         
320         create_tanker(tanker_node, course);
321 }
322
323 var request_random = func(tanker_node=nil) {
324         var tanker = values(Tanker.active);
325         if (size(tanker))
326                 return tanker[0].identify();
327
328         if (tanker_node == nil) {
329                 var type = props.globals.getNode("systems/refuel", 1).getChildren("type");
330                 if (!size(type))
331                         return;
332                 type = type[rand() * size(type)].getValue();
333                 
334                 var tankers = props.globals.getNode("/sim/ai/tankers", 1).getChildren("tanker");
335                 foreach (var tanker; tankers) {
336                   if (tanker.getNode("type", 1).getValue() == type) {
337                     tanker_node = tanker;
338                     break;                
339                   }             
340                 }               
341         }
342
343         var course = rand() * 360;
344         create_tanker(tanker_node, course);
345 }
346
347
348 var report = func {
349         var tanker = values(Tanker.active);
350         if (size(tanker))
351                 tanker[0].report();
352 }
353
354
355 var reinit = func {
356         foreach (var t; values(Tanker.active))
357                 t.del();
358 }
359
360
361 _setlistener("/sim/signals/nasal-dir-initialized", func {
362         var aar_capable = size(props.globals.getNode("systems/refuel", 1).getChildren("type"));
363         gui.menuEnable("tanker", aar_capable);
364         if (!aar_capable)
365                 request = func { atc_msg("no tanker in range") }; # braces mandatory
366
367         setlistener("/sim/signals/reinit", reinit, 1);
368 });
369