WorksAsDesigned
Published © CC BY-NC

๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ Ultimate 1D RGB Invaders (ESP32-Edition) ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ

ESP32+WS2812B Tired of spending $2, 000 on a graphics card just to get bored in 4K? Welcome to the future of gaming: 1 Dimension.

BeginnerFull instructions provided2 hours47
๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ Ultimate 1D RGB Invaders (ESP32-Edition) ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ

Things used in this project

Hardware components

ws2812B 60led/m
×1
ESP32-S3
×1
arcade buttons 60mm
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Custom parts and enclosures

case

box

cover

Schematics

how to wire it up

Code

V10.9.3

C/C++
Uoload in ArduinoIDE (see description)
// ==========================================================================
// PROJECT: ULTIMATE RGB INVADERS - V10.9.3 (BALANCING FIX)
// HARDWARE: ESP32-S3 (N16), MAX98357A, WS2812B
// CORE VERSION: 2.0.17 (Required!)
// ==========================================================================

#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <driver/i2s.h>
#include <vector>
#include <Update.h>

// --- LED CONFIGURATION ---
#define FASTLED_ESP32_S3_PIN 7 
#define FASTLED_RMT_MAX_CHANNELS 1
#include <FastLED.h>

// --------------------------------------------------------------------------
// 1. DEFINITIONS & DATA TYPES
// --------------------------------------------------------------------------
#define I2S_BCLK        4
#define I2S_LRC         5
#define I2S_DOUT        6

#define PIN_LED_DATA    7
#define MAX_LEDS        1200 
#define LED_TYPE        WS2812B
#define COLOR_ORDER     GRB

#define PIN_BTN_BLUE    15
#define PIN_BTN_RED     16
#define PIN_BTN_GREEN   17
#define PIN_BTN_WHITE   18 

#define CONFIG_VERSION  40 // Version 10.9.3
#define FRAME_DELAY     16 // ~60 FPS
#define INPUT_BUFFER_MS 60 
#define SAMPLE_RATE     44100

const int FIRE_COOLDOWN = 100;

struct ToneCmd { int freq; int duration; };
typedef std::vector<ToneCmd> Melody;

enum SoundEvent { 
  EVT_NONE=0, EVT_START, EVT_WIN, EVT_LOSE, EVT_MISTAKE,      
  EVT_HIT_SUCCESS, 
  EVT_SHOT_BLUE, EVT_SHOT_RED, EVT_SHOT_GREEN, EVT_SHOT_WHITE,
  EVT_SHOT_YELLOW, EVT_SHOT_MAGENTA, EVT_SHOT_CYAN, 
  EVT_FINAL_WIN,
  EVT_BONUS_START, EVT_BONUS_WAVE, EVT_BONUS_SPEEDUP
};

enum GameState { 
  STATE_MENU, STATE_INTRO, STATE_PLAYING, STATE_BOSS_PLAYING, 
  STATE_LEVEL_COMPLETED, STATE_GAME_FINISHED, STATE_BASE_DESTROYED, STATE_GAMEOVER,
  STATE_BONUS_INTRO, STATE_BONUS_PLAYING, // Beatsaber
  STATE_BONUS_SIMON // Simon Says
};

enum Boss2State { B2_MOVE, B2_CHARGE, B2_SHOOT };
enum Boss3State { B3_MOVE, B3_PHASE_CHANGE, B3_BURST, B3_WAIT };

// Simon Says States
enum SimonState { S_MOVE, S_PREPARE, S_SHOW, S_INPUT, S_SUCCESS, S_FAIL };

struct LevelConfig { int speed; int length; int bossType; };
struct BossConfig { int moveSpeed; int shotSpeed; int hpPerLed; int shotFreq; int burstCount; int m1; int m2; int m3; };
struct Enemy { int color; float pos; bool flash; };
struct BossSegment { int color; int hp; int maxHp; bool active; int originalIndex; };
struct Shot { float position; int color; }; 
struct BossProjectile { float pos; int color; };

// --------------------------------------------------------------------------
// 2. GLOBAL VARIABLES
// --------------------------------------------------------------------------
CRGB leds[MAX_LEDS];
Preferences preferences;
WebServer server(80);
QueueHandle_t audioQueue;
TaskHandle_t audioTaskHandle;

// Statistics
unsigned long stat_totalShots = 0;
unsigned long stat_totalKills = 0;
unsigned long stat_boss3Kills = 0; 
int stat_lastGameShots = 0;

// Default Sound Strings
const String DEF_SND_START       = "523,80;659,80;784,80;1047,300";
const String DEF_SND_WIN         = "523,80;659,80;784,80;1047,300;0,150;1047,60;1319,60";
const String DEF_SND_FINAL_WIN   = "523,150;659,150;784,150;1047,400;784,150;1047,600;1319,150;1568,150;2093,800";
const String DEF_SND_LOSE        = "370,100;349,100;330,100;311,400";
const String DEF_SND_MISTAKE     = "60,150";
const String DEF_SND_SHOT_BLUE   = "698,50;659,50";
const String DEF_SND_SHOT_RED    = "784,30;1047,30;1319,30";
const String DEF_SND_SHOT_GREEN  = "523,30;554,30;523,30";
const String DEF_SND_SHOT_WHITE  = "1047,20;1319,20;1568,20;2093,4";
const String DEF_SND_HIT         = "2093,30";
const String DEF_SND_SPEEDUP     = "1500,80;0,50;1500,80"; 

