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
}
}
}
|