748 lines
22 KiB
C
748 lines
22 KiB
C
/*
|
|
* PANAGOTCHI -- a Tamagotchi-style virtual pet for the CH32V003F4P6 badge.
|
|
*
|
|
* Hardware (see pinout.md):
|
|
* PA1 indicator LED (active-high)
|
|
* PC0 OK button (active-low, internal pull-up)
|
|
* PD5 Right button (active-low, internal pull-up)
|
|
* PD6 Left button (active-low, internal pull-up)
|
|
* PC1/PC2 SSD1306 128x64 OLED over I2C1 (driven by ssd1306_i2c.h)
|
|
* PD2 passive piezo buzzer (square-wave bit-bang tone)
|
|
*
|
|
* Controls: Left / Right move the menu cursor, OK activates the selected
|
|
* action. On the stats / dead screens any button returns / restarts.
|
|
*/
|
|
|
|
#include "ch32fun.h"
|
|
|
|
// The bundled I2C driver calls printf() only to log a timeout we can't act on
|
|
// (by then the display is dead). Stub it to a no-op so the ~1 KB printf
|
|
// formatter is never referenced and the linker discards it. We format our own
|
|
// numbers with the tiny s_cat/u_cat helpers below.
|
|
#define printf(...) ((void)0)
|
|
|
|
#define SSD1306_128X64 // 128x64 panel (see pinout.md / OLED size)
|
|
#include "ssd1306_i2c.h"
|
|
#include "ssd1306.h"
|
|
#include "sprites.h"
|
|
|
|
// ----------------------------------------------------------------- pins
|
|
#define LED_PIN PA1
|
|
#define BTN_OK PC0
|
|
#define BTN_RIGHT PD5
|
|
#define BTN_LEFT PD6
|
|
#define BUZZER PD2
|
|
|
|
// ----------------------------------------------------------------- tuning
|
|
#define FRAME_MS 60 // main loop period
|
|
#define TICK_FRAMES 250 // stat update every ~15 s (250 * 60 ms)
|
|
#define ANIM_FRAMES 12 // how long an action reaction is shown
|
|
|
|
// With the values below an untended pet survives roughly half an hour:
|
|
// a need empties in ~20 min, after which health bleeds down over ~10 more.
|
|
|
|
// ----------------------------------------------------------------- state
|
|
enum { SCR_SELECT, SCR_MAIN, SCR_STATS, SCR_DEAD, SCR_SETTINGS };
|
|
|
|
// notification thresholds: gentle chirp at <50%, urgent at <20%
|
|
enum { N_FOOD, N_FUN, N_ENGY, N_WASH, N_COUNT };
|
|
|
|
typedef struct {
|
|
int8_t satiety; // 0..100 (fullness)
|
|
int8_t happy; // 0..100
|
|
int8_t energy; // 0..100
|
|
int8_t hygiene; // 0..100
|
|
int8_t health; // 0..100
|
|
uint16_t age_min; // age in game-minutes (one per tick)
|
|
uint16_t xp; // experience earned by good care
|
|
uint8_t species; // SP_* starter type chosen on the welcome screen
|
|
uint8_t level; // 1..MAX_LEVEL
|
|
uint8_t form; // FORM_* (display + life stage), derived from level
|
|
uint8_t ascend; // 0 undecided, 1 Claude, 2 glitch (latched at max lvl)
|
|
uint8_t poop; // 0..3 piles on the floor
|
|
uint8_t sleeping; // 1 while napping
|
|
uint8_t alive;
|
|
} Pet;
|
|
|
|
// Hacker privilege-escalation life stages: proc -> user -> root, then the pet
|
|
// either ascends to "Claude" (well-tended) or rots into a glitch.
|
|
enum { FORM_PROC, FORM_USER, FORM_ROOT, FORM_CLAUDE, FORM_GLITCH, FORM_COUNT };
|
|
static const char *const form_names[FORM_COUNT] = {
|
|
"proc", "user", "root", "Claude", "glitch",
|
|
};
|
|
#define MAX_LEVEL 4
|
|
// cumulative XP required to reach level (index = level-1)
|
|
static const uint16_t level_xp[MAX_LEVEL] = { 0, 45, 120, 240 };
|
|
|
|
static Pet pet;
|
|
static uint8_t screen;
|
|
static uint8_t cursor; // selected menu icon 0..ICON_COUNT-1
|
|
static uint8_t anim_exp; // expression forced during a reaction
|
|
static uint8_t anim_timer; // frames remaining for the reaction
|
|
static uint32_t frame; // free-running frame counter
|
|
static uint16_t tick_acc; // frames since last stat tick
|
|
static uint8_t warn_flag[N_COUNT]; // already chirped a <50% warning
|
|
static uint8_t crit_flag[N_COUNT]; // already chirped a <20% warning
|
|
static uint8_t evo_anim; // frames remaining for an evolution banner
|
|
|
|
// settings
|
|
static uint8_t sound_on = 1;
|
|
static uint8_t timeout_idx = 1; // index into screen_timeout_opts (default 30 s)
|
|
static uint8_t set_sel = 0; // highlighted row on the settings screen
|
|
static uint8_t sel_species = 0; // highlighted species on the welcome screen
|
|
|
|
// screen-off-on-inactivity options, in frames (0 = never sleep the display)
|
|
static const uint16_t screen_timeout_opts[] = { 250, 500, 1000, 0 };
|
|
static const char *const screen_timeout_names[] = { "15s", "30s", "60s", "Off" };
|
|
#define TIMEOUT_OPT_COUNT 4
|
|
#define SETTINGS_ROWS 3 // screen-off, sound, back
|
|
|
|
// display power state
|
|
static uint8_t disp_on = 1;
|
|
static uint32_t idle_frames = 0; // frames since the last button activity
|
|
|
|
// menu icon order must match sprites.h menu_icons[] / menu_labels[]
|
|
enum { ACT_FEED, ACT_PLAY, ACT_SLEEP, ACT_CLEAN, ACT_HEAL, ACT_STATS, ACT_SETTINGS };
|
|
|
|
// ----------------------------------------------------------------- helpers
|
|
static int8_t clamp100(int v) { return v < 0 ? 0 : (v > 100 ? 100 : v); }
|
|
|
|
// Tiny string builders used instead of snprintf: we only ever paste short
|
|
// strings and small unsigned ints, and avoiding <stdio.h> keeps the whole
|
|
// printf machinery (~1 KB) out of the image. Each returns the new end pointer
|
|
// so calls chain; remember to NUL-terminate the final buffer yourself.
|
|
static char *s_cat(char *p, const char *s) { while (*s) *p++ = *s++; return p; }
|
|
static char *u_cat(char *p, unsigned v)
|
|
{
|
|
char tmp[5];
|
|
int n = 0;
|
|
do { tmp[n++] = '0' + v % 10; v /= 10; } while (v);
|
|
while (n) *p++ = tmp[--n];
|
|
return p;
|
|
}
|
|
|
|
// short square-wave beep on the passive piezo (blocking, ms-scale only)
|
|
static void beep(uint16_t freq, uint16_t ms)
|
|
{
|
|
if (!sound_on) return;
|
|
if (freq == 0) { Delay_Ms(ms); return; }
|
|
uint32_t half = 500000u / freq; // half-period in us
|
|
uint32_t cycles = ((uint32_t)ms * 1000u) / (half * 2u);
|
|
for (uint32_t i = 0; i < cycles; i++) {
|
|
funDigitalWrite(BUZZER, FUN_HIGH);
|
|
Delay_Us(half);
|
|
funDigitalWrite(BUZZER, FUN_LOW);
|
|
Delay_Us(half);
|
|
}
|
|
}
|
|
|
|
static void led_blink(void)
|
|
{
|
|
funDigitalWrite(LED_PIN, FUN_HIGH);
|
|
Delay_Ms(30);
|
|
funDigitalWrite(LED_PIN, FUN_LOW);
|
|
}
|
|
|
|
static void pet_reset(void)
|
|
{
|
|
pet.satiety = 80;
|
|
pet.happy = 80;
|
|
pet.energy = 90;
|
|
pet.hygiene = 100;
|
|
pet.health = 100;
|
|
pet.age_min = 0;
|
|
pet.xp = 0;
|
|
pet.species = sel_species;
|
|
pet.level = 1;
|
|
pet.form = FORM_PROC;
|
|
pet.ascend = 0;
|
|
pet.poop = 0;
|
|
pet.sleeping = 0;
|
|
pet.alive = 1;
|
|
screen = SCR_MAIN;
|
|
cursor = 0;
|
|
evo_anim = 0;
|
|
for (int i = 0; i < N_COUNT; i++) { warn_flag[i] = 0; crit_flag[i] = 0; }
|
|
}
|
|
|
|
// Map the current level (and, at the top, care quality) to a display form.
|
|
static void compute_form(void)
|
|
{
|
|
switch (pet.level) {
|
|
case 1: pet.form = FORM_PROC; break;
|
|
case 2: pet.form = FORM_USER; break;
|
|
case 3: pet.form = FORM_ROOT; break;
|
|
default: // max level: branch once, latched
|
|
if (!pet.ascend) pet.ascend = (pet.health >= 60) ? 1 : 2;
|
|
pet.form = (pet.ascend == 1) ? FORM_CLAUDE : FORM_GLITCH;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Award XP; promote (and trigger the evolution banner) when a tier is reached.
|
|
static void add_xp(uint8_t n)
|
|
{
|
|
if (!pet.alive) return;
|
|
if (pet.xp < 60000) pet.xp += n;
|
|
|
|
uint8_t nl = 1;
|
|
for (uint8_t i = 1; i < MAX_LEVEL; i++)
|
|
if (pet.xp >= level_xp[i]) nl = i + 1;
|
|
|
|
if (nl > pet.level) {
|
|
pet.level = nl;
|
|
compute_form();
|
|
evo_anim = 30; // ~1.8 s celebratory banner
|
|
beep(784, 60); beep(988, 60); beep(1175, 60); beep(1568, 140);
|
|
led_blink();
|
|
}
|
|
}
|
|
|
|
// Chirp once when a need first drops below 50% (gentle) or 20% (urgent),
|
|
// re-arming only after it recovers. Audible even while the screen is off,
|
|
// so it works as a "tend to me" reminder.
|
|
static void notify(uint8_t idx, int8_t val)
|
|
{
|
|
if (val < 20) {
|
|
if (!crit_flag[idx]) { // urgent: descending triple
|
|
beep(1568, 60); beep(1175, 60); beep(880, 90);
|
|
crit_flag[idx] = 1; warn_flag[idx] = 1;
|
|
}
|
|
} else if (val < 50) {
|
|
if (!warn_flag[idx]) { // gentle: single chirp
|
|
beep(1568, 50);
|
|
warn_flag[idx] = 1;
|
|
}
|
|
crit_flag[idx] = 0;
|
|
} else {
|
|
warn_flag[idx] = 0; crit_flag[idx] = 0;
|
|
}
|
|
}
|
|
|
|
static void check_notifications(void)
|
|
{
|
|
notify(N_FOOD, pet.satiety);
|
|
notify(N_FUN, pet.happy);
|
|
notify(N_ENGY, pet.energy);
|
|
notify(N_WASH, pet.hygiene);
|
|
}
|
|
|
|
// pick the expression frame to draw this instant
|
|
static uint8_t current_expression(void)
|
|
{
|
|
if (!pet.alive) return EXP_DEAD;
|
|
if (anim_timer) return anim_exp;
|
|
if (pet.sleeping) return EXP_SLEEP;
|
|
if (pet.health < 30 || pet.happy < 25 || pet.satiety < 20)
|
|
return EXP_SAD;
|
|
// idle, with an occasional blink
|
|
if ((frame % 50) < 4) return EXP_BLINK;
|
|
return EXP_IDLE;
|
|
}
|
|
|
|
// ----------------------------------------------------------------- actions
|
|
static void react(uint8_t exp) { anim_exp = exp; anim_timer = ANIM_FRAMES; }
|
|
|
|
static void do_action(uint8_t act)
|
|
{
|
|
switch (act) {
|
|
case ACT_FEED:
|
|
pet.satiety = clamp100(pet.satiety + 30);
|
|
pet.hygiene = clamp100(pet.hygiene - 5);
|
|
react(EXP_EAT);
|
|
beep(880, 60); beep(1175, 80);
|
|
led_blink();
|
|
add_xp(4);
|
|
break;
|
|
|
|
case ACT_PLAY:
|
|
if (pet.energy < 15) { beep(220, 120); break; } // too tired
|
|
pet.happy = clamp100(pet.happy + 25);
|
|
pet.energy = clamp100(pet.energy - 12);
|
|
react(EXP_HAPPY);
|
|
beep(988, 60); beep(1319, 60); beep(1568, 90);
|
|
led_blink();
|
|
add_xp(6);
|
|
break;
|
|
|
|
case ACT_SLEEP:
|
|
pet.sleeping = !pet.sleeping;
|
|
beep(pet.sleeping ? 440 : 660, 120);
|
|
break;
|
|
|
|
case ACT_CLEAN:
|
|
pet.poop = 0;
|
|
pet.hygiene = 100;
|
|
pet.happy = clamp100(pet.happy + 5);
|
|
beep(1319, 50); beep(1047, 50);
|
|
add_xp(3);
|
|
break;
|
|
|
|
case ACT_HEAL:
|
|
pet.health = clamp100(pet.health + 35);
|
|
beep(784, 60); beep(1047, 60); beep(1319, 90);
|
|
led_blink();
|
|
add_xp(4);
|
|
break;
|
|
|
|
case ACT_STATS:
|
|
screen = SCR_STATS;
|
|
beep(659, 40);
|
|
break;
|
|
|
|
case ACT_SETTINGS:
|
|
screen = SCR_SETTINGS;
|
|
set_sel = 0;
|
|
beep(659, 40);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------- game tick
|
|
static void game_tick(void)
|
|
{
|
|
pet.age_min++;
|
|
|
|
if (pet.sleeping) {
|
|
pet.energy = clamp100(pet.energy + 6);
|
|
// needs still drift down slowly even while asleep
|
|
pet.satiety = clamp100(pet.satiety - 1);
|
|
if (pet.energy >= 100) pet.sleeping = 0; // wakes when rested
|
|
} else {
|
|
pet.satiety = clamp100(pet.satiety - 1);
|
|
pet.energy = clamp100(pet.energy - 1);
|
|
pet.happy = clamp100(pet.happy - 1);
|
|
}
|
|
|
|
// hygiene drifts every other tick
|
|
if (pet.age_min & 1) pet.hygiene = clamp100(pet.hygiene - 1);
|
|
|
|
// occasional bowel movements (derived from counters, no RNG needed)
|
|
if (!pet.sleeping && pet.poop < 3 &&
|
|
((pet.age_min * 7u + frame) & 0x1F) == 0)
|
|
pet.poop++;
|
|
|
|
// being dirty / full of poop slowly saps happiness
|
|
if (pet.poop && (pet.age_min & 1)) pet.happy = clamp100(pet.happy - 1);
|
|
if (pet.hygiene < 20 && (pet.age_min & 1)) pet.happy = clamp100(pet.happy - 1);
|
|
|
|
// health responds to overall wellbeing
|
|
int8_t lo = pet.satiety;
|
|
if (pet.happy < lo) lo = pet.happy;
|
|
if (pet.energy < lo) lo = pet.energy;
|
|
if (pet.hygiene < lo) lo = pet.hygiene;
|
|
if (lo < 15) pet.health = clamp100(pet.health - 3);
|
|
else if (lo > 50) pet.health = clamp100(pet.health + 2);
|
|
|
|
// a well-kept pet matures: passive XP while every need stays healthy
|
|
if (lo > 50 && !pet.sleeping) add_xp(3);
|
|
|
|
check_notifications();
|
|
|
|
if (pet.health <= 0) {
|
|
pet.alive = 0;
|
|
screen = SCR_DEAD;
|
|
beep(330, 200); beep(247, 200); beep(165, 400);
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------- drawing
|
|
static void draw_center_str(int y, const char *s)
|
|
{
|
|
int len = 0;
|
|
while (s[len]) len++;
|
|
int x = (SSD1306_W - len * 8) / 2;
|
|
if (x < 0) x = 0;
|
|
ssd1306_drawstr(x, y, s, 1);
|
|
}
|
|
|
|
static void draw_menu_bar(void)
|
|
{
|
|
// 7 icons across the bottom (18 px slots), selected one boxed
|
|
for (uint8_t i = 0; i < ICON_COUNT; i++) {
|
|
int x = i * 18 + 1;
|
|
ssd1306_drawImage(x, 48, menu_icons[i], ICON_W, ICON_H, 0);
|
|
if (i == cursor)
|
|
ssd1306_drawRect(x - 1, 47, ICON_W + 1, ICON_H + 1, 1);
|
|
}
|
|
}
|
|
|
|
// Per-species eye geometry, so the shades land on the right spot.
|
|
static const uint8_t eye_cy[SPECIES_COUNT] = { 11, 17, 12 }; // SP_SHELL,PANKO,TMUX
|
|
static const uint8_t eye_lx[SPECIES_COUNT] = { 11, 11, 9 };
|
|
static const uint8_t eye_rx[SPECIES_COUNT] = { 20, 21, 23 };
|
|
|
|
// Hacker garb earned as the pet levels up, drawn dark over the lit body so it
|
|
// reads as black gear.
|
|
static void draw_accessories(int x, int y)
|
|
{
|
|
uint8_t sp = pet.species;
|
|
uint8_t cy = eye_cy[sp], lx = eye_lx[sp], rx = eye_rx[sp];
|
|
|
|
if (pet.form >= FORM_USER) { // shades
|
|
ssd1306_fillRect(x + lx - 3, y + cy - 2, 7, 4, 0);
|
|
ssd1306_fillRect(x + rx - 3, y + cy - 2, 7, 4, 0);
|
|
if (rx - lx > 7)
|
|
ssd1306_drawFastHLine(x + lx + 4, y + cy - 1, rx - lx - 7, 0);
|
|
}
|
|
if (pet.form >= FORM_ROOT) { // gaming headset + mic boom
|
|
ssd1306_drawLine(x + 3, y + 10, x + 9, y + 2, 0);
|
|
ssd1306_drawLine(x + 9, y + 2, x + 23, y + 2, 0);
|
|
ssd1306_drawLine(x + 23, y + 2, x + 29, y + 10, 0);
|
|
ssd1306_fillRect(x + 1, y + 12, 4, 7, 0);
|
|
ssd1306_drawLine(x + 3, y + 18, x + 10, y + 24, 0);
|
|
}
|
|
}
|
|
|
|
static void draw_creature(int x, int y)
|
|
{
|
|
const unsigned char *spr;
|
|
if (pet.form == FORM_CLAUDE) spr = spr_claude;
|
|
else if (pet.form == FORM_GLITCH) spr = spr_glitch;
|
|
else spr = species_frames[pet.species][current_expression()];
|
|
|
|
ssd1306_drawImage(x, y, spr, PANA_W, PANA_H, 0);
|
|
|
|
// growing forms wear gear; the ascended/glitched forms speak for themselves
|
|
if (spr != spr_claude && spr != spr_glitch && !pet.sleeping)
|
|
draw_accessories(x, y);
|
|
|
|
// Claude radiates: a few twinkling pixels that shift each frame
|
|
if (pet.form == FORM_CLAUDE) {
|
|
ssd1306_drawPixel(x - 2 + (frame & 3), y + 4, 1);
|
|
ssd1306_drawPixel(x + PANA_W + (frame & 1), y + 10, 1);
|
|
}
|
|
}
|
|
|
|
static void draw_evo_banner(void)
|
|
{
|
|
ssd1306_fillRect(6, 18, SSD1306_W - 12, 24, 0);
|
|
ssd1306_drawRect(6, 18, SSD1306_W - 12, 24, 1);
|
|
draw_center_str(21, ">> evolving");
|
|
char b[17], *p = b;
|
|
p = s_cat(p, "[ ");
|
|
p = s_cat(p, form_names[pet.form]);
|
|
p = s_cat(p, " ]");
|
|
*p = 0;
|
|
draw_center_str(31, b);
|
|
}
|
|
|
|
static void draw_main(void)
|
|
{
|
|
ssd1306_setbuf(0);
|
|
|
|
// top bar: action label + level badge, with a tiny low-health warning
|
|
draw_center_str(0, menu_labels[cursor]);
|
|
if (pet.alive && pet.health < 30 && (frame & 8))
|
|
ssd1306_drawstr(0, 0, "!", 1);
|
|
char badge[6], *bp = badge;
|
|
*bp++ = 'L';
|
|
bp = u_cat(bp, pet.level);
|
|
*bp = 0;
|
|
ssd1306_drawstr(SSD1306_W - 2 * 8, 0, badge, 1);
|
|
ssd1306_drawFastHLine(0, 10, SSD1306_W, 1);
|
|
|
|
// pet: gentle horizontal stroll + vertical bob (still while asleep)
|
|
int px, py;
|
|
if (pet.sleeping) {
|
|
px = 48; py = 16;
|
|
ssd1306_drawstr(px + PANA_W - 2, 14, "z", 1);
|
|
ssd1306_drawstr(px + PANA_W + 4, 12, "Z", 1);
|
|
} else {
|
|
uint32_t t = frame % 80;
|
|
px = (t < 40) ? 30 + t : 30 + (80 - t); // 30..70..30
|
|
py = 14 + ((frame % 16) < 8 ? 0 : 1);
|
|
}
|
|
draw_creature(px, py);
|
|
|
|
// floor + poop piles
|
|
ssd1306_drawFastHLine(0, 46, SSD1306_W, 1);
|
|
for (uint8_t i = 0; i < pet.poop; i++)
|
|
ssd1306_drawImage(8 + i * 12, 38, spr_poop, 8, 8, 0);
|
|
|
|
draw_menu_bar();
|
|
if (evo_anim) draw_evo_banner(); // celebratory overlay on top
|
|
ssd1306_refresh();
|
|
}
|
|
|
|
static void draw_bar(int y, const char *label, int8_t val)
|
|
{
|
|
ssd1306_drawstr(0, y, label, 1);
|
|
int bx = 48, bw = 76;
|
|
ssd1306_drawRect(bx, y, bw, 7, 1);
|
|
int fill = (val * (bw - 2)) / 100;
|
|
if (fill > 0) ssd1306_fillRect(bx + 1, y + 1, fill, 5, 1);
|
|
}
|
|
|
|
static void draw_stats(void)
|
|
{
|
|
ssd1306_setbuf(0);
|
|
draw_center_str(0, "~/pet $ stat");
|
|
draw_bar(10, "FOOD", pet.satiety);
|
|
draw_bar(19, "HAPPY", pet.happy);
|
|
draw_bar(28, "ENERGY", pet.energy);
|
|
draw_bar(37, "CLEAN", pet.hygiene);
|
|
draw_bar(46, "HEALTH", pet.health);
|
|
|
|
// no form name here: keep room for the XP-to-next-level readout
|
|
char line[17], *p = line;
|
|
*p++ = 'L';
|
|
p = u_cat(p, pet.level);
|
|
p = s_cat(p, " xp ");
|
|
if (pet.level < MAX_LEVEL) {
|
|
p = u_cat(p, pet.xp);
|
|
*p++ = '/';
|
|
p = u_cat(p, level_xp[pet.level]);
|
|
} else {
|
|
p = s_cat(p, "MAX");
|
|
}
|
|
*p = 0;
|
|
ssd1306_drawstr(0, 56, line, 1);
|
|
ssd1306_refresh();
|
|
}
|
|
|
|
static void draw_dead(void)
|
|
{
|
|
ssd1306_setbuf(0);
|
|
ssd1306_drawImage(48, 2, spr_grave, 32, 32, 0);
|
|
draw_center_str(36, pet.ascend == 2 ? "** CORRUPTED **" : "SEGFAULT (11)");
|
|
char line[17], *p = line;
|
|
p = s_cat(p, "core dumped L");
|
|
p = u_cat(p, pet.level);
|
|
*p = 0;
|
|
draw_center_str(46, line);
|
|
draw_center_str(56, "OK: respawn");
|
|
ssd1306_refresh();
|
|
}
|
|
|
|
// welcome screen: choose a starter species before booting the pet
|
|
static void draw_select(void)
|
|
{
|
|
ssd1306_setbuf(0);
|
|
draw_center_str(0, "~ $ adopt --pet");
|
|
ssd1306_drawFastHLine(0, 10, SSD1306_W, 1);
|
|
|
|
// big idle preview of the highlighted species (no gear at level 1)
|
|
ssd1306_drawImage(48, 13, species_frames[sel_species][EXP_IDLE],
|
|
PANA_W, PANA_H, 0);
|
|
|
|
char line[17], *p = line;
|
|
p = s_cat(p, "< ");
|
|
p = s_cat(p, species_names[sel_species]);
|
|
p = s_cat(p, " >");
|
|
*p = 0;
|
|
draw_center_str(47, line);
|
|
draw_center_str(56, "L/R pick OK go");
|
|
ssd1306_refresh();
|
|
}
|
|
|
|
static void draw_settings(void)
|
|
{
|
|
ssd1306_setbuf(0);
|
|
draw_center_str(0, "- SETTINGS -");
|
|
ssd1306_drawFastHLine(0, 10, SSD1306_W, 1);
|
|
|
|
char rows[SETTINGS_ROWS][17];
|
|
*s_cat(s_cat(rows[0], "Screen off:"), screen_timeout_names[timeout_idx]) = 0;
|
|
*s_cat(s_cat(rows[1], "Sound: "), sound_on ? "On" : "Off") = 0;
|
|
*s_cat(rows[2], "Back") = 0;
|
|
|
|
for (uint8_t r = 0; r < SETTINGS_ROWS; r++) {
|
|
int y = 18 + r * 12;
|
|
if (r == set_sel) ssd1306_fillRect(0, y - 1, SSD1306_W, 10, 1);
|
|
// selected row drawn inverted (color 0 on the lit bar)
|
|
ssd1306_drawstr(4, y, rows[r], r == set_sel ? 0 : 1);
|
|
}
|
|
|
|
draw_center_str(56, "L/R move OK use");
|
|
ssd1306_refresh();
|
|
}
|
|
|
|
// ----------------------------------------------------------------- input
|
|
typedef struct { uint8_t ok, left, right; } Buttons;
|
|
|
|
static Buttons read_buttons(void)
|
|
{
|
|
// active-low: pressed reads 0
|
|
Buttons b;
|
|
b.ok = !funDigitalRead(BTN_OK);
|
|
b.left = !funDigitalRead(BTN_LEFT);
|
|
b.right = !funDigitalRead(BTN_RIGHT);
|
|
return b;
|
|
}
|
|
|
|
// ----------------------------------------------------------------- setup
|
|
static void gpio_setup(void)
|
|
{
|
|
funGpioInitAll();
|
|
|
|
funPinMode(LED_PIN, GPIO_Speed_10MHz | GPIO_CNF_OUT_PP);
|
|
funDigitalWrite(LED_PIN, FUN_LOW);
|
|
|
|
funPinMode(BUZZER, GPIO_Speed_10MHz | GPIO_CNF_OUT_PP);
|
|
funDigitalWrite(BUZZER, FUN_LOW);
|
|
|
|
// buttons: input with internal pull-up (ODR high selects pull-up)
|
|
funPinMode(BTN_OK, GPIO_CNF_IN_PUPD);
|
|
funPinMode(BTN_LEFT, GPIO_CNF_IN_PUPD);
|
|
funPinMode(BTN_RIGHT, GPIO_CNF_IN_PUPD);
|
|
funDigitalWrite(BTN_OK, FUN_HIGH);
|
|
funDigitalWrite(BTN_LEFT, FUN_HIGH);
|
|
funDigitalWrite(BTN_RIGHT, FUN_HIGH);
|
|
}
|
|
|
|
static void splash(void)
|
|
{
|
|
// fake terminal boot sequence, revealed line by line
|
|
static const char *const boot[] = {
|
|
"panagotchi v2",
|
|
"booting kernel..",
|
|
"mount /pet ok",
|
|
"spawn shell ok",
|
|
"> ./tamagotchi_",
|
|
};
|
|
ssd1306_setbuf(0);
|
|
for (uint8_t i = 0; i < 5; i++) {
|
|
ssd1306_drawstr(0, i * 10, boot[i], 1);
|
|
ssd1306_refresh();
|
|
beep(1200 + i * 120, 25);
|
|
Delay_Ms(220);
|
|
}
|
|
draw_center_str(54, "press any key");
|
|
ssd1306_refresh();
|
|
beep(988, 120);
|
|
|
|
// wait until a button is pressed (or ~4 s timeout)
|
|
for (int i = 0; i < 200; i++) {
|
|
Buttons b = read_buttons();
|
|
if (b.ok || b.left || b.right) break;
|
|
Delay_Ms(20);
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------- main
|
|
int main(void)
|
|
{
|
|
SystemInit();
|
|
gpio_setup();
|
|
|
|
Delay_Ms(120); // let the OLED rail settle
|
|
if (ssd1306_i2c_init()) { // non-zero == failure
|
|
// no display: fall back to a heartbeat blink so the board still shows life
|
|
while (1) { led_blink(); Delay_Ms(700); }
|
|
}
|
|
ssd1306_init();
|
|
|
|
splash();
|
|
screen = SCR_SELECT; // pick a species before a pet is born
|
|
|
|
Buttons prev = {0, 0, 0};
|
|
|
|
while (1) {
|
|
Buttons b = read_buttons();
|
|
uint8_t ok_edge = b.ok && !prev.ok;
|
|
uint8_t left_edge = b.left && !prev.left;
|
|
uint8_t right_edge = b.right && !prev.right;
|
|
uint8_t any_edge = ok_edge | left_edge | right_edge;
|
|
prev = b;
|
|
|
|
if (any_edge) idle_frames = 0;
|
|
|
|
if (!disp_on) {
|
|
// screen is asleep: the first press only wakes it (not an action)
|
|
if (any_edge) {
|
|
ssd1306_cmd(SSD1306_DISPLAYON);
|
|
disp_on = 1;
|
|
}
|
|
} else {
|
|
switch (screen) {
|
|
case SCR_SELECT:
|
|
if (left_edge) {
|
|
sel_species = (sel_species + SPECIES_COUNT - 1) % SPECIES_COUNT;
|
|
beep(1047, 25);
|
|
}
|
|
if (right_edge) {
|
|
sel_species = (sel_species + 1) % SPECIES_COUNT;
|
|
beep(1047, 25);
|
|
}
|
|
if (ok_edge) { // born! boot the chosen species
|
|
pet_reset(); // sets screen = SCR_MAIN, species = sel_species
|
|
beep(659, 80); beep(988, 120);
|
|
}
|
|
draw_select();
|
|
break;
|
|
|
|
case SCR_MAIN:
|
|
if (left_edge) {
|
|
cursor = (cursor + ICON_COUNT - 1) % ICON_COUNT;
|
|
beep(1047, 25);
|
|
}
|
|
if (right_edge) {
|
|
cursor = (cursor + 1) % ICON_COUNT;
|
|
beep(1047, 25);
|
|
}
|
|
if (ok_edge) do_action(cursor);
|
|
draw_main();
|
|
break;
|
|
|
|
case SCR_STATS:
|
|
if (any_edge) { screen = SCR_MAIN; beep(880, 30); }
|
|
draw_stats();
|
|
break;
|
|
|
|
case SCR_SETTINGS:
|
|
if (left_edge) {
|
|
set_sel = (set_sel + SETTINGS_ROWS - 1) % SETTINGS_ROWS;
|
|
beep(1047, 25);
|
|
}
|
|
if (right_edge) {
|
|
set_sel = (set_sel + 1) % SETTINGS_ROWS;
|
|
beep(1047, 25);
|
|
}
|
|
if (ok_edge) {
|
|
if (set_sel == 0) { // cycle screen-off timeout
|
|
timeout_idx = (timeout_idx + 1) % TIMEOUT_OPT_COUNT;
|
|
beep(880, 40);
|
|
} else if (set_sel == 1) { // toggle sound
|
|
sound_on = !sound_on;
|
|
beep(988, 60); // only audible if just enabled
|
|
} else { // Back
|
|
screen = SCR_MAIN;
|
|
beep(880, 30);
|
|
}
|
|
}
|
|
draw_settings();
|
|
break;
|
|
|
|
case SCR_DEAD:
|
|
if (ok_edge) { // back to the welcome screen to re-pick
|
|
screen = SCR_SELECT;
|
|
beep(659, 80); beep(988, 120);
|
|
}
|
|
draw_dead();
|
|
break;
|
|
}
|
|
|
|
// sleep the display after the configured idle period
|
|
uint16_t timeout = screen_timeout_opts[timeout_idx];
|
|
if (timeout && idle_frames >= timeout) {
|
|
ssd1306_cmd(SSD1306_DISPLAYOFF);
|
|
disp_on = 0;
|
|
}
|
|
}
|
|
|
|
if (anim_timer) anim_timer--;
|
|
if (evo_anim) evo_anim--;
|
|
|
|
// advance game time independent of screen / display state
|
|
if (pet.alive && ++tick_acc >= TICK_FRAMES) {
|
|
tick_acc = 0;
|
|
game_tick();
|
|
}
|
|
|
|
frame++;
|
|
idle_frames++;
|
|
Delay_Ms(FRAME_MS);
|
|
}
|
|
}
|