/*
    TWANG32 - An ESP32 port of TWANG
    (c) B. Dring 3/2018
    License: Creative Commons 4.0 Attribution - Share Alike

    TWANG was originally created by Critters
    https://github.com/Critters/TWANG

    It was inspired by Robin Baumgarten's Line Wobbler Game

    Recent Changes
    - Updated to move FastLEDshow to core 0
    Fixed Neopixel
      - Used latest FastLED library..added compile check
        - Updated to move FastLEDshow to core 0
        - Reduced MAX_LEDS on Neopixel
        - Move some defines to a separate config.h file to make them accessible to other files
    - Changed volume typo on serial port settings menu

    Project TODO
        - Make the strip type configurable via serial and wifi


    Usage Notes:
        - Changes attack model and added Player class
        - Changes to LED strip and other hardware are in config.h
        - Change the strip type to what you are using and compile/load firmware
        - Use Serial port or Wifi to set your strip length.
*/

//

#define VERSION "2018-06-28"

#include <FastLED.h>
#include <Wire.h>
#include "Arduino.h"
#include "RunningMedian.h"

// twang files
#include "config.h"
#ifdef USE_GYRO_JOYSTICK
#include "twang_mpu.h"
#endif
#include "Enemy.h"
#include "Player.h"
#include "Particle.h"
#include "Spawner.h"
#include "Lava.h"
#include "Boss.h"
#include "Conveyor.h"
#include "iSin.h"
#include "sound.h"
#include "settings.h"
#include "wifi_ap.h"
#include "colors.h"

#if defined(FASTLED_VERSION) && (FASTLED_VERSION < 3001000)
    #error "Requires FastLED 3.1 or later; check github for latest code."
#endif



#define DIRECTION            1
#define USE_GRAVITY          0     // 0/1 use gravity (LED strip going up wall)
#define BEND_POINT           550   // 0/1000 point at which the LED strip goes up the wall

// Players settings
#define MAX_PLAYERS 4
// this can become a setting in the future
#define NUM_PLAYERS 2

// GAME
long previousMillis = 0;           // Time of the last redraw
int levelNumber = 0;

#define TIMEOUT              60000  // time until screen saver in milliseconds

int joystickTilt[NUM_PLAYERS] = {0};              // Stores the angle of the joystick
int joystickWobble[NUM_PLAYERS] = {0};            // Stores the max amount of acceleration (wobble)

// WOBBLE ATTACK
#define DEFAULT_ATTACK_WIDTH 70  // Width of the wobble attack, world is 1000 wide
int attack_width = DEFAULT_ATTACK_WIDTH;
#define ATTACK_DURATION     1000   // Duration of a wobble attack (ms)
#define BOSS_WIDTH          40

// TODO all animation durations should be defined rather than literals
// because they are used in main loop and some sounds too.
#define STARTUP_WIPEUP_DUR 200
#define STARTUP_SPARKLE_DUR 1300
#define STARTUP_FADE_DUR 1500
#define COLOR_SELECT_DUR 1000

#define GAMEOVER_SPREAD_DURATION 1000
#define GAMEOVER_FADE_DURATION 1500

#define WIN_FILL_DURATION 500     // sound has a freq effect that might need to be adjusted
#define WIN_CLEAR_DURATION 1000
#define WIN_OFF_DURATION 1200

#ifdef USE_GYRO_JOYSTICK
Twang_MPU accelgyro = Twang_MPU();
#endif
CRGB leds[VIRTUAL_LED_COUNT];
RunningMedian MPUAngleSamples = RunningMedian(5);
RunningMedian MPUWobbleSamples = RunningMedian(5);
iSin isin = iSin();

//#define JOYSTICK_DEBUG  // comment out to stop serial debugging

// POOLS
#define LIFE_LEDS 3
int lifeLEDs[LIFE_LEDS] = {7, 6, 5}; // these numbers are Arduino GPIO numbers...this is not used in the B. Dring enclosure design

#define ENEMY_COUNT 10
Enemy enemyPool[ENEMY_COUNT] = {
    Enemy(), Enemy(), Enemy(), Enemy(), Enemy(), Enemy(), Enemy(), Enemy(), Enemy(), Enemy()
};


#define PARTICLE_COUNT 100
Particle particlePool[PARTICLE_COUNT] = {
    Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle()
};

#define SPAWN_COUNT 5
Spawner spawnPool[SPAWN_COUNT] = {
    Spawner(), Spawner()
};

#define LAVA_COUNT 5
Lava lavaPool[LAVA_COUNT] = {
    Lava(), Lava(), Lava(), Lava()
};

#define CONVEYOR_COUNT 2
Conveyor conveyorPool[CONVEYOR_COUNT] = {
    Conveyor(), Conveyor()
};

Boss boss = Boss();

Player player[NUM_PLAYERS] = {
    Player(0, LIVES_PER_LEVEL, ATTACK_DURATION),
    Player(1, LIVES_PER_LEVEL, ATTACK_DURATION)
};

enum stages {
    STARTUP,
    PLAY,
    WIN,
    DEAD,
    SCREENSAVER,
    BOSS_KILLED,
    GAMEOVER
} stage;

long stageStartTime;               // Stores the time the stage changed for stages that are time based
long killTime;
bool lastLevel = false;
int killPos = 0;

int score = 0;

#define FASTLED_SHOW_CORE 0  // -- The core to run FastLED.show()

// -- Task handles for use in the notifications
static TaskHandle_t FastLEDshowTaskHandle = 0;
static TaskHandle_t userTaskHandle = 0;

/** show() for ESP32
 *  Call this function instead of FastLED.show(). It signals core 0 to issue a show,
 *  then waits for a notification that it is done.
 */
void FastLEDshowESP32()
{
    if (userTaskHandle == 0) {
        // -- Store the handle of the current task, so that the show task can
        //    notify it when it's done
        userTaskHandle = xTaskGetCurrentTaskHandle();

        // -- Trigger the show task
        xTaskNotifyGive(FastLEDshowTaskHandle);

        // -- Wait to be notified that it's done
        const TickType_t xMaxBlockTime = pdMS_TO_TICKS( 200 );
        ulTaskNotifyTake(pdTRUE, xMaxBlockTime);
        userTaskHandle = 0;
    }
}

/** show Task
 *  This function runs on core 0 and just waits for requests to call FastLED.show()
 */
void FastLEDshowTask(void *pvParameters)
{
    // -- Run forever...
    for(;;) {
        // -- Wait for the trigger
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);

        // -- Do the show (synchronously)
        FastLED.show();

        // -- Notify the calling task
        xTaskNotifyGive(userTaskHandle);
    }
}


