Phi: nicer scroll animation for METAR widget
[fg:fgdata.git] / Nasal / controls.nas
1 var startEngine = func(v = 1, which...) {
2     if (!v and !size(which))
3         return props.setAll("/controls/engines/engine", "starter", 0);
4     if(size(which)) {
5         foreach(var i; which)
6             foreach(var e; engines)
7                 if(e.index == i)
8                     e.controls.getNode("starter").setBoolValue(v);
9     } else {
10         foreach(var e; engines)
11             if(e.selected.getValue())
12                 e.controls.getNode("starter").setBoolValue(v);
13     }
14 }
15
16 var selectEngine = func(which) {
17     foreach(var e; engines) e.selected.setBoolValue(which == e.index);
18 }
19
20 # Selects (state=1) or deselects (state=0) a list of engines, or all
21 # engines if no list is specified. Example:  selectEngines(1, 1, 3, 5);
22 #
23 var selectEngines = func (state, which...) {
24     if(size(which)) {
25         foreach(var i; which)
26             foreach(var e; engines)
27                 if(e.index == i)
28                     e.selected.setBoolValue(state);
29     } else {
30         foreach(var e; engines)
31             e.selected.setBoolValue(state);
32     }
33 }
34
35 var selectAllEngines = func {
36     foreach(var e; engines) e.selected.setBoolValue(1);
37 }
38
39 var stepMagnetos = func(change) {
40     if (!change)
41         return;
42     foreach(var e; engines) {
43         if(e.selected.getValue()) {
44             var mag = e.controls.getNode("magnetos", 1);
45             mag.setIntValue(mag.getValue() + change);
46         }
47     }
48 }
49
50 var centerFlightControls = func {
51     setprop("/controls/flight/elevator", 0);
52     setprop("/controls/flight/aileron", 0);
53     setprop("/controls/flight/rudder", 0);
54 }
55
56 var throttleMouse = func {
57     if(!getprop("/devices/status/mice/mouse[0]/button[1]")) return;
58     var delta = cmdarg().getNode("offset").getValue() * -4;
59     foreach(var e; engines) {
60         if(!e.selected.getValue()) continue;
61         var throttle = e.controls.getNode("throttle");
62         var val = throttle.getValue() + delta;
63         if(size(arg) > 0) val = -val;
64         throttle.setDoubleValue(val);
65     }
66 }
67
68 # Joystick axis handlers (use cmdarg).  Shouldn't be called from
69 # other contexts.  A non-null argument reverses the axis direction.
70 var axisHandler = func(pre, post) {
71     func(invert = 0) {
72         var val = cmdarg().getNode("setting").getValue();
73         if(invert) val = -val;
74         foreach(var e; engines)
75             if(e.selected.getValue())
76                 setprop(pre ~ e.index ~ post, (1 - val) / 2);
77     }
78 }
79 var throttleAxis = axisHandler("/controls/engines/engine[", "]/throttle");
80 var mixtureAxis = axisHandler("/controls/engines/engine[", "]/mixture");
81 var propellerAxis = axisHandler("/controls/engines/engine[", "]/propeller-pitch");
82 var carbHeatAxis = axisHandler("/controls/anti-ice/engine[", "]/carb-heat");
83 var conditionAxis = axisHandler("/controls/engines/engine[", "]/condition");
84
85 # Joystick axis handler for controlling subsets of similar properties.
86 # Shouldn't be called from other contexts.
87 # The argument engine can be either an index number or a list of
88 # index numbers.
89 # Use only when perEngineSelectedAxisHandler() below will not do.
90 var perIndexAxisHandler = func(pre, post) {
91     return
92         func(index, invert = 0) {
93             var val = cmdarg().getNode("setting").getValue();
94             if(invert) val = -val;
95             if (typeof(index) == "scalar") {
96                 setprop(pre ~ index ~ post, (1 - val) / 2);
97             } else {
98                 foreach (var e; index) {
99                     setprop(pre ~ e ~ post, (1 - val) / 2);
100                 }
101             }
102         };
103 }
104
105 # Joystick axis handler for controlling a selected axis on specific engines.
106 # Shouldn't be called from other contexts.
107 # The argument mode can be
108 #   0  - throttle
109 #   1  - mixture
110 #   2  - propeller-pitch
111 # The argument engine to the returned function can be either an
112 # engine number or a list of engine numbers.
113 # Usage example (controlling the mixture of engines 1 and 2):
114 #   <script>
115 #     controls.perEngineSelectedAxisHandler(1)([1,2]);
116 #   </script>
117 var _axisMode = {
118   0: perIndexAxisHandler("/controls/engines/engine[",
119                          "]/throttle"),
120   1: perIndexAxisHandler("/controls/engines/engine[",
121                          "]/mixture"),
122   2: perIndexAxisHandler("/controls/engines/engine[",
123                          "]/propeller-pitch")
124 };
125 var perEngineSelectedAxisHandler = func(mode) {
126     return _axisMode[mode];
127 }
128
129
130 ##
131 # Wrapper around stepProps() which emulates the "old" flap behavior for
132 # configurations that aren't using the new mechanism.
133 #
134 var flapsDown = func(step) {
135     if(step == 0) return;
136     if(props.globals.getNode("/sim/flaps") != nil) {
137         stepProps("/controls/flight/flaps", "/sim/flaps", step);
138         return;
139     }
140     # Hard-coded flaps movement in 3 equal steps:
141     var val = 0.3333334 * step + getprop("/controls/flight/flaps");
142     setprop("/controls/flight/flaps", val > 1 ? 1 : val < 0 ? 0 : val);
143 }
144
145 var wingSweep = func(step) {
146     if(step == 0) return;
147     if(props.globals.getNode("/sim/wing-sweep") != nil) {
148         stepProps("/controls/flight/wing-sweep", "/sim/wing-sweep", step);
149         return;
150     }
151     # Hard-coded wing movement in 5 equal steps:
152     var val = 0.20 * step + getprop("/controls/flight/wing-sweep");
153     setprop("/controls/flight/wing-sweep", val > 1 ? 1 : val < 0 ? 0 : val);
154 }
155
156 var wingsDown = func(v) {
157     if(v) setprop("/controls/flight/wing-fold", v > 0);
158 }
159
160 var stepSpoilers = func(step) {
161     if(props.globals.getNode("/sim/spoilers") != nil) {
162         stepProps("/controls/flight/spoilers", "/sim/spoilers", step);
163         return;
164     }
165     # Hard-coded spoilers movement in 4 equal steps:
166     var val = 0.25 * step + getprop("/controls/flight/spoilers");
167     setprop("/controls/flight/spoilers", val > 1 ? 1 : val < 0 ? 0 : val);
168 }
169
170 var stepSlats = func(step) {
171     if(props.globals.getNode("/sim/slats") != nil) {
172         stepProps("/controls/flight/slats", "/sim/slats", step);
173         return;
174     }
175     # Hard-coded slats movement in 4 equal steps:
176     var val = 0.25 * step + getprop("/controls/flight/slats");
177     setprop("/controls/flight/slats", val > 1 ? 1 : val < 0 ? 0 : val);
178 }
179
180 ##
181 # Steps through an "array" of property settings.  The first argument
182 # specifies a destination property.  The second is a string containing
183 # a global property tree.  This tree should contain an array of
184 # indexed <setting> children.  This function will maintain a
185 # <current-setting> child, which contains the index of the currently
186 # active setting.  The third argument specifies an integer delta,
187 # indicating how many steps to move through the setting array.
188 # Note that because of the magic of the property system, this
189 # mechanism works for all scalar property types (bool, int, double,
190 # string).
191 #
192 # TODO: This interface could easily be extended to allow for wrapping,
193 # in addition to clamping, allowing a "cycle" of settings to be
194 # defined.  It could also be hooked up with the interpolate() call,
195 # which would allow the removal of the transition-time feature from
196 # YASim.  Finally, other pre-existing features (the views and engine
197 # magnetos, for instance), work similarly but not compatibly, and
198 # could be integrated.
199 #
200 var stepProps = func(dst, array, delta) {
201     dst = props.globals.getNode(dst);
202     array = props.globals.getNode(array);
203     if(dst == nil or array == nil) { return; }
204
205     var sets = array.getChildren("setting");
206
207     var curr = array.getNode("current-setting", 1).getValue();
208     if(curr == nil) { curr = 0; }
209     curr = curr + delta;
210     if   (curr < 0)           { curr = 0; }
211     elsif(curr >= size(sets)) { curr = size(sets) - 1; }
212
213     array.getNode("current-setting").setIntValue(curr);
214     dst.setValue(sets[curr].getValue());
215 }
216
217 ##
218 # "Slews" a property smoothly, without dependence on the simulator
219 # frame rate.  The first argument is the property name.  The second is
220 # a rate, in units per second.  NOTE: this modifies the property for
221 # the current frame only; it is intended to be called by bindings
222 # which repeat each frame.  If you want to cause motion over time, see
223 # interpolate(). Returns new value.
224 #
225 var slewProp = func(prop, delta) {
226     delta *= getprop("/sim/time/delta-realtime-sec");
227     setprop(prop, getprop(prop) + delta);
228     return getprop(prop); # must read again because of clamping
229 }
230
231 # Standard trim rate, in units per second.  Remember that the full
232 # range of a trim axis is 2.0.  Should probably read this out of a
233 # property...
234 var TRIM_RATE = 0.045;
235
236 ##
237 # Handlers.  These are suitable for binding to repeatable button press
238 # events.  They are *not* good for binding to the keyboard, since (at
239 # least) X11 synthesizes its own key repeats.
240 #
241 var elevatorTrim = func(speed) {
242     slewProp("/controls/flight/elevator-trim", speed * TRIM_RATE); }
243 var aileronTrim = func(speed) {
244     slewProp("/controls/flight/aileron-trim", speed * TRIM_RATE); }
245 var rudderTrim = func(speed) {
246     slewProp("/controls/flight/rudder-trim", speed * TRIM_RATE); }
247
248 var THROTTLE_RATE = 0.33;
249
250 var adjThrottle = func(speed) {
251     adjEngControl("throttle", speed); }
252 var adjMixture = func(speed) {
253     adjEngControl("mixture", speed); }
254 var adjCondition = func(speed) {
255     adjEngControl("condition", speed); }
256 var adjPropeller = func(speed) {
257     adjEngControl("propeller-pitch", speed); }
258
259 var adjEngControl = func(prop, speed) {
260     var delta = speed * THROTTLE_RATE * getprop("/sim/time/delta-realtime-sec");
261     var (value, count) = (0, 0);
262     foreach(var e; engines) {
263         if(e.selected.getValue()) {
264             var node = e.controls.getNode(prop, 1);
265             node.setValue(node.getValue() + delta);
266             value += node.getValue(); # must read again because of clamping
267             count += 1;
268         }
269     }
270     return value / count;
271 }
272
273 ##
274 # arg[0] is the throttle increment
275 # arg[1] is the auto-throttle target speed increment
276 var incThrottle = func {
277     var passive = getprop("/autopilot/locks/passive-mode");
278     var locked = getprop("/autopilot/locks/speed");
279     # Note: passive/locked may be nil on aircraft without A/P
280     if ((passive == 0) and (locked))
281     {
282         var node = props.globals.getNode("/autopilot/settings/target-speed-kt", 1);
283         if (node.getValue() == nil) {
284             node.setValue(0.0);
285         }
286         node.setValue(node.getValue() + arg[1]);
287         if (node.getValue() < 0.0) {
288             node.setValue(0.0);
289         }
290     }
291     else
292     {
293         foreach(var e; engines)
294         {
295             if(e.selected.getValue())
296             {
297                 var node = e.controls.getNode("throttle", 1);
298                 var val = node.getValue() + arg[0];
299                 node.setValue(val < -1.0 ? -1.0 : val > 1.0 ? 1.0 : val);
300             }
301         }
302     }
303 }
304
305 ##
306 # arg[0] is the aileron increment
307 # arg[1] is the autopilot target heading increment
308 var incAileron = func {
309     var passive = getprop("/autopilot/locks/passive-mode");
310     var locked = getprop("/autopilot/locks/heading");
311     # Note: passive/locked may be nil on aircraft without A/P
312     if ((passive == 0) and (locked == "dg-heading-hold"))
313     {
314         var node = props.globals.getNode("/autopilot/settings/heading-bug-deg", 1);
315         if (node.getValue() == nil) {
316             node.setValue(0.0);
317         }
318         node.setValue(node.getValue() + arg[1]);
319         if (node.getValue() < 0.0) {
320             node.setValue(node.getValue() + 360.0);
321         }
322         if (node.getValue() > 360.0) {
323             node.setValue(node.getValue() - 360.0);
324         }
325     }
326     else if ((passive == 0) and (locked == "true-heading-hold"))
327     {
328         var node = props.globals.getNode("/autopilot/settings/true-heading-deg", 1);
329         if (node.getValue() == nil) {
330             node.setValue(0.0);
331         }
332         node.setValue(node.getValue() + arg[1]);
333         if (node.getValue() < 0.0) {
334             node.setValue(node.getValue() + 360.0);
335         }
336         if (node.getValue() > 360.0) {
337             node.setValue(node.getValue() - 360.0);
338         }
339     }
340     else
341     {
342         var aileron = props.globals.getNode("/controls/flight/aileron");
343         if (aileron.getValue() == nil) {
344            aileron.setValue(0.0);
345         }
346         aileron.setValue(aileron.getValue() + arg[0]);
347         if (aileron.getValue() < -1.0) {
348             aileron.setValue(-1.0);
349         }
350         if (aileron.getValue() > 1.0) {
351             aileron.setValue(1.0);
352         }
353     }
354 }
355
356 ##
357 # arg[0] is the elevator increment
358 # arg[1] is the autopilot target altitude increment
359 var incElevator = func {
360     var passive = getprop("/autopilot/locks/passive-mode");
361     var locked = getprop("/autopilot/locks/altitude");
362     # Note: passive/locked may be nil on aircraft without A/P
363     if ((passive == 0) and (locked =="altitude-hold"))
364     {
365         var node = props.globals.getNode("/autopilot/settings/target-altitude-ft", 1);
366         if (node.getValue() == nil) {
367             node.setValue(0.0);
368         }
369         node.setValue(node.getValue() + arg[1]);
370         if (node.getValue() < 0.0) {
371             node.setValue(0.0);
372         }
373     }
374     else
375     {
376         var elevator = props.globals.getNode("/controls/flight/elevator");
377         if (elevator.getValue() == nil) {
378             elevator.setValue(0.0);
379         }
380         elevator.setValue(elevator.getValue() + arg[0]);
381         if (elevator.getValue() < -1.0) {
382             elevator.setValue(-1.0);
383         }
384         if (elevator.getValue() > 1.0) {
385             elevator.setValue(1.0);
386         }
387     }
388 }
389
390 ##
391 # Joystick axis handlers.  Don't call from other contexts.
392 #
393 var elevatorTrimAxis = func { elevatorTrim(cmdarg().getNode("setting").getValue()); }
394 var aileronTrimAxis = func { aileronTrim(cmdarg().getNode("setting").getValue()); }
395 var rudderTrimAxis = func { rudderTrim(cmdarg().getNode("setting").getValue()); }
396
397 ##
398 # Gear handling.
399 #
400 var gearDown = func(v) {
401     if (v < 0) {
402       setprop("/controls/gear/gear-down", 0);
403     } elsif (v > 0) {
404       setprop("/controls/gear/gear-down", 1);
405     }
406 }
407 var gearToggle = func { gearDown(getprop("/controls/gear/gear-down") > 0 ? -1 : 1); }
408
409 ##
410 # Brake handling.
411 #
412 var fullBrakeTime = 0.5;
413 var applyBrakes = func(v, which = 0) {
414     if (which <= 0) { interpolate("/controls/gear/brake-left", v, fullBrakeTime); }
415     if (which >= 0) { interpolate("/controls/gear/brake-right", v, fullBrakeTime); }
416 }
417
418 var applyParkingBrake = func(v) {
419     if (!v) { return; }
420     var p = "/controls/gear/brake-parking";
421     setprop(p, var i = !getprop(p));
422     return i;
423 }
424
425 # 1: Deploy, -1: Release
426 var deployChute = func(v) setprop("/controls/flight/drag-chute", v);
427
428 ##
429 # Weapon handling.
430 #
431 var trigger = func(b) setprop("/controls/armament/trigger", b);
432 var weaponSelect = func(d) {
433     var ws = props.globals.getNode("/controls/armament/selected", 1);
434     var n = ws.getValue();
435     if (n == nil) { n = 0; }
436     ws.setIntValue(n + d);
437 }
438
439 ##
440 # Communication.
441 #
442 var ptt = func(b) setprop("/instrumentation/comm/ptt", b);
443
444 ##
445 # Lighting.
446 #
447 var toggleLights = func {
448     if (getprop("/controls/switches/panel-lights")) {
449         setprop("/controls/switches/panel-lights-factor", 0);
450         setprop("/controls/switches/panel-lights", 0);
451         setprop("/controls/switches/landing-light", 0);
452         setprop("/controls/switches/flashing-beacon", 0);
453         setprop("/controls/switches/strobe-lights", 0);
454         setprop("/controls/switches/map-lights", 0);
455         setprop("/controls/switches/cabin-lights", 0);
456         setprop("/controls/switches/nav-lights", 0);
457     } else {
458         setprop("/controls/electric/battery-switch", 1);
459         setprop("/controls/electric/alternator-switch", 1);
460         setprop("/controls/switches/panel-lights-factor", 0.1);
461         setprop("/controls/switches/panel-lights", 1);
462         setprop("/controls/switches/landing-light", 1);
463         setprop("/controls/switches/flashing-beacon", 1);
464         setprop("/controls/switches/strobe-lights", 1);
465         setprop("/controls/switches/map-lights", 1);
466         setprop("/controls/switches/cabin-lights", 1);
467         setprop("/controls/switches/nav-lights", 1);
468     }
469 }
470
471 ##
472 # Initialization.
473 #
474 var engines = [];
475 _setlistener("/sim/signals/fdm-initialized", func {
476     var sel = props.globals.getNode("/sim/input/selected", 1);
477     var engs = props.globals.getNode("/controls/engines").getChildren("engine");
478
479     # need to reset engine list on every FDM reset
480     engines = [];
481     # process all engines
482     foreach(var e; engs) {
483         var index = e.getIndex();
484         var s = sel.getChild("engine", index, 1);
485         if(s.getType() == "NONE") s.setBoolValue(1);
486         append(engines, { index: index, controls: e, selected: s });
487     }
488 });
489
490 var replaySkip = func(skip_time)
491 {
492     var t = getprop("/sim/replay/time");
493     if (t != "")
494     {
495         t+=skip_time;
496         if (t>getprop("/sim/replay/end-time"))
497             t = getprop("/sim/replay/end-time");
498         if (t<0)
499             t=0;
500         setprop("/sim/replay/time", t);
501     }
502 }
503
504 var speedup = func(speed_up)
505 {
506     var t = getprop("/sim/speed-up");
507     if (speed_up < 0)
508     {
509         t = (t > 1/32) ? t/2 : 1/32;
510     }
511     else
512     {
513         t = (t < 32) ? t*2 : 32;
514     }
515     setprop("/sim/speed-up", t);
516     
517     # reformat as a string, this is borrowed from replay.xml
518     
519                 if (t<0.9)
520                 {
521                         t=1/t; # invert the value
522                         t = "1/" ~ t; # convert to a string and show inverted
523                 }
524         
525     gui.popupTip("Time speed-up: " ~ t ~ "x");
526 }
527
528 # mouse-mode handling 
529
530 var cycleMouseMode = func(node)
531 {
532     var reason = node.getChild("reason").getValue();
533     if (reason == "right-click") {
534         if (!getprop("/sim/mouse/right-button-mode-cycle-enabled")) {
535             return;
536         }
537     }
538     
539     var modeNode = props.globals.getNode('/devices/status/mice/mouse[0]/mode');    
540     var mode = modeNode.getValue() + 1;
541     
542     if ((mode == 1) and getprop('/sim/mouse/skip-flight-controls-mode')) {
543         mode +=1;
544     }
545     
546     if (mode == 3) mode = 0;
547     modeNode.setIntValue(mode);
548     
549     # this is really a 'show on-screen hints' control
550     if (getprop('/sim/view-name-popup') == 0)
551       return;
552     
553     # some people like popups but strongly object to this one. As you wish. 
554     if (getprop('/sim/mouse/cycle-mode-popup') == 0)
555       return;
556       
557     if (mode == 0) {
558       fgcommand("clear-message", props.Node.new({ "id":"mouse-mode" }));
559       return;
560     }
561     
562     var msg = "";
563     if (mode == 1)
564         msg = "Mouse is controlling flight controls. Press TAB to change.";
565     else
566         msg = "Mouse is controlling view direction. Press TAB to change.";
567     
568         fgcommand("show-message", props.Node.new({ "label": msg, "id":"mouse-mode" }));
569 }
570
571 addcommand("cycle-mouse-mode", cycleMouseMode);