Files
tactility_apps/Apps/TamaTac/main/Source/MainView.cpp
T
2026-02-13 21:26:23 +01:00

626 lines
24 KiB
C++

/**
* @file MainView.cpp
* @brief Main pet view implementation
*/
#include "MainView.h"
#include "TamaTac.h"
#include <Tactility/kernel/Kernel.h>
#include <cstdio>
extern lv_color_t TamaTac_canvasBuffer[72 * 72];
extern lv_color_t TamaTac_iconBuffers[12][16 * 16];
void MainView::onStart(lv_obj_t* parentWidget, TamaTac* appInstance) {
parent = parentWidget;
app = appInstance;
// Detect screen size for responsive layout
lv_coord_t screenWidth = lv_display_get_horizontal_resolution(nullptr);
lv_coord_t screenHeight = lv_display_get_vertical_resolution(nullptr);
bool isSmall = (screenWidth < 280 || screenHeight < 180);
bool isXLarge = (screenWidth >= 600);
bool isLarge = !isXLarge && (screenWidth >= 400 && screenHeight >= 300);
// Scaled dimensions based on screen tier
int petAreaSize, statBarWidth, statBarHeight;
int btnWidth, btnHeight, padMain, padRow, poopHeight;
if (isSmall) {
petAreaSize = 56; petCanvasSize = 48; spriteScale = 2; // 24*2=48
statBarWidth = 85; statBarHeight = 5;
btnWidth = 54; btnHeight = 32;
padMain = 1; padRow = 1; poopHeight = 14;
} else if (isXLarge) {
petAreaSize = 136; petCanvasSize = 72; spriteScale = 3; // 24*3=72
statBarWidth = 300; statBarHeight = 10;
btnWidth = 120; btnHeight = 56;
padMain = 8; padRow = 6; poopHeight = 24;
} else if (isLarge) {
petAreaSize = 100; petCanvasSize = 72; spriteScale = 3;
statBarWidth = 200; statBarHeight = 8;
btnWidth = 90; btnHeight = 50;
padMain = 4; padRow = 4; poopHeight = 22;
} else {
// Medium (default)
petAreaSize = 76; petCanvasSize = 72; spriteScale = 3;
statBarWidth = 135; statBarHeight = 6;
btnWidth = 70; btnHeight = 42;
padMain = 1; padRow = 1; poopHeight = 16;
}
// Initialize animation state
animStartTime = tt::kernel::getMillis();
currentSpriteId = SPRITE_EGG_IDLE;
// Main content wrapper
mainWrapper = lv_obj_create(parent);
lv_obj_set_size(mainWrapper, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_pad_all(mainWrapper, padMain, 0);
lv_obj_set_style_bg_opa(mainWrapper, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(mainWrapper, 0, 0);
lv_obj_set_flex_flow(mainWrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(mainWrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_remove_flag(mainWrapper, LV_OBJ_FLAG_SCROLLABLE);
// Pet and stats container (horizontal layout)
lv_obj_t* petStatsRow = lv_obj_create(mainWrapper);
lv_obj_set_size(petStatsRow, LV_PCT(98), LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(petStatsRow, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(petStatsRow, 0, 0);
lv_obj_set_style_pad_all(petStatsRow, 0, 0);
lv_obj_set_style_pad_left(petStatsRow, isSmall ? 2 : (isXLarge ? 16 : 8), 0);
lv_obj_set_flex_flow(petStatsRow, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(petStatsRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER);
// Pet column (left side)
lv_obj_t* petColumn = lv_obj_create(petStatsRow);
lv_obj_set_size(petColumn, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(petColumn, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(petColumn, 0, 0);
lv_obj_set_style_pad_all(petColumn, 0, 0);
lv_obj_set_style_pad_row(petColumn, isSmall ? 1 : (isXLarge ? 8 : 2), 0);
lv_obj_set_flex_flow(petColumn, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(petColumn, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
// Pet display area
petArea = lv_obj_create(petColumn);
lv_obj_set_size(petArea, petAreaSize, petAreaSize);
lv_obj_set_style_bg_color(petArea, lv_color_hex(0x333333), 0);
lv_obj_set_style_border_color(petArea, lv_color_hex(0x666666), 0);
lv_obj_set_style_border_width(petArea, isSmall ? 1 : (isXLarge ? 3 : 2), 0);
lv_obj_set_style_pad_all(petArea, 0, 0);
// Make pet area tappable for pet interaction
lv_obj_add_flag(petArea, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(petArea, onPetTapped, LV_EVENT_CLICKED, app);
// Create canvas for sprite rendering
petCanvas = lv_canvas_create(petArea);
lv_canvas_set_buffer(petCanvas, TamaTac_canvasBuffer, petCanvasSize, petCanvasSize, LV_COLOR_FORMAT_NATIVE);
lv_obj_center(petCanvas);
// Poop container
poopContainer = lv_obj_create(petColumn);
lv_obj_set_size(poopContainer, petAreaSize, poopHeight);
lv_obj_set_style_bg_opa(poopContainer, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(poopContainer, 0, 0);
lv_obj_set_style_pad_all(poopContainer, 1, 0);
lv_obj_set_style_pad_column(poopContainer, isSmall ? 1 : (isXLarge ? 4 : 2), 0);
lv_obj_set_flex_flow(poopContainer, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(poopContainer, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_remove_flag(poopContainer, LV_OBJ_FLAG_SCROLLABLE);
// Create 3 poop icon canvases
for (int i = 0; i < 3; i++) {
poopIcons[i] = lv_canvas_create(poopContainer);
lv_canvas_set_buffer(poopIcons[i], TamaTac_iconBuffers[i], 16, 16, LV_COLOR_FORMAT_NATIVE);
lv_obj_add_flag(poopIcons[i], LV_OBJ_FLAG_HIDDEN);
}
// Stat bars container
lv_obj_t* statsContainer = lv_obj_create(petStatsRow);
lv_obj_set_size(statsContainer, LV_SIZE_CONTENT, petAreaSize);
lv_obj_set_style_bg_opa(statsContainer, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(statsContainer, 0, 0);
lv_obj_set_style_pad_all(statsContainer, padRow, 0);
lv_obj_set_style_pad_left(statsContainer, isSmall ? 4 : (isXLarge ? 16 : 8), 0);
lv_obj_set_style_pad_row(statsContainer, padRow, 0);
lv_obj_set_flex_flow(statsContainer, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(statsContainer, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
lv_obj_set_flex_grow(statsContainer, 1);
lv_obj_remove_flag(statsContainer, LV_OBJ_FLAG_SCROLLABLE);
// Helper to create stat row
auto createStatRow = [&](lv_obj_t* parent, int iconBufferIdx, IconId iconId,
lv_obj_t** bar, lv_color_t barColor) {
lv_obj_t* row = lv_obj_create(parent);
lv_obj_set_size(row, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(row, 0, 0);
lv_obj_set_style_pad_all(row, 0, 0);
lv_obj_set_style_pad_column(row, isSmall ? 2 : (isXLarge ? 6 : 3), 0);
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_t* iconCanvas = lv_canvas_create(row);
lv_canvas_set_buffer(iconCanvas, TamaTac_iconBuffers[iconBufferIdx], 16, 16, LV_COLOR_FORMAT_NATIVE);
drawIcon(iconCanvas, iconId);
*bar = lv_bar_create(row);
lv_obj_set_size(*bar, statBarWidth, statBarHeight);
lv_bar_set_range(*bar, 0, 100);
lv_obj_set_style_bg_color(*bar, lv_color_hex(0x444444), LV_PART_MAIN);
lv_obj_set_style_bg_color(*bar, barColor, LV_PART_INDICATOR);
return iconCanvas;
};
statIcons[0] = createStatRow(statsContainer, 3, ICON_HUNGER, &hungerBar, lv_color_hex(0xFF9900));
statIcons[1] = createStatRow(statsContainer, 4, ICON_HAPPINESS, &happinessBar, lv_color_hex(0xFFCC00));
statIcons[2] = createStatRow(statsContainer, 5, ICON_HEALTH, &healthBar, lv_color_hex(0x00FF00));
statIcons[3] = createStatRow(statsContainer, 6, ICON_ENERGY, &energyBar, lv_color_hex(0x00CCFF));
// Status label
statusLabel = lv_label_create(mainWrapper);
lv_label_set_text(statusLabel, "Your pet is ready!");
lv_obj_set_style_text_align(statusLabel, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_pad_top(statusLabel, 0, 0);
lv_obj_set_style_pad_bottom(statusLabel, 0, 0);
// Button container
lv_obj_t* btnContainer = lv_obj_create(mainWrapper);
lv_obj_set_width(btnContainer, LV_PCT(100));
lv_obj_set_height(btnContainer, LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(btnContainer, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(btnContainer, 0, 0);
lv_obj_set_style_pad_all(btnContainer, 0, 0);
lv_obj_set_style_pad_column(btnContainer, isSmall ? 3 : (isXLarge ? 16 : 6), 0);
lv_obj_set_style_pad_top(btnContainer, padRow, 0);
lv_obj_set_flex_flow(btnContainer, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(btnContainer, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_align(btnContainer, LV_ALIGN_BOTTOM_MID, 0, 0);
// Helper to create action button with icon
auto createActionBtn = [&](lv_obj_t** btn, const char* text, IconId iconId, int bufferIdx, lv_event_cb_t callback) {
*btn = lv_btn_create(btnContainer);
lv_obj_set_size(*btn, btnWidth, btnHeight);
lv_obj_set_flex_flow(*btn, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(*btn, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_all(*btn, isSmall ? 1 : (isXLarge ? 4 : 2), 0);
lv_obj_set_style_pad_row(*btn, isSmall ? 1 : (isXLarge ? 4 : 2), 0);
// Get the actual button background color from the theme
lv_color_t btnBgColor = lv_theme_get_color_primary(*btn);
// Icon canvas with button background color
lv_obj_t* iconCanvas = lv_canvas_create(*btn);
lv_canvas_set_buffer(iconCanvas, TamaTac_iconBuffers[bufferIdx], 16, 16, LV_COLOR_FORMAT_NATIVE);
drawIconWithBg(iconCanvas, iconId, btnBgColor, lv_color_hex(0xFFFFFF));
// Label
if (!isSmall) {
lv_obj_t* label = lv_label_create(*btn);
lv_label_set_text(label, text);
lv_obj_set_style_text_font(label, lv_font_get_default(), 0);
}
lv_obj_add_event_cb(*btn, callback, LV_EVENT_CLICKED, app);
};
createActionBtn(&feedBtn, "Feed", ICON_FEED, 7, onFeedClicked);
createActionBtn(&playBtn, "Play", ICON_PLAY, 8, onPlayClicked);
createActionBtn(&medicineBtn, "Med", ICON_MEDICINE, 9, onMedicineClicked);
createActionBtn(&sleepBtn, "Sleep", ICON_SLEEP, 10, onSleepClicked);
// Refresh timer for periodic UI updates (day/night, random events, etc.)
refreshTimer = lv_timer_create(onRefreshTimer, 5000, this);
// Animation timer (~5fps for smooth frame cycling)
animTimer = lv_timer_create(onAnimTimer, 200, this);
}
void MainView::onStop() {
if (refreshTimer) {
lv_timer_del(refreshTimer);
refreshTimer = nullptr;
}
if (animTimer) {
lv_timer_del(animTimer);
animTimer = nullptr;
}
mainWrapper = nullptr;
statusLabel = nullptr;
petArea = nullptr;
petCanvas = nullptr;
poopContainer = nullptr;
for (int i = 0; i < 3; i++) {
poopIcons[i] = nullptr;
}
for (int i = 0; i < 4; i++) {
statIcons[i] = nullptr;
}
hungerBar = nullptr;
happinessBar = nullptr;
healthBar = nullptr;
energyBar = nullptr;
feedBtn = nullptr;
playBtn = nullptr;
medicineBtn = nullptr;
sleepBtn = nullptr;
parent = nullptr;
app = nullptr;
}
void MainView::updateUI(PetLogic* petLogic, LifeStage& lastKnownStage) {
updateStatBars(petLogic);
updatePetDisplay(petLogic, lastKnownStage);
}
void MainView::updateStatBars(PetLogic* petLogic) {
if (petLogic == nullptr) return;
const PetStats& stats = petLogic->getStats();
if (hungerBar) lv_bar_set_value(hungerBar, stats.hunger, LV_ANIM_OFF);
if (happinessBar) lv_bar_set_value(happinessBar, stats.happiness, LV_ANIM_OFF);
if (healthBar) lv_bar_set_value(healthBar, stats.health, LV_ANIM_OFF);
if (energyBar) lv_bar_set_value(energyBar, stats.energy, LV_ANIM_OFF);
}
void MainView::updatePetDisplay(PetLogic* petLogic, LifeStage& lastKnownStage) {
if (petLogic == nullptr) return;
const PetStats& stats = petLogic->getStats();
// Check for evolution (Ghost is death, not evolution)
if (stats.stage != lastKnownStage && statusLabel != nullptr) {
lastKnownStage = stats.stage;
if (stats.stage == LifeStage::Ghost) {
lv_label_set_text(statusLabel, LV_SYMBOL_WARNING " Your pet has died...");
} else {
char msg[64];
snprintf(msg, sizeof(msg), LV_SYMBOL_UP " Evolved to %s!", lifeStageToString(stats.stage));
lv_label_set_text(statusLabel, msg);
// Trigger evolution flash effect (400ms white flash)
evolutionFlashUntil = tt::kernel::getMillis() + 400;
}
}
// Update day/night visual
DayPhase phase = petLogic->getDayPhase();
currentDayPhase = phase;
if (petArea) {
lv_color_t bg = (phase == DayPhase::Night) ? lv_color_hex(0x1a1a2e) : lv_color_hex(0x333333);
lv_obj_set_style_bg_color(petArea, bg, 0);
}
// Update current sprite (animation timer will handle actual drawing)
SpriteId newSprite = getSpriteForCurrentState(stats);
if (newSprite != currentSpriteId) {
currentSpriteId = newSprite;
animStartTime = tt::kernel::getMillis();
}
drawSprite(currentSpriteId, &stats);
// Update poop display
for (int i = 0; i < 3; i++) {
if (poopIcons[i]) {
if (i < stats.poopCount) {
drawIcon(poopIcons[i], ICON_POOP);
lv_obj_clear_flag(poopIcons[i], LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(poopIcons[i], LV_OBJ_FLAG_HIDDEN);
}
}
}
}
void MainView::setStatusText(const char* text) {
if (statusLabel) {
lv_label_set_text(statusLabel, text);
}
}
SpriteId MainView::getSpriteForCurrentState(const PetStats& stats) const {
if (stats.isDead) {
return SPRITE_GHOST;
}
// Egg always shows egg — no action/mood sprite overrides
if (stats.stage == LifeStage::Egg) {
return SPRITE_EGG_IDLE;
}
switch (stats.currentAnim) {
case AnimState::Eating: return SPRITE_EATING;
case AnimState::Playing: return SPRITE_PLAYING;
case AnimState::Sleeping: return SPRITE_SLEEPING;
case AnimState::Sick: return SPRITE_SICK;
case AnimState::Sad: return SPRITE_SAD;
default: break;
}
if (stats.isSick) {
return SPRITE_SICK;
}
if (stats.isAsleep) {
return SPRITE_SLEEPING;
}
if (stats.happiness >= 70) {
return SPRITE_HAPPY;
} else if (stats.happiness < 30) {
return SPRITE_SAD;
}
switch (stats.stage) {
case LifeStage::Baby: return SPRITE_BABY_IDLE;
case LifeStage::Teen: return SPRITE_TEEN_IDLE;
case LifeStage::Adult: return SPRITE_ADULT_IDLE;
case LifeStage::Elder: return SPRITE_ELDER_IDLE;
case LifeStage::Ghost: return SPRITE_GHOST;
default: break;
}
return SPRITE_EGG_IDLE;
}
void MainView::drawSprite(SpriteId spriteId, const PetStats* stats) {
if (petCanvas == nullptr) return;
uint32_t now = tt::kernel::getMillis();
int canvasSize = SPRITE_WIDTH * spriteScale;
if (canvasSize > petCanvasSize) return;
// Evolution flash: fill entire canvas with white
if (evolutionFlashUntil > 0 && now < evolutionFlashUntil) {
lv_canvas_fill_bg(petCanvas, lv_color_hex(0xFFFFFF), LV_OPA_COVER);
return;
} else if (evolutionFlashUntil > 0) {
evolutionFlashUntil = 0;
}
// Fill background
lv_color_t bgColor = (currentDayPhase == DayPhase::Night) ? lv_color_hex(0x1a1a2e) : lv_color_hex(0x333333);
lv_canvas_fill_bg(petCanvas, bgColor, LV_OPA_COVER);
// Night stars: draw twinkly dots in the background
if (currentDayPhase == DayPhase::Night) {
// Update star seed every 2 seconds for twinkling effect
uint32_t starPhase = now / 2000;
uint32_t seed = starPhase * 7919; // Simple deterministic hash
for (int i = 0; i < 6; i++) {
seed = seed * 1103515245 + 12345; // LCG
int sx = (seed >> 16) % canvasSize;
seed = seed * 1103515245 + 12345;
int sy = (seed >> 16) % (canvasSize / 3); // Stars only in top third
// Alternate bright/dim stars
lv_color_t starColor = (i % 2 == 0) ? lv_color_hex(0xFFFFFF) : lv_color_hex(0x888899);
lv_canvas_set_px(petCanvas, sx, sy, starColor, LV_OPA_COVER);
}
}
// Determine current animation frame
const AnimatedSprite& anim = getAnimSprite(spriteId);
int frameIdx = 0;
if (anim.frameCount > 1 && anim.frameDelayMs > 0) {
uint32_t elapsed = now - animStartTime;
if (anim.loop) {
frameIdx = (elapsed / anim.frameDelayMs) % anim.frameCount;
} else {
frameIdx = elapsed / anim.frameDelayMs;
if (frameIdx >= anim.frameCount) frameIdx = anim.frameCount - 1;
}
}
// Render RGB565 sprite with transparency and scaling
const uint16_t* pixelData = anim.frames[frameIdx].data;
for (int y = 0; y < SPRITE_HEIGHT; y++) {
for (int x = 0; x < SPRITE_WIDTH; x++) {
uint16_t pixel = pixelData[y * SPRITE_WIDTH + x];
if (pixel == SPRITE_TRANSPARENT) continue;
uint8_t r = (pixel >> 11) << 3;
uint8_t g = ((pixel >> 5) & 0x3F) << 2;
uint8_t b = (pixel & 0x1F) << 3;
lv_color_t color = lv_color_make(r, g, b);
for (int dy = 0; dy < spriteScale; dy++) {
for (int dx = 0; dx < spriteScale; dx++) {
lv_canvas_set_px(petCanvas,
x * spriteScale + dx,
y * spriteScale + dy,
color, LV_OPA_COVER);
}
}
}
}
// Draw overlays (Z's, mood indicator)
drawOverlays(spriteId, stats);
}
void MainView::drawOverlays(SpriteId spriteId, const PetStats* stats) {
if (petCanvas == nullptr) return;
int canvasSize = SPRITE_WIDTH * spriteScale;
uint32_t now = tt::kernel::getMillis();
lv_color_t white = lv_color_hex(0xFFFFFF);
lv_color_t gray = lv_color_hex(0xAAAAAA);
// Floating Z's during sleep
if (spriteId == SPRITE_SLEEPING) {
// Two Z's at different positions, bobbing up and down
int phase = (now / 500) % 4; // 4-step bob cycle
int bobOffset = (phase < 2) ? phase : (4 - phase); // 0,1,2,1 pattern
// Large Z (top-right area)
int zx = canvasSize - 8 * spriteScale / 3;
int zy = 2 + bobOffset;
if (zx >= 0 && zx + 3 < canvasSize && zy + 3 < canvasSize) {
// Z shape: top bar, diagonal, bottom bar
lv_canvas_set_px(petCanvas, zx, zy, white, LV_OPA_COVER);
lv_canvas_set_px(petCanvas, zx + 1, zy, white, LV_OPA_COVER);
lv_canvas_set_px(petCanvas, zx + 2, zy, white, LV_OPA_COVER);
lv_canvas_set_px(petCanvas, zx + 1, zy + 1, white, LV_OPA_COVER);
lv_canvas_set_px(petCanvas, zx, zy + 2, white, LV_OPA_COVER);
lv_canvas_set_px(petCanvas, zx + 1, zy + 2, white, LV_OPA_COVER);
lv_canvas_set_px(petCanvas, zx + 2, zy + 2, white, LV_OPA_COVER);
}
// Small z (offset from large Z, staggered bob)
int zx2 = zx - 5;
int bobOffset2 = ((phase + 2) % 4 < 2) ? 0 : 1;
int zy2 = zy + 4 + bobOffset2;
if (zx2 >= 0 && zx2 + 2 < canvasSize && zy2 >= 0 && zy2 + 2 < canvasSize) {
lv_canvas_set_px(petCanvas, zx2, zy2, gray, LV_OPA_COVER);
lv_canvas_set_px(petCanvas, zx2 + 1, zy2, gray, LV_OPA_COVER);
lv_canvas_set_px(petCanvas, zx2, zy2 + 1, gray, LV_OPA_COVER);
lv_canvas_set_px(petCanvas, zx2 + 1, zy2 + 1, gray, LV_OPA_COVER);
}
}
// Mood indicator: small colored dot in bottom-right corner
if (stats != nullptr && !stats->isDead) {
lv_color_t moodColor;
bool showMood = true;
if (stats->isSick) {
moodColor = lv_color_hex(0xFF0000); // Red for sick
} else if (stats->hunger < 30 || stats->happiness < 30 || stats->energy < 30) {
moodColor = lv_color_hex(0xFF8800); // Orange for warning
} else if (stats->hunger >= 70 && stats->happiness >= 70 && stats->health >= 70 && stats->energy >= 70) {
moodColor = lv_color_hex(0x00FF00); // Green for happy
} else {
showMood = false; // Neutral — no indicator
}
if (showMood) {
// 2x2 dot in bottom-right corner
int mx = canvasSize - 3;
int my = canvasSize - 3;
for (int dy = 0; dy < 2; dy++) {
for (int dx = 0; dx < 2; dx++) {
lv_canvas_set_px(petCanvas, mx + dx, my + dy, moodColor, LV_OPA_COVER);
}
}
}
}
}
void MainView::drawIcon(lv_obj_t* canvas, IconId iconId) {
drawIconWithBg(canvas, iconId, lv_color_hex(0x333333), lv_color_hex(0xFFFFFF));
}
void MainView::drawIconWithBg(lv_obj_t* canvas, IconId iconId, lv_color_t bgColor, lv_color_t fgColor) {
if (canvas == nullptr) return;
const Icon& icon = getIcon(iconId);
const uint8_t* data = icon.data;
if (icon.width > 8 || icon.height > 8) return;
lv_canvas_fill_bg(canvas, bgColor, LV_OPA_COVER);
const int scale = 2;
const int width = icon.width;
const int height = icon.height;
for (int y = 0; y < height; y++) {
uint8_t row = data[y];
for (int x = 0; x < width; x++) {
bool pixelOn = (row >> (7 - x)) & 1;
if (pixelOn) {
for (int dy = 0; dy < scale; dy++) {
for (int dx = 0; dx < scale; dx++) {
lv_canvas_set_px(canvas, x * scale + dx, y * scale + dy, fgColor, LV_OPA_COVER);
}
}
}
}
}
}
void MainView::onAnimTimer(lv_timer_t* timer) {
MainView* view = static_cast<MainView*>(lv_timer_get_user_data(timer));
if (view == nullptr || view->petCanvas == nullptr) return;
// Pick up deferred reset quickly (200ms vs 5s refresh timer)
if (TamaTac::pendingResetUI) {
TamaTac::pendingResetUI = false;
PetLogic* petLogic = TamaTac::petLogic;
if (petLogic) {
view->updateUI(petLogic, TamaTac::lastKnownStage);
}
return;
}
const PetStats* stats = TamaTac::petLogic ? &TamaTac::petLogic->getStats() : nullptr;
view->drawSprite(view->currentSpriteId, stats);
}
void MainView::onRefreshTimer(lv_timer_t* timer) {
MainView* view = static_cast<MainView*>(lv_timer_get_user_data(timer));
if (view == nullptr || view->app == nullptr) return;
PetLogic* petLogic = TamaTac::petLogic;
if (petLogic == nullptr) return;
view->updateUI(petLogic, TamaTac::lastKnownStage);
// Display random event messages
RandomEvent event = petLogic->getLastEvent();
if (event != RandomEvent::None && view->statusLabel) {
const char* msg = nullptr;
switch (event) {
case RandomEvent::FoundTreat: msg = "Found a treat!"; break;
case RandomEvent::MadeFriend: msg = "Made a friend!"; break;
case RandomEvent::CaughtCold: msg = "Caught a cold!"; break;
case RandomEvent::GotMuddy: msg = "Got muddy!"; break;
case RandomEvent::HadNap: msg = "Had a nap!"; break;
case RandomEvent::SunnyDay: msg = "Sunny day!"; break;
default: break;
}
if (msg) {
lv_label_set_text(view->statusLabel, msg);
}
petLogic->clearLastEvent();
}
}
// Event handlers
void MainView::onFeedClicked(lv_event_t* e) {
TamaTac* app = static_cast<TamaTac*>(lv_event_get_user_data(e));
if (app == nullptr) return;
app->handleFeedAction();
}
void MainView::onPlayClicked(lv_event_t* e) {
TamaTac* app = static_cast<TamaTac*>(lv_event_get_user_data(e));
if (app == nullptr) return;
app->handlePlayAction();
}
void MainView::onMedicineClicked(lv_event_t* e) {
TamaTac* app = static_cast<TamaTac*>(lv_event_get_user_data(e));
if (app == nullptr) return;
app->handleMedicineAction();
}
void MainView::onSleepClicked(lv_event_t* e) {
TamaTac* app = static_cast<TamaTac*>(lv_event_get_user_data(e));
if (app == nullptr) return;
app->handleSleepAction();
}
void MainView::onPetTapped(lv_event_t* e) {
TamaTac* app = static_cast<TamaTac*>(lv_event_get_user_data(e));
if (app == nullptr) return;
app->handlePetTap();
}