void setup() {
    Serial.begin(115200);
    Serial.print("\r\nTWANG32 VERSION: "); Serial.println(VERSION);

    settings_init();    // load the user settings from EEPROM

    Wire.begin();
#ifdef USE_GYRO_JOYSTICK
    accelgyro.initialize();
#endif

#ifdef USE_NEOPIXEL
    Serial.print("\r\nCompiled for WS2812B (Neopixel) LEDs");
    FastLED.addLeds<LED_TYPE, DATA_PIN>(leds, MAX_LEDS);
#endif

#ifdef USE_APA102
    Serial.print("\r\nCompiled for APA102 (Dotstar) LEDs");
    FastLED.addLeds<LED_TYPE, DATA_PIN, CLOCK_PIN, LED_COLOR_ORDER>(leds, MAX_LEDS);
#endif

    FastLED.setBrightness(user_settings.led_brightness);
    FastLED.setDither(1);

    // -- Create the ESP32 FastLED show task
    xTaskCreatePinnedToCore(FastLEDshowTask, "FastLEDshowTask", 2048, NULL, 2, &FastLEDshowTaskHandle, FASTLED_SHOW_CORE);

    sound_init(DAC_AUDIO_PIN);

#ifndef USE_GYRO_JOYSTICK
    pinMode(upButtonPinNumber1, INPUT_PULLUP);
    pinMode(downButtonPinNumber1, INPUT_PULLUP);
    pinMode(leftButtonPinNumber1, INPUT_PULLUP);
    //pinMode(rightButtonPinNumber, INPUT_PULLUP);

    pinMode(upButtonPinNumber2, INPUT_PULLUP);
    pinMode(downButtonPinNumber2, INPUT_PULLUP);
    pinMode(leftButtonPinNumber2, INPUT_PULLUP);
    //pinMode(rightButtonPinNumber, INPUT_PULLUP);
#endif

    ap_setup();

    stage = STARTUP;
    stageStartTime = millis();

    // set players color
    player[0].setColor(0,255,0);
    player[1].setColor(127,2,255);
    for (Player& p : player)
        p.Lives ( user_settings.lives_per_level );

}

void loop() {
  long mm = millis();
  int brightness = 0;

  ap_client_check(); // check for web client
  checkSerialInput();

  if(stage == PLAY){
      bool attacking = false;
      for (const Player& p: player) attacking = attacking || p.attacking;

      if(attacking){
          SFXattacking();
      }else{
          int tilt = -1000;
          for (int i=0; i<NUM_PLAYERS; i++) tilt = max(joystickTilt[i], tilt);
          SFXtilt(tilt);
      }
  }else if(stage == DEAD){
      SFXdead();
  }

  if (mm - previousMillis >= MIN_REDRAW_INTERVAL) {
      getInput();

      long frameTimer = mm;
      previousMillis = mm;

      // check activity of any player, used to stop screensaver
      bool isMoving = false;
      for (int i=0; i < NUM_PLAYERS; i++) {
          isMoving = isMoving ||
         ((abs(joystickTilt[i]) > user_settings.joystick_deadzone) ||
          (abs(joystickWobble[i]) >= user_settings.attack_threshold));
      }

      if(isMoving)
        {
            lastInputTime = mm;
            if(stage == SCREENSAVER){
                levelNumber = -1;
                stageStartTime = mm;
                stage = WIN;
            }
        }else{
            if(lastInputTime+TIMEOUT < mm){
                stage = SCREENSAVER;
            }
        }

        if(stage == SCREENSAVER){
            screenSaverTick();
        }else if(stage == STARTUP){
            if (stageStartTime+STARTUP_FADE_DUR > mm) {
                tickStartup(mm);
            }
            else
            {
            SFXcomplete();
            levelNumber = 0;
            loadLevel();
            }
        }else if(stage == PLAY){
            // PLAYING

            // allow selecting player color during first sec of first level
            if (levelNumber == 0) {
                if ((stageStartTime + COLOR_SELECT_DUR) > mm) {
                    for (Player& p : player) {
                        if (joystickWobble[p.Id()] >= user_settings.attack_threshold) {
                            random_player_color(p);
                            stageStartTime = mm;
                            return;
                        }
                    }
                }
            }

            // real action here
            for (Player& p : player) {

                if (p.captured) continue;

                if (joystickWobble[p.Id()] >= user_settings.attack_threshold) p.startAttack(mm);

                p.updateState(
                    abs(joystickTilt[p.Id()]) > user_settings.joystick_deadzone,
                    abs(joystickWobble[p.Id()]) >= user_settings.attack_threshold,
                    mm);

                // If still not attacking, move!
                p.moveby(p.speed);
                if(!p.attacking){
                    SFXtilt(joystickTilt[p.Id()]);
                    //int moveAmount = (joystickTilt/6.0);  // 6.0 is ideal at 16ms interval (6.0 / (16.0 / MIN_REDRAW_INTERVAL))
                    int moveAmount = (joystickTilt[p.Id()]/(6.0));  // 6.0 is ideal at 16ms interval
                    if(DIRECTION) moveAmount = -moveAmount;
                    moveAmount = constrain(moveAmount, -MAX_PLAYER_SPEED, MAX_PLAYER_SPEED);

                    p.moveby( -moveAmount );

                    // stop player from leaving if boss is alive
                    if (boss.Alive() && p.position >= VIRTUAL_LED_COUNT) // move player back
                        p.moveto( 999 ); //(user_settings.led_count - 1) * (1000.0/user_settings.led_count);

                    if(p.position >= VIRTUAL_LED_COUNT && !boss.Alive()) {
                        // Reached exit!
                        levelComplete();
                        return;
                    }
                }

                if(inLava(p.position)){
                    die(p);
                }
            }

            // Ticks and draw calls
            FastLED.clear();

            // exploding players during PLAY stage
            tickParticles();

            for (Player& p : player)
                tickConveyors(p);

            tickSpawners();
            tickBoss();
            tickLava();

            for (Player& p : player) if (!p.captured) tickEnemies(p);
            for (const Player& p : player) if (!p.captured) drawAttack(p);
            for (const Player& p : player) if (!p.captured) drawPlayer(p);

            drawExit();
        }else if(stage == DEAD){
            // DEAD
            FastLED.clear();
            tickDie(mm);
            if(!tickParticles()){
                loadLevel();
            }
        }else if(stage == WIN){
            // LEVEL COMPLETE
            tickWin(mm);
        }else if(stage == BOSS_KILLED){
            tickBossKilled(mm);
        } else if (stage == GAMEOVER) {
            if (stageStartTime+GAMEOVER_FADE_DURATION > mm)
            {
                tickGameover(mm);
            } else {
                FastLED.clear();
                save_game_stats(false);     // boss not killed

                // restart from the beginning
                stage = STARTUP;
                stageStartTime = millis();
                for (Player& p : player)
                    p.Lives ( user_settings.lives_per_level );
            }
        }
        //FastLED.show();
        FastLEDshowESP32();
    }
}

