Public Access
252 lines
7.0 KiB
C++
252 lines
7.0 KiB
C++
#include "still_alive.h"
|
|||
|
|
|
||
|
|
#include <string.h>
|
||
|
|
|
||
|
|
#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
|