const String DEF_SND_SHOT_Y      = "1500,40;1800,40";        
const String DEF_SND_SHOT_M      = "800,40;2000,40";        
const String DEF_SND_SHOT_C      = "1200,40;1000,40";       

String cfg_snd_start, cfg_snd_win, cfg_snd_lose, cfg_snd_mistake;
String cfg_snd_shot_b, cfg_snd_shot_r, cfg_snd_shot_g, cfg_snd_shot_w, cfg_snd_hit;

// Color Configuration
String hex_c1 = "#0000FF"; // Blue
String hex_c2 = "#FF0000"; // Red
String hex_c3 = "#00FF00"; // Green
String hex_c4 = "#FFFF00"; // Yellow
String hex_c5 = "#FF00FF"; // Magenta
String hex_c6 = "#00FFFF"; // Cyan
String hex_cw = "#FFFFFF"; // White
String hex_cb = "#222222"; // Dark

CRGB col_c1, col_c2, col_c3, col_c4, col_c5, col_c6, col_cw, col_cb;
Melody melStart, melWin, melLose, melMistake, melShotBlue, melShotRed, melShotGreen, melShotWhite, melHit, melFinalWin, melSpeedUp;
Melody melShotY, melShotM, melShotC;

// Config
int config_num_leds = 100;
int config_brightness_pct = 50;
int config_start_level = 1;
bool config_sacrifice_led = true; 
int config_homebase_size = 3;       
int config_shot_speed_pct = 100;
int ledStartOffset = 1;
// AUDIO CONFIG
bool config_sound_on = true;
int config_volume_pct = 50;
bool config_endless_mode = false;
String currentProfilePrefix = "def_"; 
String config_ssid = "";
String config_pass = "";
bool config_static_ip = false; String config_ip = "";
String config_gateway = ""; String config_subnet = "";
String config_dns = "";
bool wifiMode = false; 

// Game State
LevelConfig levels[11];
BossConfig boss1Cfg; BossConfig boss2Cfg; BossConfig boss3Cfg;
GameState currentState = STATE_MENU;

unsigned long lastLoopTime = 0; 
unsigned long stateTimer = 0;
unsigned long lastShotMove = 0;
unsigned long lastEnemyMove = 0;
unsigned long lastFireTime = 0;
unsigned long bossActionTimer = 0;
bool buttonsReleased = true;
unsigned long btnWhitePressTime = 0;
bool btnWhiteHeld = false;
unsigned long comboTimer = 0;
bool isWaitingForCombo = false; 

std::vector<Enemy> enemies;
std::vector<Shot> shots;
std::vector<BossSegment> bossSegments; 
std::vector<BossProjectile> bossProjectiles; 
float enemyFrontIndex = -1.0; 
int currentLevel = 1;
int currentBossType = 0;

// Boss Vars
Boss2State boss2State = B2_MOVE;
int boss2Section = 0; int boss2ShotsFired = 0;
int boss2LockedColor = 1; int markerPos[3]; int boss2TargetShots = 10; 
int boss1WrongHits = 0; bool boss1RageMode = false;
int boss1RageShots = 0;
Boss3State boss3State = B3_MOVE;
int boss3PhaseIndex = 0; int boss3BurstCounter = 0; int boss3Markers[2];
int currentScore = 0;
int highScore = 0; int lastGames[3] = {0, 0, 0};
unsigned long levelStartTime = 0;
int levelMaxPossibleScore = 0;
int levelAchievedScore = 0;     

// --- BONUS 1 (BEATSABER) VARIABLES ---
bool bonusPlayedThisLevel = false;
bool autoBonusTrigger = false; // Triggered by Perfect Score
int bonusEnemiesSpawned = 0;
int bonusLives = 10;
int bonusWaveCount = 0;
unsigned long bonusPauseTimer = 0;
bool bonusInPause = false;
float bonusSpeedMultiplier = 1.0; 
unsigned long bonusFlashTimer = 0;
std::vector<Enemy> bonusEnemies; 
std::vector<Shot> bonusShots;    
int bonusReturnLevel = 1;

// --- BONUS 2 (SIMON SAYS) VARIABLES ---
SimonState simonState = S_MOVE;
int simonLives = 3;
int simonStage = 0;          
int simonStopIndex = 0;      
float simonBossPos = 0.0;
std::vector<int> simonFullSequence; // Der KOMPLETTE Code
int simonPlaybackIdx = 0;    
int simonInputIdx = 0;       
unsigned long simonTimer = 0;
int simonTargetPos = 0;      

// --------------------------------------------------------------------------
// 3. HELPER FUNCTIONS
// --------------------------------------------------------------------------
CRGB hexToCRGB(String hex) {
  long number = strtol(&hex[1], NULL, 16);
  return CRGB((number >> 16) & 0xFF, (number >> 8) & 0xFF, number & 0xFF);
}