// ---------------------------------
// ------------ LEVELS -------------
// ---------------------------------
void loadLevel(){
    // leave these alone
    FastLED.setBrightness(user_settings.led_brightness);
    updateLives();
    cleanupLevel();
    lastLevel = false; // this gets changed on the boss level

    /// Defaults...OK to change the following items in the levels below
    attack_width = DEFAULT_ATTACK_WIDTH;
    for (Player& p : player)
        p.Spawn( 0 + p.Id()*10 ); // slightly displace players

    /* ==== Level Editing Guide ===============
    Level creation is done by adding to or editing the switch statement below

    You can add as many levels as you want but you must have a "case"
    for each level. The case numbers must start at 0 and go up without skipping any numbers.

    Don't edit case 0 or the last (boss) level. They are special cases and editing them could
    break the code.

    TWANG uses a virtual 1000 LED grid. It will then scale that number to your strip, so if you
    want something in the middle of your strip use the number 500. Consider the size of your strip
    when adding features. All time values are specified in milliseconds (1/1000 of a second)

    You can add any of the following features.

    Enemies: You add up to 10 enemies with the spawnEnemy(...) functions.
        spawnEnemy(position, direction, speed, wobble);
            position: Where the enemy starts
            direction: If it moves, what direction does it go 0=down, 1=away
            speed: How fast does it move. Typically 1 to 4.
            wobble: 0=regular movement, 1=bouncing back and forth use the speed value
                to set the length of the wobble.

    Spawn Pools: This generates and endless source of new enemies. 2 pools max
        spawnPool[index].Spawn(position, rate, speed, direction, activate);
            index: You can have up to 2 pools, use an index of 0 for the first and 1 for the second.
            position: The location the enemies with be generated from.
            rate: The time in milliseconds between each new enemy
            speed: How fast they move. Typically 1 to 4.
            direction: Directions they go 0=down, 1=away
            activate: The delay in milliseconds before the first enemy

    Lava: You can create 4 pools of lava.
        spawnLava(left, right, ontime, offtime, offset, state, grow, flow);
            left: the lower end of the lava pool
            right: the upper end of the lava pool
            ontime: How long the lave stays on.
            offset: the delay before the first switch
            state: does it start on or off
            grow: This specifies the rate of growth. Use 0 for no growth. Reasonable growth is 0.1 to 0.5
            flow: This specifies the rate/direction of flow. Reasonable numbers are 0.2 to 0.8

    Conveyor: You can create 2 conveyors.
        spawnConveyor(startPoint, endPoint, direction)
            startPoint: The close end of the conveyor
            endPoint: The far end of the conveyor
            direction(speed): positive = away, negative = towards you (must be less than +/- player speed)

    ===== Other things you can adjust per level ================

        Player Start position:
            player.Spawn( xxx );


        The size of the TWANG attack
            attack_width = xxx;


    */
    switch(levelNumber){
    case 0: // basic introduction
        for (Player& p : player)
            p.Spawn( 200 + p.Id()*10 );
        spawnEnemy(1, 0, 0, 0);
        break;
    case 1:
        // Slow moving enemy
        spawnEnemy(900, 0, 1, 0);
        break;
    case 2:
        // Spawning enemies at exit every 2 seconds
        spawnPool[0].Spawn(1000, 3000, 2, 0, 0);
        break;
    case 3:
        // Lava intro
        spawnLava(400, 490, 2000, 2000, 0, Lava::OFF, 0, 0);
        spawnEnemy(350, 0, 1, 0);
        spawnPool[0].Spawn(1000, 5500, 3, 0, 0);
        break;
    case 4:
        // intro to moving lava (down)
        spawnLava(400, 490, 2000, 2000, 0, Lava::OFF, 0, -0.5);
        spawnEnemy(350, 0, 1, 0);
        spawnPool[0].Spawn(1000, 5500, 3, 0, 0);
        break;
    case 5:
        // lava spreading
        spawnLava(400, 450, 2000, 2000, 0, Lava::OFF, 0.25, 0);
        spawnEnemy(350, 0, 1, 0);
        spawnPool[0].Spawn(1000, 5500, 3, 0, 0);
        break;
    case 6:
        // Sin wave enemy
        spawnEnemy(700, 1, 7, 275);
        spawnEnemy(500, 1, 5, 250);
        break;
    case 7:
        // Sin enemy swarm
        spawnEnemy(700, 1, 7, 275);
        spawnEnemy(500, 1, 5, 250);

        spawnEnemy(600, 1, 7, 200);
        spawnEnemy(800, 1, 5, 350);

        spawnEnemy(400, 1, 7, 150);
        spawnEnemy(450, 1, 5, 400);
        break;
    case 8:
        // lava moving up
        for (Player& p : player)
            p.Spawn( 200 + p.Id()*10 );
        spawnLava(10, 180, 2000, 2000, 0, Lava::OFF, 0, 0.5);
        spawnEnemy(350, 0, 1, 0);
        spawnPool[0].Spawn(1000, 5500, 3, 0, 0);
        break;
    case 9:
        // Conveyor
        spawnConveyor(100, 600, -6);
        spawnEnemy(800, 0, 0, 0);
        break;
    case 10:
        // Conveyor of enemies
        spawnConveyor(50, 1000, 6);
        spawnEnemy(300, 0, 0, 0);
        spawnEnemy(400, 0, 0, 0);
        spawnEnemy(500, 0, 0, 0);
        spawnEnemy(600, 0, 0, 0);
        spawnEnemy(700, 0, 0, 0);
        spawnEnemy(800, 0, 0, 0);
        spawnEnemy(900, 0, 0, 0);
        break;
    case 11:
        // lava spread and fall
        spawnLava(400, 450, 2000, 2000, 0, Lava::OFF, 0.2, -0.5);
        spawnEnemy(350, 0, 1, 0);
        spawnPool[0].Spawn(1000, 5500, 3, 0, 0);
        break;
    case 12:   // spawn train;
        spawnPool[0].Spawn(900, 1300, 2, 0, 0);
        break;
    case 13:   // spawn train skinny attack width;
        attack_width = 32;
        spawnPool[0].Spawn(900, 1800, 2, 0, 0);
        break;
    case 14:  // evil fast split spawner
        spawnPool[0].Spawn(550, 1500, 2, 0, 0);
        spawnPool[1].Spawn(550, 1500, 2, 1, 0);
        break;
    case 15: // split spawner with exit blocking lava
        spawnPool[0].Spawn(500, 1200, 2, 0, 0);
        spawnPool[1].Spawn(500, 1200, 2, 1, 0);
        spawnLava(900, 950, 2200, 800, 2000, Lava::OFF, 0, 0);
        break;
    case 16:
        // Lava run
        spawnLava(195, 300, 2000, 2000, 0, Lava::OFF, 0, 0);
        spawnLava(400, 500, 2000, 2000, 0, Lava::OFF, 0, 0);
        spawnLava(600, 700, 2000, 2000, 0, Lava::OFF, 0, 0);
        spawnPool[0].Spawn(1000, 3800, 4, 0, 0);
        break;
    case 17:
        // Sin enemy #2 practice (slow conveyor)
        spawnEnemy(700, 1, 7, 275);
        spawnEnemy(500, 1, 5, 250);
        spawnPool[0].Spawn(1000, 5500, 4, 0, 3000);
        spawnPool[1].Spawn(0, 5500, 5, 1, 10000);
        spawnConveyor(100, 900, -4);
        break;
    case 18:
        // Sin enemy #2 (fast conveyor)
        spawnEnemy(800, 1, 7, 275);
        spawnEnemy(700, 1, 7, 275);
        spawnEnemy(500, 1, 5, 250);
        spawnPool[0].Spawn(1000, 3000, 4, 0, 3000);
        spawnPool[1].Spawn(0, 5500, 5, 1, 10000);
        spawnConveyor(100, 900, -6);
        break;
    case 19: // (don't edit last level)
        // Boss this should always be the last level
        spawnBoss();
        break;
    }
    stageStartTime = millis();
    stage = PLAY;
}

