#include "still_alive.h" #include #include "el512_font.h" #include "el512_panel.h" #include "still_alive_data.h" namespace el512sa { namespace { uint8_t fit(int16_t px, int16_t cell, uint8_t cap) { const int n = (px - 2 * TextBox::PAD) / cell; return n > cap ? cap : (uint8_t) n; } bool due(uint32_t now, uint32_t at) { return (int32_t) (now - at) >= 0; } void drawChar(EL512Panel &panel, int16_t x, int16_t y, char c) { if (c < EL512_FONT_FIRST || c > EL512_FONT_LAST) c = '?'; const uint8_t *glyph = &EL512_FONT[(c - EL512_FONT_FIRST) * 5]; for (int16_t col = 0; col < 5; col++) { const uint8_t bits = glyph[col]; for (int16_t row = 0; row < 8; row++) { if (bits & (1 << row)) panel.setPixel(x + col, y + row, true); } } } } // namespace // --- TextBox ----------------------------------------------------------------- TextBox::TextBox(int16_t x, int16_t y, int16_t w, int16_t h) : x_(x), y_(y), w_(w), h_(h), cols_(fit(w, CHAR_W, MAX_COLS)), rows_(fit(h, CHAR_H, MAX_ROWS)) { clear(); } void TextBox::clear() { memset(text_, 0, sizeof(text_)); row_ = 0; col_ = 0; } void TextBox::put(char c) { if (c == '\n') { newLine(); return; } if (col_ >= cols_) newLine(); text_[row_][col_++] = c; } void TextBox::print(const char *s) { while (*s) put(*s++); } void TextBox::newLine() { col_ = 0; if (row_ + 1 < rows_) { row_++; return; } memmove(text_[0], text_[1], (size_t) (rows_ - 1) * sizeof(text_[0])); memset(text_[rows_ - 1], 0, sizeof(text_[0])); } void TextBox::draw(EL512Panel &panel, bool cursor, bool frame) const { if (frame) { // Dashed outline, 4 px on / 2 px off. for (int16_t i = 0; i < w_; i++) { if (i % 6 < 4) { panel.setPixel(x_ + i, y_, true); panel.setPixel(x_ + i, y_ + h_ - 1, true); } } for (int16_t i = 0; i < h_; i++) { if (i % 6 < 4) { panel.setPixel(x_, y_ + i, true); panel.setPixel(x_ + w_ - 1, y_ + i, true); } } } for (uint8_t r = 0; r < rows_; r++) { for (uint8_t c = 0; c < cols_ && text_[r][c]; c++) { if (text_[r][c] != ' ') drawChar(panel, x_ + PAD + c * CHAR_W, y_ + PAD + r * CHAR_H, text_[r][c]); } } if (cursor) { for (int16_t dy = 6; dy < 8; dy++) for (int16_t dx = 0; dx < 5; dx++) panel.setPixel(x_ + PAD + col_ * CHAR_W + dx, y_ + PAD + row_ * CHAR_H + dy, true); } } // --- StillAlivePlayer ---------------------------------------------------------- // 41 x 31 lyrics console | 41 x 9 credits over a 41 x 20 picture. StillAlivePlayer::StillAlivePlayer() : lyrics_(0, 0, 254, 256), credits_(256, 0, 256, 86), art_(256, 88, 256, 168) {} uint32_t StillAlivePlayer::songStartMs() { return START_DELAY_MS + SONG_START_MS; } void StillAlivePlayer::start(uint32_t nowMs) { lyrics_.clear(); credits_.clear(); showArt(ART_NONE); showStart_ = nowMs + START_DELAY_MS; step_ = 0; charIdx_ = 0; stepStarted_ = false; done_ = false; doneAt_ = 0; creditIdx_ = 0; creditChar_ = 0; creditLineOpen_ = false; creditsDone_ = false; creditLineAt_ = CREDITS_START_MS; // Credits are typed on the bottom row and scroll up, as in the game. for (uint8_t r = 1; r < credits_.rows(); r++) credits_.put('\n'); } bool StillAlivePlayer::finished(uint32_t nowMs) const { return done_ && due(nowMs, doneAt_ + HOLD_MS); } void StillAlivePlayer::showArt(int8_t id) { art_.clear(); if (id <= ART_NONE || (size_t) id >= ART_COUNT) return; const Art &art = ARTS[id]; size_t width = 0; for (uint8_t i = 0; i < art.count; i++) { const size_t len = strlen(art.lines[i]); if (len > width) width = len; } const uint8_t top = art.count < art_.rows() ? (art_.rows() - art.count) / 2 : 0; const uint8_t left = width < art_.cols() ? (art_.cols() - width) / 2 : 0; for (uint8_t r = 0; r < top; r++) art_.put('\n'); for (uint8_t i = 0; i < art.count && top + i < art_.rows(); i++) { if (i > 0) art_.put('\n'); for (uint8_t c = 0; c < left; c++) art_.put(' '); // Truncate instead of wrapping so a wide picture keeps its shape. for (const char *p = art.lines[i]; *p && left + (p - art.lines[i]) < art_.cols(); p++) art_.put(*p); } } bool StillAlivePlayer::update(uint32_t nowMs) { bool changed = false; if (due(nowMs, showStart_)) { const uint32_t t = nowMs - showStart_; changed |= updateLyrics(t, nowMs); changed |= updateCredits(t); } const bool blink = (nowMs / CURSOR_BLINK_MS) & 1; const bool creditsBlink = blink && !creditsDone_ && due(nowMs, showStart_ + CREDITS_START_MS); changed |= blink != cursor_ || creditsBlink != creditsCursor_; cursor_ = blink; creditsCursor_ = creditsBlink; return changed; } // Type whatever the clock says should be on screen by now. bool StillAlivePlayer::updateLyrics(uint32_t t, uint32_t nowMs) { bool changed = false; while (step_ < LYRIC_COUNT && t >= LYRICS[step_].startMs) { const LyricStep &s = LYRICS[step_]; if (!stepStarted_) { stepStarted_ = true; if (s.flags & STEP_CLEAR) lyrics_.clear(); if (s.art != ART_KEEP) showArt(s.art); changed = true; } const size_t len = strlen(s.text); const uint32_t elapsed = t - s.startMs; const size_t want = (s.durMs == 0 || elapsed >= s.durMs) ? len : len * elapsed / s.durMs; while (charIdx_ < want) { lyrics_.put(s.text[charIdx_++]); changed = true; } if (charIdx_ < len) break; // still typing this step if (s.flags & STEP_NEWLINE) lyrics_.put('\n'); step_++; charIdx_ = 0; stepStarted_ = false; changed = true; if (step_ == LYRIC_COUNT) { done_ = true; doneAt_ = nowMs; } } return changed; } // Each credits line takes CREDIT_CHAR_US per character (an empty line counts as // one), typed evenly; then the next line starts on a fresh bottom row. bool StillAlivePlayer::updateCredits(uint32_t t) { if (creditsDone_ || t < CREDITS_START_MS) return false; bool changed = false; while (true) { const char *line = CREDITS[creditIdx_]; const size_t len = strlen(line); if (!creditLineOpen_) { creditLineOpen_ = true; credits_.put('\n'); changed = true; } const uint32_t dur = (uint32_t) ((uint64_t) CREDIT_CHAR_US * (len ? len : 1) / 1000); const uint32_t elapsed = t - creditLineAt_; const size_t want = elapsed >= dur ? len : len * elapsed / dur; while (creditChar_ < want) { credits_.put(line[creditChar_++]); changed = true; } if (elapsed < dur) break; creditLineAt_ += dur; creditChar_ = 0; creditLineOpen_ = false; if (++creditIdx_ == CREDIT_COUNT) { creditIdx_ = 0; if (!CREDITS_LOOP) { creditsDone_ = true; break; } } } return changed; } void StillAlivePlayer::draw(EL512Panel &panel) const { lyrics_.draw(panel, cursor_); credits_.draw(panel, creditsCursor_); art_.draw(panel, false, false); // no frame around the picture, as in the game } } // namespace el512sa