Melody parseSoundString(String data) {
  Melody m;
  if (data.length() == 0) return m;
  int start = 0;
  int end = data.indexOf(';');
  while (end != -1) {
    String pair = data.substring(start, end);
    int comma = pair.indexOf(',');
    if (comma != -1) {
      ToneCmd t;
      t.freq = pair.substring(0, comma).toInt();
      t.duration = pair.substring(comma + 1).toInt();
      m.push_back(t);
    }
    start = end + 1;
    end = data.indexOf(';', start);
  }
  String pair = data.substring(start);
  int comma = pair.indexOf(',');
  if (comma != -1) {
    ToneCmd t;
    t.freq = pair.substring(0, comma).toInt();
    t.duration = pair.substring(comma + 1).toInt();
    m.push_back(t);
  }
  return m;
}

void melodyFromStr(Melody& m, String s) { m = parseSoundString(s); }

// --------------------------------------------------------------------------
// 4. AUDIO ENGINE
// --------------------------------------------------------------------------
void playSound(SoundEvent evt) {
    if (!config_sound_on) return;
    xQueueSend(audioQueue, &evt, 0);
}

void playShotSound(int color) {
    switch(color) {
        case 1: playSound(EVT_SHOT_BLUE); break;
        case 2: playSound(EVT_SHOT_RED); break;
        case 3: playSound(EVT_SHOT_GREEN); break;
        case 4: playSound(EVT_SHOT_YELLOW); break; 
        case 5: playSound(EVT_SHOT_MAGENTA); break; 
        case 6: playSound(EVT_SHOT_CYAN); break; 
        case 7: playSound(EVT_SHOT_WHITE); break;
        default: playSound(EVT_SHOT_BLUE); break;
    }
}

void playToneI2S(int freq, int durationMs) {
    if (freq <= 0) {
        size_t bytes_written;
        int samples = (SAMPLE_RATE * durationMs) / 1000;
        int16_t *buffer = (int16_t *)malloc(samples * 2);
        memset(buffer, 0, samples * 2);
        i2s_write(I2S_NUM_0, buffer, samples * 2, &bytes_written, portMAX_DELAY);
        free(buffer);
        return;
    }
    size_t bytes_written;
    int samples = (SAMPLE_RATE * durationMs) / 1000;
    int16_t *buffer = (int16_t *)malloc(samples * 2);
    int halfPeriod = SAMPLE_RATE / freq / 2;
    int16_t volume = map(config_volume_pct, 0, 100, 0, 10000);
    for (int i = 0; i < samples; i++) {
        buffer[i] = ((i / halfPeriod) % 2 == 0) ? volume : -volume;
    }
    i2s_write(I2S_NUM_0, buffer, samples * 2, &bytes_written, portMAX_DELAY);
    free(buffer);
}

void audioTask(void *parameter) {
    i2s_config_t i2s_config = {
        .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
        .sample_rate = SAMPLE_RATE,
        .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
        .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, 
        .communication_format = I2S_COMM_FORMAT_STAND_I2S,
        .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
        .dma_buf_count = 4,
        .dma_buf_len = 512,
        .use_apll = false,
        .tx_desc_auto_clear = true
    };
    i2s_pin_config_t pin_config = {
        .bck_io_num = I2S_BCLK,
        .ws_io_num = I2S_LRC,
        .data_out_num = I2S_DOUT,
        .data_in_num = I2S_PIN_NO_CHANGE 
    };
    i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
    i2s_set_pin(I2S_NUM_0, &pin_config);
    i2s_zero_dma_buffer(I2S_NUM_0);

    const Melody* currentMelody = nullptr;
    int noteIndex = 0;
    while(true) {
        SoundEvent newEvent;
        if (xQueueReceive(audioQueue, &newEvent, 0) == pdTRUE) {
            bool play = true;
            if ((currentMelody == &melWin || currentMelody == &melLose || currentMelody == &melFinalWin) && newEvent != EVT_START) {
                 play = false;
            }
            if (play) {
                switch(newEvent) {
                    case EVT_START:      currentMelody = &melStart; break;
                    case EVT_SHOT_BLUE:  currentMelody = &melShotBlue; break;
                    case EVT_SHOT_RED:   currentMelody = &melShotRed; break;
                    case EVT_SHOT_GREEN: currentMelody = &melShotGreen; break;
                    case EVT_SHOT_WHITE: currentMelody = &melShotWhite; break;
                    case EVT_SHOT_YELLOW: currentMelody = &melShotY; break;
                    case EVT_SHOT_MAGENTA:currentMelody = &melShotM; break;
                    case EVT_SHOT_CYAN:   currentMelody = &melShotC; break;
                    case EVT_MISTAKE:    currentMelody = &melMistake; break;
                    case EVT_HIT_SUCCESS:currentMelody = &melHit; break;
                    case EVT_WIN:        currentMelody = &melWin; break;
                    case EVT_FINAL_WIN:  currentMelody = &melFinalWin; break;
                    case EVT_LOSE:       currentMelody = &melLose; break;
                    case EVT_BONUS_START: currentMelody = &melWin; break; 
                    case EVT_BONUS_WAVE: currentMelody = &melShotWhite; break;
                    case EVT_BONUS_SPEEDUP: currentMelody = &melSpeedUp; break;
                    default: break;
                }
                noteIndex = 0;
                i2s_zero_dma_buffer(I2S_NUM_0);
            }
        }

        if (currentMelody != nullptr) {
            if (noteIndex >= currentMelody->size()) {
                currentMelody = nullptr;
                playToneI2S(0, 20); 
                i2s_zero_dma_buffer(I2S_NUM_0);
            } else {
                ToneCmd t = (*currentMelody)[noteIndex];
                playToneI2S(t.freq, t.duration);
                noteIndex++;
            }
        } else {
            vTaskDelay(5 / portTICK_PERIOD_MS);
        }
    }
}