void spawnBoss(){
    lastLevel = true;
    boss.Spawn();
    moveBoss();
}

void moveBoss(){
    int spawnSpeed = 1800;
    if(boss._lives == 2) spawnSpeed = 1600;
    if(boss._lives == 1) spawnSpeed = 1000;
    spawnPool[0].Spawn(boss._pos, spawnSpeed, 3, 0, 0);
    spawnPool[1].Spawn(boss._pos, spawnSpeed, 3, 1, 0);
}

/* ======================== spawn Functions =====================================

   The following spawn functions add items to pools by looking for an unactive
   item in the pool. You can only add as many as the ..._COUNT. Additonal attemps
   to add will be ignored.

   ==============================================================================
*/
void spawnEnemy(int pos, int dir, int speed, int wobble){
    for(int e = 0; e<ENEMY_COUNT; e++){  // look for one that is not alive for a place to add one
        if(!enemyPool[e].Alive()){
            enemyPool[e].Spawn(pos, dir, speed, wobble);
            // for each player, save if it is above or below
            for (const Player& p : player)
                enemyPool[e].playersSide[p.Id()] = pos > p.position?1:-1;
            return;
        }
    }
}

void spawnLava(int left, int right, int ontime, int offtime, int offset, int state, float grow, float flow){
    for(int i = 0; i<LAVA_COUNT; i++){
        if(!lavaPool[i].Alive()){
            lavaPool[i].Spawn(left, right, ontime, offtime, offset, state, grow, flow);
            return;
        }
    }
}

void spawnConveyor(int startPoint, int endPoint, int dir){
    for(int i = 0; i<CONVEYOR_COUNT; i++){
        if(!conveyorPool[i]._alive){
            conveyorPool[i].Spawn(startPoint, endPoint, dir);
            return;
        }
    }
}

void cleanupLevel(){
    for(int i = 0; i<ENEMY_COUNT; i++){
        enemyPool[i].Kill();
    }
    for(int i = 0; i<PARTICLE_COUNT; i++){
        particlePool[i].Kill();
    }
    for(int i = 0; i<SPAWN_COUNT; i++){
        spawnPool[i].Kill();
    }
    for(int i = 0; i<LAVA_COUNT; i++){
        lavaPool[i].Kill();
    }
    for(int i = 0; i<CONVEYOR_COUNT; i++){
        conveyorPool[i].Kill();
    }
    boss.Kill();
}

void levelComplete(){
    stageStartTime = millis();
    stage = WIN;

    if (lastLevel) {
        stage = BOSS_KILLED;
    }
    if (levelNumber != 0)  // no points for the first level
    {
        score = score + (player[0].Lives() * 10);  //
    }
}

void nextLevel(){
    levelNumber ++;

    if(lastLevel)   {
        stage = STARTUP;
        stageStartTime = millis();

        for (Player& p : player)
            p.Lives ( user_settings.lives_per_level );

    } else {
        for (Player& p : player) {
            if (!p.Alive()) p.Lives ( user_settings.lives_per_level );
        }

        loadLevel();
    }
}

void gameOver(){

    levelNumber = 0;
    loadLevel();
}

void die(Player& p){

    //if(levelNumber > 0) // This messes up everything in 2-players mode, but can be restored...
    p.Kill();

    // how many players are still playing the level?
    int stillPlaying = 0;
    // how many players have lives?
    bool allDead = true;
    for (const Player& pp : player) {
        stillPlaying += !pp.captured;
        allDead &= !pp.Alive();
    }

    for(int ip = 0; ip < PARTICLE_COUNT; ip++){
        if(stillPlaying>0) particlePool[ip].Spawn(p.position, 25, 3, p.color);
        else particlePool[ip].Spawn(p.position, 200, 1, p.color); // final explosion
    }

    // someone is still playing the level, continue (an animation for the dead player should be added)
    if(stillPlaying>0) return;

    // all players have been captured, do any of them have lives left?
    // This has already been checked in allDead above.
    if( allDead ){
       stage = GAMEOVER;
       stageStartTime = millis();
    }
    else // some still have lifes to live, go to DEAD stage and repat the level
    {
      stageStartTime = millis();
      stage = DEAD;
    }
    killTime = millis();
    killPos = p.position;
}

// ----------------------------------
// -------- TICKS & RENDERS ---------
// ----------------------------------
void tickStartup(long mm)
{
  FastLED.clear();
  if(stageStartTime+STARTUP_WIPEUP_DUR > mm) // fill to the top with green
  {
    int n = _min(map(((mm-stageStartTime)), 0, STARTUP_WIPEUP_DUR, 0, user_settings.led_count), user_settings.led_count);  // fill from top to bottom
    for(int i = 0; i<= n; i++){
      leds[i] = CRGB(0, 255, 0);
    }
  }
  else if(stageStartTime+STARTUP_SPARKLE_DUR > mm) // sparkle the full green bar
  {
    for(int i = 0; i< user_settings.led_count; i++){
      if(random8(30) < 28)
        leds[i] = CRGB(0, 255, 0);  // most are green
      else {
        int flicker = random8(250);
        leds[i] = CRGB(flicker, 150, flicker); // some flicker brighter
      }
    }

  }
  else if (stageStartTime+STARTUP_FADE_DUR > mm) // fade it out to bottom
  {
    int n = _max(map(((mm-stageStartTime)), STARTUP_SPARKLE_DUR, STARTUP_FADE_DUR, 0, user_settings.led_count), 0);  // fill from top to bottom
    int brightness = _max(map(((mm-stageStartTime)), STARTUP_SPARKLE_DUR, STARTUP_FADE_DUR, 255, 0), 0);
    // for(int i = 0; i<= n; i++){

      // leds[i] = CRGB(0, brightness, 0);
    // }
    for(int i = n; i< user_settings.led_count; i++){

      leds[i] = CRGB(0, brightness, 0);
    }
  }
  SFXFreqSweepWarble(STARTUP_FADE_DUR, millis()-stageStartTime, 40, 400, 20);

}

