|
|
#include "draw.h"
|
|
|
#include "config.h"
|
|
|
#include "game.h"
|
|
|
#include "font.h"
|
|
|
#include "state.h"
|
|
|
#include "driver/ledc.h"
|
|
|
#include "driver/i2c.h"
|
|
|
#include "freertos/FreeRTOS.h"
|
|
|
#include <string.h>
|
|
|
#include <stdio.h>
|
|
|
|
|
|
const int menu_slot[NUM_MENUS] = {0, 1, 2, 7};
|
|
|
const char *setting_names[NUM_SETTINGS] = {
|
|
|
"LED BRIGHTNESS", "STARTING LIFE", "NUM OPPONENTS", "BLE",
|
|
|
"RESET", "RESET ALL", "GAME ID", "PLAYER NAME",
|
|
|
"MENU HOLD MS", "LR HOLD MS", "FLIP DISPLAY", "DELTA TIMEOUT",
|
|
|
};
|
|
|
|
|
|
#define LEDC_TIMER_SEL LEDC_TIMER_0
|
|
|
#define LEDC_MODE_ LEDC_LOW_SPEED_MODE
|
|
|
#define LEDC_DUTY_RES_ LEDC_TIMER_8_BIT
|
|
|
#define LEDC_FREQ_HZ_ 5000
|
|
|
|
|
|
// ── LED ───────────────────────────────────────────────────────────────────────
|
|
|
void led_init(void)
|
|
|
{
|
|
|
ledc_timer_config_t timer = {
|
|
|
.speed_mode = LEDC_MODE_,
|
|
|
.timer_num = LEDC_TIMER_SEL,
|
|
|
.duty_resolution = LEDC_DUTY_RES_,
|
|
|
.freq_hz = LEDC_FREQ_HZ_,
|
|
|
.clk_cfg = LEDC_AUTO_CLK,
|
|
|
};
|
|
|
ledc_timer_config(&timer);
|
|
|
ledc_channel_config_t ch[] = {
|
|
|
{.gpio_num=LED_R_GPIO,.speed_mode=LEDC_MODE_,.channel=LEDC_CHANNEL_0,.timer_sel=LEDC_TIMER_SEL,.duty=0,.hpoint=0},
|
|
|
{.gpio_num=LED_G_GPIO,.speed_mode=LEDC_MODE_,.channel=LEDC_CHANNEL_1,.timer_sel=LEDC_TIMER_SEL,.duty=0,.hpoint=0},
|
|
|
{.gpio_num=LED_B_GPIO,.speed_mode=LEDC_MODE_,.channel=LEDC_CHANNEL_2,.timer_sel=LEDC_TIMER_SEL,.duty=0,.hpoint=0},
|
|
|
};
|
|
|
for (int i = 0; i < 3; i++) ledc_channel_config(&ch[i]);
|
|
|
}
|
|
|
|
|
|
void led_update_for_count(int count, uint8_t scale)
|
|
|
{
|
|
|
int sl = start_life_opts[g_start_life_index];
|
|
|
int maxl = 2 * sl;
|
|
|
int low = sl / 4;
|
|
|
int range = sl - low;
|
|
|
int c = count < 0 ? 0 : count > maxl ? maxl : count;
|
|
|
uint8_t r, g, b;
|
|
|
if (c <= sl) {
|
|
|
int cc = c < low ? low : c;
|
|
|
r = (uint8_t)((uint32_t)(sl - cc) * scale * g_led_max / range / 255);
|
|
|
g = (uint8_t)((uint32_t)(cc - low) * scale * g_led_max / range / 255);
|
|
|
b = 0;
|
|
|
} else {
|
|
|
r = 0;
|
|
|
g = (uint8_t)((uint32_t)(maxl - c) * g_led_max / sl);
|
|
|
b = (uint8_t)((uint32_t)(c - sl) * g_led_max / sl);
|
|
|
}
|
|
|
ledc_set_duty(LEDC_MODE_, LEDC_CHANNEL_0, r); ledc_update_duty(LEDC_MODE_, LEDC_CHANNEL_0);
|
|
|
ledc_set_duty(LEDC_MODE_, LEDC_CHANNEL_1, g); ledc_update_duty(LEDC_MODE_, LEDC_CHANNEL_1);
|
|
|
ledc_set_duty(LEDC_MODE_, LEDC_CHANNEL_2, b); ledc_update_duty(LEDC_MODE_, LEDC_CHANNEL_2);
|
|
|
}
|
|
|
|
|
|
void led_off(void)
|
|
|
{
|
|
|
ledc_set_duty(LEDC_MODE_, LEDC_CHANNEL_0, 0); ledc_update_duty(LEDC_MODE_, LEDC_CHANNEL_0);
|
|
|
ledc_set_duty(LEDC_MODE_, LEDC_CHANNEL_1, 0); ledc_update_duty(LEDC_MODE_, LEDC_CHANNEL_1);
|
|
|
ledc_set_duty(LEDC_MODE_, LEDC_CHANNEL_2, 0); ledc_update_duty(LEDC_MODE_, LEDC_CHANNEL_2);
|
|
|
}
|
|
|
|
|
|
// ── OLED ──────────────────────────────────────────────────────────────────────
|
|
|
static void oled_write(const uint8_t *buf, size_t len)
|
|
|
{
|
|
|
i2c_master_write_to_device(I2C_PORT, OLED_ADDR, buf, len, pdMS_TO_TICKS(100));
|
|
|
}
|
|
|
static void oled_cmd(uint8_t c) { uint8_t b[2]={0x00,c}; oled_write(b,2); }
|
|
|
static void oled_write_page(int p, const uint8_t *row);
|
|
|
|
|
|
void oled_init(void)
|
|
|
{
|
|
|
static const uint8_t seq[] = {
|
|
|
0xAE,0xD5,0x80,0xA8,0x3F,0xD3,0x00,0x40,0x8D,0x14,0x20,0x00,
|
|
|
0xA0,0xC0,0xDA,0x12,0x81,0xCF,0xD9,0xF1,0xDB,0x40,0xA4,0xA6,0xAF,
|
|
|
};
|
|
|
for (int i = 0; i < (int)sizeof(seq); i++) oled_cmd(seq[i]);
|
|
|
}
|
|
|
|
|
|
void oled_set_flip(int flip)
|
|
|
{
|
|
|
oled_cmd(flip ? 0xA1 : 0xA0);
|
|
|
oled_cmd(flip ? 0xC8 : 0xC0);
|
|
|
}
|
|
|
|
|
|
void oled_clear(void)
|
|
|
{
|
|
|
uint8_t row[OLED_WIDTH]; memset(row, 0, sizeof(row));
|
|
|
for (int p = 0; p < OLED_PAGES; p++) oled_write_page(p, row);
|
|
|
}
|
|
|
|
|
|
void oled_set_contrast(uint8_t val)
|
|
|
{
|
|
|
oled_cmd(0x81);
|
|
|
oled_cmd(val);
|
|
|
}
|
|
|
|
|
|
void oled_draw_sleep(void)
|
|
|
{
|
|
|
// Crescent moon: outer circle (64,31) r=27, inner cutout (76,25) r=23
|
|
|
uint8_t row[OLED_WIDTH];
|
|
|
for (int p = 0; p < OLED_PAGES; p++) {
|
|
|
memset(row, 0, sizeof(row));
|
|
|
for (int x = 0; x < OLED_WIDTH; x++) {
|
|
|
uint8_t byte = 0;
|
|
|
for (int bit = 0; bit < 8; bit++) {
|
|
|
int y = p * 8 + bit;
|
|
|
int dox = x - 64, doy = y - 31;
|
|
|
int dix = x - 76, diy = y - 25;
|
|
|
if (dox*dox + doy*doy <= 27*27 &&
|
|
|
dix*dix + diy*diy > 23*23) {
|
|
|
byte |= (1 << bit);
|
|
|
}
|
|
|
}
|
|
|
row[x] = byte;
|
|
|
}
|
|
|
oled_write_page(p, row);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
static void oled_write_page(int p, const uint8_t *row)
|
|
|
{
|
|
|
oled_cmd(0x21); oled_cmd(0); oled_cmd(127);
|
|
|
oled_cmd(0x22); oled_cmd(p); oled_cmd(p);
|
|
|
uint8_t buf[OLED_WIDTH+1]; buf[0]=0x40;
|
|
|
memcpy(buf+1, row, OLED_WIDTH); oled_write(buf, sizeof(buf));
|
|
|
}
|
|
|
|
|
|
void oled_draw_header(void)
|
|
|
{
|
|
|
uint8_t pages[HEADER_PAGES][OLED_WIDTH];
|
|
|
memset(pages, 0xFF, sizeof(pages));
|
|
|
for (int seg = 0; seg < NUM_MENUS; seg++) {
|
|
|
uint8_t bg = (seg == g_active_menu) ? 0x00 : 0xFF;
|
|
|
int sx = menu_slot[seg] * SEG_W;
|
|
|
const uint8_t (*icon)[ICON_W] = (seg == MENU_CMDR && g_ble_enabled) ? icon_net : icons[seg];
|
|
|
for (int col = 0; col < ICON_W; col++) {
|
|
|
pages[0][sx+col] = bg ^ icon[0][col];
|
|
|
pages[1][sx+col] = bg ^ icon[1][col];
|
|
|
}
|
|
|
}
|
|
|
for (int p = 0; p < HEADER_PAGES; p++) oled_write_page(p, pages[p]);
|
|
|
}
|
|
|
|
|
|
void oled_draw_centered(const char *str)
|
|
|
{
|
|
|
int len = strlen(str);
|
|
|
int start = (OLED_WIDTH - len * CHAR_WIDTH) / 2;
|
|
|
if (start < 0) start = 0;
|
|
|
for (int p = HEADER_PAGES; p < OLED_PAGES; p++) {
|
|
|
uint8_t row[OLED_WIDTH]; memset(row, 0, sizeof(row));
|
|
|
if (p >= START_PAGE && p < START_PAGE + CHAR_PAGES) {
|
|
|
int char_page = p - START_PAGE;
|
|
|
for (int i = 0; i < len; i++) {
|
|
|
char c = str[i];
|
|
|
if (c < FONT_BASE || c > FONT_MAX) c = ' ';
|
|
|
const uint8_t *glyph = font5x8[(uint8_t)c - FONT_BASE];
|
|
|
int col = start + i * CHAR_WIDTH;
|
|
|
for (int sc = 0; sc < 5*SCALE; sc++) {
|
|
|
int x = col + sc;
|
|
|
if (x < 0 || x >= OLED_WIDTH) continue;
|
|
|
uint8_t font_col = glyph[sc/SCALE];
|
|
|
uint8_t pixel_byte = 0;
|
|
|
for (int b = 0; b < 8; b++) {
|
|
|
int src_row = (char_page*8+b)/SCALE;
|
|
|
if (src_row < 8 && ((font_col >> src_row) & 1)) pixel_byte |= (1<<b);
|
|
|
}
|
|
|
row[x] = pixel_byte;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
oled_write_page(p, row);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// ── Pixel framebuffer helpers ─────────────────────────────────────────────────
|
|
|
|
|
|
static uint8_t row_mask(int p, int base, int h)
|
|
|
{
|
|
|
uint8_t mask = 0;
|
|
|
for (int r = 0; r < 8; r++)
|
|
|
if (8*p+r >= base && 8*p+r < base+h) mask |= (1<<r);
|
|
|
return mask;
|
|
|
}
|
|
|
|
|
|
static void pxbuf_fill(uint8_t pages[][OLED_WIDTH], int content_h, int base, int entry_h)
|
|
|
{
|
|
|
for (int p = base/8; p <= (base+entry_h-1)/8 && p < content_h/8; p++) {
|
|
|
uint8_t mask = row_mask(p, base, entry_h);
|
|
|
for (int x = 0; x < OLED_WIDTH; x++) pages[p][x] |= mask;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
static int pxbuf_str(uint8_t pages[][OLED_WIDTH], int content_h,
|
|
|
int base, int x, const char *str, int inv)
|
|
|
{
|
|
|
for (; *str && x+5 <= OLED_WIDTH; str++) {
|
|
|
uint8_t c = (uint8_t)*str;
|
|
|
if (c < FONT_BASE || c > FONT_MAX) c = ' ';
|
|
|
const uint8_t *glyph = font5x8[c - FONT_BASE];
|
|
|
for (int sc = 0; sc < 5; sc++, x++) {
|
|
|
for (int g = 0; g < 8; g++) {
|
|
|
if ((glyph[sc] >> g) & 1) {
|
|
|
int row = base + 1 + g;
|
|
|
if (row < content_h) {
|
|
|
if (inv) pages[row/8][x] &= ~(1 << (row%8));
|
|
|
else pages[row/8][x] |= (1 << (row%8));
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
x++;
|
|
|
}
|
|
|
return x;
|
|
|
}
|
|
|
|
|
|
static void fill_cols(uint8_t pages[][OLED_WIDTH], int content_h,
|
|
|
int base, int h, int x0, int x1)
|
|
|
{
|
|
|
for (int p = base/8; p <= (base+h-1)/8 && p < content_h/8; p++) {
|
|
|
uint8_t mask = row_mask(p, base, h);
|
|
|
for (int x = x0; x < x1 && x < OLED_WIDTH; x++)
|
|
|
pages[p][x] |= mask;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 5×8 mini-icons (vertical-byte, bit0=top), same format as font5x8 columns.
|
|
|
// Teardrop: pointed top, round bottom (for poison)
|
|
|
static const uint8_t icon_mini_drop[5] = {0x3C, 0x7E, 0x7F, 0x7E, 0x3C};
|
|
|
// Lightning bolt: zigzag (for storm)
|
|
|
static const uint8_t icon_mini_bolt[5] = {0x0C, 0x0E, 0x1F, 0x39, 0x70};
|
|
|
|
|
|
static int pxbuf_icon(uint8_t pages[][OLED_WIDTH], int content_h,
|
|
|
int base, int x, const uint8_t *cols, int width, int inv)
|
|
|
{
|
|
|
for (int sc = 0; sc < width && x < OLED_WIDTH; sc++, x++) {
|
|
|
for (int g = 0; g < 8; g++) {
|
|
|
if ((cols[sc] >> g) & 1) {
|
|
|
int row = base + 1 + g;
|
|
|
if (row < content_h) {
|
|
|
if (inv) pages[row/8][x] &= ~(1 << (row%8));
|
|
|
else pages[row/8][x] |= (1 << (row%8));
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
return x + 1; // 1px gap after icon
|
|
|
}
|
|
|
|
|
|
// ── Core list renderer ────────────────────────────────────────────────────────
|
|
|
|
|
|
void oled_draw_rows(const oled_row_t *rows, int count, int active)
|
|
|
{
|
|
|
const int content_h = (OLED_PAGES - HEADER_PAGES) * 8;
|
|
|
const int entry_h = 9;
|
|
|
const int max_full = content_h / entry_h;
|
|
|
|
|
|
int start = active - max_full / 2;
|
|
|
if (start < 0) start = 0;
|
|
|
if (start + max_full > count) start = count - max_full;
|
|
|
if (start < 0) start = 0;
|
|
|
|
|
|
uint8_t pages[OLED_PAGES - HEADER_PAGES][OLED_WIDTH];
|
|
|
memset(pages, 0, sizeof(pages));
|
|
|
|
|
|
for (int ei = 0; ei < max_full+1 && (start+ei) < count; ei++) {
|
|
|
int entry = start + ei;
|
|
|
int inv = (entry == active);
|
|
|
int base = ei * entry_h;
|
|
|
|
|
|
if (inv) pxbuf_fill(pages, content_h, base, entry_h);
|
|
|
|
|
|
if (rows[entry].label && rows[entry].label[0])
|
|
|
pxbuf_str(pages, content_h, base, 2, rows[entry].label, inv);
|
|
|
|
|
|
if (rows[entry].value[0]) {
|
|
|
int vlen = (int)strlen(rows[entry].value);
|
|
|
int rx = OLED_WIDTH - 2 - vlen*6;
|
|
|
if (rx < 0) rx = 0;
|
|
|
pxbuf_str(pages, content_h, base, rx, rows[entry].value, inv);
|
|
|
|
|
|
if (inv && rows[entry].cursor >= 0 && rows[entry].cursor < vlen) {
|
|
|
int cursor_x = rx + rows[entry].cursor * 6;
|
|
|
for (int p = base/8; p <= (base+entry_h-1)/8 && p < content_h/8; p++) {
|
|
|
uint8_t mask = row_mask(p, base, entry_h);
|
|
|
for (int j = 0; j < 6 && cursor_x+j < OLED_WIDTH; j++)
|
|
|
pages[p][cursor_x+j] &= ~mask;
|
|
|
}
|
|
|
char one[2] = { rows[entry].value[rows[entry].cursor], 0 };
|
|
|
pxbuf_str(pages, content_h, base, cursor_x, one, 0);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
for (int p = 0; p < OLED_PAGES - HEADER_PAGES; p++)
|
|
|
oled_write_page(HEADER_PAGES + p, pages[p]);
|
|
|
}
|
|
|
|
|
|
// ── Menu draw functions (thin wrappers that build rows and call oled_draw_rows) ──
|
|
|
|
|
|
void oled_draw_list(const char **labels, const int *values, int count, int active)
|
|
|
{
|
|
|
oled_row_t rows[8];
|
|
|
int n = count < 8 ? count : 8;
|
|
|
for (int i = 0; i < n; i++) {
|
|
|
rows[i].label = labels[i];
|
|
|
rows[i].cursor = -1;
|
|
|
snprintf(rows[i].value, sizeof(rows[i].value), "%d", values[i]);
|
|
|
}
|
|
|
oled_draw_rows(rows, n, active);
|
|
|
}
|
|
|
|
|
|
void oled_draw_players(void)
|
|
|
{
|
|
|
int total = MAX_BLE_PEERS + 1 + (!g_ble_enabled ? 1 : 0);
|
|
|
oled_row_t rows[MAX_BLE_PEERS + 2];
|
|
|
|
|
|
rows[0].label = g_player_name;
|
|
|
rows[0].cursor = -1;
|
|
|
snprintf(rows[0].value, sizeof(rows[0].value),
|
|
|
g_eliminated ? "ELIM" : "%d/%d", g_life, g_counters[0]);
|
|
|
|
|
|
for (int pi = 0; pi < MAX_BLE_PEERS; pi++) {
|
|
|
rows[pi+1].cursor = -1;
|
|
|
if (g_peers[pi].active) {
|
|
|
rows[pi+1].label = g_peers[pi].name;
|
|
|
snprintf(rows[pi+1].value, sizeof(rows[pi+1].value),
|
|
|
g_peers[pi].eliminated ? "ELIM" : "%d/%d/%d",
|
|
|
g_peers[pi].life, g_peers[pi].poison, g_cmdr_damage[pi]);
|
|
|
} else {
|
|
|
rows[pi+1].label = "";
|
|
|
rows[pi+1].value[0] = 0;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (!g_ble_enabled) {
|
|
|
rows[MAX_BLE_PEERS+1].label = "BLE OFF";
|
|
|
rows[MAX_BLE_PEERS+1].value[0] = 0;
|
|
|
rows[MAX_BLE_PEERS+1].cursor = -1;
|
|
|
}
|
|
|
|
|
|
oled_draw_rows(rows, total, g_active_player);
|
|
|
}
|
|
|
|
|
|
void oled_draw_settings(void)
|
|
|
{
|
|
|
oled_row_t rows[NUM_SETTINGS];
|
|
|
for (int i = 0; i < NUM_SETTINGS; i++) {
|
|
|
rows[i].label = setting_names[i];
|
|
|
rows[i].cursor = -1;
|
|
|
}
|
|
|
snprintf(rows[SET_BRIGHTNESS].value, sizeof(rows[0].value), "%d%%", g_brightness_pct);
|
|
|
snprintf(rows[SET_START_LIFE].value, sizeof(rows[0].value), "%d", start_life_opts[g_start_life_index]);
|
|
|
snprintf(rows[SET_NUM_OPP].value, sizeof(rows[0].value), "%d", g_num_opponents);
|
|
|
memcpy(rows[SET_PLAYER_NAME].value, g_player_name, PLAYER_NAME_LEN);
|
|
|
rows[SET_PLAYER_NAME].value[PLAYER_NAME_LEN] = 0;
|
|
|
rows[SET_PLAYER_NAME].cursor = g_name_cursor;
|
|
|
snprintf(rows[SET_BLE].value, sizeof(rows[0].value), "%s", g_ble_enabled ? "ON" : "OFF");
|
|
|
snprintf(rows[SET_GAME_ID].value, sizeof(rows[0].value), "%02X%02X", g_game_id[0], g_game_id[1]);
|
|
|
rows[SET_GAME_ID].cursor = g_game_id_cursor;
|
|
|
snprintf(rows[SET_RESET].value, sizeof(rows[0].value), "---");
|
|
|
snprintf(rows[SET_RESET_ALL].value, sizeof(rows[0].value), "---");
|
|
|
snprintf(rows[SET_MENU_HOLD].value, sizeof(rows[0].value), "%d", g_menu_hold_ms);
|
|
|
snprintf(rows[SET_LR_HOLD].value, sizeof(rows[0].value), "%d", g_lr_hold_ms);
|
|
|
snprintf(rows[SET_DISPLAY_FLIP].value, sizeof(rows[0].value), "%s", g_display_flip ? "ON" : "OFF");
|
|
|
snprintf(rows[SET_DELTA_TIMEOUT].value, sizeof(rows[0].value), "%dms", g_delta_timeout_ms);
|
|
|
oled_draw_rows(rows, NUM_SETTINGS, g_active_setting);
|
|
|
}
|
|
|
|
|
|
void oled_draw_life(void)
|
|
|
{
|
|
|
const int content_pages = OLED_PAGES - HEADER_PAGES;
|
|
|
const int content_h = content_pages * 8;
|
|
|
uint8_t pages[6][OLED_WIDTH];
|
|
|
memset(pages, 0, sizeof(pages));
|
|
|
|
|
|
int lcp = START_PAGE - HEADER_PAGES; // content-local start page
|
|
|
|
|
|
// ── Life delta overlay (top-left, small 1x font) ──────────────────────────
|
|
|
if (g_life_delta != 0) {
|
|
|
char dbuf[8];
|
|
|
snprintf(dbuf, sizeof(dbuf), "%+d", g_life_delta);
|
|
|
pxbuf_str(pages, content_h, 0, 2, dbuf, 0);
|
|
|
}
|
|
|
|
|
|
if (g_eliminated) {
|
|
|
// ── Skull (10-col × 8-row glyph rendered at SCALE×) ──────────────────
|
|
|
// round cranium, hollow eyes, full cheek row, two teeth
|
|
|
static const uint8_t skull[10] = {
|
|
|
0x3C,0x62,0x21,0x79,0x21,0x21,0x79,0x21,0x62,0x3C
|
|
|
};
|
|
|
int sx = (OLED_WIDTH - 10 * SCALE) / 2;
|
|
|
for (int sc = 0; sc < 10 * SCALE; sc++) {
|
|
|
int x = sx + sc;
|
|
|
if (x < 0 || x >= OLED_WIDTH) continue;
|
|
|
uint8_t col = skull[sc / SCALE];
|
|
|
for (int cp = 0; cp < CHAR_PAGES; cp++) {
|
|
|
if (lcp + cp >= content_pages) continue;
|
|
|
uint8_t pixel_byte = 0;
|
|
|
for (int b = 0; b < 8; b++) {
|
|
|
int src_row = (cp * 8 + b) / SCALE;
|
|
|
if (src_row < 8 && ((col >> src_row) & 1))
|
|
|
pixel_byte |= (1 << b);
|
|
|
}
|
|
|
pages[lcp + cp][x] |= pixel_byte;
|
|
|
}
|
|
|
}
|
|
|
} else {
|
|
|
// ── Large centered life number ────────────────────────────────────────
|
|
|
char nbuf[12];
|
|
|
snprintf(nbuf, sizeof(nbuf), "%d", g_life);
|
|
|
int len = strlen(nbuf);
|
|
|
int life_x = (OLED_WIDTH - len * CHAR_WIDTH) / 2;
|
|
|
if (life_x < 0) life_x = 0;
|
|
|
|
|
|
for (int i = 0; i < len; i++) {
|
|
|
uint8_t ch = (uint8_t)nbuf[i];
|
|
|
if (ch < FONT_BASE || ch > FONT_MAX) ch = ' ';
|
|
|
const uint8_t *glyph = font5x8[ch - FONT_BASE];
|
|
|
int cx = life_x + i * CHAR_WIDTH;
|
|
|
for (int sc = 0; sc < 5 * SCALE; sc++) {
|
|
|
int x = cx + sc;
|
|
|
if (x < 0 || x >= OLED_WIDTH) continue;
|
|
|
uint8_t font_col = glyph[sc / SCALE];
|
|
|
for (int cp = 0; cp < CHAR_PAGES; cp++) {
|
|
|
if (lcp + cp >= content_pages) continue;
|
|
|
uint8_t pixel_byte = 0;
|
|
|
for (int b = 0; b < 8; b++) {
|
|
|
int src_row = (cp * 8 + b) / SCALE;
|
|
|
if (src_row < 8 && ((font_col >> src_row) & 1))
|
|
|
pixel_byte |= (1 << b);
|
|
|
}
|
|
|
pages[lcp + cp][x] |= pixel_byte;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// ── Build commander damage slot list ──────────────────────────────────────
|
|
|
int cmdr_slots[MAX_OPPONENTS];
|
|
|
int num_cmdr = 0;
|
|
|
if (g_ble_enabled) {
|
|
|
for (int i = 0; i < MAX_BLE_PEERS; i++)
|
|
|
if (g_peers[i].active) cmdr_slots[num_cmdr++] = i;
|
|
|
} else {
|
|
|
for (int i = 0; i < g_num_opponents; i++)
|
|
|
cmdr_slots[num_cmdr++] = i;
|
|
|
}
|
|
|
|
|
|
int total = 3 + num_cmdr;
|
|
|
if (g_life_select >= total) g_life_select = 0;
|
|
|
|
|
|
// ── Storm counter (bottom-left, above poison) ─────────────────────────────
|
|
|
{
|
|
|
char sbuf[8];
|
|
|
snprintf(sbuf, sizeof(sbuf), "%d", g_counters[COUNTER_STORM]);
|
|
|
int slen = strlen(sbuf);
|
|
|
int sbase = content_h - 18;
|
|
|
int ssel = (g_life_select == 1);
|
|
|
if (ssel) fill_cols(pages, content_h, sbase, 9, 0, 7 + slen * 6 + 2);
|
|
|
int sx = pxbuf_icon(pages, content_h, sbase, 2, icon_mini_bolt, 5, ssel);
|
|
|
pxbuf_str(pages, content_h, sbase, sx, sbuf, ssel);
|
|
|
}
|
|
|
|
|
|
// ── Poison counter (bottom-left, teardrop icon + number) ──────────────────
|
|
|
{
|
|
|
char pbuf[8];
|
|
|
snprintf(pbuf, sizeof(pbuf), "%d", g_counters[COUNTER_POISON]);
|
|
|
int plen = strlen(pbuf);
|
|
|
int pbase = content_h - 9;
|
|
|
int psel = (g_life_select == 2);
|
|
|
if (psel) fill_cols(pages, content_h, pbase, 9, 0, 7 + plen * 6 + 2);
|
|
|
int px = pxbuf_icon(pages, content_h, pbase, 2, icon_mini_drop, 5, psel);
|
|
|
pxbuf_str(pages, content_h, pbase, px, pbuf, psel);
|
|
|
}
|
|
|
|
|
|
// ── Commander damage (right side, stacked, 1x font) ───────────────────────
|
|
|
for (int si = 0; si < num_cmdr; si++) {
|
|
|
int val = g_cmdr_damage[cmdr_slots[si]];
|
|
|
char vbuf[6];
|
|
|
snprintf(vbuf, sizeof(vbuf), "%d", val);
|
|
|
int vlen = strlen(vbuf);
|
|
|
int cbase = si * 9;
|
|
|
int rx = OLED_WIDTH - 2 - vlen * 6;
|
|
|
int csel = (g_life_select == 3 + si);
|
|
|
if (csel) fill_cols(pages, content_h, cbase, 9, rx - 2, OLED_WIDTH);
|
|
|
pxbuf_str(pages, content_h, cbase, rx, vbuf, csel);
|
|
|
}
|
|
|
|
|
|
for (int p = 0; p < content_pages; p++)
|
|
|
oled_write_page(HEADER_PAGES + p, pages[p]);
|
|
|
}
|