// --------------------------------------------------------------------------
// 5. GRAPHICS ENGINE
// --------------------------------------------------------------------------
CRGB getColor(int colorCode) {
  switch (colorCode) {
    case 1: return col_c1; // Blue
    case 2: return col_c2; // Red
    case 3: return col_c3; // Green
    case 4: return col_c4; // Yellow
    case 5: return col_c5; // Magenta
    case 6: return col_c6; // Cyan
    case 7: return col_cw; // White  
    default: return CRGB::Black;
  }
}

void drawCrispPixel(float pos, CRGB color) {
    int idx = round(pos);
    if (idx < 0 || idx >= config_num_leds) return;
    leds[idx + ledStartOffset] = color;
}

void flashPixel(int pos) {
  if(pos >= 0 && pos < config_num_leds) leds[pos + ledStartOffset] = CRGB::White;
}

// --------------------------------------------------------------------------
// 6. LOGIC & CONFIGURATION
// --------------------------------------------------------------------------
void saveHighscores() {
  preferences.begin("game", false);
  preferences.putInt((currentProfilePrefix + "hs").c_str(), highScore);
  preferences.putInt((currentProfilePrefix + "l1").c_str(), lastGames[0]);
  preferences.putInt((currentProfilePrefix + "l2").c_str(), lastGames[1]);
  preferences.putInt((currentProfilePrefix + "l3").c_str(), lastGames[2]);
  preferences.putULong("st_shots", stat_totalShots);
  preferences.putULong("st_kills", stat_totalKills);
  preferences.putULong("st_b3kills", stat_boss3Kills); 
  preferences.end();
}

void loadHighscores() {
  preferences.begin("game", true);
  highScore = preferences.getInt((currentProfilePrefix + "hs").c_str(), 0);
  lastGames[0] = preferences.getInt((currentProfilePrefix + "l1").c_str(), 0);
  lastGames[1] = preferences.getInt((currentProfilePrefix + "l2").c_str(), 0);
  lastGames[2] = preferences.getInt((currentProfilePrefix + "l3").c_str(), 0);
  stat_totalShots = preferences.getULong("st_shots", 0);
  stat_totalKills = preferences.getULong("st_kills", 0);
  stat_boss3Kills = preferences.getULong("st_b3kills", 0); 
  preferences.end();
}

void registerGameEnd(int finalScore) {
  lastGames[2] = lastGames[1];
  lastGames[1] = lastGames[0];
  lastGames[0] = finalScore;
  if (finalScore > highScore) highScore = finalScore;
  saveHighscores();
  bonusPlayedThisLevel = false; 
}

void triggerBaseDestruction() {
  playSound(EVT_LOSE);
  registerGameEnd(currentScore);
  currentState = STATE_BASE_DESTROYED;
  stateTimer = millis();
}

void calculateLevelScore() {
  unsigned long duration = millis() - levelStartTime;
  int entityCount = 0;
  int calcLevel = currentLevel;
  if(currentLevel > 10) calcLevel = ((currentLevel - 1) % 10) + 1;
  if (currentLevel <= 10 && levels[currentLevel].bossType > 0) {
    if (levels[currentLevel].bossType == 1) entityCount = 9 * boss1Cfg.hpPerLed;
    else if (levels[currentLevel].bossType == 2) entityCount = 9 * boss2Cfg.hpPerLed;
    else if (levels[currentLevel].bossType == 3) entityCount = 15 * boss3Cfg.hpPerLed;
  } else {
    entityCount = levels[calcLevel].length;
  }
  
  int levelMultiplier = currentLevel;
  int basePoints = entityCount * 100 * levelMultiplier;
  
  unsigned long targetTime = 0;
  
  // BALANCING
  if (currentLevel <= 10 && levels[currentLevel].bossType == 2) {
      // MASTERBLASTER (Boss 2): Harder Time Limit
      targetTime = 36000;
  }
  else {
      // Tank & Normal Levels: Standard calc
      unsigned long travelTime = config_num_leds * 15;
      unsigned long processingTime = entityCount * 300; 
      targetTime = 3000 + travelTime + processingTime;
  }
  
  int timeBonus = 0;
  int maxTimeBonus = basePoints * 3;
  
  if (currentLevel <= 10 && levels[currentLevel].bossType == 3) {
      timeBonus = maxTimeBonus;
  } else {
      if (duration <= targetTime) timeBonus = maxTimeBonus;
      else {
        float ratio = (float)targetTime / (float)duration;
        timeBonus = (int)(maxTimeBonus * ratio);
      }
  }
  
  levelAchievedScore = basePoints + timeBonus;
  levelMaxPossibleScore = basePoints * 4; 
  currentScore += levelAchievedScore;
}

