Public Access
tools/render_pages.py compiles the page code natively with g++ against ESPHome's own display and font code and the font bitmaps of the last `esphome compile`, and draws pages to PNG at chosen moments with sample data (normal, empty, alert, busy), as a contact sheet, or as one example per page in docs/images/pages (now shown in the README). Fixes found with it: Cyber HUD zone names ran into the temperatures, the breach header label hit the tag and the column marker the matrix label; KonoSuba subtitle, guild weather icon, cabbage text and all-day deadline, and hills through the party; Steins;Gate D-mail recipient, the microwave display, the gadget card value and world line labels; LCARS room rows, readout bars and data cascade, warp core readouts, and the captain's log weather wording. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
430 lines
17 KiB
C++
430 lines
17 KiB
C++
#include "trek.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <vector>
|
|
|
|
#include "el512_display.h"
|
|
#include "esphome/core/hal.h"
|
|
#include "trek_sprites.h"
|
|
|
|
namespace esphome::el512::trek {
|
|
|
|
using display::COLOR_OFF;
|
|
using display::COLOR_ON;
|
|
using display::Display;
|
|
using display::TextAlign;
|
|
|
|
namespace {
|
|
|
|
using namespace fx;
|
|
|
|
constexpr int W = 512, H = 256;
|
|
const uint8_t BAYER[4][4] = {{0, 8, 2, 10}, {12, 4, 14, 6}, {3, 11, 1, 9}, {15, 7, 13, 5}};
|
|
|
|
// --- LCARS shapes --------------------------------------------------------------------
|
|
|
|
// Bar with round ends.
|
|
void pill(Display &it, int x, int y, int w, int h, Color c = COLOR_ON) {
|
|
const int r = h / 2;
|
|
it.filled_rectangle(x + r, y, w - 2 * r, h, c);
|
|
it.filled_circle(x + r, y + r, r, c);
|
|
it.filled_circle(x + w - 1 - r, y + r, r, c);
|
|
}
|
|
|
|
// Lit block with a dark label at its bottom right, as LCARS buttons have.
|
|
void block(Display &it, display::BaseFont *font, int x, int y, int w, int h, const char *label) {
|
|
it.filled_rectangle(x, y, w, h);
|
|
if (label != nullptr)
|
|
it.print(x + w - 4, y + h - 1, font, COLOR_OFF, TextAlign::BOTTOM_RIGHT, label);
|
|
}
|
|
|
|
// LCARS elbow: a vertical arm (vw wide) joined to a horizontal arm (hh thick)
|
|
// by a large outer curve; the horizontal arm runs along the top (top = true)
|
|
// or the bottom of the w x h box.
|
|
void elbow(Display &it, int x, int y, int w, int h, int vw, int hh, bool top) {
|
|
const int ro = vw * 3 / 4, ri = hh;
|
|
const int by = top ? y : y + h - hh; // horizontal arm
|
|
it.filled_rectangle(x, by, w, hh);
|
|
it.filled_rectangle(x, y, vw, h);
|
|
// Round the outer corner.
|
|
const int oy = top ? y : y + h - ro;
|
|
it.filled_rectangle(x, oy, ro, ro, COLOR_OFF);
|
|
it.start_clipping(x, oy, x + ro, oy + ro);
|
|
it.filled_circle(x + ro, top ? y + ro : y + h - 1 - ro, ro);
|
|
it.end_clipping();
|
|
// Fillet the inner corner.
|
|
const int iy = top ? by + hh : by - ri;
|
|
it.filled_rectangle(x + vw, iy, ri, ri);
|
|
it.start_clipping(x + vw, iy, x + vw + ri, iy + ri);
|
|
it.filled_circle(x + vw + ri, top ? iy + ri : iy - 1, ri, COLOR_OFF);
|
|
it.end_clipping();
|
|
}
|
|
|
|
// Horizontal readout bar: dim track and a lit pill up to frac.
|
|
void readout(Display &it, int x, int y, int w, int h, float frac) {
|
|
it.horizontal_line(x, y + h / 2, w);
|
|
if (!std::isnan(frac) && frac > 0)
|
|
pill(it, x, y, std::max(h, (int) (w * std::clamp(frac, 0.0f, 1.0f))), h);
|
|
}
|
|
|
|
std::vector<std::string> wrap(Display &it, display::BaseFont *font, const std::string &text, int width,
|
|
size_t max_lines) {
|
|
std::vector<std::string> lines;
|
|
std::string line;
|
|
size_t i = 0;
|
|
while (i <= text.size()) {
|
|
size_t j = text.find(' ', i);
|
|
if (j == std::string::npos)
|
|
j = text.size();
|
|
const std::string word = text.substr(i, j - i);
|
|
const std::string joined = line.empty() ? word : line + " " + word;
|
|
if (!line.empty() && text_width(it, font, joined.c_str()) > width) {
|
|
lines.push_back(line);
|
|
line = word;
|
|
} else {
|
|
line = joined;
|
|
}
|
|
i = j + 1;
|
|
}
|
|
if (!line.empty())
|
|
lines.push_back(line);
|
|
if (lines.size() > max_lines) {
|
|
lines.resize(max_lines);
|
|
lines.back() += "...";
|
|
}
|
|
for (auto &l : lines)
|
|
l = fit(font, l, width);
|
|
return lines;
|
|
}
|
|
|
|
std::string typed(const std::string &s, uint32_t t, int cps) { return s.substr(0, std::min<size_t>(s.size(), t * cps / 1000)); }
|
|
|
|
std::string kw(float w) { return std::isnan(w) ? "--" : upper(watts(w)); }
|
|
std::string deg(float v) { return std::isnan(v) ? "--" : str("%.1f", v); }
|
|
|
|
float stardate(ESPTime now) {
|
|
if (!now.is_valid())
|
|
return 58000.0f;
|
|
return 58000.0f + (now.year - 2026) * 1000.0f + (now.day_of_year - 1 + now.hour / 24.0f) * 1000.0f / 365.25f;
|
|
}
|
|
|
|
// LCARS-style serial numbers for blocks and the data cascade.
|
|
std::string serial(uint32_t seed) { return str("%02u-%04u", (unsigned) (seed % 100), (unsigned) ((seed >> 8) % 10000)); }
|
|
|
|
} // namespace
|
|
|
|
bool red_alert(const HomeData &d) { return d.printer == "paused" || d.printer == "error"; }
|
|
|
|
// --- 1. LCARS ship status (red alert when the printer needs a look) ----------------------
|
|
|
|
void lcars_status(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime now) {
|
|
if (f.large == nullptr)
|
|
return;
|
|
const uint32_t t = it.page_age_ms();
|
|
const bool alert = red_alert(d);
|
|
const bool blink = (t / 500) % 2;
|
|
|
|
// Frame: two elbows, sidebar blocks between them, bars along top and bottom.
|
|
if (!alert || blink) {
|
|
elbow(it, 0, 0, 300, 100, 72, 18, true);
|
|
elbow(it, 0, 156, 300, 100, 72, 18, false);
|
|
} else {
|
|
elbow(it, 0, 0, 300, 100, 72, 6, true);
|
|
elbow(it, 0, 156, 300, 100, 72, 6, false);
|
|
}
|
|
for (int i = 0; i < 2; i++) {
|
|
const uint32_t r = hash(i, (t / 1700) * (i + 1));
|
|
if (!(alert ? blink : r % 7 == 0))
|
|
block(it, f.pixel, 0, 104 + i * 26, 72, 22, serial(hash(i, 4)).c_str());
|
|
}
|
|
pill(it, 306, 0, 50, 18);
|
|
pill(it, 306, 238, 70, 18);
|
|
it.print(508, 22, f.medium, TextAlign::BASELINE_RIGHT, alert ? "ALERT CONDITION RED" : "SHIP STATUS");
|
|
it.printf(508, 256, f.small, TextAlign::BASELINE_RIGHT, "STARDATE %.1f", stardate(now));
|
|
|
|
if (alert) {
|
|
// Red alert: bars sweep outward from the centre around the warning.
|
|
const int cx = 292, cy = 128;
|
|
const int phase = (int) (t % 900) * 120 / 900;
|
|
for (int k = 0; k < 3; k++) {
|
|
const int s = (phase + k * 40) % 120;
|
|
it.rectangle(cx - 70 - s, cy - 30 - s / 3, 140 + 2 * s, 60 + 2 * (s / 3));
|
|
}
|
|
if ((t % 1000) < 700) {
|
|
it.filled_rectangle(cx - 110, cy - 34, 220, 68, COLOR_OFF);
|
|
it.print(cx, cy + 4, f.large, TextAlign::CENTER, "RED ALERT");
|
|
}
|
|
it.print(cx, 196, f.medium, TextAlign::TOP_CENTER, d.printer == "error" ? "3D PRINTER ERROR" : "3D PRINTER PAUSED");
|
|
it.print(cx, 222, f.small, TextAlign::TOP_CENTER, "REPORT TO THE FABRICATION BAY");
|
|
return;
|
|
}
|
|
|
|
// Power distribution.
|
|
it.print(84, 24, f.small, TextAlign::TOP_LEFT, "POWER DISTRIBUTION");
|
|
const float grid = d.grid;
|
|
struct Row {
|
|
const char *name;
|
|
float w;
|
|
};
|
|
const Row rows[3] = {{"SOLAR", d.solar},
|
|
{"HOUSE", d.house},
|
|
{std::isnan(grid) || grid >= 0 ? "GRID IN" : "GRID OUT", std::fabs(grid)}};
|
|
for (int i = 0; i < 3; i++) {
|
|
const int y = 48 + i * 24;
|
|
block(it, f.small, 84, y, 66, 18, rows[i].name);
|
|
readout(it, 156, y + 4, 80, 10, rows[i].w / 5000.0f);
|
|
it.print(298, y + 18, f.small, TextAlign::BOTTOM_RIGHT, kw(rows[i].w).c_str());
|
|
}
|
|
|
|
// Environmental.
|
|
it.print(84, 128, f.small, TextAlign::TOP_LEFT, "ENVIRONMENTAL DECKS 1-4");
|
|
for (int i = 0; i < 4; i++) {
|
|
const int x = 84 + (i % 2) * 110, y = 148 + (i / 2) * 44;
|
|
it.print(x, y, f.small, TextAlign::TOP_LEFT, fit(f.small, upper(d.rooms[i].name), 104).c_str());
|
|
it.print(x, y + 14, f.medium, TextAlign::TOP_LEFT, deg(d.rooms[i].value).c_str());
|
|
}
|
|
|
|
// Clock, sensors, fabrication bay.
|
|
it.vertical_line(306, 26, 206);
|
|
if (now.is_valid())
|
|
it.strftime(316, 22, f.large, TextAlign::TOP_LEFT, "%H:%M", now);
|
|
it.print(316, 96, f.small, TextAlign::TOP_LEFT, "LONG RANGE SENSORS");
|
|
it.printf(316, 114, f.small, TextAlign::TOP_LEFT, "%s %s", deg(d.temp).c_str(), upper(weather_text(d.weather)).c_str());
|
|
if (!std::isnan(d.humidity))
|
|
it.printf(316, 132, f.small, TextAlign::TOP_LEFT, "HUMIDITY %.0f%%", d.humidity);
|
|
it.print(316, 160, f.small, TextAlign::TOP_LEFT, "FABRICATION BAY");
|
|
it.print(316, 178, f.small, TextAlign::TOP_LEFT, d.printer.empty() ? "NO SIGNAL" : upper(d.printer).c_str());
|
|
|
|
// Data cascade: columns of numbers rolling over.
|
|
for (int col = 0; col < 2; col++) {
|
|
for (int row = 0; row < 9; row++) {
|
|
const uint32_t r = hash(col * 31 + row, (t / 180) + row * 3);
|
|
const int x = 456 + col * 30, y = 96 + row * 12;
|
|
if (r % 5 == 0)
|
|
continue;
|
|
it.printf(x, y, f.pixel, TextAlign::TOP_LEFT, "%03u", (unsigned) (r % 1000));
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- 2. Lower Decks duty roster ---------------------------------------------------------
|
|
|
|
void duty_roster(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime now) {
|
|
if (f.text == nullptr)
|
|
return;
|
|
const uint32_t t = it.page_age_ms();
|
|
pill(it, 0, 0, 40, 18);
|
|
it.filled_rectangle(44, 0, 160, 18);
|
|
it.print(508, 22, f.medium, TextAlign::BASELINE_RIGHT, "U.S.S. CERRITOS DUTY ROSTER");
|
|
|
|
struct Ensign {
|
|
const char *name;
|
|
const Sprite *sprite;
|
|
const char *chore;
|
|
};
|
|
static const Ensign ENSIGNS[4] = {
|
|
{"MARINER", &MARINER, "Sneak off to the holodeck before anyone notices"},
|
|
{"BOIMLER", &BOIMLER, "Reorganize the cargo bay alphabetically, again"},
|
|
{"TENDI", &TENDI, "Run a full diagnostic of the science lab"},
|
|
{"RUTHERFORD", &RUTHERFORD, "Scrub the plasma conduits on deck 12"},
|
|
};
|
|
// The next agenda events go to the ensigns, in order; the rest get chores.
|
|
const auto events = split(d.agenda, ';');
|
|
const int alarmed = (int) ((t / 5000) % 4); // who gets the "!" this time
|
|
for (int i = 0; i < 4; i++) {
|
|
const int x = i * 128, cx = x + 64;
|
|
const Ensign &e = ENSIGNS[i];
|
|
const int bob = ((t / 350 + i) % 2) ? 1 : 0;
|
|
sprite(it, *e.sprite, cx - 16, 28 - bob, 2);
|
|
if (i == alarmed && (t % 5000) > 3000) {
|
|
it.filled_rectangle(cx + 18, 26, 12, 18);
|
|
it.print(cx + 24, 35, f.medium, COLOR_OFF, TextAlign::CENTER, "!");
|
|
}
|
|
pill(it, cx - 54, 80, 108, 18);
|
|
it.print(cx, 89, f.small, COLOR_OFF, TextAlign::CENTER, e.name);
|
|
|
|
std::string title = e.chore, when = "BETA SHIFT";
|
|
if ((size_t) i < events.size()) {
|
|
title = field(events[i], 2);
|
|
const std::string day = field(events[i], 0), time = field(events[i], 1);
|
|
when = upper(day) + " " + (time.empty() ? "ALL DAY" : time);
|
|
}
|
|
if (i > 0)
|
|
it.vertical_line(x, 104, 126);
|
|
it.print(x + 8, 106, f.pixel, TextAlign::TOP_LEFT, "ASSIGNMENT");
|
|
int y = 120;
|
|
for (const auto &l : wrap(it, f.text, title, 112, 4)) {
|
|
it.print(x + 8, y, f.text, TextAlign::TOP_LEFT, l.c_str());
|
|
y += 18;
|
|
}
|
|
it.print(x + 8, 206, f.pixel, TextAlign::TOP_LEFT, "REPORT");
|
|
it.print(x + 8, 218, f.pixel, TextAlign::TOP_LEFT, fit(f.pixel, when, 112).c_str());
|
|
}
|
|
|
|
// Bottom bar with a scrolling notice.
|
|
pill(it, 0, 238, 40, 18);
|
|
const std::string notice = "ALL ENSIGNS REPORT TO YOUR STATIONS // SENIOR STAFF MEETING IN CONFERENCE ROOM 1 // "
|
|
"REMINDER: THE HOLODECK IS NOT A NAP ROOM // ";
|
|
const int span = text_width(it, f.small, notice.c_str());
|
|
const int nx = 46 - (int) ((t / 25) % (uint32_t) span);
|
|
it.start_clipping(46, 236, W, H);
|
|
it.print(nx, 256, f.small, TextAlign::BASELINE_LEFT, notice.c_str());
|
|
it.print(nx + span, 256, f.small, TextAlign::BASELINE_LEFT, notice.c_str());
|
|
it.end_clipping();
|
|
}
|
|
|
|
// --- 3. Warp core and engineering ---------------------------------------------------------
|
|
|
|
void warp_core(EL512Display &it, const Fonts &f, const HomeData &d) {
|
|
if (f.large == nullptr)
|
|
return;
|
|
const uint32_t t = it.page_age_ms();
|
|
const float solar = std::isnan(d.solar) ? 0 : std::max(0.0f, d.solar);
|
|
const bool breach = !std::isnan(d.grid) && d.grid > 3000;
|
|
const float speed = 40 + std::min(solar, 5000.0f) / 5000.0f * 160 + (breach ? 120 : 0); // px/s
|
|
|
|
// The core: a column of rings with plasma pulses rushing into the chamber.
|
|
constexpr int X0 = 224, X1 = 288, CH0 = 104, CH1 = 152;
|
|
it.vertical_line(X0, 0, H);
|
|
it.vertical_line(X1, 0, H);
|
|
for (int y = 4; y < H; y += 16) {
|
|
if (y > CH0 - 8 && y < CH1 + 8)
|
|
continue;
|
|
it.horizontal_line(X0 - 6, y, X1 - X0 + 13);
|
|
it.horizontal_line(X0 + 6, y + 6, X1 - X0 - 11);
|
|
}
|
|
const uint32_t travel = (uint32_t) (t * speed / 1000);
|
|
for (int k = 0; k < 4; k++) {
|
|
const int p = (int) ((travel + k * 26) % CH0);
|
|
it.filled_rectangle(X0 + 4, p - 3, X1 - X0 - 7, 6); // from the top
|
|
it.filled_rectangle(X0 + 4, H - p - 3, X1 - X0 - 7, 6); // from the bottom
|
|
}
|
|
// Reaction chamber.
|
|
it.filled_rectangle(X0 - 16, CH0, X1 - X0 + 33, CH1 - CH0, COLOR_OFF);
|
|
it.rectangle(X0 - 16, CH0, X1 - X0 + 33, CH1 - CH0);
|
|
const int level = 7 + (int) (6 * std::sin(t * speed / 9000.0f));
|
|
for (int y = CH0 + 3; y < CH1 - 3; y++)
|
|
for (int x = X0 - 13; x < X1 + 14; x++)
|
|
if (BAYER[y & 3][x & 3] < level)
|
|
it.draw_pixel_at(x, y, COLOR_ON);
|
|
|
|
// Engineering readouts on the left.
|
|
it.print(0, 0, f.medium, TextAlign::TOP_LEFT, "ENGINEERING");
|
|
const float containment = std::isnan(d.house) ? NAN : 1.0f - std::min(d.house, 5000.0f) / 5000.0f;
|
|
struct Row {
|
|
const char *name;
|
|
std::string value;
|
|
float frac;
|
|
};
|
|
const Row rows[] = {
|
|
{"WARP CORE OUTPUT", kw(solar), solar / 5000.0f},
|
|
{"CONTAINMENT FIELD", std::isnan(containment) ? "--" : str("%.0f%%", containment * 100), containment},
|
|
{"PLASMA INJECTORS", kw(std::fabs(d.grid)), std::fabs(d.grid) / 5000.0f},
|
|
};
|
|
for (int i = 0; i < 3; i++) {
|
|
const int y = 36 + i * 44;
|
|
it.print(0, y, f.small, TextAlign::TOP_LEFT, rows[i].name);
|
|
it.print(196, y, f.small, TextAlign::TOP_RIGHT, rows[i].value.c_str());
|
|
readout(it, 0, y + 22, 196, 10, rows[i].frac);
|
|
}
|
|
it.print(0, 172, f.small, TextAlign::TOP_LEFT, "MATTER / ANTIMATTER 1:1");
|
|
it.print(0, 192, f.small, TextAlign::TOP_LEFT, "DILITHIUM MATRIX STABLE");
|
|
it.printf(0, 212, f.small, TextAlign::TOP_LEFT, "PULSE RATE %.0f", speed);
|
|
|
|
// Rutherford with advice.
|
|
sprite(it, RUTHERFORD, 382, 158 - ((t / 400) % 2), 3);
|
|
const std::string tips[] = {
|
|
"Core's purring! The sun's giving us " + kw(solar) + ".",
|
|
"Containment's holding. Don't touch the blue button.",
|
|
"If it glows brighter, that's normal. Probably.",
|
|
"House is pulling " + kw(d.house) + ". Totally manageable.",
|
|
};
|
|
const std::string &tip = breach ? "Uh... that's a LOT of grid draw. Is the oven on?" : tips[(t / 5000) % 4];
|
|
it.rectangle(306, 6, 204, 104);
|
|
it.filled_triangle(400, 109, 420, 109, 410, 128);
|
|
it.filled_triangle(402, 108, 418, 108, 410, 124, COLOR_OFF);
|
|
int y = 14;
|
|
for (const auto &l : wrap(it, f.text, typed(tip, t % 5000, 30), 188, 5)) {
|
|
it.print(314, y, f.text, TextAlign::TOP_LEFT, l.c_str());
|
|
y += 18;
|
|
}
|
|
if (breach && (t / 300) % 2) {
|
|
it.filled_rectangle(0, 236, 300, 20);
|
|
it.print(150, 246, f.small, COLOR_OFF, TextAlign::CENTER, "CORE BREACH IMMINENT (IT'S THE OVEN)");
|
|
}
|
|
}
|
|
|
|
// --- 4. Captain's log under the viewscreen -----------------------------------------------
|
|
|
|
void captains_log(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime now) {
|
|
if (f.text == nullptr)
|
|
return;
|
|
const uint32_t t = it.page_age_ms();
|
|
constexpr uint32_t CYCLE = 18000, DROP = 11000;
|
|
const uint32_t c = t % CYCLE;
|
|
const bool warp = c < DROP;
|
|
constexpr int VX = 4, VY = 2, VW = 504, VH = 146, CX = 256, CY = 74;
|
|
|
|
// Viewscreen: stars streak past at warp, then drift once we drop out.
|
|
it.start_clipping(VX + 2, VY + 2, VX + VW - 2, VY + VH - 2);
|
|
for (int i = 0; i < 70; i++) {
|
|
const uint32_t r = hash(i, 1701);
|
|
const float a = (r % 3600) / 10.0f;
|
|
if (warp) {
|
|
const float dmod = std::fmod((r >> 12) % 300 + c * (0.12f + (r >> 20) % 10 * 0.02f), 300.0f);
|
|
int x0, y0, x1, y1;
|
|
polar(CX, CY, dmod, a, &x0, &y0);
|
|
polar(CX, CY, dmod + 4 + dmod / 5, a, &x1, &y1);
|
|
it.line(x0, y0, x1, y1);
|
|
} else {
|
|
int x, y;
|
|
polar(CX, CY, (float) ((r >> 12) % 300) + (c - DROP) * 0.004f, a, &x, &y);
|
|
it.draw_pixel_at(x, y, COLOR_ON);
|
|
}
|
|
}
|
|
if (!warp) {
|
|
// A planet slides in after dropping out of warp.
|
|
const int px = 600 - (int) std::min<uint32_t>(c - DROP, 4000) / 25;
|
|
it.filled_circle(px, 170, 110, COLOR_OFF);
|
|
it.circle(px, 170, 110);
|
|
for (int y = 60; y < VY + VH; y++)
|
|
for (int x = px - 110; x < px; x++)
|
|
if ((x - px) * (x - px) + (y - 170) * (y - 170) < 108 * 108 && BAYER[y & 3][x & 3] < 3)
|
|
it.draw_pixel_at(x, y, COLOR_ON);
|
|
}
|
|
it.end_clipping();
|
|
const int bob = (t / 600) % 2;
|
|
it.filled_rectangle(CX - 50, CY - 13 + bob, 100, 26, COLOR_OFF);
|
|
sprite(it, SHIP, CX - 48, CY - 12 + bob, 2);
|
|
it.rectangle(VX, VY, VW, VH);
|
|
it.rectangle(VX + 1, VY + 1, VW - 2, VH - 2);
|
|
if (c >= DROP && c < DROP + 120)
|
|
it.invert_rect(VX, VY, VW, VH); // dropping out of warp
|
|
it.print(VX + VW - 8, VY + VH - 6, f.pixel, TextAlign::BOTTOM_RIGHT, warp ? "WARP 6" : "FULL STOP");
|
|
|
|
// Captain's log, written from today's data.
|
|
pill(it, 0, 154, 30, 18);
|
|
it.printf(36, 154, f.small, TextAlign::TOP_LEFT, "CAPTAIN'S LOG, STARDATE %.1f", stardate(now));
|
|
std::string entry = "We are holding position at home.";
|
|
if (!std::isnan(d.temp))
|
|
entry += str(" Outside it is %.0f degrees", d.temp) +
|
|
(d.weather.empty() ? std::string(".") : " and " + lower(weather_text(d.weather)) + ".");
|
|
if (!std::isnan(d.solar))
|
|
entry += " The solar collectors deliver " + watts(d.solar) +
|
|
(std::isnan(d.house) ? std::string(".") : " while the replicators draw " + watts(d.house) + ".");
|
|
const auto events = split(d.agenda, ';');
|
|
entry += events.empty() ? " No away missions are scheduled."
|
|
: " Next mission: " + field(events[0], 2) + ", " + field(events[0], 0) +
|
|
(field(events[0], 1).empty() ? "" : " " + field(events[0], 1)) + ".";
|
|
if (d.printer == "printing")
|
|
entry += " The fabrication bay is busy printing.";
|
|
int y = 178;
|
|
for (const auto &l : wrap(it, f.text, typed(entry, c, 34), 500, 4)) {
|
|
it.print(6, y, f.text, TextAlign::TOP_LEFT, l.c_str());
|
|
y += 19;
|
|
}
|
|
}
|
|
|
|
} // namespace esphome::el512::trek
|