| Scripts/rockHermitBeacons.js | "use strict";
this.name        = "RockHermitBeacons";
this.author      = "Dybal";
this.copyright   = "2020 Dybal";
this.license     = "CC BY-NC-SA 4.0";
this.description = "Adds beacons to Rock Hermits";
this.version     = "1.3";
this.$debug = false;
this.$logging = true;
this.$beep = new SoundSource;
this.$beep.sound = "rhb_chime.ogg";
this.$beaconCreationTimer;
// this.$rangeProbabilitiesProfiles has a list of profiles, each profile is a list of probablities for each range in this.$beaconRanges
this.$rangeProbabilitiesProfiles = [
                    // array of profiles with the probabilities for each beacon range
                    // the actual range value is found at similar position from the profile array in this.$beaconRanges
                    // probability for range 5 (range value 100000, unlimited) is 1-sum(probablities other ranges)
                    [ 0.02, 0.05, 0.15, 0.25, 0.30 ], // profile 0: system free+dangerous, honest
                    [ 0.10, 0.30, 0.20, 0.20, 0.15 ], // profile 1: system controlled+safe, honest 
                    [ 0.05, 0.20, 0.40, 0.20, 0.10 ], // profile 2: system meddling+safe, honest 
                    [ 0.05, 0.10, 0.40, 0.35, 0.10 ], // profile 3: system chaotic, honest 
                    [ 0.03, 0.07, 0.30, 0.30, 0.30 ], // profile 4: system free+dangerous, pirate
                    [ 0.05, 0.10, 0.40, 0.40, 0.05 ], // profile 5: system controlled+safe, pirate 
                    [ 0.05, 0.10, 0.30, 0.50, 0.05 ], // profile 6: system meddling+safe, pirate 
                    [ 0.05, 0.10, 0.40, 0.35, 0.10 ], // profile 7: system chaotic, pirate 
];
this.$beaconRanges = [   0,  100,  500, 1000, 2000, 100000 ]; // in km
this.$asteroidQtt =  [  50,  100,   50,   20,   20,     20 ];
this.$accumProbProfiles = []; // derived from this.$rangeProbabilitiesProfiles, each profile has the accumulated probabilities up to that range inclusive
this.$rolesRH = [ // Rock Hermit roles and the range probability profile (index of this.$rangeProbabilitiesProfiles) based on system government
//                                  by Government: An Fe Mu Di Cm Cf De Cp
    { role: "rockhermit",           rangeProfile: [ 0, 0, 3, 2, 1, 3, 2, 1 ], salt: 1 },
    { role: "rockhermit-chaotic",   rangeProfile: [ 0, 0, 3, 2, 1, 3, 2, 1 ], salt: 2 },
    { role: "rockhermit-pirate",    rangeProfile: [ 4, 4, 7, 6, 5, 7, 6, 5 ], salt: 3 },
    { role: "pirate-cove",          rangeProfile: [ 4, 4, 7, 6, 5, 7, 6, 5 ], salt: 4 }
];
this.$RHs = []; // list of Rock Hermits with beacons
this.$rangeVerifyTimer;
this.$populationSize = 20;  
//------------------------------------------------------------------------------------------------------------
// WorldScript Event Handlers
//
//------------------------------------------------------------------------------------------------------------
this.startUpComplete = this.shipExitedWitchspace = function _init() {
    var _rangeProfiles = this.$rangeProbabilitiesProfiles;
    var _acRangeProfiles = this.$accumProbProfiles;
    var i, j, r;
    for (i=0; i<_rangeProfiles.length; i++) {
        r = [];
        r.push(_rangeProfiles[i][0])
        for (j=1; j<_rangeProfiles[i].length; j++) {
            r.push(r[j-1] + _rangeProfiles[i][j]);
        }
        _acRangeProfiles.push(r);
    }
    // allow some some time for other OXPs to create the Rock Hermits
    if (this.$beaconsCreationTimer)
        this.$beaconsCreationTimer.start();
    else
        this.$beaconsCreationTimer = new Timer(this, this.$addBeacons, 3);
}
//------------------------------------------------------------------------------------------------------------
this.shipExitedWitchspace = function _shipExitedWitchspace() {
    if (this.$beaconsCreationTimer)
        this.$beaconsCreationTimer.start();
    else
        this.$beaconsCreationTimer = new Timer(this, this.$addBeacons, 3);
    this.$startRangeVerifyTimer();
}
//------------------------------------------------------------------------------------------------------------
this.shipWillEnterWitchspace = function _shipWillEnterWitchspace() {
    this.$stopRangeVerifyTimer(); 
    this.$stopAsteroidRepopulatorTimer();
    this.$RHs.length = 0;
}
//------------------------------------------------------------------------------------------------------------
this.shipLaunchedFromStation = function _shipWilllLaunchFromStation() {
    this.$startRangeVerifyTimer(); 
}
//------------------------------------------------------------------------------------------------------------
this.shipWillDockWithStation = function _shipWilllDockWithStation() {
    this.$stopRangeVerifyTimer(); 
}
//------------------------------------------------------------------------------------------------------------
// Internal functions
//
this.$addBeacons = function _addBeacons() {
    var ws = this;
    var _shipsWithPrimaryRole = system.shipsWithPrimaryRole.bind(system);
    var _scrambledPseudoRandomNumber = system.scrambledPseudoRandomNumber.bind(system);
    var _rolesDescList = this.$rolesRH;
    var _RHsWithBeacons = this.$RHs;
    var gov = system.government;
    var logging = ws.$logging;
    var i = _rolesDescList.length;
    var roleDesc, rh_list, j, rh, index, asteroids;
    delete this.$beaconsCreationTimer;
    while (i--) {
        roleDesc = _rolesDescList[i];
        if (ws.$debug) log (ws.name, "Adding beacons to Rock Hermits with role "+roleDesc.role);
        rh_list = _shipsWithPrimaryRole(roleDesc.role);
        var j = rh_list.length;
        while (j--) {
            rh = rh_list[j];
            if (rh.roles.indexOf("astromine") >= 0) {
                rh.script.$rhbCreateBeacon = "no";
                rh.script.$rhbAsteroidsQtt = 100;
            }
            if (rh.isValid) {
                if (!rh.script || !rh.script.$rhbCreateBeacon || rh.script.$rhbCreateBeacon !== "no") {
                    //  RH doesn't have a (creator/owner)-managed beacon
                    if (!rh.script) rh.setScript("oolite-default-ship-script");
                    rh.beaconLabel = rh.displayName; // nice name for ASC to display
                    rh.script.$rhbBeaconRange = this.$giveRange(_scrambledPseudoRandomNumber(roleDesc.salt), roleDesc.rangeProfile[gov]);
                    index = this.$beaconRanges.indexOf(rh.script.$rhbBeaconRange);
                    if (index >= 0) {
                        asteroids = this.$addAsteroids(this.$asteroidQtt[index], rh.position);
                        rh.script.$rhbAsteroidsQtt = asteroids.length;
                        if (logging) log(this.name, "Created "+asteroids.length+" asteroids around "+rh.displayName);
                    }
                    if (logging) log(ws.name, rh.displayName+" (role "+roleDesc.role+") has a beacon with range of "+rh.script.$rhbBeaconRange+"km");
                } else {
                    // RH is an astromine, OR
                    // RH creator said we shouldn't create a beacon, but let's look if we should manage visibility or asteroid field
                    if (logging) log(ws.name, rh.displayName+" is a managed Rock Hermit, any beacon should be turned On/Off by its creator");
                    if (rh.script && rh.script.$rhbAsteroidsQtt && rh.script.$rhbAsteroidsQtt > 0) {
                        asteroids = this.$addAsteroids(rh.script.$rhbAsteroidsQtt, rh.position);
                        if (logging) log(this.name, "Created "+asteroids.length+" asteroids around "+rh.displayName);
                    }
                }
                if (rh.script && rh.script.$rhbBeaconRange && rh.script.$rhbBeaconRange > 0)
                    _RHsWithBeacons.push(rh);
            }
        }
    }
    this.$startAsteroidRepopulatorTimer();
}
//------------------------------------------------------------------------------------------------------------
this.$repopulateAsteroids = function _repopulateAsteroids() {
    var ws = this;
    var _shipsWithPrimaryRole = system.shipsWithPrimaryRole.bind(system);
    var _scrambledPseudoRandomNumber = system.scrambledPseudoRandomNumber.bind(system);
    var _rolesDescList = this.$rolesRH;
    var _RHsWithBeacons = this.$RHs;
    var gov = system.government;
    var population_size = this.$populationSize;
    var logging = ws.$logging;
    var i = _rolesDescList.length;
    var roleDesc, rh_list, j, rh, n, asteroids;
    if (ws.$debug) log(this.name, "Verifying asteroid population around Rock Hermits");
    while (i--) {
        roleDesc = _rolesDescList[i];
        rh_list = _shipsWithPrimaryRole(roleDesc.role);
        var j = rh_list.length;
        while (j--) {
            rh = rh_list[j];
            if (rh.isValid) {
                if (rh.script && rh.script.$rhbAsteroidsQtt && rh.script.$rhbAsteroidsQtt > 0) {
                    n = system.countShipsWithPrimaryRole("asteroid", rh, 12000*Math.ceil(rh.script.$rhbAsteroidsQtt/population_size));
                    if (ws.$debug) log(this.name, "Found "+n+" asteroids less than "+12*Math.ceil(rh.script.$rhbAsteroidsQtt/population_size)+" km from "+rh.displayName);
                    if (n < rh.script.$rhbAsteroidsQtt) {
                        if (logging) log(this.name, "Asteroid population around "+rh.displayName+" decreased, there should be "+rh.script.$rhbAsteroidsQtt+", creating "+(rh.script.$rhbAsteroidsQtt - n)+" new asteroids");
                        this.$addAsteroids(rh.script.$rhbAsteroidsQtt - n, rh.position);
                    }
                }
            }
        }
    }
}
//------------------------------------------------------------------------------------------------------------
this.$addAsteroids = function _addAsteroids(num, pos) {
    var radius, n, i, asteroid;
    var asteroids = [];
    var population_size = this.$populationSize;
    var steps = Math.floor(n/population_size);
    i = 0;
    while (num > 0) {
        i++; 
        n = (num > population_size) ? population_size : num;
        num -= population_size;
        while (n--) { 
            asteroid = system.addShips("asteroid", 1, pos.add(Vector3D.randomDirection(i*12000)), 1);
            if (!asteroid) 
                log(this.name, "Could not add asteroid near Rock Hermit");
            else
                asteroids.push(asteroid);
        }
    }
    return asteroids;
}
//------------------------------------------------------------------------------------------------------------
this.$giveRange = function _giveRange(rnd, rangeProfileIndex) {
    var ws = this;
    var _rangeProfile = this.$accumProbProfiles[rangeProfileIndex];
    var _ranges = this.$beaconRanges;
    var i = 0;
    if (ws.$debug) log(ws.name, "accumulated range probabilities:"+_rangeProfile);
    while (i < _rangeProfile.length && rnd > _rangeProfile[i]) {
        if (ws.$debug) log(ws.name, i+":"+_rangeProfile[i]+", random:"+rnd.toFixed(3));
        i++ ;
    }
    if (ws.$debug) log(this.name, i+":"+_rangeProfile[i]+", random:"+rnd.toFixed(3));
    return _ranges[i];
}
//------------------------------------------------------------------------------------------------------------
this.$verifyBeaconsRange = function _switchBeaconsOn() {
    var ws = this;
    var _ship = player.ship;
    var _RHsWithBeacons = this.$RHs;
    var pos = _ship.position;
    var debug = ws.$debug;
    var logging = ws.$logging;
    var i = _RHsWithBeacons.length;
    var rh, distance;
    while (i--) {
        rh = _RHsWithBeacons[i];
        if (rh.isValid && rh.script && rh.script.$rhbBeaconRange) {
            distance = pos.distanceTo(rh)
            if (debug) log(this.name, rh.displayName+" has beacon range "+rh.script.$rhbBeaconRange+"km, distance:"+(distance/1000).toFixed(3)+", isVisible:"+rh.isVisible+", is ON:"+(rh.beaconCode != null))
            if (pos.distanceTo(rh) <= 1000 * rh.script.$rhbBeaconRange) {
                // beacon in range 
                if (!rh.script.$rhbCreateBeacon || rh.script.$rhbCreateBeacon != "no") {
                    //  RH's creator alows beacon visibility management
                    if (rh.beaconCode == null) {
                        // beaocn is Off, turn it On
                        rh.beaconCode = (rh.script.$rhbBeaconCode ? rh.script.$rhbBeaconCode : "Rock Hermit");
                        if (logging) log(ws.name, rh.displayName+" beacon in range");
                        if (_ship.equipmentStatus("EQ_ADVANCED_COMPASS") === "EQUIPMENT_OK") {
                            this.$beep.play();
                            player.commsMessage(rh.displayName+" beacon detected", 5)
                        }
                    }
                }
            } else if (rh.beaconCode) {
                // RH out of beacon range, turn beacon OFF
                rh.script.$rhbBeaconCode = rh.beaconCode;
                rh.beaconCode = null;
                if (logging) log(ws.name, rh.displayName+" beacon out of range");
            }
        }
    }
}
//------------------------------------------------------------------------------------------------------------
this.$startRangeVerifyTimer = function _startRangeVerifyTimer() {
    if (this.$rangeVerifyTimer)
        this.$rangeVerifyTimer.start()
    else
        this.$rangeVerifyTimer = new Timer(this, this.$verifyBeaconsRange, 1, 2);
}
//------------------------------------------------------------------------------------------------------------
this.$stopRangeVerifyTimer = function _stopRangeVerifyTimer() {
    if (this.$rangeVerifyTimer) {
        if (this.$rangeVerifyTimer.isRunning)
            this.$rangeVerifyTimer.stop();
        delete this.$rangeVerifyTimer;
    }
}
//------------------------------------------------------------------------------------------------------------
this.$startAsteroidRepopulatorTimer = function _startAsteroidRepopulatorTimer() {
    if (this.$asteroidRepopulatorTimer)
        this.$asteroidRepopulatorTimer.start()
    else
        this.$asteroidRepopulatorTimer = new Timer(this, this.$repopulateAsteroids, 1, 300); // every 5 minutes
}
//------------------------------------------------------------------------------------------------------------
this.$stopAsteroidRepopulatorTimer = function _stopAsteroidRepopulatorTimer() {
    if (this.$asteroidRepopulatorTimer) {
        if (this.$asteroidRepopulatorTimer.isRunning)
            this.$asteroidRepopulatorTimer.stop()
        delete this.$asteroidRepopulatorTimer;
    }
}
 |