void checkWinCondition() {
  bool won = false;
  if (currentState == STATE_PLAYING && enemies.empty()) won = true;
  if (currentState == STATE_BOSS_PLAYING && bossSegments.empty()) won = true;
  if (won) {
    calculateLevelScore();
    
    // CHECK PERFECT SCORE (Automatic Bonus Trigger)
    if (levelAchievedScore >= levelMaxPossibleScore) {
        autoBonusTrigger = true;
    } else {
        autoBonusTrigger = false;
    }

    if (currentLevel <= 10 && levels[currentLevel].bossType == 3) {
        stat_boss3Kills++;
        saveHighscores();
    }

    if (!config_endless_mode && currentLevel >= 10) {
      playSound(EVT_FINAL_WIN); 
      registerGameEnd(currentScore);
      currentState = STATE_GAME_FINISHED;
    } else {
      playSound(EVT_WIN);
      currentState = STATE_LEVEL_COMPLETED;
      stateTimer = millis();
    }
  }
}

void startLevelIntro(int level) {
  playSound(EVT_START);
  if (level == config_start_level) {
     currentScore = 0;
     stat_lastGameShots = 0; 
  }
  
  if (level != currentLevel) {
      bonusPlayedThisLevel = false;
  }
  
  currentLevel = level; currentState = STATE_INTRO; stateTimer = millis();
  FastLED.clear();
  for(int i=0; i<config_num_leds; i++) leds[i+ledStartOffset] = CRGB(10,10,10);
  CRGB barColor = (level <= 10 && levels[level].bossType > 0) ? col_c2 : col_c3;
  int center = config_num_leds / 2;
  
  int displayLevel = (level > 10) ? 10 : level;
  int totalWidth = (displayLevel * 6) + ((displayLevel-1)*4); 
  int startPos = center - (totalWidth/2);
  if(startPos < 0) startPos = 0;
  int cursor = startPos;
  for(int i=0; i<displayLevel; i++) {
    for(int k=0; k<6; k++) { if(cursor < config_num_leds) leds[cursor + ledStartOffset] = barColor; cursor++; }
    cursor += 4; 
  }
  if(config_sacrifice_led) leds[0] = CRGB(20, 0, 0); 
  FastLED.show();
}

void drawLevelIntro(int level) {
  FastLED.clear();
  for(int i=0; i<config_num_leds; i++) leds[i+ledStartOffset] = CRGB(5,5,5);
  CRGB barColor = (level <= 10 && levels[level].bossType > 0) ? col_c2 : col_c3;
  int center = config_num_leds / 2;
  int displayLevel = (level > 10) ? 10 : level;
  int totalWidth = (displayLevel * 6) + ((displayLevel-1)*4);
  int startPos = center - (totalWidth/2); if(startPos < 0) startPos = 0;
  int cursor = startPos;
  for(int i=0; i<displayLevel; i++) {
    for(int k=0; k<6; k++) { if(cursor < config_num_leds) leds[cursor + ledStartOffset] = barColor; cursor++; }
    cursor += 4; 
  }
  if(config_sacrifice_led) leds[0] = CRGB(20, 0, 0);
  FastLED.show();
}

// --------------------------------------------------------------------------
// SIMON SAYS ENGINE (CUMULATIVE VERSION)
// --------------------------------------------------------------------------
int getSimonStageLength(int stage) {
    int lens[] = {4, 5, 6, 8, 9, 11, 13, 15, 17};
    if (stage >= 0 && stage <= 8) return lens[stage];
    return 17;
}

void generateSimonSequence() {
    simonFullSequence.clear();
    int totalMaxLen = 17; 
    
    for(int i=0; i<totalMaxLen; i++) {
        int c = 1;
        if (i < 8) {
            c = random(1, 4); 
        } else {
            c = random(1, 7);
        }
        simonFullSequence.push_back(c);
    }
}

void startSimonBonus() {
    simonLives = 3;
    simonStage = 0;
    simonStopIndex = 0;
    simonBossPos = (float)config_num_leds - 1.0;
    generateSimonSequence(); 
    currentState = STATE_BONUS_SIMON;
    simonState = S_MOVE;
    simonTargetPos = config_num_leds - ((simonStopIndex + 1) * (config_num_leds / 12));
    playSound(EVT_BONUS_WAVE);
}