void tickEnemies(Player& p){
    for(int i = 0; i<ENEMY_COUNT; i++){
        if(enemyPool[i].Alive()){
            enemyPool[i].Tick();
            // Hit attack?
            if(p.attacking){
                if(enemyPool[i]._pos > p.position-(attack_width/2) && enemyPool[i]._pos < p.position+(attack_width/2)){
                   enemyPool[i].Kill();
                   SFXkill();
                }
            }
            if(inLava(enemyPool[i]._pos)){
                enemyPool[i].Kill();
                SFXkill();
            }
            // Draw (if still alive)
            if(enemyPool[i].Alive()) {
                leds[getLED(enemyPool[i]._pos)] = CRGB(255, 0, 0);
            }
            // Hit player?
            if(
                (enemyPool[i].playersSide[p.Id()] == 1 && enemyPool[i]._pos <= p.position) ||
                (enemyPool[i].playersSide[p.Id()] == -1 && enemyPool[i]._pos >= p.position)
            ){
                Serial.println("Killing player\n");
                Serial.println(p.Id());
                die(p);
                return;
            }
        }
    }
}

void tickBoss(){
    // DRAW
    if(boss.Alive()){
        boss._ticks ++;
        for(int i = getLED(boss._pos-BOSS_WIDTH/2); i<=getLED(boss._pos+BOSS_WIDTH/2); i++){
            leds[i] = CRGB::DarkRed;
            leds[i] %= 100;
        }

        for (Player& p : player)
        {
            // CHECK COLLISION
            if(getLED(p.position) > getLED(boss._pos - BOSS_WIDTH/2) && getLED(p.position) < getLED(boss._pos + BOSS_WIDTH)){
                die(p);
                return;
            }
            // CHECK FOR ATTACK
            if(p.attacking){
                if(
                        (getLED(p.position+(attack_width/2)) >= getLED(boss._pos - BOSS_WIDTH/2) && getLED(p.position+(attack_width/2)) <= getLED(boss._pos + BOSS_WIDTH/2)) ||
                        (getLED(p.position-(attack_width/2)) <= getLED(boss._pos + BOSS_WIDTH/2) && getLED(p.position-(attack_width/2)) >= getLED(boss._pos - BOSS_WIDTH/2))
                        ){
                    boss.Hit();
                    if(boss.Alive()){
                        moveBoss();
                    }else{
                        spawnPool[0].Kill();
                        spawnPool[1].Kill();
                    }
                }
            }
        }
    }
}

void drawPlayer(const Player& p){
    if (p.captured) return;
    if (!p.Alive()) return;
    leds[getLED(p.position)] = CRGB(p.color[0], p.color[1], p.color[2]);
}

void drawExit(){
    if(!boss.Alive()){
        leds[user_settings.led_count-1] = CRGB(0, 0, 255);
    }
}

void tickSpawners(){
    long mm = millis();
    for(int s = 0; s<SPAWN_COUNT; s++){
        if(spawnPool[s].Alive() && spawnPool[s]._activate < mm){
            if(spawnPool[s]._lastSpawned + spawnPool[s]._rate < mm || spawnPool[s]._lastSpawned == 0){
                spawnEnemy(spawnPool[s]._pos, spawnPool[s]._dir, spawnPool[s]._sp, 0);
                spawnPool[s]._lastSpawned = mm;
            }
        }
    }
}

void tickLava(){
    int A, B, p, i, brightness, flicker;
    long mm = millis();

    Lava LP;
    for(i = 0; i<LAVA_COUNT; i++){
        LP = lavaPool[i];
        if(LP.Alive()){
            LP.Update(); // for grow and flow
            A = getLED(LP._left);
            B = getLED(LP._right);
            if(LP._state == Lava::OFF){
                if(LP._lastOn + LP._offtime < mm){
                    LP._state = Lava::ON;
                    LP._lastOn = mm;
                }
                for(p = A; p<= B; p++){
                    flicker = random8(LAVA_OFF_BRIGHTNESS);
                    leds[p] = CRGB(LAVA_OFF_BRIGHTNESS+flicker, (LAVA_OFF_BRIGHTNESS+flicker)/1.5, 0);
                }
            }else if(LP._state == Lava::ON){
                if(LP._lastOn + LP._ontime < mm){
                    LP._state = Lava::OFF;
                    LP._lastOn = mm;
                }
                for(p = A; p<= B; p++){
                    if(random8(30) < 29)
                    leds[p] = CRGB(150, 0, 0);
                    else
                    leds[p] = CRGB(180, 100, 0);
                }
            }
        }
        lavaPool[i] = LP;
    }
}

bool tickParticles(){
    // TODO: the new version of this function is a bit stupid. Should be rewritten.
    bool stillActive = false;
    float brightness;
    uint8_t r, g, b;

    // loop over particles
    for(int p = 0; p < PARTICLE_COUNT; p++){
        if(particlePool[p].Alive()){
            particlePool[p].Tick(USE_GRAVITY);

            if (particlePool[p]._power < 5) brightness = (float) (5 - particlePool[p]._power) * 10 / 255;
            else brightness = (float) particlePool[p]._power / 255;

            r = (uint8_t) ( particlePool[p]._color[0] * brightness);
            g = (uint8_t) ( particlePool[p]._color[1] * brightness);
            b = (uint8_t) ( particlePool[p]._color[2] * brightness);

            leds[getLED(particlePool[p]._pos)] += CRGB(r, g, b);

            stillActive = true;
        }
    }
    return stillActive;
}

void tickConveyors(Player& p){

    //TODO should the visual speed be proportional to the conveyor speed?

    int b, speed, n, i, ss, ee, led;
    long m = 10000+millis();
    p.speed = 0;

    int levels = 5; // brightness levels in conveyor


    for(i = 0; i<CONVEYOR_COUNT; i++){
        if(conveyorPool[i]._alive){
            speed = constrain(conveyorPool[i]._speed, -MAX_PLAYER_SPEED+1, MAX_PLAYER_SPEED-1);
            ss = getLED(conveyorPool[i]._startPoint);
            ee = getLED(conveyorPool[i]._endPoint);
            for(led = ss; led<ee; led++){

                n = (-led + (m/100)) % levels;
                if(speed < 0)
                    n = (led + (m/100)) % levels;

                b = map(n, 5, 0, 0, CONVEYOR_BRIGHTNESS);
                if(b > 0)
                    leds[led] = CRGB(0, 0, b);
            }

            if(p.position > conveyorPool[i]._startPoint && p.position < conveyorPool[i]._endPoint){
                p.speed = speed;
            }
        }
    }
}

