| Scripts/trails.js | "use strict";
this.name	= "trails";
this.author	= "Norby";
this.copyright	= "2015 Norby";
this.description= "Ships leaves condensation trails";
this.licence	= "CC BY-NC-SA 4.0";
this.$GoodFPS = 50; //if FPS is over this and quality is reduced then increase back
this.$LowFPS = 25; //if fall below then reduce quality using smaller number in dataKey
this.$Zoom = false; //increase brightness over 12km to detect far ships, disabled in ambience pack
//internal properties, should not touch
this.$Delta = 0; //delta counter for FCB
this.$Delta2 = 0; //store first delta in the last second for FCB
this.$Distance = 60; //distance between trail elements based on effectada.plist
this.$ED = null; //store a pointer to escortdeck worldscript
this.$FC = 0; //frame counter for calculating FPS
this.$FP = null; //FCB pointer
this.$FPS = 60; //lastly measured FPS
this.$Length = this.$MaxLength; //actual length to avoid "Universe is full"
this.$MaxAngle = 0.7; //max. angle of sight in radian (0.7 for FOV80), move far away if out of sight
this.$MaxKey = 3; //max. used dataKey from effectdata.plist ("trails3")
this.$Key = this.$MaxKey; //start at the max. available dataKey
this.$MaxLength = 50; //how many elements are in a trail (60m each) based on effectdata.plist
this.$MaxSec = 3 + 2*this.$MaxKey; //max timeout of trail elements in seconds (9)
this.$Old = false; //old version before Oolite v1.82
this.$Red = false; //No trails in red alert below 50 FPS if weapons are online
this.$S = [];	//tracked ships and visualEffects
		//$S[i][0]: the ship object,
		//$S[i][1]: last position of the ship where a visualEffect is created - unused
		//$S[i][2]: lastly used "k" indexes
                //$S[i][k] where k >= 3: visualEffects