void updateSimonBonus() {
    unsigned long now = millis();
    int bossLen = 9 - simonStage; 
    int currentSeqLen = getSimonStageLength(simonStage);

    if (simonLives <= 0 || (simonState == S_MOVE && simonBossPos <= config_homebase_size)) {
        playSound(EVT_LOSE);
        startLevelIntro(bonusReturnLevel);
        return;
    }

    switch(simonState) {
        case S_MOVE: {
            float spd = 20.0 + (simonStage * 3.0);
            simonBossPos -= (spd / 60.0);
            if (simonBossPos <= simonTargetPos) {
                simonBossPos = (float)simonTargetPos; 
                simonState = S_PREPARE;
                simonTimer = now;
                playSound(EVT_SHOT_WHITE); 
            }
            break;
        }
        case S_PREPARE: {
            if (now - simonTimer > 1000) {
                simonState = S_SHOW;
                simonPlaybackIdx = 0;
                simonTimer = now;
            }
            break;
        }
        case S_SHOW: {
            int delayMs = 600 - (simonStage * 40); 
            if (simonStage >= 5) delayMs = 400; 
            if (simonStage >= 7) delayMs = 300;

            if (now - simonTimer > delayMs) {
                if (simonPlaybackIdx < currentSeqLen) {
                    playShotSound(simonFullSequence[simonPlaybackIdx]);
                    simonPlaybackIdx++;
                    simonTimer = now;
                } else {
                    simonState = S_INPUT;
                    simonInputIdx = 0;
                    buttonsReleased = true; 
                    isWaitingForCombo = false;
                }
            }
            break;
        }
        case S_INPUT: {
            bool b = (digitalRead(PIN_BTN_BLUE) == LOW);
            bool r = (digitalRead(PIN_BTN_RED) == LOW); 
            bool g = (digitalRead(PIN_BTN_GREEN) == LOW);
            bool pressed = (b || r || g);

            if (!pressed) { buttonsReleased = true; isWaitingForCombo = false; }
            
            if (pressed && buttonsReleased && !isWaitingForCombo && (now - lastFireTime > FIRE_COOLDOWN)) {
                 isWaitingForCombo = true;
                 comboTimer = now;
            }
            
            if (isWaitingForCombo && (now - comboTimer >= INPUT_BUFFER_MS)) {
                int c = 0;
                b = (digitalRead(PIN_BTN_BLUE) == LOW); 
                r = (digitalRead(PIN_BTN_RED) == LOW); 
                g = (digitalRead(PIN_BTN_GREEN) == LOW);
                
                if (r && g && b) c = 7; 
                else if (r && g) c = 4; // Yellow
                else if (r && b) c = 5; // Magenta
                else if (g && b) c = 6; // Cyan
                else if (b) c = 1; 
                else if (r) c = 2; 
                else if (g) c = 3;

                if (c > 0) {
                    playShotSound(c);
                    if (c == simonFullSequence[simonInputIdx]) {
                        simonInputIdx++;
                        if (simonInputIdx >= currentSeqLen) {
                            simonState = S_SUCCESS;
                            simonTimer = now;
                            playSound(EVT_HIT_SUCCESS);
                            currentScore += (250 * (simonStage + 1));
                        }
                    } else {
                        simonState = S_FAIL;
                        simonTimer = now;
                        playSound(EVT_MISTAKE);
                        simonLives--;
                    }
                    lastFireTime = now;
                }
                buttonsReleased = false; 
                isWaitingForCombo = false;
            }
            break;
        }
        case S_SUCCESS: {
            if (now - simonTimer > 1000) {
                 simonStage++;
                 if (simonStage >= 9) {
                     playSound(EVT_FINAL_WIN);
                     startLevelIntro(bonusReturnLevel);
                     return;
                 }
                 simonStopIndex++;
                 simonTargetPos = config_num_leds - ((simonStopIndex + 1) * (config_num_leds / 12));
                 simonState = S_MOVE;
            }
            break;
        }
        case S_FAIL: {
            if (now - simonTimer > 1000) {
                simonStopIndex++;
                simonTargetPos = config_num_leds - ((simonStopIndex + 1) * (config_num_leds / 12));
                simonState = S_MOVE;
            }
            break;
        }
    }

    FastLED.clear();
    
    // Draw Marker
    if (simonState == S_MOVE) {
        if(simonTargetPos >= 0 && simonTargetPos < config_num_leds)
            leds[simonTargetPos + ledStartOffset] = CRGB::Red;
    }

    // Draw Boss
    for(int i=0; i < bossLen; i++) {
        int pixelPos = (int)simonBossPos + i;
        if (pixelPos >= config_num_leds) continue;
        
        CRGB c = CRGB::Black;
        if (simonState == S_MOVE || simonState == S_PREPARE) {
             c = CHSV((i*20) + (millis()/10), 255, 255);
        } else if (simonState == S_SHOW) {
             if (i == 0) { 
                 int delayMs = 600 - (simonStage * 40); if(simonStage>=5) delayMs=400; if(simonStage>=7) delayMs=300;
                 long elapsedShow = now - simonTimer;
                 if (simonPlaybackIdx < currentSeqLen && elapsedShow < (delayMs - 100)) {
                     c = getColor(simonFullSequence[simonPlaybackIdx]);
                 } else {
                     c = CRGB::White; 
                 }
             } else {
                 c = CRGB::White;
             }
        } else if (simonState == S_INPUT) {
             c = CRGB::White;
        } else if (simonState == S_SUCCESS) {
             if (i==0) c = ((millis()/50)%2==0) ? CRGB::Red : CRGB::Yellow;
             else c = CRGB::Green;
        } else if (simonState == S_FAIL) {
             c = CRGB::Red;
        }

        if(pixelPos >= 0) leds[pixelPos + ledStartOffset] = c;
    }

    for(int i=0; i<simonLives; i++) {
        if(i+ledStartOffset < config_num_leds) leds[i+ledStartOffset] = CRGB::Blue;
    }

    if(config_sacrifice_led) leds[0] = CRGB(20,0,0); 
    FastLED.show();
}