void tickComplete(long mm)  // the boss is dead
{
  int brightness = 0;
  FastLED.clear();
  SFXcomplete();
  if(stageStartTime+500 > mm){
    int n = _max(map(((mm-stageStartTime)), 0, 500, user_settings.led_count, 0), 0);
    for(int i = user_settings.led_count; i>= n; i--){
      brightness = (sin(((i*10)+mm)/500.0)+1)*255;
      leds[i].setHSV(brightness, 255, 50);
    }
  }else if(stageStartTime+5000 > mm){
    for(int i = user_settings.led_count; i>= 0; i--){
      brightness = (sin(((i*10)+mm)/500.0)+1)*255;
      leds[i].setHSV(brightness, 255, 50);
    }
  }else if(stageStartTime+5500 > mm){
    int n = _max(map(((mm-stageStartTime)), 5000, 5500, user_settings.led_count, 0), 0);
    for(int i = 0; i< n; i++){
      brightness = (sin(((i*10)+mm)/500.0)+1)*255;
      leds[i].setHSV(brightness, 255, 50);
    }
  }else{
    nextLevel();
  }
}

void tickBossKilled(long mm) // boss funeral
{
    static uint8_t gHue = 0;

    FastLED.setBrightness(255); // super bright!

    int brightness = 0;
    FastLED.clear();

    if(stageStartTime+6500 > mm){
        gHue++;
        fill_rainbow( leds, user_settings.led_count, gHue, 7); // FastLED's built in rainbow
        if( random8() < 200) {  // add glitter
            leds[ random16(user_settings.led_count) ] += CRGB::White;
        }
        SFXbosskilled();
    }else if(stageStartTime+7000 > mm){
        int n = _max(map(((mm-stageStartTime)), 5000, 5500, user_settings.led_count, 0), 0);
        for(int i = 0; i< n; i++){
            brightness = (sin(((i*10)+mm)/500.0)+1)*255;
            leds[i].setHSV(brightness, 255, 50);
        }
        SFXcomplete();
    }else{
        nextLevel();
    }
}

void tickDie(long mm) { // a short bright explosion...particles persist after it.
    const int duration = 200; // milliseconds
    const int width = 20;     // half width of the explosion

    if(stageStartTime+duration > mm) {// Spread red from player position up and down the width

        int brightness = map((mm-stageStartTime), 0, duration, 255, 150); // this allows a fade from white to red

        // fill up
        int n = _max(map(((mm-stageStartTime)), 0, duration, getLED(killPos), getLED(killPos)+width), 0);
        for(int i = getLED(killPos); i<= n; i++){
            leds[i] = CRGB(255, brightness, brightness);
        }

        // fill to down
        n = _max(map(((mm-stageStartTime)), 0, duration, getLED(killPos), getLED(killPos)-width), 0);
        for(int i = getLED(killPos); i>= n; i--){
            leds[i] = CRGB(255, brightness, brightness);
        }
    }
}

void tickGameover(long mm) {

    int brightness = 0;

  if(stageStartTime+GAMEOVER_SPREAD_DURATION > mm) // Spread red from player position to top and bottom
  {
    // fill to top
    int n = _max(map(((mm-stageStartTime)), 0, GAMEOVER_SPREAD_DURATION, getLED(player[0].position), user_settings.led_count), 0);
    for(int i = getLED(player[0].position); i<= n; i++){
      leds[i] = CRGB(255, 0, 0);
    }
    // fill to bottom
    n = _max(map(((mm-stageStartTime)), 0, GAMEOVER_SPREAD_DURATION, getLED(player[0].position), 0), 0);
    for(int i = getLED(player[0].position); i>= n; i--){
      leds[i] = CRGB(255, 0, 0);
    }
    SFXgameover();
  }
  else if(stageStartTime+GAMEOVER_FADE_DURATION > mm)  // fade down to bottom and fade brightness
  {
    int n = _max(map(((mm-stageStartTime)), GAMEOVER_FADE_DURATION, GAMEOVER_SPREAD_DURATION, user_settings.led_count, 0), 0);
    brightness =  map(((mm-stageStartTime)), GAMEOVER_SPREAD_DURATION, GAMEOVER_FADE_DURATION, 200, 0);

    for(int i = 0; i<= n; i++){
      leds[i] = CRGB(brightness, 0, 0);
    }
    SFXcomplete();
  }

}



void tickWin(long mm) {
  int brightness = 0;
  FastLED.clear();
  if(stageStartTime+WIN_FILL_DURATION > mm){
    int n = _max(map(((mm-stageStartTime)), 0, WIN_FILL_DURATION, user_settings.led_count, 0), 0);  // fill from top to bottom
    for(int i = user_settings.led_count; i>= n; i--){
      brightness = user_settings.led_brightness;
      leds[i] = CRGB(0, brightness, 0);
    }
    SFXwin();
  }else if(stageStartTime+WIN_CLEAR_DURATION > mm){
    int n = _max(map(((mm-stageStartTime)), WIN_FILL_DURATION, WIN_CLEAR_DURATION, user_settings.led_count, 0), 0);  // clear from top to bottom
    for(int i = 0; i< n; i++){
      brightness = user_settings.led_brightness;
      leds[i] = CRGB(0, brightness, 0);
    }
    SFXwin();
  }else if(stageStartTime+WIN_OFF_DURATION > mm){   // wait a while with leds off
    leds[0] = CRGB(0, user_settings.led_brightness, 0);
  }else{
    nextLevel();
  }
}


void drawLives()
{
    // show how many lives are left by drawing a short line of green leds for each life
    SFXcomplete();  // stop any sounds
    FastLED.clear();

    for (const Player& p : player) {
        int pos = 0;
        for (int i = 0; i < p.Lives(); i++)
        {
            for (int j=0; j<4; j++)
            {
                leds[pos++] = CRGB(p.color[0], p.color[1], p.color[2]);
                FastLED.show();
            }
            leds[pos++] = CRGB(0, 0, 0);
            delay(20);
        }
        FastLED.show();
        delay(400);
        FastLED.clear();
    }
}



