/*
Code for tracking Life Total, Poison Counters, and Commander Damage
in EDH/Commander. Uses a SSD1306 OLED Display and a rotary encoder
with push-button.
*/
#include <Arduino.h>
#include <SPI.h>
#include <Wire.h>
#include <U8g2lib.h>
#include <RotaryEncoder.h>
#include "Counter.h" // Class for tracking counters
#include "Splash.h"
// U8G2_SSD1306_128X64_NONAME_F_HW_I2C(<rotation>, [reset [, clock, data]]); For full framebuffer mode
U8G2_SSD1306_128X64_NONAME_F_HW_I2C display(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
// HW Pins required
#define PIN_BTN 13 // RotaryEncoder SW Pin
#define PIN_ROTA 14 // RotaryEncoder DT Pin
#define PIN_ROTB 15 // RotaryEncoder CLK Pin
#define PIN_LED 25 // Builtin LED
RotaryEncoder encoder(PIN_ROTA, PIN_ROTB, RotaryEncoder::LatchMode::FOUR3);
#define MAX_OPPONENTS 5
Counter lifeTotal("Life Total", 40, 0, 100, 0, true);
Counter poisonCounters("Poison Counters", 0, 0, 10, 10);
Counter* commanderDamage = nullptr; // Placeholder before selecting number of opponents
Counter** counters = nullptr; // Pointer to an array of pointers to Counter objects
uint8_t numCounters = 2; // Initially, we have lifeTotal and poisonCounters
uint8_t currentCounterIndex = 0;
uint8_t numOpponents = 0;
// For non-blocking button handling
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 200; // Debounce delay in milliseconds
// Function prototypes
int8_t handleRotation();
void selectNumOpponents();
void setupCommanderDamageCounters();
void displayCounter();
// Setup
void setup() {
// Initialize button
pinMode(PIN_BTN, INPUT_PULLUP);
pinMode(PIN_LED, OUTPUT);
// Initialize display
display.begin();
display.clearBuffer();
display.drawBitmap(0, 0, 64/8, 64, splash_data);
display.setFont(u8g2_font_luBIS08_tf);
display.setCursor(64, 20);
display.println("Commander");
display.println("Tracker");
display.sendBuffer();
delay(3000);
display.clear();
display.setFont(u8g2_font_ncenB08_tr); // Choose a suitable font
display.drawStr(0, 12, "Select Opponents");
display.setFont(u8g2_font_ncenB14_tr);
display.setCursor(0, 30);
display.print(numOpponents);
display.sendBuffer();
// Select number of opponents
selectNumOpponents();
// Display the initial counter
displayCounter();
}
// Main loop
void loop() {
int8_t rotation = handleRotation();
if (rotation != 0) {
if (rotation == 1) {
// Update the current counter based on rotation direction
counters[currentCounterIndex]->increment();
} else if (rotation == -1) {
counters[currentCounterIndex]->decrement();
}
displayCounter();
}
if (digitalRead(PIN_BTN) == LOW && (millis() - lastDebounceTime) > debounceDelay) {
lastDebounceTime = millis();
currentCounterIndex = (currentCounterIndex + 1) % numCounters;
displayCounter();
}
}
// Non-blocking rotation handling
int8_t handleRotation() {
encoder.tick();
switch (encoder.getDirection()) {
case RotaryEncoder::Direction::CLOCKWISE:
return 1;
case RotaryEncoder::Direction::COUNTERCLOCKWISE:
return -1;
case RotaryEncoder::Direction::NOROTATION:
default:
return 0;
}
}
// Loop for selecting number of opponents
void selectNumOpponents() {
bool selectingOpponents = true;
while (selectingOpponents) {
int8_t rotation = handleRotation();
if (rotation != 0) {
if (rotation == 1 && numOpponents < MAX_OPPONENTS) {
numOpponents++;
} else if (rotation == -1 && numOpponents > 0) {
numOpponents--;
}
display.clearBuffer();
display.setFont(u8g2_font_ncenB08_tr);
display.drawStr(0, 12, "Select Opponents");
display.setFont(u8g2_font_ncenB14_tr);
display.setCursor(0, 30);
display.print(numOpponents);
display.sendBuffer();
}
// Check for button press to confirm the selection
if (digitalRead(PIN_BTN) == LOW && (millis() - lastDebounceTime) > debounceDelay) {
lastDebounceTime = millis();
selectingOpponents = false;
}
}
// Apply opponents after selection
setupCommanderDamageCounters();
}
void setupCommanderDamageCounters() {
numCounters = 2 + numOpponents; // Update total number of counters
// Allocate memory for all counters, including lifeTotal, poisonCounters, and commanderDamage counters
counters = new Counter*[numCounters];
// Assign lifeTotal and poisonCounters to the counters array
counters[0] = &lifeTotal;
counters[1] = &poisonCounters;
static char names[MAX_OPPONENTS][12]; // Name buffer
// Allocate memory for commander damage counters and assign them to the counters array
commanderDamage = new Counter[numOpponents];
for (uint8_t i = 0; i < numOpponents; i++) {
snprintf(names[i], sizeof(names[i]), "Cmdr Dmg %d", i + 1); // Safely format the name into the buffer
commanderDamage[i] = Counter(names[i], 0, 0, 100, 21); // Initialize each with commander damage rules
counters[2 + i] = &commanderDamage[i]; // Assign to the counters array
}
}
// Function to check if any counter is dead
bool checkIfAnyCounterIsDead() {
for (uint8_t i = 0; i < numCounters; i++) {
if (counters[i]->isDead) {
return true;
}
}
return false;
}
void displayCounter() {
display.clearBuffer();
Counter* counter = counters[currentCounterIndex];
display.setFont(u8g2_font_ncenB08_tr);
display.setCursor(0, 12);
display.print(counter->getName());
display.setFont(u8g2_font_ncenB14_tr); // Larger text size for the counter value
display.setCursor(0, 30);
display.print(counter->currentCount);
display.sendBuffer(); // Update the display with the new information
// Check if any of the counters are lethal and power LED if so
if (checkIfAnyCounterIsDead()) {
digitalWrite(PIN_LED, HIGH); // Turn on LED if any counter is dead
} else {
digitalWrite(PIN_LED, LOW); // Turn off LED if no counter is dead
}
}
pico:GP0
pico:GP1
pico:GND.1
pico:GP2
pico:GP3
pico:GP4
pico:GP5
pico:GND.2
pico:GP6
pico:GP7
pico:GP8
pico:GP9
pico:GND.3
pico:GP10
pico:GP11
pico:GP12
pico:GP13
pico:GND.4
pico:GP14
pico:GP15
pico:GP16
pico:GP17
pico:GND.5
pico:GP18
pico:GP19
pico:GP20
pico:GP21
pico:GND.6
pico:GP22
pico:RUN
pico:GP26
pico:GP27
pico:GND.7
pico:GP28
pico:ADC_VREF
pico:3V3
pico:3V3_EN
pico:GND.8
pico:VSYS
pico:VBUS
oled1:GND
oled1:VCC
oled1:SCL
oled1:SDA
encoder1:CLK
encoder1:DT
encoder1:SW
encoder1:VCC
encoder1:GND