// --------------------------------------------------------------------------
// ORIGINAL BONUS GAME & LEVEL LOGIC
// --------------------------------------------------------------------------
void startBonusGame() {
    bonusEnemiesSpawned = 0;
    bonusLives = 10;
    bonusWaveCount = 0;
    bonusInPause = false;
    bonusSpeedMultiplier = 1.0;
    bonusFlashTimer = 0;
    bonusEnemies.clear();
    bonusShots.clear();
    lastFireTime = millis();
    currentState = STATE_BONUS_PLAYING;
    playSound(EVT_BONUS_WAVE);
}

void updateLevelIntro() {
  if (!bonusPlayedThisLevel && (currentLevel <= 10 && levels[currentLevel].bossType > 0)) {
      if (digitalRead(PIN_BTN_RED) == LOW && digitalRead(PIN_BTN_BLUE) == LOW && digitalRead(PIN_BTN_GREEN) == LOW) {
           bonusPlayedThisLevel = true;
           bonusReturnLevel = currentLevel;
           currentState = STATE_BONUS_INTRO;
           stateTimer = millis();
           playSound(EVT_BONUS_START);
           return;
      }
  }

  unsigned long elapsed = millis() - stateTimer;
  if (elapsed > 2000 && elapsed < 4000) {
    if ((elapsed / 250) % 2 == 0) drawLevelIntro(currentLevel);
    else { FastLED.clear(); if(config_sacrifice_led) leds[0] = CRGB(20, 0, 0); FastLED.show(); }
  } else if (elapsed <= 2000) { drawLevelIntro(currentLevel);
  }

  if (elapsed >= 4000) {
    uint8_t bright = map(config_brightness_pct, 10, 100, 25, 255);
    FastLED.setBrightness(bright);
    levelStartTime = millis(); 
    
    bool isBossLevel = (currentLevel <= 10 && levels[currentLevel].bossType > 0);
    if (isBossLevel) {
      currentBossType = levels[currentLevel].bossType;
      bossSegments.clear(); enemies.clear(); shots.clear(); bossProjectiles.clear();
      enemyFrontIndex = (float)config_num_leds - 1.0;
      if (currentBossType == 1) {
        for(int i=0; i<3; i++) bossSegments.push_back({3, boss1Cfg.hpPerLed, boss1Cfg.hpPerLed, true, 0});
        for(int i=0; i<3; i++) bossSegments.push_back({1, boss1Cfg.hpPerLed, boss1Cfg.hpPerLed, true, 0});
        for(int i=0; i<3; i++) bossSegments.push_back({3, boss1Cfg.hpPerLed, boss1Cfg.hpPerLed, true, 0});
        bossActionTimer = millis();
        boss1WrongHits = 0;
        boss1RageMode = false;
      } 
      else if (currentBossType == 2) {
        for(int i=0; i<9; i++) bossSegments.push_back({0, boss2Cfg.hpPerLed, boss2Cfg.hpPerLed, false, i});
        boss2Section = 0; boss2State = B2_MOVE;
        markerPos[0] = (int)(config_num_leds * (boss2Cfg.m1 / 100.0));
        markerPos[1] = (int)(config_num_leds * (boss2Cfg.m2 / 100.0));
        markerPos[2] = (int)(config_num_leds * (boss2Cfg.m3 / 100.0));
      } 
      else if (currentBossType == 3) {
        for(int i=0; i<15; i++) { int mixColor = random(4, 7);
        bossSegments.push_back({mixColor, boss3Cfg.hpPerLed, boss3Cfg.hpPerLed, true, i}); }
        boss3State = B3_MOVE; 
        boss3PhaseIndex = 0;
        boss3Markers[0] = (int)(config_num_leds * 0.66);
        boss3Markers[1] = (int)(config_num_leds * 0.50);
        bossActionTimer = millis();
      }
      currentState = STATE_BOSS_PLAYING;
    } else {
      currentBossType = 0;
      enemies.clear(); shots.clear(); bossProjectiles.clear();
      
      int effectiveLevel = currentLevel;
      if (currentLevel > 10) effectiveLevel = ((currentLevel - 1) % 10) + 1;
      int count = levels[effectiveLevel].length;
      if (count <= 0) count = 10;
      for (int i = 0; i < count; i++) {
          int color = random(1, 4);
          if (currentLevel >= 11) color = random(1, 7); 
          enemies.push_back({color, 0.0});
      }
      enemyFrontIndex = (float)config_num_leds - 1.0;
      currentState = STATE_PLAYING;
    }
  }
}