void drawAttack(const Player& p){
    if(p.captured) return;
    if(!p.attacking) return;
    int n = map(millis() - p.last_attack, 0, ATTACK_DURATION, 100, 5);
    for(int i = getLED(p.position-(attack_width/2))+1; i<=getLED(p.position+(attack_width/2))-1; i++){
        leds[i] = CRGB(0, 0, n);
    }
    if(n > 90) {
        n = 255;
        leds[getLED(p.position)] = CRGB(255, 255, 255);
    }else{
        n = 0;
        leds[getLED(p.position)] = CRGB(p.color[0], p.color[1], p.color[2]);
    }
    leds[getLED(p.position-(attack_width/2))] = CRGB(n, n, 255);
    leds[getLED(p.position+(attack_width/2))] = CRGB(n, n, 255);
}

int getLED(int pos){
    // The world is 1000 pixels wide, this converts world units into an LED number
    return constrain((int)map(pos, 0, VIRTUAL_LED_COUNT, 0, user_settings.led_count-1), 0, user_settings.led_count-1);
}

bool inLava(int pos){
    // Returns if the player is in active lava
    int i;
    Lava LP;
    for(i = 0; i<LAVA_COUNT; i++){
        LP = lavaPool[i];
        if(LP.Alive() && LP._state == Lava::ON){
            if(LP._left <= pos && LP._right >= pos) return true;
        }
    }
    return false;
}

void updateLives(){
    drawLives();
}

void save_game_stats(bool bossKill)
{
    user_settings.games_played += 1;
    user_settings.total_points += score;
    if (score > user_settings.high_score)
        user_settings.high_score += score;
    if (bossKill)
        user_settings.boss_kills += 1;

    show_game_stats();
    settings_eeprom_write();
}

// ---------------------------------
// --------- SCREENSAVER -----------
// ---------------------------------
void screenSaverTick(){
    long mm = millis();
    int mode = (mm/30000)%4;

    SFXcomplete(); // turn off sound...play testing showed this to be a problem

    if (mode == 0) {
        Fire2012();
    }
    else if (mode == 1)
        sinelon();
    else if (mode == 2)
        juggle();
    else {
        random_LED_flashes();
    }

}

#ifdef USE_GYRO_JOYSTICK
// ---------------------------------
// ----------- JOYSTICK ------------
// ---------------------------------
void getInput(){
    // This is responsible for the player movement speed and attacking.
    // You can replace it with anything you want that passes a -90>+90 value to joystickTilt
    // and any value to joystickWobble that is greater than ATTACK_THRESHOLD (defined at start)
    // For example you could use 3 momentary buttons:
        // if(digitalRead(leftButtonPinNumber) == HIGH) joystickTilt = -90;
        // if(digitalRead(rightButtonPinNumber) == HIGH) joystickTilt = 90;
        // if(digitalRead(attackButtonPinNumber) == HIGH) joystickWobble = ATTACK_THRESHOLD;

    // NB: This is not multiplayer yet

    int16_t ax, ay, az;
    int16_t gx, gy, gz;

    accelgyro.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);



    int a = (JOYSTICK_ORIENTATION == 0?ax:(JOYSTICK_ORIENTATION == 1?ay:az))/166;
    int g = (JOYSTICK_ORIENTATION == 0?gx:(JOYSTICK_ORIENTATION == 1?gy:gz));

        #ifdef JOYSTICK_DEBUG
            Serial.print("a:"); Serial.println(a);
            Serial.print("g:"); Serial.println(g);
        #endif

    if(abs(a) < user_settings.joystick_deadzone) a = 0;
    if(a > 0) a -= user_settings.joystick_deadzone;
    if(a < 0) a += user_settings.joystick_deadzone;
    MPUAngleSamples.add(a);
    MPUWobbleSamples.add(g);

    joystickTilt = MPUAngleSamples.getMedian();
    if(JOYSTICK_DIRECTION == 1) {
        joystickTilt = 0-joystickTilt;
    }
    joystickWobble = abs(MPUWobbleSamples.getHighest());

        #ifdef JOYSTICK_DEBUG
            //Serial.print("tilt:"); Serial.println(joystickTilt);
            //Serial.print("wobble:"); Serial.println(joystickWobble);
        #endif

}
#else

void getInput() {
  // This is responsible for the player movement speed and attacking.
  // You can replace it with anything you want that passes a -90>+90 value to joystickTilt
  // and any value to joystickWobble that is greater than ATTACK_THRESHOLD (defined at start)
  // For example you could use 3 momentary buttons:

  bool up = digitalRead(upButtonPinNumber1) == LOW;
  bool down = digitalRead(downButtonPinNumber1) == LOW;
  bool left = digitalRead(leftButtonPinNumber1) == LOW;
  //bool right = digitalRead(rightButtonPinNumber) == LOW;
  bool right = left;

  // Player 1

  joystickTilt[0] = 0;
  if (up) {
    joystickTilt[0] = 90 ;
  } else if (down) {
    joystickTilt[0] = -90;
  }

  if (left || right) {
    joystickWobble[0] = user_settings.attack_threshold;
  } else {
    joystickWobble[0] = 0;
  }


  // Player 2

  up = digitalRead(upButtonPinNumber2) == LOW;
  down = digitalRead(downButtonPinNumber2) == LOW;
  left = digitalRead(leftButtonPinNumber2) == LOW;
  //bool right = digitalRead(rightButtonPinNumber) == LOW;
  right = left;

  joystickTilt[1] = 0;
  if (up) {
    joystickTilt[1] = 90 ;
  } else if (down) {
    joystickTilt[1] = -90;
  }

  if (left || right) {
    joystickWobble[1] = user_settings.attack_threshold;
  } else {
    joystickWobble[1] = 0;
  }
}

#endif

// ---------------------------------
// -------------- SFX --------------
// ---------------------------------

/*
   This is used sweep across (up or down) a frequency range for a specified duration.
   A sin based warble is added to the frequency. This function is meant to be called
   on each frame to adjust the frequency in sync with an animation

   duration     = over what time period is this mapped
   elapsedTime  = how far into the duration are we in
   freqStart    = the beginning frequency
   freqEnd      = the ending frequency
   warble       = the amount of warble added (0 disables)


*/
void SFXFreqSweepWarble(int duration, int elapsedTime, int freqStart, int freqEnd, int warble)
{
    int freq = map_constrain(elapsedTime, 0, duration, freqStart, freqEnd);
    if (warble)
            warble = map(sin(millis()/20.0)*1000.0, -1000, 1000, 0, warble);

    sound(freq + warble, user_settings.audio_volume);
}

/*

   This is used sweep across (up or down) a frequency range for a specified duration.
   Random noise is optionally added to the frequency. This function is meant to be called
   on each frame to adjust the frequency in sync with an animation

   duration     = over what time period is this mapped
   elapsedTime  = how far into the duration are we in
   freqStart    = the beginning frequency
   freqEnd      = the ending frequency
   noiseFactor  = the amount of noise to added/subtracted (0 disables)


*/
void SFXFreqSweepNoise(int duration, int elapsedTime, int freqStart, int freqEnd, uint8_t noiseFactor)
{
    int freq;

    if (elapsedTime > duration)
        freq = freqEnd;
    else
        freq = map(elapsedTime, 0, duration, freqStart, freqEnd);

    if (noiseFactor)
            noiseFactor = noiseFactor - random8(noiseFactor / 2);

    sound(freq + noiseFactor, user_settings.audio_volume);
}


