424 lines
11 KiB
C++
424 lines
11 KiB
C++
#include "Aquarium.h"
|
|
#include "../core/Config.h"
|
|
#include "../entities/Bubble.h"
|
|
#include "../entities/Castle.h"
|
|
#include "../entities/Fish.h"
|
|
#include "../entities/SeaMonster.h"
|
|
#include "../entities/Seaweed.h"
|
|
#include "../entities/Ship.h"
|
|
#include "../entities/Waterline.h"
|
|
#include "../entities/Whale.h"
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <iomanip>
|
|
#include <iostream>
|
|
#include <signal.h>
|
|
#include <sstream>
|
|
#include <sys/ioctl.h>
|
|
#include <termios.h>
|
|
#include <unistd.h>
|
|
|
|
// ANSI color codes
|
|
namespace ANSI {
|
|
const char *RESET = "\033[0m";
|
|
const char *BOLD = "\033[1m";
|
|
const char *CLEAR_SCREEN = "\033[2J";
|
|
const char *CURSOR_HOME = "\033[H";
|
|
const char *HIDE_CURSOR = "\033[?25l";
|
|
const char *SHOW_CURSOR = "\033[?25h";
|
|
|
|
// Colors (foreground)
|
|
const char *BLACK = "\033[30m";
|
|
const char *RED = "\033[31m";
|
|
const char *GREEN = "\033[32m";
|
|
const char *YELLOW = "\033[33m";
|
|
const char *BLUE = "\033[34m";
|
|
const char *MAGENTA = "\033[35m";
|
|
const char *CYAN = "\033[36m";
|
|
const char *WHITE = "\033[37m";
|
|
// Colors (background)
|
|
const char *BG_BLACK = "\033[40m";
|
|
const char *RESET_BLACK_BG = "\033[0;40m";
|
|
|
|
// Move cursor to position
|
|
std::string moveTo(int row, int col) {
|
|
char buffer[32];
|
|
snprintf(buffer, sizeof(buffer), "\033[%d;%dH", row + 1, col + 1);
|
|
return std::string(buffer);
|
|
}
|
|
} // namespace ANSI
|
|
|
|
// Global terminal state
|
|
static struct termios original_termios;
|
|
static bool termios_saved = false;
|
|
|
|
// Signal handler for cleanup
|
|
void cleanup_terminal(int sig) {
|
|
if (termios_saved) {
|
|
tcsetattr(STDIN_FILENO, TCSANOW, &original_termios);
|
|
}
|
|
printf("%s%s", ANSI::SHOW_CURSOR, ANSI::RESET);
|
|
fflush(stdout);
|
|
if (sig != 0) {
|
|
exit(sig);
|
|
}
|
|
}
|
|
|
|
Aquarium::Aquarium() {
|
|
// Save original terminal settings
|
|
if (tcgetattr(STDIN_FILENO, &original_termios) == 0) {
|
|
termios_saved = true;
|
|
}
|
|
|
|
// Set up signal handlers for cleanup
|
|
signal(SIGINT, cleanup_terminal);
|
|
signal(SIGTERM, cleanup_terminal);
|
|
signal(SIGQUIT, cleanup_terminal);
|
|
|
|
// Set terminal to raw mode
|
|
struct termios raw = original_termios;
|
|
raw.c_lflag &= ~(ECHO | ICANON);
|
|
raw.c_iflag &= ~(IXON | ICRNL);
|
|
raw.c_oflag &= ~(OPOST);
|
|
raw.c_cc[VMIN] = 0; // Non-blocking read
|
|
raw.c_cc[VTIME] = 1; // 100ms timeout
|
|
|
|
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
|
|
|
|
// Initialize display
|
|
printf("%s%s%s%s", ANSI::CLEAR_SCREEN, ANSI::CURSOR_HOME, ANSI::HIDE_CURSOR,
|
|
ANSI::BG_BLACK);
|
|
fflush(stdout);
|
|
|
|
// Get terminal size
|
|
getTerminalSize();
|
|
|
|
currentFrame.assign(height, std::vector<Cell>(width));
|
|
previousFrame.assign(height, std::vector<Cell>(width));
|
|
|
|
if (!colorLookupInitialized) {
|
|
initColorLookup();
|
|
colorLookupInitialized = true;
|
|
}
|
|
}
|
|
|
|
void Aquarium::getTerminalSize() {
|
|
struct winsize ws;
|
|
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) {
|
|
height = ws.ws_row;
|
|
width = ws.ws_col;
|
|
} else {
|
|
cleanup_terminal(0);
|
|
std::cerr << "Error: Unable to determine terminal size.\n";
|
|
std::exit(1);
|
|
}
|
|
}
|
|
|
|
void Aquarium::ensureEntitiesSorted() {
|
|
if (entities_need_sorting) {
|
|
std::sort(entities.begin(), entities.end(),
|
|
[](const auto &a, const auto &b) {
|
|
int layerA = a->getPreferredLayer();
|
|
int layerB = b->getPreferredLayer();
|
|
if (layerA != layerB)
|
|
return layerA < layerB;
|
|
return a->getId() < b->getId();
|
|
});
|
|
entities_need_sorting = false;
|
|
}
|
|
}
|
|
|
|
void Aquarium::redraw() {
|
|
clearCurrentFrame();
|
|
|
|
if (g_config.enable_big_entities) {
|
|
ensureBigEntityExists();
|
|
}
|
|
|
|
static std::vector<std::unique_ptr<Entity>> newEntities;
|
|
static std::vector<size_t> entitiesToRemove;
|
|
|
|
newEntities.clear();
|
|
entitiesToRemove.clear();
|
|
|
|
// Count current bubbles
|
|
int bubble_count = 0;
|
|
for (const auto &entity : entities) {
|
|
if (dynamic_cast<Bubble *>(entity.get())) {
|
|
bubble_count++;
|
|
}
|
|
}
|
|
|
|
// Update all entities and collect changes
|
|
for (size_t i = 0; i < entities.size(); ++i) {
|
|
auto &entity = entities[i];
|
|
entity->update();
|
|
|
|
// Handle fish bubble spawning with configuration
|
|
if (g_config.enable_bubbles && bubble_count < g_config.max_bubbles) {
|
|
if (auto *fish = dynamic_cast<Fish *>(entity.get())) {
|
|
if (fish->shouldSpawnBubble()) {
|
|
// Use configured bubble rate
|
|
if (rand() % g_config.fish_bubble_rate == 0) {
|
|
newEntities.emplace_back(
|
|
std::make_unique<Bubble>(fish->getX(), fish->getY()));
|
|
bubble_count++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (entity->shouldBeRemoved()) {
|
|
auto replacement = entity->createReplacement();
|
|
if (replacement) {
|
|
entity = std::move(replacement);
|
|
entities_need_sorting = true;
|
|
} else {
|
|
entitiesToRemove.push_back(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove entities in reverse order to maintain indices
|
|
for (auto it = entitiesToRemove.rbegin(); it != entitiesToRemove.rend();
|
|
++it) {
|
|
entities.erase(entities.begin() + *it);
|
|
entities_need_sorting = true;
|
|
}
|
|
|
|
// Add new entities if we have them and haven't exceeded max
|
|
if (!newEntities.empty() &&
|
|
entities.size() < static_cast<size_t>(g_config.max_entities)) {
|
|
entities.reserve(entities.size() + newEntities.size());
|
|
for (auto &newEntity : newEntities) {
|
|
entities.emplace_back(std::move(newEntity));
|
|
}
|
|
entities_need_sorting = true;
|
|
}
|
|
|
|
ensureEntitiesSorted();
|
|
|
|
// Draw all entities
|
|
for (const auto &entity : entities) {
|
|
entity->draw();
|
|
}
|
|
|
|
// Draw debug information
|
|
if (g_config.show_fps || g_config.show_entity_count) {
|
|
drawDebugInfo();
|
|
}
|
|
|
|
renderToScreen();
|
|
}
|
|
|
|
void Aquarium::drawDebugInfo() {
|
|
static double last_fps = 0.0;
|
|
static int frame_counter = 0;
|
|
static auto last_time = std::chrono::steady_clock::now();
|
|
|
|
frame_counter++;
|
|
auto current_time = std::chrono::steady_clock::now();
|
|
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
current_time - last_time)
|
|
.count();
|
|
|
|
if (elapsed >= 1000) {
|
|
last_fps = (frame_counter * 1000.0) / elapsed;
|
|
frame_counter = 0;
|
|
last_time = current_time;
|
|
}
|
|
|
|
std::stringstream debug_info;
|
|
if (g_config.show_fps) {
|
|
debug_info << "FPS: " << std::fixed << std::setprecision(1) << last_fps;
|
|
}
|
|
if (g_config.show_entity_count) {
|
|
if (g_config.show_fps)
|
|
debug_info << " | ";
|
|
debug_info << "Entities: " << entities.size();
|
|
}
|
|
|
|
std::string debug_str = debug_info.str();
|
|
if (!debug_str.empty()) {
|
|
std::string color_str(debug_str.length(), 'w'); // White color
|
|
drawToFrame(0, width - static_cast<int>(debug_str.length()), debug_str,
|
|
color_str);
|
|
}
|
|
}
|
|
|
|
void Aquarium::resize() {
|
|
printf("%s%s%s", ANSI::CLEAR_SCREEN, ANSI::CURSOR_HOME, ANSI::BG_BLACK);
|
|
fflush(stdout);
|
|
|
|
getTerminalSize();
|
|
|
|
currentFrame.assign(height, std::vector<Cell>(width));
|
|
previousFrame.assign(height, std::vector<Cell>(width));
|
|
|
|
entities.clear();
|
|
entities_need_sorting = true;
|
|
|
|
addWaterline();
|
|
addCastle();
|
|
|
|
// Use configured seaweed count or auto-calculate
|
|
int seaweed_count = g_config.initial_seaweed_count;
|
|
if (seaweed_count == -1) {
|
|
seaweed_count = width / 15;
|
|
}
|
|
for (int i = 0; i < seaweed_count; i++) {
|
|
addSeaweed();
|
|
}
|
|
|
|
// Use configured fish count or auto-calculate
|
|
int fish_count = g_config.initial_fish_count;
|
|
if (fish_count == -1) {
|
|
fish_count = width * (height - 9) / 350;
|
|
}
|
|
for (int i = 0; i < fish_count; i++) {
|
|
addFish();
|
|
}
|
|
}
|
|
|
|
void Aquarium::addFish() { addEntityImpl<Fish>(); }
|
|
void Aquarium::addBubble(float x, float y) { addEntityImpl<Bubble>(x, y); }
|
|
void Aquarium::addSeaweed() { addEntityImpl<Seaweed>(); }
|
|
void Aquarium::addWaterline() { addEntityImpl<Waterline>(); }
|
|
void Aquarium::addCastle() { addEntityImpl<Castle>(); }
|
|
void Aquarium::addShip() { addEntityImpl<Ship>(); }
|
|
void Aquarium::addSeaMonster() { addEntityImpl<SeaMonster>(); }
|
|
void Aquarium::addWhale() { addEntityImpl<Whale>(); }
|
|
|
|
void Aquarium::ensureBigEntityExists() {
|
|
// Check if any big entities exist on screen
|
|
for (const auto &entity : entities) {
|
|
if (dynamic_cast<Ship *>(entity.get()) ||
|
|
dynamic_cast<SeaMonster *>(entity.get()) ||
|
|
dynamic_cast<Whale *>(entity.get())) {
|
|
return; // Big entity found, do nothing
|
|
}
|
|
}
|
|
|
|
// No big entity found, spawn next in cycle
|
|
int entity_type = big_entity_index % 3;
|
|
if (entity_type == 0) {
|
|
addEntityImpl<Ship>();
|
|
} else if (entity_type == 1) {
|
|
addEntityImpl<SeaMonster>();
|
|
} else {
|
|
addEntityImpl<Whale>();
|
|
}
|
|
++big_entity_index;
|
|
}
|
|
|
|
void Aquarium::clearCurrentFrame() {
|
|
static const Cell empty_cell{};
|
|
|
|
for (auto &row : currentFrame) {
|
|
row.assign(width, empty_cell);
|
|
}
|
|
}
|
|
|
|
void Aquarium::drawToFrame(int y, int x, const std::string &line,
|
|
const std::string &colorLine) {
|
|
const size_t len = std::min(line.size(), colorLine.size());
|
|
|
|
for (size_t j = 0; j < len; ++j) {
|
|
int cx = x + static_cast<int>(j);
|
|
if (cx < 0 || cx >= width)
|
|
continue;
|
|
|
|
const char ch = line[j];
|
|
const char colorChar = colorLine[j];
|
|
const bool isBold = (colorChar >= 'A' && colorChar <= 'Z');
|
|
|
|
currentFrame[y][cx] = {
|
|
ch, static_cast<char>(isBold ? colorChar + 32 : colorChar), isBold};
|
|
}
|
|
}
|
|
|
|
void Aquarium::initColorLookup() {
|
|
for (int i = 0; i < 256; ++i)
|
|
colorLookup[i] = ANSI::BLACK; // Default black
|
|
|
|
colorLookup['r'] = ANSI::RED;
|
|
colorLookup['g'] = ANSI::GREEN;
|
|
colorLookup['y'] = ANSI::YELLOW;
|
|
colorLookup['b'] = ANSI::BLUE;
|
|
colorLookup['m'] = ANSI::MAGENTA;
|
|
colorLookup['c'] = ANSI::CYAN;
|
|
colorLookup['w'] = ANSI::WHITE;
|
|
colorLookup['k'] = ANSI::BLACK;
|
|
}
|
|
|
|
void Aquarium::renderToScreen() {
|
|
static std::string output;
|
|
output.clear();
|
|
output.reserve(height * width * 20);
|
|
|
|
int cursor_y = -1, cursor_x = -1;
|
|
|
|
for (int y = 0; y < height; ++y) {
|
|
for (int x = 0; x < width; ++x) {
|
|
const Cell &newCell = currentFrame[y][x];
|
|
Cell &oldCell = previousFrame[y][x];
|
|
|
|
if (newCell == oldCell)
|
|
continue;
|
|
|
|
oldCell = newCell;
|
|
|
|
// Move cursor only when needed
|
|
if (cursor_y != y || cursor_x != x) {
|
|
output += ANSI::moveTo(y, x);
|
|
cursor_y = y;
|
|
cursor_x = x;
|
|
}
|
|
|
|
// Apply cell formatting and character
|
|
output += ANSI::RESET_BLACK_BG;
|
|
|
|
// Only apply bold if configured
|
|
if (g_config.use_bold && newCell.bold) {
|
|
output += ANSI::BOLD;
|
|
}
|
|
|
|
// Only apply colors if configured
|
|
if (g_config.use_colors) {
|
|
output += colorLookup[static_cast<unsigned char>(newCell.colorChar)];
|
|
}
|
|
|
|
output += newCell.ch;
|
|
++cursor_x;
|
|
}
|
|
}
|
|
|
|
if (!output.empty()) {
|
|
std::cout << output << std::flush;
|
|
}
|
|
}
|
|
|
|
// Check for input (non-blocking)
|
|
int Aquarium::checkInput() {
|
|
char c;
|
|
if (read(STDIN_FILENO, &c, 1) == 1) {
|
|
return c;
|
|
}
|
|
return -1; // No input available
|
|
}
|
|
|
|
// Check if terminal was resized
|
|
bool Aquarium::checkResize() {
|
|
struct winsize ws;
|
|
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) {
|
|
if (ws.ws_row != height || ws.ws_col != width) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Aquarium::~Aquarium() { cleanup_terminal(0); }
|