this.$Sec = this.$MaxSec; //timeout of trail elements in seconds (3-6)
this.$Shift = 10000000000; //x coord. shift if the trail element is not in sight
this.$T = [];	//timestamps of the creation of the trail elements
//this.$V = [];	//storage of all visualEffects
//worldscript events
this.startUp = function() {
	this.$ED = worldScripts.escortdeck;
	this.$Clear(this);
	this.$FC = 0; //frame counter
	this.$Delta = 0; //delta counter
	if (0 < oolite.compareVersion("1.82")) {
		this.$Old = true;
/*		
		//pre-creating visualEffects
		var key = "trails";
		var pos = Vector3D(100000000,0,0);
		for( var i = 0; i < 100; i++ ) {
			this.$V.push(system.addVisualEffect( key, pos ));
		}
*/		
	} else { //from Oolite v1.82 flashers in subentities are usable, like in "trails+"
		this.$Old = false;
	}
	if( !isValidFrameCallback( this.$FP ) )
                this.$FP = addFrameCallback( this.$FCB.bind(this) );
}
this.shipWillEnterWitchspace = function() {
	this.$Clear(this);
}
//local methods
this.$Clear = function(w) {  //start a new array, free up memory
	w.$Length = w.$MaxLength;
        w.$RemoveAllTrails(w);
	delete w.$S;
	w.$S = [];
	w.$S[0] = [];
	w.$S[0][0] = player.ship; //the first ship
        delete w.$T;
        w.$T = [];
        w.$T[0] = [];
        w.$Red = false;
}
this.$FCB = function( delta ) {
	var w = this; //worldscript
	var p = player.ship;
	var pp = false;
	if( p && p.isValid ) pp = p.position;
	w.$FC++; //frame counter for calculating FPS
	w.$Delta += delta;
	if( w.$Delta > w.$Delta2 + 1 && pp ) { //like an 1s timer, search for new ships
		w.$FPS = w.$FC; //save measured fps for the trail length reducer code below
		w.$FC = 0; //reset frame counter
		w.$Delta2 = w.$Delta; //save delta
		
		//No trails in red alert below 50 FPS if weapons are online
                if( player.alertCondition > 2 && p.weaponsOnline ) {
                        if( !w.$Red && w.$FPS < w.$GoodFPS ) {
                                //first time after red alert started
                                w.$RemoveAllTrails(w);
                                w.$Red = true;
                        } //restore trails in red alert over 100FPS only
                        if( w.$Red && w.$FPS < 2 * w.$GoodFPS ) return;
                }
                w.$Red = false;
		
		var ships = [];
		if( !p.docked ) ships = p.checkScanner(false);//faster than filteredEntities
				//must use checkScanner with false to add the "green" escorts
				//and a good side effect boulders get steam tails
				//moreover true return the full list in Oolite 1.80 (fixed in 1.81)
		for( var i = 1; i < w.$S.length; i++ ) { //remove trails of far ships
			var o = w.$S[i]; //data of an old ship
			var x = -2;
			if( o && o[0] && o[0].isValid ) {
				x = ships.indexOf( o[0] );
				if( w.$ED ) { //disable escort trails on deck, enable if launched
					if( !o[0].script ) //for sure
						o[0].setScript("oolite-default-ship-script.js");
					var pad = w.$ED.$EscortDeckShip.indexOf( o[0] );
					if( pad != -1 ) { //change on ships used by escortdeck only
                                                if( w.$ED.$EscortDeckShipPos[pad] )
                                                        o[0].script.$TrailsDisabled = true;
                                                else o[0].script.$TrailsDisabled = false;
					}
				}
			}
			//if invalid then docked, jumped or destroyed, leave to run remove below
			if( x == -1 ) { //not in the scanner anymore?
				//must check distance due to many ships near exhibitions
				//can leave out important ones from the limited checkScanner list
				if( pp.distanceTo( o[0].position ) > 2 * p.scannerRange ) {
					for( var k = 3; k <= w.$Length; k++ ) {
						var v = o[k]; //remove visualEffects
						if( v && v.isValid ) v.remove();
					}
					w.$S[i] = false; //remove this ship from the tracked array
				}
			} else if( x >= 0 ) ships[x] = false; //don't add into $S if already in
		}
		
		if( p.docked ) {
			if( w.$S[1] ) w.$Clear(w); //start a new array, free up memory
		} else for( var i = 0; i < ships.length; i++ ) { //add new ships into $S
			var ship = ships[i];
			if( ship && ship.isValid && ship.maxSpeed > 0 //do not add buoy, etc.
				&& ( ship.exhausts && ship.exhausts[0] //need valid exhaust
					|| ship.scanClass == "CLASS_ROCK" )
                                && !ship.isMissile
                                && ship.primaryRole != "wreckage" //exclude for explosions
                                && ship.primaryRole != "oolite-wreckage-chunk"
                                && ship.primaryRole != "alloy"
                                && ship.primaryRole != "cargopod"
                                && ship.primaryRole != "griffspark" //explosion in griff's wreckages
				&& ship.dataKey != "telescopemarker" //exclude for Telescope
                                && ship.mass > 10000 //exclude TIE fighters and too small ships
                                && ship.scanClass != "CLASS_NO_DRAW" ) { //exclude for sure
				//do not add escorts landed on EscortDeck
				var add = true;
				if( w.$ED ) {
					var pad = w.$ED.$EscortDeckShip.indexOf( ship );
					if( pad != -1 && w.$ED.$EscortDeckShipPos[pad] ) {
						add = false;
					}
				}
				if( add ) {
					var l = w.$S.length; //add after the last ship
					w.$S[l] = [];
					w.$S[l][0] = ship;
                                        w.$T[l] = [];
				}
			}
		}
	}
	if( w.$Red ) return; //No trails in red alert below 50 FPS if weapons are online
	//reduce the number of flashers if FPS is low
        var newkey = false;
        if( w.$FPS < w.$LowFPS ) {
		if( w.$Key > 1 ) {
			w.$Key--;
                        newkey = true;
//                      player.consoleMessage("FPS:"+w.$FPS+" key:"+w.$Key); //debug
		}
                w.$FPS = w.$LowFPS; //do not reduce again until the next measue
	} else if( w.$FPS > w.$GoodFPS ) { //extend up to maxkey
                if( w.$Key < w.$MaxKey ) {
                        w.$Key++;
                        newkey = true;
//                      player.consoleMessage("FPS:"+w.$FPS+" key:"+w.$Key); //debug
		}
                w.$FPS = w.$GoodFPS; //do not extend again until the next measue
	}
	var key = "trails"+w.$Key;
        if( newkey ) w.$Sec = w.$MaxSec - 2 * w.$MaxKey + 2 * w.$Key;
	//reduce length over 1500 entity to prevent "Universe is full" error at 2047
	var a = system.allShips.length + system.allVisualEffects.length;
        var r = w.$Length;
	if( a > 1500 && w.$Length > 10 ) { //too much entity
		r = Math.ceil( w.$Length/2 ); //reduced length
                w.$RemoveAllTrails(w); //restart trails
		w.$Length = r;
	}
	
	var zoom = w.$Zoom;
	
	//create new trail elements
	var dist = w.$Distance;
        var shift = w.$Shift;
        var ma = w.$MaxAngle;
	for( var i = 1; i < w.$S.length; i++ ) { //start from 0 and player ship will make trail also
		var t = w.$S[i];
		if( t ) {
			var ship = t[0];
			if( ship && ship.isValid && !ship.isDerelict
				&& !ship.script.$TrailsDisabled ) {
				var sp = ship.position;
				var sc = 1;
				
                                if( ship.scanClass == "CLASS_ROCK" ) key = "trails-rock";
                                else {
                                        key = "trails"+w.$Key;//need if the previous ship was over 12km
				        if( pp ) { //create less flashers over 12km
					        var pd = sp.distanceTo( pp ) / 1000;
					        if( pd > 12 ) {
						        key = "trails0";
						        if( zoom ) //increase size in 12-20km for brightness
                                                            sc = 1.5+Math.max(0,Math.min(2,pd/5-2));
					        } //below the same formula is used again, keep these equal
				        }
                                }
				var c = 0; //safety counter to surely exit from the next cycle soon
				//check distance of the last position where a trail is created
				//need to do in cycle if travelled more in the last frame than
				//the length of a visualEffect element in effectdata.plist (20m)
				//which is happen often with more than 1x TAF
				var k = t[2];
                                var bp = sp;//.add(ship.heading.multiply(w.$Distance));//start earlier
                                var ez = 1; //default for ships without engine like rocks
                                var e = ship.exhausts; //use engine exhaust z distance from ship center
                                if( e && e[0] && e[0].position && e[0].position.z )
                                        ez = 1-Math.min(0, e[0].position.z); //ez must >=1 for Gnat
                                var kp = bp;
                                if(t[k] && t[k].isValid) {
                                        kp = t[k].position;
                                        if( kp.x > 0.9*shift ) {//moved far away
                                                kp = Vector3D(kp.x-shift, kp.y, kp.z);
//                                              if(p.target == ship) log(w.name, ship.name+" far i:"+i); //debug
                                        }
                                }
                                var kv = t[k];
				while( c < 50 && ( !k || !kv || !kv.isValid
					|| bp.distanceTo(kp) > dist/2+ez )) {
					c++;
					if( !w.$S[i][2] ) w.$S[i][2] = 3; //new trail
					k = w.$S[i][2];
					if( !k ) k = 3;
					var a = w.$S[i][k]; //actual (lastly created) visualEffect
					var pos = bp;
					var q = ship.orientation; //for the first element
					if( a && a.isValid ) { //from the second element
                                                var ap = a.position;
                                                if( ap.x > 0.9*shift ) //moved far away
                                                        ap = Vector3D(ap.x-shift, ap.y, ap.z);
						var u = bp.subtract(ap).direction();
						pos = ap.add(u.multiply(dist));
						if( a && a.isVisualEffect ) {
							//facing from the previous trail element
							q = a.orientation;
							var z=pos.subtract(ap).direction();
							var ah = a.heading;
							var angle = ah.angleTo(z);
							//set the plane where we should rotate in
							var cr = ah.cross(z).direction();
							q = q.rotate( cr, -angle );
						}
					}
//					w.$S[i][1] = pos; //save the new position - unused
					k++; //index of the next item
					if( k > w.$Length ) k = 3; //restart over $Length
					w.$S[i][2] = k; //save the new index
					
/*                                      //make the end of the trail darker but prevent blue end on gray dust
					if( ship.scanClass != "CLASS_ROCK" ) {
					        var pk = k+1; //previous k points to the oldest visualEffect
					        if( pk > w.$Length ) pk = 3;
					        var pv = w.$S[i][pk]; //the oldest visualEffect
					        if( pv && pv.isVisualEffect ) {
						        var nv = system.addVisualEffect(
							        "trails-end", pv.position );
						        if( nv ) {
							        nv.orientation = pv.orientation;
                                                                nv.scaleX = nv.scaleY = 0.5; //shrink
							        pv.remove();
							        w.$S[i][pk] = nv; //save the new item
                                                        }
						}
					}
*/
					//move or replace old visual item
					var ov = w.$S[i][k];
                                        if( ov && ov.isVisualEffect && ov.dataKey == key ) {
                                                ov.position = pos;
                                                ov.orientation = q;
                                        } else {
                                                if( ov && ov.isVisualEffect ) {
                                                        ov.remove();
                                                }
                                                var v = system.addVisualEffect( key, pos );
                                                if( v ) {
                                                        v.orientation = q;
                                                        w.$S[i][k] = v; //save the new element
                                                }
                                        }
                                        w.$T[i][k] = w.$Delta; //Timestamp of creation
                                        kv = w.$S[i][k];
                                        if( kv && kv.isValid ) {
                                                kp = kv.position;
                                                if( kp.x > 0.9*shift ) //moved far away
                                                        kp = Vector3D(kp.x-shift, kp.y, kp.z);
                                        } else kp = bp;
				}
//				if(c > 40) log(w.name, ship.name+" new i:"+i+" c:"+c); //debug
				//increase flasher size with the distance
				if( sc > 1 ) for( var j = 3; j <= w.$Length; j++ ) {
					var v = w.$S[i][j];
					if( v && v.isValid ) v.scaleX = v.scaleY = sc;
				}				//but not scaleZ!
			} else { //ship docked, jumped or destroyed
				var k = t[2]; //actual item index - old code without timestamp
				if( k ) {
					k++;// += 0.5; //slower count for slower remove
					if( k > w.$Length ) k = 3; //restart over $Length
					w.$S[i][2] = k; //save the new index
					if( k == Math.floor(k) ) {
						if( w.$S[i][k] == -1 ) { //all visualEffect removed
							//so remove this ship from the tracked array
							for( var k = 3; k <= w.$Length; k++ ) {
								var v = t[k]; //remove again for sure
								if( v && v.isValid ) v.remove();
							}
							w.$S[i] = false; 
						} else {
							var ov = t[k]; //remove old visual item
							if( ov && ov.isVisualEffect ) ov.remove();
							w.$S[i][k] = -1; //flag for remove ship
						}
					}
				}
			}
			
                        if( pp && ship && ship.isValid && ship.scanClass != "CLASS_ROCK" ) {
                                var max = 1;
                                var sd = ship.position.distanceTo( pp );
                                if( !zoom ) {
                                        if( ship.mass < 20000 ) max = 0.2;
                                        else if( ship.mass < 30000 ) max = 0.4;
                                        else if( ship.mass < 130000 ) max = 0.8;
                                } else {
                                        if( sd > 12000 ) //zoom in 12-20km, same formula as above
                                                max = 1.5+Math.max(0,Math.min(2,sd/5000-2));
                                        else if( ship.mass < 20000 ) max = 0.4;
                                        else if( ship.mass < 30000 ) max = 0.8;
                                }
//                              var c = 0;
                                for( var j = 3; j <= w.$Length; j++ ) {
                                        var v = w.$S[i][j];
                                        if( v && v.isValid && w.$T[i][j] ) {
//                                              c++;
                                                if( w.$Delta > w.$T[i][j] + w.$Sec ) { //timeout
                                                        v.remove();
                                                        w.$S[i][j] = null;
                                                        w.$T[i][j] = null;
                                                } else {
                                                        var vp = v.position;
                                                        var ph = p.heading;
                                                        switch( p.viewDirection ) {
                                                            case "VIEW_AFT":
                                                                    ph = ph.multiply(-1);
                                                                    break;
                                                            case "VIEW_PORT":
                                                                    ph = p.orientation.vectorRight()
                                                                            .multiply(-1);
                                                                    break;
                                                            case "VIEW_STARBOARD":
                                                                    ph = p.orientation.vectorRight();
                                                                    break;
                                                        }
/*                                                      //reduce flashers if far or near parallel - cause jams
                                                        var key2 = key;
                                                        if( w.$Key == 1 ) {
                                                                var an = vp.subtract(pp).angleTo(v.heading);
                                                                if( vp.distanceTo( pp )>12000 || an<0.3 ) {
                                                                        key2 = "trails0";
                                                                //improve if not enough paralell
                                                                } else key2 = "trails1";
                                                                if(p.target==ship) log(w.name, //debug
                                                                    an+" i:"+i+" j:"+j+" x:"+vp.x+" "+key2);
                                                        }*/
                                                        if( w.$Key < 2 && v.dataKey != key ) {
                                                                //immediately reduce quality in lowest mode
                                                                z = system.addVisualEffect(key, v.position);
                                                                if( z ) {
                                                                        z.orientation = v.orientation;
                                                                        v.remove();
                                                                        w.$S[i][j] = z;
                                                                        v = z;
                                                                }
                                                        }
                                                        //time-based trail dissipation
                                                        var s = 4;
                                                        if( !zoom || sd < 12000 )
                                                                s = 2.01-((w.$Delta-w.$T[i][j])/w.$Sec)*2;
                                                        //s must be always greater than 0 for scale!
                                                        v.scaleX = v.scaleY = Math.min(max, s);//shrink
                                                        //performance boost:
                                                        //move far away if not in sight or over 15km
                                                        if( vp.x < 0.9*shift ) {
                                                                var ang = ph.angleTo(vp.subtract(pp));
                                                                if( ang >= ma || !zoom && sd > 15000 ) {
                                                                        v.position = Vector3D(
                                                                        vp.x + shift, vp.y, vp.z );
                                                                }
                                                        } else { //move back if in sight again
                                                                vp = Vector3D(vp.x-shift, vp.y, vp.z);
                                                                if( ph.angleTo(vp.subtract(pp)) < ma
                                                                        && ( zoom || sd <= 15000 ) ) {
                                                                        v.position = vp;
                                                                }
                                                        }
                                                }
                                        }
                                }
                                //if(c > 45) log(w.name, ship.name+" end i:"+i+" c:"+c); //debug
			}
		}
	}
}
this.$RemoveAllTrails = function(w) {  
        var i, k, t, v;
        for( i = 0; i < w.$S.length; i++ ) {
                t = w.$S[i];
                if( t ) {
                        //remove the whole long trail
                        for( k = 3; k <= w.$Length; k++ ) {
                                v = t[k];
                                if( v && v.isValid ) v.remove();
                        }
                        w.$S[i][2] = 3;//reset visualEffect index
                }
        }
}
 |