void SFXtilt(int amount){
    if  (amount == 0){
        SFXcomplete();
        return;
    }

    int f = map(abs(amount), 0, 90, 80, 900)+random8(100);
    // TODO, how to modify for multiplayer?
    //if(playerPositionModifier < 0) f -= 500;
    //if(playerPositionModifier > 0) f += 200;
    int vol = map(abs(amount), 0, 90, user_settings.audio_volume / 2, user_settings.audio_volume * 3/4);
    sound(f,vol);
}
void SFXattacking(){
    int freq = map(sin(millis()/2.0)*1000.0, -1000, 1000, 500, 600);
    if(random8(5)== 0){
      freq *= 3;
    }
    sound(freq, user_settings.audio_volume);
}
void SFXdead(){
    SFXFreqSweepNoise(1000, millis()-killTime, 1000, 10, 200);
}

void SFXgameover(){
    SFXFreqSweepWarble(GAMEOVER_SPREAD_DURATION, millis()-killTime, 440, 20, 60);
}

void SFXkill(){
    sound(2000, user_settings.audio_volume);
}
void SFXwin(){
    SFXFreqSweepWarble(WIN_OFF_DURATION, millis()-stageStartTime, 40, 400, 20);
}

void SFXbosskilled()
{
    SFXFreqSweepWarble(7000, millis()-stageStartTime, 75, 1100, 60);
}

void SFXcomplete(){
    soundOff();
}


/*
  This works just like the map function except x is constrained to the range of in_min and in_max
*/
long map_constrain(long x, long in_min, long in_max, long out_min, long out_max)
{
  // constain the x value to be between in_min and in_max
  if (in_max > in_min){   // map allows min to be larger than max, but constrain does not
    x = constrain(x, in_min, in_max);
  }
  else {
    x = constrain(x, in_max, in_min);
  }
  return map(x, in_min, in_max, out_min, out_max);
}

// Fire2012 by Mark Kriegsman, July 2012
// as part of "Five Elements" shown here: http://youtu.be/knWiGsmgycY
////
// This basic one-dimensional 'fire' simulation works roughly as follows:
// There's a underlying array of 'heat' cells, that model the temperature
// at each point along the line.  Every cycle through the simulation,
// four steps are performed:
//  1) All cells cool down a little bit, losing heat to the air
//  2) The heat from each cell drifts 'up' and diffuses a little
//  3) Sometimes randomly new 'sparks' of heat are added at the bottom
//  4) The heat from each cell is rendered as a color into the leds array
//     The heat-to-color mapping uses a black-body radiation approximation.
//
// Temperature is in arbitrary units from 0 (cold black) to 255 (white hot).
//
// This simulation scales it self a bit depending on NUM_LEDS; it should look
// "OK" on anywhere from 20 to 100 LEDs without too much tweaking.
//
// I recommend running this simulation at anywhere from 30-100 frames per second,
// meaning an interframe delay of about 10-35 milliseconds.
//
// Looks best on a high-density LED setup (60+ pixels/meter).
//
//
// There are two main parameters you can play with to control the look and
// feel of your fire: COOLING (used in step 1 above), and SPARKING (used
// in step 3 above).
//
// COOLING: How much does the air cool as it rises?
// Less cooling = taller flames.  More cooling = shorter flames.
// Default 50, suggested range 20-100
#define COOLING  75

// SPARKING: What chance (out of 255) is there that a new spark will be lit?
// Higher chance = more roaring fire.  Lower chance = more flickery fire.
// Default 120, suggested range 50-200.
#define SPARKING 40

//======================================== SCREEN SAVERS =================


void Fire2012()
{
// Array of temperature readings at each simulation cell
  static byte heat[VIRTUAL_LED_COUNT]; // the most possible
  bool gReverseDirection = false;

  // Step 1.  Cool down every cell a little
    for( int i = 0; i < user_settings.led_count; i++) {
      heat[i] = qsub8( heat[i],  random8(0, ((COOLING * 10) / user_settings.led_count) + 2));
    }

    // Step 2.  Heat from each cell drifts 'up' and diffuses a little
    for( int k= user_settings.led_count - 1; k >= 2; k--) {
      heat[k] = (heat[k - 1] + heat[k - 2] + heat[k - 2] ) / 3;
    }

    // Step 3.  Randomly ignite new 'sparks' of heat near the bottom
    if( random8() < SPARKING ) {
      int y = random8(7);
      heat[y] = qadd8( heat[y], random8(160,255) );
    }

    // Step 4.  Map from heat cells to LED colors
    for( int j = 0; j < user_settings.led_count; j++) {
      CRGB color = HeatColor( heat[j]);
      int pixelnumber;
      if( gReverseDirection ) {
        pixelnumber = (user_settings.led_count-1) - j;
      } else {
        pixelnumber = j;
      }
      leds[pixelnumber] = color;
    }
}

void LED_march() {
    int n, b, c, i;

    long mm = millis();


    for(i = 0; i<user_settings.led_count; i++){
        leds[i].nscale8(250);
    }

        // Marching green <> orange
        n = (mm/250)%10;
        b = 10+((sin(mm/500.00)+1)*20.00);
        c = 20+((sin(mm/5000.00)+1)*33);
        for(i = 0; i<user_settings.led_count; i++){
            if(i%10 == n){
                leds[i] = CHSV( c, 255, 150);
            }
        }

}

void random_LED_flashes() {
    long mm = millis();
    int i;

    for(i = 0; i<user_settings.led_count; i++){
        leds[i].nscale8(250);
    }


    randomSeed(mm);
    for(i = 0; i<user_settings.led_count; i++){
        if(random8(20) == 0){
            leds[i] = CHSV( 25, 255, 100);
        }
    }

}

void sinelon()
{
  static uint8_t gHue = 0; // rotating "base color" used by many of the patterns

  gHue++;

  // a colored dot sweeping back and forth, with fading trails
  fadeToBlackBy( leds, user_settings.led_count, 20);
  int pos = beatsin16(13,0,user_settings.led_count);
  leds[pos] += CHSV( gHue, 255, 192);
}



void juggle() {
  // eight colored dots, weaving in and out of sync with each other
  fadeToBlackBy( leds, user_settings.led_count, 20);
  byte dothue = 0;
  for( int i = 0; i < 4; i++) {
    leds[beatsin16( i+7, 0, user_settings.led_count-1 )] |= CHSV(dothue, 200, 255);
    dothue += 64;
  }
}