void updateBonusIntro() {
    unsigned long elapsed = millis() - stateTimer;
    if (elapsed < 2500) {
        if ((elapsed / 250) % 2 == 0) {
            FastLED.clear();
            for(int i=0; i<config_num_leds; i+=2) leds[i+ledStartOffset] = CRGB::Yellow;
        } else {
            FastLED.clear();
        }
        if(config_sacrifice_led) leds[0] = CRGB(20,0,0);
        FastLED.show();
    } else {
        if (random(0, 100) < 50) {
            startSimonBonus();
        } else {
            startBonusGame();
        }
    }
}

void updateBonusGame() {
    unsigned long now = millis();
    bool r = (digitalRead(PIN_BTN_RED) == LOW);
    bool g = (digitalRead(PIN_BTN_GREEN) == LOW);
    bool pressed = (r || g);
    
    if (!pressed) buttonsReleased = true;
    if (pressed && buttonsReleased && (now - lastFireTime > FIRE_COOLDOWN)) {
        int c = 0;
        if (r) c = 2; // Red
        else if (g) c = 3; // Green
        
        if (c > 0) {
            bonusShots.push_back({0.0, c});
            playShotSound(c);
            lastFireTime = now;
            buttonsReleased = false;
        }
    }
    
    if (bonusEnemiesSpawned < 200) {
        if (bonusInPause) {
            if (now - bonusPauseTimer > 2000) {
                bonusInPause = false;
                bonusWaveCount = 0; 
                bonusSpeedMultiplier += 0.2;
                bonusFlashTimer = now;
                playSound(EVT_BONUS_SPEEDUP);
            }
        } else {
            static unsigned long lastBonusSpawn = 0;
            int spawnRate = (int)(600.0 / bonusSpeedMultiplier);
            if (now - lastBonusSpawn > spawnRate) { 
                lastBonusSpawn = now;
                int color = (random(0,2) == 0) ? 2 : 3; 
                bonusEnemies.push_back({color, (float)config_num_leds - 1.0, false});
                bonusEnemiesSpawned++;
                bonusWaveCount++;
                if (bonusWaveCount >= 25) {
                    bonusInPause = true;
                    bonusPauseTimer = now;
                }
            }
        }
    }
    
    float shotSpeed = (float)config_shot_speed_pct / 60.0 * 0.8;
    for (int i = bonusShots.size() - 1; i >= 0; i--) {
        bonusShots[i].position += shotSpeed;
        bool remove = false;
        
        if (!bonusEnemies.empty()) {
             for(int e=0; e<bonusEnemies.size(); e++) {
                 if (abs(bonusShots[i].position - bonusEnemies[e].pos) < 1.0) {
                     if (bonusShots[i].color == bonusEnemies[e].color) {
                         bonusEnemies.erase(bonusEnemies.begin() + e);
                         currentScore += 500; 
                         flashPixel((int)bonusShots[i].position);
                         remove = true;
                         break;
                     } else {
                         remove = true;
                         bonusLives--;
                         playSound(EVT_MISTAKE);
                         break;
                     }
                 }
             }
        }
        
        if (!remove && bonusShots[i].position >= config_num_leds) {
            remove = true;
            bonusLives--;
            playSound(EVT_MISTAKE);
        }
        
        if (remove) bonusShots.erase(bonusShots.begin() + i);
    }
    
    float enemySpeed = (25.0 / 60.0) * bonusSpeedMultiplier;
    for (int i = bonusEnemies.size() - 1; i >= 0; i--) {
        bonusEnemies[i].pos -= enemySpeed;
        if (bonusEnemies[i].pos <= config_homebase_size) {
            bonusEnemies.erase(bonusEnemies.begin() + i);
            bonusLives--;
            playSound(EVT_MISTAKE);
        }
    }
    
    if (bonusLives <= 0) {
        playSound(EVT_LOSE);
        startLevelIntro(bonusReturnLevel); 
        return;
    }
    
    if (bonusEnemiesSpawned >= 200 && bonusEnemies.empty()) {
        playSound(EVT_FINAL_WIN);
        startLevelIntro(bonusReturnLevel); 
        return;
    }

    FastLED.clear();
    bool doFlash = (now - bonusFlashTimer < 200) && (bonusFlashTimer > 0);
    for(auto &e : bonusEnemies) {
        CRGB c = getColor(e.color);
        if (doFlash) c = CRGB::White; 
        drawCrispPixel(e.pos, c);
    }
    for(auto &s : bonusShots) {
        drawCrispPixel(s.position, getColor(s.color));
    }
    for(int i=0; i<bonusLives; i++) {
        if(i+ledStartOffset < config_num_leds) leds[i+ledStartOffset] = CRGB::Yellow;
    }
    if(config_sacrifice_led) leds[0] = CRGB(20,0,0); 
    FastLED.show();
}

void updateLevelCompletedAnim() {
  unsigned long elapsed = millis() - stateTimer;
  if (elapsed < 1000) {
    fill_solid(leds, config_num_leds + ledStartOffset, col_c3);
    if(config_sacrifice_led) leds[0] = CRGB(20,0,0);
  } else if (elapsed < 5000) { 
    FastLED.clear();
...

This file has been truncated, please download it to see its full contents.

Credits

WorksAsDesigned
1 project โ€ข 0 followers
DIY

Comments