// ==========================================================================
// 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.
Comments