From bb6114e8aaa7749f93ab50103c8b1e22fd8156bc Mon Sep 17 00:00:00 2001 From: BobLoeffler68 Date: Sun, 14 Dec 2025 04:36:51 -0700 Subject: [PATCH] PacMan effect (#4891) * PacMan effect added --- wled00/FX.cpp | 197 ++++++++++++++++++++++++++++++++++++++++++++++++++ wled00/FX.h | 1 + 2 files changed, 198 insertions(+) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 38f1b7cb..ffa7e2c6 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -3178,6 +3178,202 @@ static uint16_t rolling_balls(void) { static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of balls,,,,Collide,Overlay,Trails;!,!,!;!;1;m12=1"; //bar #endif // WLED_PS_DONT_REPLACE_1D_FX + +/* +/ Pac-Man by Bob Loeffler with help from @dedehai and @blazoncek +* speed slider is for speed. +* intensity slider is for selecting the number of power dots. +* custom1 slider is for selecting the LED where the ghosts will start blinking blue. +* custom2 slider is for blurring the LEDs in the segment. +* custom3 slider is for selecting the # of ghosts (between 2 and 8). +* check1 is for displaying White Dots that PacMan eats. Enabled will show white dots. Disabled will not show any white dots (all leds will be black). +* check2 is for Smear mode (enabled will smear/persist the LED colors, disabled will not). +* check3 is for the Compact Dots mode of displaying white dots. Enabled will show white dots in every LED. Disabled will show black LEDs between the white dots. +* aux0 is used to keep track of the previous number of power dots in case the user selects a different number with the intensity slider. +* aux1 is the main counter for timing. +*/ +typedef struct PacManChars { + signed pos; + signed topPos; // LED position of farthest PacMan has moved + uint32_t color; + bool direction; // true = moving away from first LED + bool blue; // used for ghosts only + bool eaten; // used for power dots only +} pacmancharacters_t; + +static uint16_t mode_pacman(void) { + constexpr unsigned ORANGEYELLOW = 0xFFCC00; + constexpr unsigned PURPLEISH = 0xB000B0; + constexpr unsigned ORANGEISH = 0xFF8800; + constexpr unsigned WHITEISH = 0x999999; + constexpr unsigned PACMAN = 0; // PacMan is character[0] + constexpr uint32_t ghostColors[] = {RED, PURPLEISH, CYAN, ORANGEISH}; + + unsigned maxPowerDots = min(SEGLEN / 10U, 255U); // cap the max so packed state fits in 8 bits + unsigned numPowerDots = map(SEGMENT.intensity, 0, 255, 1, maxPowerDots); + unsigned numGhosts = map(SEGMENT.custom3, 0, 31, 2, 8); + bool smearMode = SEGMENT.check2; + + // Pack two 8-bit values into one 16-bit field (stored in SEGENV.aux0) + uint16_t combined_value = uint16_t(((numPowerDots & 0xFF) << 8) | (numGhosts & 0xFF)); + if (combined_value != SEGENV.aux0) SEGENV.call = 0; // Reinitialize on setting change + SEGENV.aux0 = combined_value; + + // Allocate segment data + unsigned dataSize = sizeof(pacmancharacters_t) * (numGhosts + maxPowerDots + 1); // +1 is the PacMan character + if (SEGLEN <= 16 + (2*numGhosts) || !SEGENV.allocateData(dataSize)) return mode_static(); + pacmancharacters_t *character = reinterpret_cast(SEGENV.data); + + // Calculate when blue ghosts start blinking. + // On first call (or after settings change), `topPos` is not known yet, so fall back to the full segment length in that case. + int maxBlinkPos = (SEGENV.call == 0) ? (int)SEGLEN - 1 : character[PACMAN].topPos; + if (maxBlinkPos < 20) maxBlinkPos = 20; + int startBlinkingGhostsLED = (SEGLEN < 64) + ? (int)SEGLEN / 3 + : map(SEGMENT.custom1, 0, 255, 20, maxBlinkPos); + + // Initialize characters on first call + if (SEGENV.call == 0) { + // Initialize PacMan + character[PACMAN].color = YELLOW; + character[PACMAN].pos = 0; + character[PACMAN].topPos = 0; + character[PACMAN].direction = true; + character[PACMAN].blue = false; + + // Initialize ghosts with alternating colors + for (int i = 1; i <= numGhosts; i++) { + character[i].color = ghostColors[(i-1) % 4]; + character[i].pos = -2 * (i + 1); + character[i].direction = true; + character[i].blue = false; + } + + // Initialize power dots + for (int i = 0; i < numPowerDots; i++) { + character[i + numGhosts + 1].color = ORANGEYELLOW; + character[i + numGhosts + 1].eaten = false; + } + character[numGhosts + 1].pos = SEGLEN - 1; // Last power dot at end + } + + if (strip.now > SEGENV.step) { + SEGENV.step = strip.now; + SEGENV.aux1++; + } + + // Clear background if not in smear mode + if (!smearMode) SEGMENT.fill(BLACK); + + // Draw white dots in front of PacMan if option selected + if (SEGMENT.check1) { + int step = SEGMENT.check3 ? 1 : 2; // Compact or spaced dots + for (int i = SEGLEN - 1; i > character[PACMAN].topPos; i -= step) { + SEGMENT.setPixelColor(i, WHITEISH); + } + } + + // Update power dot positions dynamically + uint32_t everyXLeds = (((uint32_t)SEGLEN - 10U) << 8) / numPowerDots; // Fixed-point spacing for power dots: use 32-bit math to avoid overflow on long segments. + for (int i = 1; i < numPowerDots; i++) { + character[i + numGhosts + 1].pos = 10 + ((i * everyXLeds) >> 8); + } + + // Blink power dots every 10 ticks + if (SEGENV.aux1 % 10 == 0) { + uint32_t dotColor = (character[numGhosts + 1].color == ORANGEYELLOW) ? BLACK : ORANGEYELLOW; + for (int i = 0; i < numPowerDots; i++) { + character[i + numGhosts + 1].color = dotColor; + } + } + + // Blink blue ghosts when nearing start + if (SEGENV.aux1 % 15 == 0 && character[1].blue && character[PACMAN].pos <= startBlinkingGhostsLED) { + uint32_t ghostColor = (character[1].color == BLUE) ? WHITEISH : BLUE; + for (int i = 1; i <= numGhosts; i++) { + character[i].color = ghostColor; + } + } + + // Draw uneaten power dots + for (int i = 0; i < numPowerDots; i++) { + if (!character[i + numGhosts + 1].eaten && (unsigned)character[i + numGhosts + 1].pos < SEGLEN) { + SEGMENT.setPixelColor(character[i + numGhosts + 1].pos, character[i + numGhosts + 1].color); + } + } + + // Check if PacMan ate a power dot + for (int j = 0; j < numPowerDots; j++) { + auto &dot = character[j + numGhosts + 1]; + if (character[PACMAN].pos == dot.pos && !dot.eaten) { + // Reverse all characters - PacMan now chases ghosts + for (int i = 0; i <= numGhosts; i++) { + character[i].direction = false; + } + // Turn ghosts blue + for (int i = 1; i <= numGhosts; i++) { + character[i].color = BLUE; + character[i].blue = true; + } + dot.eaten = true; + break; // only one power dot per frame + } + } + + // Reset when PacMan reaches start with blue ghosts + if (character[1].blue && character[PACMAN].pos <= 0) { + // Reverse direction back + for (int i = 0; i <= numGhosts; i++) { + character[i].direction = true; + } + // Reset ghost colors + for (int i = 1; i <= numGhosts; i++) { + character[i].color = ghostColors[(i-1) % 4]; + character[i].blue = false; + } + // Reset power dots if last one was eaten + if (character[numGhosts + 1].eaten) { + for (int i = 0; i < numPowerDots; i++) { + character[i + numGhosts + 1].eaten = false; + } + character[PACMAN].topPos = 0; // set the top position of PacMan to LED 0 (beginning of the segment) + } + } + + // Update and draw characters based on speed setting + bool updatePositions = (SEGENV.aux1 % map(SEGMENT.speed, 0, 255, 15, 1) == 0); + + // update positions of characters if it's time to do so + if (updatePositions) { + character[PACMAN].pos += character[PACMAN].direction ? 1 : -1; + for (int i = 1; i <= numGhosts; i++) { + character[i].pos += character[i].direction ? 1 : -1; + } + } + + // Draw PacMan + if ((unsigned)character[PACMAN].pos < SEGLEN) { + SEGMENT.setPixelColor(character[PACMAN].pos, character[PACMAN].color); + } + + // Draw ghosts + for (int i = 1; i <= numGhosts; i++) { + if ((unsigned)character[i].pos < SEGLEN) { + SEGMENT.setPixelColor(character[i].pos, character[i].color); + } + } + + // Track farthest position of PacMan + if (character[PACMAN].topPos < character[PACMAN].pos) { + character[PACMAN].topPos = character[PACMAN].pos; + } + + SEGMENT.blur(SEGMENT.custom2>>1); + return FRAMETIME; +} +static const char _data_FX_MODE_PACMAN[] PROGMEM = "PacMan@Speed,# of PowerDots,Blink distance,Blur,# of Ghosts,Dots,Smear,Compact;;!;1;m12=0,sx=192,ix=64,c1=64,c2=0,c3=12,o1=1,o2=0"; + + /* * Sinelon stolen from FASTLED examples */ @@ -10929,6 +11125,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_BLENDS, &mode_blends, _data_FX_MODE_BLENDS); addEffect(FX_MODE_TV_SIMULATOR, &mode_tv_simulator, _data_FX_MODE_TV_SIMULATOR); addEffect(FX_MODE_DYNAMIC_SMOOTH, &mode_dynamic_smooth, _data_FX_MODE_DYNAMIC_SMOOTH); + addEffect(FX_MODE_PACMAN, &mode_pacman, _data_FX_MODE_PACMAN); // --- 1D audio effects --- addEffect(FX_MODE_PIXELS, &mode_pixels, _data_FX_MODE_PIXELS); diff --git a/wled00/FX.h b/wled00/FX.h index fbea92bf..bcbab69a 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -310,6 +310,7 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_2DFIRENOISE 149 #define FX_MODE_2DSQUAREDSWIRL 150 // #define FX_MODE_2DFIRE2012 151 +#define FX_MODE_PACMAN 151 // gap fill (non-SR). Do NOT renumber; SR-ID range must remain stable. #define FX_MODE_2DDNA 152 #define FX_MODE_2DMATRIX 153 #define FX_MODE_2DMETABALLS 154