commeownder/main/draw.c

503 lines
19 KiB
C

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#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]);
}