#include "konosuba.h" #include #include #include #include #include "el512_display.h" #include "esphome/core/hal.h" #include "konosuba_sprites.h" namespace esphome::el512::konosuba { 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}}; // --- Drawing helpers ----------------------------------------------------------- // Four-point twinkle. void sparkle(Display &it, int x, int y, int r) { it.horizontal_line(x - r, y, 2 * r + 1); it.vertical_line(x, y - r, 2 * r + 1); if (r >= 3) { it.draw_pixel_at(x - 1, y - 1); it.draw_pixel_at(x + 1, y - 1); it.draw_pixel_at(x - 1, y + 1); it.draw_pixel_at(x + 1, y + 1); } } void diamond(Display &it, int cx, int cy, int r) { it.filled_triangle(cx - r, cy, cx + r, cy, cx, cy - r); it.filled_triangle(cx - r, cy, cx + r, cy, cx, cy + r); } // Disc filled with an ordered dither, level 0 (empty) .. 16 (solid). void dither_disc(Display &it, int cx, int cy, int r, int level) { for (int dy = -r; dy <= r; dy++) { const int span = (int) std::sqrt((float) (r * r - dy * dy)); const int y = cy + dy; for (int x = cx - span; x <= cx + span; x++) if (BAYER[y & 3][x & 3] < level) it.draw_pixel_at(x, y); } } // Double frame with diamond studs in the corners. void ornate_frame(Display &it, int x, int y, int w, int h) { it.rectangle(x, y, w, h); it.rectangle(x + 3, y + 3, w - 6, h - 6); diamond(it, x + 1, y + 1, 5); diamond(it, x + w - 2, y + 1, 5); diamond(it, x + 1, y + h - 2, 5); diamond(it, x + w - 2, y + h - 2, 5); } // JRPG window: rounded outer line, inner line, dark inside. void rpg_window(Display &it, int x, int y, int w, int h) { it.filled_rectangle(x, y, w, h, COLOR_OFF); it.horizontal_line(x + 2, y, w - 4); it.horizontal_line(x + 2, y + h - 1, w - 4); it.vertical_line(x, y + 2, h - 4); it.vertical_line(x + w - 1, y + 2, h - 4); it.draw_pixel_at(x + 1, y + 1); it.draw_pixel_at(x + w - 2, y + 1); it.draw_pixel_at(x + 1, y + h - 2); it.draw_pixel_at(x + w - 2, y + h - 2); it.rectangle(x + 3, y + 3, w - 6, h - 6); } // Paper note with a pin on top and a folded bottom-right corner. void note(Display &it, int x, int y, int w, int h) { constexpr int FOLD = 10; it.filled_rectangle(x, y, w, h, COLOR_OFF); it.horizontal_line(x, y, w); it.vertical_line(x, y, h); it.horizontal_line(x, y + h - 1, w - FOLD); it.vertical_line(x + w - 1, y, h - FOLD); it.line(x + w - FOLD, y + h - 1, x + w - 1, y + h - FOLD); it.vertical_line(x + w - FOLD, y + h - FOLD, FOLD); it.horizontal_line(x + w - FOLD, y + h - FOLD, FOLD); it.filled_circle(x + w / 2, y + 2, 4); it.draw_pixel_at(x + w / 2 - 1, y + 1, COLOR_OFF); } // Horizontal bar, lit up to frac. void bar(Display &it, int x, int y, int w, int h, float frac) { it.rectangle(x, y, w, h); if (!std::isnan(frac)) it.filled_rectangle(x + 2, y + 2, (int) ((w - 4) * std::clamp(frac, 0.0f, 1.0f)), h - 4); } // Dissolve the page in over its first 400 ms. void intro(EL512Display &it, uint32_t t) { if (t < 400) it.dither_mask((int) (t * 16 / 400)); } std::string watts_text(float w) { return std::isnan(w) ? "--" : watts(w); } std::string thousands(int n) { std::string s = std::to_string(n); for (int i = (int) s.size() - 3; i > 0; i -= 3) s.insert(i, ","); return s; } // --- Explosion counter (shared by the Explosion page and the card) --------------- struct Counter { uint32_t count{0}; int day{-1}; uint32_t key{0}; }; Counter counter; void roll_day(ESPTime now) { if (now.is_valid() && now.day_of_year != counter.day) { counter.day = now.day_of_year; counter.count = 0; } } } // namespace uint32_t explosions_today() { return counter.count; } // --- 1. Megumin's daily explosion --------------------------------------------------- void explosion(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime now) { if (f.big == nullptr) return; const uint32_t t = it.page_age_ms(); constexpr uint32_t CHANT = 1600, BLAST = 6600, AFTER = 8800, RATE = 13400, CYCLE = 17000; constexpr int GROUND = 226, CX = 360, CY = 116, R = 90; // magic circle constexpr int BX = 360, BY = 178; // where it hits constexpr int MX = 20, MY = GROUND - 84; // Megumin, 2x constexpr int SX = MX + 52, SY = MY - 14; // staff, 2x constexpr int ORB_X = SX + 9, ORB_Y = SY + 7; const uint32_t cycle = t / CYCLE, c = t % CYCLE; roll_day(now); if (c >= BLAST) { const uint32_t key = hash(it.page_shown_at(), cycle); if (key != counter.key) { counter.key = key; counter.count++; } } const bool blasting = c >= BLAST && c < AFTER; // Night sky, moon, ground. for (int i = 0; i < 46; i++) { const uint32_t r = hash(i, 77); const int x = (int) (r % W), y = 60 + (int) ((r >> 9) % 150); if (x < 110 && y > MY - 20) continue; const uint32_t tw = (t / 170 + (r >> 20)) % 13; if (tw == 0) sparkle(it, x, y, 2); else if (tw < 10) it.draw_pixel_at(x, y, COLOR_ON); } it.filled_circle(488, 70, 12); it.filled_circle(482, 66, 11, COLOR_OFF); it.horizontal_line(0, GROUND, W); for (int i = 0; i < 22; i++) { const int x = (int) (hash(i, 5) % W); it.line(x, GROUND + 4, x - 2, GROUND + 1); it.line(x, GROUND + 4, x + 2, GROUND + 1); } // Title and clock (hidden while the sky is on fire). if (!blasting) { it.print(8, 4, f.jp_big, TextAlign::TOP_LEFT, "爆裂魔法"); it.print(10, 56, f.pixel, TextAlign::TOP_LEFT, "EXPLOSION MAGIC"); if (now.is_valid()) it.strftime(504, 6, f.pixel_l, TextAlign::TOP_RIGHT, "%H:%M", now); } std::string say; if (c < AFTER) { // Megumin on her feet, staff raised. const bool blink = (t % 2900) < 140; sprite(it, blink ? MEGUMIN_BLINK : MEGUMIN, MX, MY, 2); sprite(it, STAFF, SX, SY, 2); } if (c < CHANT) { say = typed("Time for today's explosion!", c, 30); } else if (c < BLAST) { // The magic circle draws itself, ring by ring. const uint32_t e = c - CHANT; auto progress = [e](uint32_t from, uint32_t dur) { return e <= from ? 0.0f : std::min(1.0f, (float) (e - from) / dur); }; const float rot = e / 22.0f; if (progress(0, 1300) > 0) arc(it, CX, CY, R, rot, rot + 360 * progress(0, 1300)); if (progress(350, 1300) > 0) arc(it, CX, CY, R - 14, -rot, -rot - 360 * progress(350, 1300)); if (progress(1100, 1100) > 0) arc(it, CX, CY, 48, rot * 2, rot * 2 + 360 * progress(1100, 1100)); // Hexagram, one line after another. const float hex = progress(1900, 1500) * 6; for (int i = 0; i < 6 && hex > i; i++) { const float a0 = (i % 2) * 60 + (i / 2) * 120 - rot, a1 = a0 + 120; int x0, y0, x1, y1; polar(CX, CY, 48, a0, &x0, &y0); polar(CX, CY, 48, a1, &x1, &y1); const float k = std::min(1.0f, hex - i); it.line(x0, y0, x0 + (int) ((x1 - x0) * k), y0 + (int) ((y1 - y0) * k)); } // Runes between the outer rings. if (e > 2400) { const int shown = std::min(24, (e - 2400) / 40); for (int i = 0; i < shown; i++) { int x, y; polar(CX, CY, R - 7, i * 15 + rot, &x, &y); switch (hash(i, 3) % 3) { case 0: diamond(it, x, y, 2); break; case 1: it.filled_rectangle(x - 1, y - 2, 2, 5); break; default: it.draw_pixel_at(x - 2, y, COLOR_ON); it.draw_pixel_at(x + 2, y, COLOR_ON); it.draw_pixel_at(x, y - 2, COLOR_ON); } } } if (e > 3400) sparkle(it, CX, CY, 3 + (int) ((e / 90) % 4)); // Mana streams into the staff. for (int i = 0; i < 12; i++) { const uint32_t r = hash(i, 9); const float k = (float) ((e + i * 173) % 900) / 900.0f; const int x0 = (int) (r % 220), y0 = 70 + (int) ((r >> 10) % 140); it.filled_rectangle(x0 + (int) ((ORB_X - x0) * k), y0 + (int) ((ORB_Y - y0) * k), 2, 2); } sparkle(it, ORB_X, ORB_Y, 4 + (int) ((e / 120) % 4)); const char *chant[] = {"Crimson flame asleep within my staff...", "drink every last drop of my mana...", "and paint the far hills into a crater!"}; const int line = std::min(2, e / 1650); say = typed(chant[line], e - line * 1650, 34); } else if (c < AFTER) { const uint32_t b = c - BLAST; // Shockwave rings, fireball, mushroom cloud. const int ring = (int) (b * 0.45f); it.circle(BX, BY, ring); it.circle(BX, BY, ring + 1); if (ring > 30) it.circle(BX, BY, ring - 26); if (b < 700) { it.filled_circle(BX, BY, std::min(84, 30 + b / 6)); } else { const uint32_t m = b - 700; const int rise = std::min(70, m / 14); dither_disc(it, BX, BY - rise, 70 - rise / 3, std::max(2, 16 - (int) (m / 110))); it.filled_rectangle(BX - 14, BY - rise, 28, GROUND - (BY - rise), COLOR_ON); dither_disc(it, BX, GROUND - 6, 60 - rise / 2, 10); } // EXPLOSION! over a dark band. it.filled_rectangle(70, 2, 372, 52, COLOR_OFF); it.print(256, 4, f.big, TextAlign::TOP_CENTER, "EXPLOSION!"); if (b < 130) it.invert_rect(0, 0, W, H); // the flash if (b < 1500) { const int amp = std::max(1, 7 - (int) (b / 220)); it.shift_rows(0, H, ((b / 45) & 1) ? amp : -amp); } } else { // Aftermath: smoke drifting off, crater, Megumin flat on the ground. const uint32_t a = c - AFTER; for (int i = 0; i < 3; i++) { const int lvl = std::max(0, 9 - (int) (a / 500) - i); if (lvl > 0) dither_disc(it, BX - 50 + i * 50, BY - 30 - (int) (a / 120) - i * 12, 34 - i * 4, lvl); } it.horizontal_line(BX - 70, GROUND, 140, COLOR_OFF); arc(it, BX, GROUND - 40, 78, 120, 240); sprite(it, MEGUMIN_DOWN, MX - 8, GROUND - 24, 2); if ((t / 400) % 2) it.print(MX + 40, GROUND - 46, f.pixel, TextAlign::TOP_LEFT, "MP 0"); if (c < RATE) { say = typed("...I can't move. Somebody carry me home.", a, 26); } else { // Kazuma scores the explosion, as he does. static const char *const COMMENTS[] = {"Crisp shockwave, great form.", "That smoke ring was pure art.", "The ground shook just right.", "A little early, still lovely.", "Best one this week!"}; const uint32_t r = hash(cycle, it.page_shown_at()); rpg_window(it, 150, 70, 250, 104); it.print(275, 84, f.pixel, TextAlign::TOP_CENTER, "KAZUMA'S RATING"); it.printf(275, 104, f.pixel_l, TextAlign::TOP_CENTER, "%u POINTS", (unsigned) (80 + r % 21)); it.print(275, 136, f.text, TextAlign::TOP_CENTER, COMMENTS[(r >> 8) % 5]); } } // Speech line and the day's tally. if (!say.empty()) it.print(110, 242, f.text, TextAlign::CENTER_LEFT, say.c_str()); if (c >= AFTER) it.printf(504, 248, f.pixel, TextAlign::BASELINE_RIGHT, "TODAY: %u", (unsigned) counter.count); intro(it, t); } // --- 2. Adventurers' Guild quest board --------------------------------------------- void guild_board(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime now) { if (f.title == nullptr) return; const uint32_t t = it.page_age_ms(); ornate_frame(it, 0, 0, W, H); it.print(16, 9, f.title, TextAlign::TOP_LEFT, "ADVENTURERS' GUILD"); it.print(496, 8, f.jp, TextAlign::TOP_RIGHT, "冒険者ギルド"); it.horizontal_line(6, 38, W - 12); it.horizontal_line(6, 40, W - 12); // Quests: the next agenda events, topped up with standing guild requests. struct Quest { std::string title, when; bool from_agenda, today; }; std::vector quests; const std::string today = now.is_valid() ? now.strftime("%a %d") : ""; for (const auto &e : split(d.agenda, ';')) { if (quests.size() == 3) break; const std::string day = field(e, 0), time = field(e, 1); quests.push_back({field(e, 2), upper(day) + (time.empty() ? "" : " " + time), true, day == today}); } static const char *const STANDING[][2] = {{"Exterminate five giant toads", "BEFORE DUSK"}, {"Escort a merchant cart to the next town", "THIS WEEK"}, {"Bring in the cabbage harvest before it flies off", "SEASONAL"}}; for (int i = 0; quests.size() < 3; i++) quests.push_back({STANDING[i][0], STANDING[i][1], false, false}); for (int i = 0; i < 3; i++) { const int dt = (int) t - i * 140; if (dt < 0) continue; const int x = 10 + i * 124, y = 48 - (dt < 300 ? (300 - dt) * 40 / 300 : 0); const Quest &q = quests[i]; const uint32_t r = hash(std::hash{}(q.title)); note(it, x, y, 118, 198); it.print(x + 59, y + 12, f.pixel, TextAlign::TOP_CENTER, "QUEST"); const int rank = 1 + (int) (r % 5); for (int s = 0; s < rank; s++) sparkle(it, x + 59 - (rank - 1) * 6 + s * 12, y + 30, 3); int ly = y + 40; for (const auto &l : wrap(it, f.text, q.title, 104, 5)) { it.print(x + 7, ly, f.text, TextAlign::TOP_LEFT, l.c_str()); ly += 18; } it.horizontal_line(x + 6, y + 136, 106); it.print(x + 7, y + 142, f.pixel, TextAlign::TOP_LEFT, "DEADLINE"); it.print(x + 7, y + 154, f.pixel, TextAlign::TOP_LEFT, fit(f.pixel, q.when, 104).c_str()); it.print(x + 7, y + 168, f.pixel, TextAlign::TOP_LEFT, "REWARD"); it.printf(x + 7, y + 180, f.pixel, TextAlign::TOP_LEFT, "%s ERIS", thousands((1 + (int) ((r >> 8) % 40)) * 5000).c_str()); if (q.today && (t / 500) % 2) { it.filled_rectangle(x + 76, y + 6, 38, 14); it.print(x + 95, y + 13, f.pixel, COLOR_OFF, TextAlign::CENTER, "NEW!"); } } // Hazard notice (weather) and the cabbage bounty (solar). note(it, 386, 48, 116, 94); it.print(444, 60, f.pixel, TextAlign::TOP_CENTER, "HAZARD"); it.print(416, 98, f.weather, TextAlign::CENTER, weather_icon(d.weather)); it.print(492, 94, f.text, TextAlign::BASELINE_RIGHT, std::isnan(d.temp) ? "--" : str("%.0f°", d.temp).c_str()); it.print(444, 131, f.pixel, TextAlign::TOP_CENTER, fit(f.pixel, upper(weather_text(d.weather)), 100).c_str()); note(it, 386, 150, 116, 94); it.print(444, 162, f.pixel, TextAlign::TOP_CENTER, "BOUNTY"); it.print(444, 176, f.text, TextAlign::TOP_CENTER, "Cabbage crop"); it.print(444, 198, f.text, TextAlign::TOP_CENTER, std::isnan(d.solar_today) ? "--" : str("%.1f kWh", d.solar_today / 1000.0f).c_str()); it.printf(444, 222, f.pixel, TextAlign::TOP_CENTER, "NOW %s", upper(watts_text(d.solar)).c_str()); // Cabbages escaping across the board. for (int i = 0; i < 3; i++) { const int x = (int) ((t / 14 + i * 233) % 760) - 60; const int y = 60 + i * 62 + (int) (12 * std::sin(t / 260.0f + i * 2)); const Sprite &s = ((t / 140 + i) % 2) ? CABBAGE_A : CABBAGE_B; it.filled_rectangle(x - 1, y - 1, s.w * 2 + 2, s.h * 2 + 2, COLOR_OFF); sprite(it, s, x, y, 2); } intro(it, t); } // --- 3. Megumin's adventurer card ---------------------------------------------------- void adventurer_card(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime now) { if (f.title == nullptr) return; const uint32_t t = it.page_age_ms(); roll_day(now); const int X = 14, Y = 6 + (t < 380 ? (int) (380 - t) * 140 / 380 : 0), CW = 484, CH = 244; // Card with cut corners and a header band. constexpr int CUT = 12; it.line(X + CUT, Y, X + CW - 1 - CUT, Y); it.line(X + CW - 1 - CUT, Y, X + CW - 1, Y + CUT); it.line(X + CW - 1, Y + CUT, X + CW - 1, Y + CH - 1 - CUT); it.line(X + CW - 1, Y + CH - 1 - CUT, X + CW - 1 - CUT, Y + CH - 1); it.line(X + CW - 1 - CUT, Y + CH - 1, X + CUT, Y + CH - 1); it.line(X + CUT, Y + CH - 1, X, Y + CH - 1 - CUT); it.line(X, Y + CH - 1 - CUT, X, Y + CUT); it.line(X, Y + CUT, X + CUT, Y); it.filled_rectangle(X + 8, Y + 6, CW - 16, 30); it.print(X + 18, Y + 21, f.title, COLOR_OFF, TextAlign::CENTER_LEFT, "ADVENTURER CARD"); it.print(X + CW - 18, Y + 20, f.jp, COLOR_OFF, TextAlign::CENTER_RIGHT, "冒険者カード"); // Portrait. it.rectangle(X + 14, Y + 44, 98, 94); it.rectangle(X + 16, Y + 46, 94, 90); const bool blink = (t % 3300) < 150; sprite(it, blink ? MEGUMIN_BLINK : MEGUMIN, X + 21, Y + 52, 3, 27); it.print(X + 63, Y + 146, f.pixel, TextAlign::TOP_CENTER, "NO. 0429"); it.print(X + 63, Y + 176, f.pixel, TextAlign::TOP_CENTER, "EXPLOSIONS"); it.print(X + 63, Y + 188, f.pixel, TextAlign::TOP_CENTER, "TODAY"); it.printf(X + 63, Y + 204, f.pixel_l, TextAlign::TOP_CENTER, "%u", (unsigned) counter.count); // Identity. const int x0 = X + 130, xv = X + 210; const int level = 10 + (std::isnan(d.solar_today) ? 0 : (int) (d.solar_today / 1000.0f)); it.print(x0, Y + 50, f.pixel, TextAlign::TOP_LEFT, "NAME"); it.print(xv, Y + 46, f.pixel_l, TextAlign::TOP_LEFT, "MEGUMIN"); it.print(x0, Y + 74, f.pixel, TextAlign::TOP_LEFT, "CLASS"); it.print(xv, Y + 70, f.pixel_l, TextAlign::TOP_LEFT, "ARCH WIZARD"); it.print(x0, Y + 98, f.pixel, TextAlign::TOP_LEFT, "LEVEL"); it.printf(xv, Y + 94, f.pixel_l, TextAlign::TOP_LEFT, "%d", level); it.print(x0, Y + 118, f.pixel, TextAlign::TOP_LEFT, "SKILL"); it.print(xv, Y + 118, f.pixel, TextAlign::TOP_LEFT, "EXPLOSION LV.MAX"); // Stats: mana is the sun, stamina what the house leaves over. const int luck = now.is_valid() ? 1 + (int) (hash(now.day_of_year, now.year) % 99) : 50; const float load = std::isnan(d.house) ? NAN : 1.0f - d.house / 5000.0f; struct Stat { const char *name; float frac; std::string value; }; const Stat stats[] = { {"MANA", d.solar / 5000.0f, upper(watts_text(d.solar))}, {"STAMINA", load, std::isnan(load) ? "--" : str("%d%%", (int) (std::clamp(load, 0.0f, 1.0f) * 100))}, {"INT", 0.99f, "99"}, {"LUCK", luck / 100.0f, str("%d", luck)}, {"STR", 0.03f, "3"}, }; for (int i = 0; i < 5; i++) { const int y = Y + 140 + i * 19; it.print(x0, y, f.pixel, TextAlign::TOP_LEFT, stats[i].name); bar(it, x0 + 70, y - 1, 200, 10, stats[i].frac); it.print(X + CW - 16, y, f.pixel, TextAlign::TOP_RIGHT, stats[i].value.c_str()); } // A glint sweeps over the card now and then; corners twinkle. const uint32_t s = t % 5200; if (s < 800 && t > 600) { const int bx = X - 130 + (int) (s * (CW + 260) / 800); for (int y = Y + 1; y < Y + CH - 1; y++) { const int x = std::max(X + 1, bx - (y - Y) / 2); const int x1 = std::min(X + CW - 1, bx - (y - Y) / 2 + 12); if (x1 > x) it.invert_rect(x, y, x1 - x, 1); } } const int tw = (int) ((t / 220) % 12); if (tw < 3) sparkle(it, X + CW - 30, Y + 48, 2 + tw); else if (tw >= 6 && tw < 9) sparkle(it, X + 120, Y + CH - 14, tw - 4); } // --- 4. Party battle against a giant toad -------------------------------------------- void party_battle(EL512Display &it, const Fonts &f, const HomeData &d) { if (f.pixel == nullptr) return; const uint32_t t = it.page_age_ms(); constexpr uint32_t TURN = 4200; constexpr int TURNS = 5, GROUND = 132; const uint32_t round = t / (TURN * TURNS); const int turn = (int) ((t / TURN) % TURNS); const uint32_t tt = t % TURN; enum { KAZUMA_STEALS, AQUA_PURIFIES, TOAD_LICKS, TOAD_SWALLOWS, MEGUMIN_CASTS }; // Scenery. it.print(8, 6, f.jp, TextAlign::TOP_LEFT, "戦闘"); it.printf(56, 12, f.pixel, TextAlign::TOP_LEFT, "ROUND %u", (unsigned) (round + 1)); // A hill between the party and the toad, clear of both. arc(it, 270, GROUND + 50, 80, -46, 46); it.horizontal_line(0, GROUND, W); for (int i = 0; i < 18; i++) { const int x = (int) (hash(i, 21) % W); it.line(x, GROUND + 4, x - 2, GROUND + 1); it.line(x, GROUND + 4, x + 2, GROUND + 1); } // Who has fallen or been eaten at this moment. const bool toad_down = turn == MEGUMIN_CASTS && tt > 1700; const bool aqua_eaten = (turn == TOAD_SWALLOWS && tt > 1100) || (turn == MEGUMIN_CASTS && !toad_down); const bool megumin_down = turn == MEGUMIN_CASTS && tt > 2300; // The party. const Sprite *party[4] = {&KAZUMA, &AQUA, &MEGUMIN_S, &DARKNESS}; const int actor = turn == KAZUMA_STEALS ? 0 : turn == AQUA_PURIFIES ? 1 : turn == MEGUMIN_CASTS ? 2 : -1; for (int i = 0; i < 4; i++) { int x = 22 + i * 46; const Sprite &s = *party[i]; if (i == 1 && aqua_eaten) continue; if (i == 3 && turn == TOAD_LICKS && tt > 1100 && tt < 2000) x += ((tt / 60) & 1) ? 3 : -3; // Darkness takes the hit if (i == 2 && megumin_down) { sprite(it, MEGUMIN_DOWN, x - 16, GROUND - 12); continue; } sprite(it, s, x, GROUND - s.h * 2, 2); if (i == actor && (t / 300) % 2) it.filled_triangle(x + 10, GROUND - s.h * 2 - 12, x + 22, GROUND - s.h * 2 - 12, x + 16, GROUND - s.h * 2 - 6); } // The toad. constexpr int TX = 336, TS = 3; const int ty = GROUND - TOAD.h * TS - ((t / 450) % 2 ? 2 : 0); const int target_x = turn == TOAD_LICKS ? 22 + 3 * 46 + 30 : 22 + 46 + 30; const bool tongue = (turn == TOAD_LICKS && tt > 800 && tt < 1900) || (turn == TOAD_SWALLOWS && tt > 600 && tt < 1100); if (!toad_down) { sprite(it, tongue ? TOAD_TONGUE : TOAD, TX, ty, TS); if (tongue) { const int mx = TX + 5 * TS, my = ty + 12 * TS; it.line(mx, my, target_x, GROUND - 20); it.line(mx, my + 1, target_x, GROUND - 19); it.filled_circle(target_x, GROUND - 20, 4); } float hp = turn == KAZUMA_STEALS && tt < 1200 ? 1.0f : 0.92f; if (turn == MEGUMIN_CASTS && tt > 1300) hp = 0.0f; it.print(TX, 30, f.pixel, TextAlign::TOP_LEFT, "GIANT TOAD"); bar(it, TX, 42, 96, 8, hp); } else if (tt < 2600) { dither_disc(it, TX + 48, GROUND - 26, 40, std::max(1, 14 - (int) ((tt - 1700) / 70))); } // Turn effects. if (turn == KAZUMA_STEALS && tt > 900 && tt < 1800) { const float k = (tt - 900) / 900.0f; const int x = TX + 20 + (int) ((22 + 16 - TX - 20) * k), y = GROUND - 60 - (int) (30 * std::sin(k * 3.14159f)); it.filled_rectangle(x - 3, y - 3, 7, 7); sparkle(it, x, y, 5); } if (turn == AQUA_PURIFIES && tt > 700 && tt < 2600) { for (int i = 0; i < 7; i++) { const int x = 22 + 46 + (int) (hash(i, 31) % 34); const int y = GROUND - 10 - (int) (((tt - 700) / 12 + i * 23) % 60); sparkle(it, x, y, 1 + (int) ((tt / 100 + i) % 3)); } } if (turn == MEGUMIN_CASTS) { if (tt > 300 && tt < 1300) { const float rot = tt / 6.0f; arc(it, TX + 48, 60, 34, rot, rot + 360 * std::min(1.0f, (tt - 300) / 500.0f)); if (tt > 700) arc(it, TX + 48, 60, 22, -rot, -rot + 360); } if (tt >= 1300 && tt < 1800) it.filled_circle(TX + 48, GROUND - 30, 30 + (int) (tt - 1300) / 6); if (tt >= 1300 && tt < 2000) it.circle(TX + 48, GROUND - 30, (int) (tt - 1300) / 2); if (tt > 2600 && (t / 250) % 2) it.print(TX + 48, 70, f.pixel_l, TextAlign::TOP_CENTER, "VICTORY!"); } // Party status window. rpg_window(it, 0, 150, 300, 106); struct Member { const char *name; float hp, mp; }; const bool cast = turn == MEGUMIN_CASTS && tt > 1300; const bool licked = turn >= TOAD_LICKS && !(turn == TOAD_LICKS && tt < 1100); const Member members[4] = { {"KAZUMA", 0.8f, turn == KAZUMA_STEALS && tt < 900 ? 0.5f : 0.4f}, {"AQUA", aqua_eaten || (turn == MEGUMIN_CASTS) ? 0.55f : 1.0f, turn >= AQUA_PURIFIES ? 0.85f : 1.0f}, {"MEGUMIN", 0.7f, cast ? 0.0f : 1.0f}, {"DARKNESS", licked ? 0.9f : 1.0f, 0.0f}, }; for (int i = 0; i < 4; i++) { const int y = 160 + i * 23; if (i == actor) it.filled_triangle(9, y, 9, y + 8, 14, y + 4); it.print(18, y, f.pixel, TextAlign::TOP_LEFT, members[i].name); it.print(96, y, f.pixel, TextAlign::TOP_LEFT, "HP"); bar(it, 116, y - 1, 64, 10, members[i].hp); it.print(190, y, f.pixel, TextAlign::TOP_LEFT, "MP"); bar(it, 210, y - 1, 78, 10, members[i].mp); } // Battle log, typed out. rpg_window(it, 304, 150, 208, 106); std::string msg; switch (turn) { case KAZUMA_STEALS: msg = std::isnan(d.grid) ? "Kazuma used STEAL! He got... a pair of socks." : d.grid >= 0 ? "Kazuma used STEAL! He took " + watts(d.grid) + " from the grid." : "Kazuma used STEAL! Wait, he gave " + watts(-d.grid) + " back to the grid?"; break; case AQUA_PURIFIES: { const Reading &room = d.rooms[0]; msg = std::isnan(room.value) ? "Aqua used PURIFICATION! ...It had no effect." : str("Aqua used PURIFICATION on the %s. It is now %.1fC. Not her doing.", room.name.c_str(), room.value); break; } case TOAD_LICKS: msg = "The toad's tongue lashed out! Darkness took " + watts_text(d.house) + " of house load and looks... delighted?"; break; case TOAD_SWALLOWS: msg = "The giant toad swallowed Aqua whole! Muffled crying can be heard."; break; default: if (tt < 2300) { const int dmg = std::isnan(d.solar) || d.solar < 1 ? 9999 : (int) d.solar; msg = "Megumin cast EXPLOSION! " + thousands(dmg) + " damage from the sun!"; } else { msg = "VICTORY! The toad is gone. Megumin can't move anymore."; } } const uint32_t since = turn == MEGUMIN_CASTS && tt >= 2300 ? tt - 2300 : tt; int ly = 162; for (const auto &l : wrap(it, f.pixel, typed(msg, since, 40), 186, 7)) { it.print(314, ly, f.pixel, TextAlign::TOP_LEFT, l.c_str()); ly += 12; } // Explosion flash and shake. if (turn == MEGUMIN_CASTS && tt >= 1300 && tt < 1420) it.invert_rect(0, 0, W, 150); if (turn == MEGUMIN_CASTS && tt >= 1300 && tt < 2100) it.shift_rows(0, 148, ((tt / 45) & 1) ? 4 : -4); intro(it, t); } } // namespace esphome::el512::konosuba