fissh

termios terminal aquarium. demo at ssh://fish@kloet.net
Download | Log | Files | Refs

commit 104834099f4f2199f59661287e6be9460e7a976d
parent 6976525c1fdb46b0bc6e3031ae29d759bed3654c
Author: amrfti <andrew@kloet.net>
Date:   Sat, 12 Jul 2025 17:33:17 -0400

add config refactor structure

Diffstat:
MMakefile | 21++++++++++++---------
Dsrc/Aquarium.cpp | 340-------------------------------------------------------------------------------
Dsrc/Aquarium.h | 74--------------------------------------------------------------------------
Dsrc/Castle.cpp | 8--------
Dsrc/Entity.cpp | 63---------------------------------------------------------------
Dsrc/Fish.cpp | 63---------------------------------------------------------------
Dsrc/Fish.h | 34----------------------------------
Dsrc/SeaMonster.cpp | 44--------------------------------------------
Dsrc/SeaMonster.h | 32--------------------------------
Dsrc/Seaweed.cpp | 63---------------------------------------------------------------
Dsrc/Ship.cpp | 25-------------------------
Dsrc/Ship.h | 25-------------------------
Dsrc/Waterline.cpp | 83-------------------------------------------------------------------------------
Dsrc/Whale.cpp | 48------------------------------------------------
Dsrc/Whale.h | 31-------------------------------
Msrc/assets/FishAssets.h | 4++--
Msrc/assets/SeaMonsterAssets.h | 4++--
Msrc/assets/ShipAssets.h | 4++--
Msrc/assets/WhaleAssets.h | 4++--
Asrc/core/Aquarium.cpp | 423+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/core/Aquarium.h | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/core/Config.cpp | 175+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/core/Config.h | 33+++++++++++++++++++++++++++++++++
Asrc/core/main.cpp | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rsrc/Bubble.cpp -> src/entities/Bubble.cpp | 0
Rsrc/Bubble.h -> src/entities/Bubble.h | 0
Asrc/entities/Castle.cpp | 8++++++++
Rsrc/Castle.h -> src/entities/Castle.h | 0
Asrc/entities/Entity.cpp | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rsrc/Entity.h -> src/entities/Entity.h | 0
Asrc/entities/Fish.cpp | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/entities/Fish.h | 34++++++++++++++++++++++++++++++++++
Asrc/entities/SeaMonster.cpp | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/entities/SeaMonster.h | 32++++++++++++++++++++++++++++++++
Asrc/entities/Seaweed.cpp | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rsrc/Seaweed.h -> src/entities/Seaweed.h | 0
Asrc/entities/Ship.cpp | 25+++++++++++++++++++++++++
Asrc/entities/Ship.h | 25+++++++++++++++++++++++++
Asrc/entities/Waterline.cpp | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rsrc/Waterline.h -> src/entities/Waterline.h | 0
Asrc/entities/Whale.cpp | 48++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/entities/Whale.h | 31+++++++++++++++++++++++++++++++
Dsrc/main.cpp | 34----------------------------------
Rsrc/Random.h -> src/utils/Random.h | 0
Rsrc/SpriteUtils.h -> src/utils/SpriteUtils.h | 0
Rsrc/defs.h -> src/utils/defs.h | 0
46 files changed, 1360 insertions(+), 984 deletions(-)

diff --git a/Makefile b/Makefile @@ -1,27 +1,30 @@ CXX = c++ -CXXFLAGS = -Wall -Wextra -O3 +CXXFLAGS = -Wall -Wextra -O3 -Isrc LDFLAGS = -static - SRC_DIR = src OBJ_DIR = build BIN_DIR = bin - SRC_EXT = cpp OBJ_EXT = o -SOURCES = $(wildcard $(SRC_DIR)/*.$(SRC_EXT)) +# Find all source files recursively in subdirectories +SOURCES = $(shell find $(SRC_DIR) -name '*.$(SRC_EXT)') +# Create corresponding object files maintaining directory structure OBJECTS = $(SOURCES:$(SRC_DIR)/%.$(SRC_EXT)=$(OBJ_DIR)/%.$(OBJ_EXT)) - EXEC = $(BIN_DIR)/fissh +# Get all unique subdirectories for build structure +OBJ_DIRS = $(sort $(dir $(OBJECTS))) + all: $(EXEC) $(EXEC): $(OBJECTS) - @mkdir -p $(BIN_DIR) # Make sure the bin dir exists + @mkdir -p $(BIN_DIR) $(CXX) $(OBJECTS) -o $(EXEC) $(LDFLAGS) -$(OBJ_DIR)/%.o: $(SRC_DIR)/%.$(SRC_EXT) - @mkdir -p $(OBJ_DIR) # Make sure the obj dir exists +# Create object files, maintaining directory structure +$(OBJ_DIR)/%.$(OBJ_EXT): $(SRC_DIR)/%.$(SRC_EXT) + @mkdir -p $(dir $@) $(CXX) $(CXXFLAGS) -c $< -o $@ clean: @@ -30,4 +33,4 @@ clean: install: $(EXEC) install -m 755 $(EXEC) /usr/local/bin/ -.PHONY: all clean run +.PHONY: all clean install diff --git a/src/Aquarium.cpp b/src/Aquarium.cpp @@ -1,340 +0,0 @@ -#include "Aquarium.h" -#include "Bubble.h" -#include "Castle.h" -#include "Fish.h" -#include "SeaMonster.h" -#include "Seaweed.h" -#include "Ship.h" -#include "Waterline.h" -#include "Whale.h" -#include <algorithm> -#include <cstdio> -#include <cstring> -#include <iostream> -#include <signal.h> -#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(); - ensureBigEntityExists(); - - static std::vector<std::unique_ptr<Entity>> newEntities; - static std::vector<size_t> entitiesToRemove; - - newEntities.clear(); - entitiesToRemove.clear(); - - // 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 - if (auto *fish = dynamic_cast<Fish *>(entity.get())) { - if (fish->shouldSpawnBubble()) { - newEntities.emplace_back( - std::make_unique<Bubble>(fish->getX(), fish->getY())); - } - } - - if (entity->shouldBeRemoved()) { - auto replacement = entity->createReplacement(); - if (replacement) { - entity = std::move(replacement); // Replace in-place - entities_need_sorting = true; - } else { - entitiesToRemove.push_back(i); // Mark for removal - } - } - } - - // 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 - if (!newEntities.empty()) { - // Reserve space to minimize reallocations - 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(); - } - - renderToScreen(); -} - -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(); - for (int i = 0; i < width / 15; i++) - addSeaweed(); - for (int i = 0; i < width * (height - 9) / 350; 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() { - for (auto &row : currentFrame) { - std::fill(row.begin(), row.end(), 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; - if (newCell.bold) - output += ANSI::BOLD; - 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); } diff --git a/src/Aquarium.h b/src/Aquarium.h @@ -1,74 +0,0 @@ -#pragma once -#include "Entity.h" -#include <memory> -#include <vector> - -extern int g_maxCells; - -class Aquarium { -private: - int width; - int height; - - struct Cell { - char ch = ' '; - char colorChar = 'k'; - bool bold = false; - bool operator==(const Cell &other) const { - return ch == other.ch && colorChar == other.colorChar && - bold == other.bold; - } - bool operator!=(const Cell &other) const { return !(*this == other); } - }; - - std::vector<std::vector<Cell>> currentFrame; - std::vector<std::vector<Cell>> previousFrame; - std::vector<std::unique_ptr<Entity>> entities; - size_t big_entity_index = 0; - void ensureBigEntityExists(); - bool entities_need_sorting = true; - static inline const char *colorLookup[256] = {nullptr}; - static inline bool colorLookupInitialized = false; - -public: - Aquarium(); - ~Aquarium(); - - static Aquarium &getInstance() { - static Aquarium instance; - return instance; - } - - [[nodiscard]] int getWidth() const { return width; } - [[nodiscard]] int getHeight() const { return height; } - - void addFish(); - void addBubble(float x, float y); - void addSeaweed(); - void addWaterline(); - void addCastle(); - void addShip(); - void addSeaMonster(); - void addWhale(); - void redraw(); - void initColors(); - void resize(); - void drawToFrame(int y, int x, const std::string &line, - const std::string &colorLine); - - // New termios-specific methods - int checkInput(); // Returns character code or -1 if no input - bool checkResize(); // Returns true if terminal was resized - -private: - void clearCurrentFrame(); - void renderToScreen(); - void ensureEntitiesSorted(); - void getTerminalSize(); - static void initColorLookup(); - - template <typename T, typename... Args> void addEntityImpl(Args &&...args) { - entities.emplace_back(std::make_unique<T>(std::forward<Args>(args)...)); - entities_need_sorting = true; - } -}; diff --git a/src/Castle.cpp b/src/Castle.cpp @@ -1,8 +0,0 @@ -#include "Castle.h" -#include "Aquarium.h" -#include "assets/CastleAssets.h" - -Castle::Castle() - : Entity(Aquarium::getInstance().getWidth() - 32, - Aquarium::getInstance().getHeight() - 13), - image(getCastleAsset().image), mask(getCastleAsset().mask) {} diff --git a/src/Entity.cpp b/src/Entity.cpp @@ -1,63 +0,0 @@ -#include "Entity.h" -#include "Aquarium.h" - -void Entity::draw() const { - auto &aquarium = Aquarium::getInstance(); - - const auto &image = getImage(); - const auto &mask = getMask(); - const char default_color = getDefaultColor(); - - const int base_x = static_cast<int>(x); - const int base_y = static_cast<int>(y); - - for (size_t i = 0; i < image.size(); ++i) { - const std::string &row = image[i]; - const std::string &mask_row = (i < mask.size()) ? mask[i] : ""; - - // Build complete line at once instead of segments - std::string line; - std::string colors; - line.reserve(row.size()); - colors.reserve(row.size()); - - int start_x = base_x; - - for (size_t j = 0; j < row.size(); ++j) { - const char ch = row[j]; - - if (ch == '?') { - // Flush current line if not empty - if (!line.empty()) { - aquarium.drawToFrame(base_y + static_cast<int>(i), start_x, line, - colors); - start_x += static_cast<int>(line.size()) + 1; // +1 for the '?' skip - line.clear(); - colors.clear(); - } else { - ++start_x; // Just skip the '?' position - } - continue; - } - - line.push_back(ch); - - char color = default_color; - if (j < mask_row.size() && mask_row[j] != ' ') { - color = mask_row[j]; - } - colors.push_back(color); - } - - // Flush remaining line - if (!line.empty()) { - aquarium.drawToFrame(base_y + static_cast<int>(i), start_x, line, colors); - } - } -} - -bool Entity::shouldBeRemoved() const noexcept { - const auto &aquarium = Aquarium::getInstance(); - // unsigned nonsense - return x < -static_cast<int>(getWidth()) || x > aquarium.getWidth(); -} diff --git a/src/Fish.cpp b/src/Fish.cpp @@ -1,63 +0,0 @@ -#include "Fish.h" -#include "Aquarium.h" -#include "Random.h" -#include "assets/FishAssets.h" -#include "defs.h" - -std::unordered_map<char, char> Fish::color_map; - -Fish::Fish() : Fish(getRandomAssetIndex()) {} - -Fish::Fish(int asset_index) - : Entity(asset_index % 2 == 0), - image(getFishAssetPairs()[asset_index].image), - mask(getFishAssetPairs()[asset_index].mask), - speed(Random::floatInRange(0.25f, 2.25f)) { - - const auto &aquarium = Aquarium::getInstance(); - y = Random::intInRange(static_cast<int>(image.size()) + 6, - aquarium.getHeight() - static_cast<int>(image.size())); - x = moving_right ? -this->getWidth() - : static_cast<float>(aquarium.getWidth()); - randomizeMask(); -} - -void Fish::randomizeMask() { - // Clear and rebuild color map with random colors each time - color_map.clear(); - color_map['4'] = 'W'; // White is always '4' for eyes - - // Assign random colors to digits 1-3, 5-9 - for (char digit = '1'; digit <= '9'; ++digit) { - if (digit != '4') { - color_map[digit] = AVAILABLE_COLORS[Random::intInRange( - 0, static_cast<int>(AVAILABLE_COLORS.size()) - 1)]; - } - } - - // Apply color mapping to mask - for (auto &line : mask) { - for (char &ch : line) { - if (auto it = color_map.find(ch); it != color_map.end()) { - ch = it->second; - } - } - } -} - -int Fish::getRandomAssetIndex() { - return Random::intInRange(0, - static_cast<int>(getFishAssetPairs().size()) - 1); -} - -void Fish::update() noexcept { x += moving_right ? speed : -speed; } - -std::unique_ptr<Entity> Fish::createReplacement() const { - return std::make_unique<Fish>(); -} - -int Fish::getPreferredLayer() const noexcept { return 10; } - -bool Fish::shouldSpawnBubble() const { - return Random::floatInRange(0, 1) < BUBBLE_SPAWN_CHANCE; -} diff --git a/src/Fish.h b/src/Fish.h @@ -1,34 +0,0 @@ -#pragma once -#include "Entity.h" -#include "assets/FishAssets.h" -#include <array> -#include <unordered_map> - -class Fish : public Entity { -private: - static constexpr std::array<char, 12> AVAILABLE_COLORS = { - 'c', 'C', 'r', 'R', 'y', 'Y', 'b', 'B', 'g', 'G', 'm', 'M'}; - - const std::vector<std::string> &image; - std::vector<std::string> mask; - const float speed; - - static std::unordered_map<char, char> color_map; - - explicit Fish(int asset_index); - static int getRandomAssetIndex(); - void randomizeMask(); - -public: - Fish(); - - void update() noexcept override; - const std::vector<std::string> &getImage() const override { return image; } - const std::vector<std::string> &getMask() const override { return mask; } - char getDefaultColor() const noexcept override { return 'k'; } - - std::unique_ptr<Entity> createReplacement() const override; - int getPreferredLayer() const noexcept override; - - bool shouldSpawnBubble() const; -}; diff --git a/src/SeaMonster.cpp b/src/SeaMonster.cpp @@ -1,44 +0,0 @@ -#include "SeaMonster.h" -#include "Aquarium.h" -#include "Random.h" -#include "assets/SeaMonsterAssets.h" - -SeaMonster::SeaMonster() : SeaMonster(getRandomDirection()) {} - -SeaMonster::SeaMonster(int asset_index) - : Entity(asset_index == 0), - frames(getSeaMonsterAssets()[asset_index].frames), - mask(getSeaMonsterAssets()[asset_index].mask), speed(SEAMONSTER_SPEED) { - - const auto &aquarium = Aquarium::getInstance(); - y = WATER_SURFACE_OFFSET; - - // Use first frame for positioning calculations - const auto &first_frame = frames[0]; - if (moving_right) { - x = -static_cast<float>(first_frame[0].length()); - } else { - x = static_cast<float>(aquarium.getWidth()); - } - - current_image = first_frame; -} - -int SeaMonster::getRandomDirection() { return Random::intInRange(0, 1); } - -void SeaMonster::update() noexcept { - x += moving_right ? speed : -speed; - - ++animation_counter; - if (animation_counter >= ANIMATION_DELAY) { - current_frame_index = (current_frame_index + 1) % frames.size(); - animation_counter = 0; - } -} - -const std::vector<std::string> &SeaMonster::getImage() const { - current_image = frames[current_frame_index]; - return current_image; -} - -int SeaMonster::getPreferredLayer() const noexcept { return 8; } diff --git a/src/SeaMonster.h b/src/SeaMonster.h @@ -1,32 +0,0 @@ -#pragma once -#include "Entity.h" -#include "assets/SeaMonsterAssets.h" - -class SeaMonster : public Entity { -private: - static constexpr float SEAMONSTER_SPEED = 0.8f; - static constexpr int WATER_SURFACE_OFFSET = 2; - - const std::vector<std::vector<std::string>> frames; - const std::vector<std::string> &mask; - const float speed; - - int current_frame_index = 0; - int animation_counter = 0; - mutable std::vector<std::string> current_image; - - static constexpr int ANIMATION_DELAY = 5; - - explicit SeaMonster(int asset_index); - static int getRandomDirection(); - -public: - SeaMonster(); - - void update() noexcept override; - const std::vector<std::string> &getImage() const override; - const std::vector<std::string> &getMask() const override { return mask; } - char getDefaultColor() const noexcept override { return 'G'; } - - int getPreferredLayer() const noexcept override; -}; diff --git a/src/Seaweed.cpp b/src/Seaweed.cpp @@ -1,63 +0,0 @@ -#include "Seaweed.h" -#include "Aquarium.h" -#include "Random.h" -#include "defs.h" - -Seaweed::Seaweed() - : Entity(), - height(Random::intInRange(SEAWEED_MIN_HEIGHT, SEAWEED_MAX_HEIGHT)), - speed(Random::floatInRange(0.1f, 0.3f)), - lifetime(Random::intInRange(SEAWEED_MIN_LIFETIME, SEAWEED_MAX_LIFETIME)) { - - x = Random::intInRange(0, Aquarium::getInstance().getWidth()); - y = Aquarium::getInstance().getHeight() - height; - - current_image.resize(height); - current_mask.resize(height); - generateCurrentFrame(); -} - -void Seaweed::update() noexcept { - frame += speed; - if (frame >= 1.0f) { - pattern_flipped = !pattern_flipped; - frame -= 1.0f; - frame_dirty = true; // mark frame as needing regeneration - } - --lifetime; -} - -const std::vector<std::string> &Seaweed::getImage() const { - if (frame_dirty) { - generateCurrentFrame(); - frame_dirty = false; - } - return current_image; -} - -const std::vector<std::string> &Seaweed::getMask() const { - if (frame_dirty) { - generateCurrentFrame(); - frame_dirty = false; - } - return current_mask; -} - -void Seaweed::generateCurrentFrame() const { - for (size_t i = 0; i < height; ++i) { - const bool use_left = (i % 2 == 0) ^ pattern_flipped; - const char ch = use_left ? PATTERN_LEFT : PATTERN_RIGHT; - const int offset = use_left ? 0 : 1; - - current_image[i] = std::string(offset, ' ') + ch; - current_mask[i] = std::string(current_image[i].size(), 'g'); - } -} - -bool Seaweed::shouldBeRemoved() const noexcept { return lifetime == 0; } - -std::unique_ptr<Entity> Seaweed::createReplacement() const { - return std::make_unique<Seaweed>(); -} - -int Seaweed::getPreferredLayer() const noexcept { return 1; } diff --git a/src/Ship.cpp b/src/Ship.cpp @@ -1,25 +0,0 @@ -#include "Ship.h" -#include "Aquarium.h" -#include "Random.h" -#include "assets/ShipAssets.h" - -Ship::Ship() : Ship(getRandomDirection()) {} - -Ship::Ship(int asset_index) - : Entity(asset_index == 0), image(getShipAssets()[asset_index].image), - mask(getShipAssets()[asset_index].mask), speed(SHIP_SPEED) { - - const auto &aquarium = Aquarium::getInstance(); - y = 0; - if (moving_right) { - x = -static_cast<float>(image[0].length()); - } else { - x = static_cast<float>(aquarium.getWidth()); - } -} - -int Ship::getRandomDirection() { return Random::intInRange(0, 1); } - -void Ship::update() noexcept { x += moving_right ? speed : -speed; } - -int Ship::getPreferredLayer() const noexcept { return 9; } diff --git a/src/Ship.h b/src/Ship.h @@ -1,25 +0,0 @@ -#pragma once -#include "Entity.h" -#include "assets/ShipAssets.h" - -class Ship : public Entity { -private: - static constexpr float SHIP_SPEED = 1.0f; - - const std::vector<std::string> &image; - const std::vector<std::string> &mask; - const float speed; - - explicit Ship(int asset_index); - static int getRandomDirection(); - -public: - Ship(); - - void update() noexcept override; - const std::vector<std::string> &getImage() const override { return image; } - const std::vector<std::string> &getMask() const override { return mask; } - char getDefaultColor() const noexcept override { return 'W'; } - - int getPreferredLayer() const noexcept override; -}; diff --git a/src/Waterline.cpp b/src/Waterline.cpp @@ -1,83 +0,0 @@ -#include "Waterline.h" -#include "Aquarium.h" -#include "Random.h" -#include "defs.h" - -Waterline::Waterline() : Entity(0, WATERLINE_Y) { - shape[0] = "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"; - shape[1] = "^^^^ ^^^ ^^^ ^^^ ^^^^ "; - shape[2] = "^^^^ ^^^^ ^^^ ^^ "; - shape[3] = "^^ ^^^^ ^^^ ^^^^^^ "; - - const size_t width = Aquarium::getInstance().getWidth(); - - for (size_t i = 0; i < NUM_WAVE_LAYERS; ++i) { - const std::string &original = shape[i]; - const size_t pattern_len = original.length(); - - // Calculate how many full patterns + remainder we need - const size_t full_patterns = width / pattern_len; - const size_t remainder = width % pattern_len; - - shape[i].reserve(width); - for (size_t p = 0; p < full_patterns; ++p) { - shape[i] += original; - } - if (remainder > 0) { - shape[i] += original.substr(0, remainder); - } - - // Create color line - colorLines[i].assign(shape[i].size(), WATERLINE_COLOR); - } - - // Initialize cache vectors - cached_image.assign(shape.begin(), shape.end()); - cached_mask.assign(colorLines.begin(), colorLines.end()); -} - -void Waterline::update() noexcept { - // Use cached probability calculations - static constexpr float thresholds[NUM_WAVE_LAYERS] = { - 0.0f, // Layer 0 never moves - 0.25f / WAVE_MOVE_CHANCE, 0.5f / WAVE_MOVE_CHANCE, - 0.75f / WAVE_MOVE_CHANCE}; - - for (size_t i = 1; i < NUM_WAVE_LAYERS; ++i) { - if (Random::floatInRange(0.0f, 1.0f) < thresholds[i]) { - if (Random::intInRange(0, 1) == 0) { - shiftStringLeft(shape[i]); - } else { - shiftStringRight(shape[i]); - } - } - } - - // Update cached vectors - std::copy(shape.begin(), shape.end(), cached_image.begin()); - std::copy(colorLines.begin(), colorLines.end(), cached_mask.begin()); -} - -void Waterline::shiftStringLeft(std::string &str) { - if (!str.empty()) { - char first = str.front(); - str.erase(0, 1); - str.push_back(first); - } -} - -void Waterline::shiftStringRight(std::string &str) { - if (!str.empty()) { - char last = str.back(); - str.pop_back(); - str.insert(0, 1, last); - } -} - -const std::vector<std::string> &Waterline::getImage() const { - return cached_image; -} - -const std::vector<std::string> &Waterline::getMask() const { - return cached_mask; -} diff --git a/src/Whale.cpp b/src/Whale.cpp @@ -1,48 +0,0 @@ -#include "Whale.h" -#include "Aquarium.h" -#include "Random.h" -#include "assets/WhaleAssets.h" - -Whale::Whale() : Whale(getRandomDirection()) {} - -Whale::Whale(int asset_index) - : Entity((asset_index == 0)), frames(getWhaleAssets()[asset_index].frames), - mask(getWhaleAssets()[asset_index].mask), speed(WHALE_SPEED) { - - const auto &aquarium = Aquarium::getInstance(); - y = 0; - - // Use first frame for positioning calculations - const auto &first_frame = frames[0]; - if (moving_right) { - x = -static_cast<float>(first_frame[6].length()); - } else { - x = static_cast<float>(aquarium.getWidth()); - } - - current_image = first_frame; -} - -int Whale::getRandomDirection() { return Random::intInRange(0, 1); } - -void Whale::update() noexcept { - x += moving_right ? speed : -speed; - - ++animation_counter; - - // Use longer delay for first frame (no water spout) - int current_delay = - (current_frame_index == 0) ? FIRST_FRAME_PAUSE : ANIMATION_DELAY; - - if (animation_counter >= current_delay) { - current_frame_index = (current_frame_index + 1) % frames.size(); - animation_counter = 0; - } -} - -const std::vector<std::string> &Whale::getImage() const { - current_image = frames[current_frame_index]; - return current_image; -} - -int Whale::getPreferredLayer() const noexcept { return 8; } diff --git a/src/Whale.h b/src/Whale.h @@ -1,31 +0,0 @@ -#pragma once -#include "Entity.h" -#include "assets/WhaleAssets.h" - -class Whale : public Entity { -private: - static constexpr float WHALE_SPEED = 1.0f; - - const std::vector<std::vector<std::string>> frames; - const std::vector<std::string> &mask; - const float speed; - - int current_frame_index = 0; - int animation_counter = 0; - mutable std::vector<std::string> current_image; - - static constexpr int ANIMATION_DELAY = 1; - static constexpr int FIRST_FRAME_PAUSE = 10; - - explicit Whale(int asset_index); - static int getRandomDirection(); - -public: - Whale(); - - void update() noexcept override; - const std::vector<std::string> &getImage() const override; - const std::vector<std::string> &getMask() const override { return mask; } - char getDefaultColor() const noexcept override { return 'B'; } - int getPreferredLayer() const noexcept override; -}; diff --git a/src/assets/FishAssets.h b/src/assets/FishAssets.h @@ -1,7 +1,7 @@ #pragma once -#include "../Entity.h" -#include "../SpriteUtils.h" +#include "../entities/Entity.h" +#include "../utils/SpriteUtils.h" #include <vector> /* diff --git a/src/assets/SeaMonsterAssets.h b/src/assets/SeaMonsterAssets.h @@ -1,6 +1,6 @@ #pragma once -#include "../Entity.h" -#include "../SpriteUtils.h" +#include "../entities/Entity.h" +#include "../utils/SpriteUtils.h" #include <vector> struct SeaMonsterAsset { diff --git a/src/assets/ShipAssets.h b/src/assets/ShipAssets.h @@ -1,6 +1,6 @@ #pragma once -#include "../Entity.h" -#include "../SpriteUtils.h" +#include "../entities/Entity.h" +#include "../utils/SpriteUtils.h" #include <vector> inline const AssetPair &getShip() { diff --git a/src/assets/WhaleAssets.h b/src/assets/WhaleAssets.h @@ -1,6 +1,6 @@ #pragma once -#include "../Entity.h" -#include "../SpriteUtils.h" +#include "../entities/Entity.h" +#include "../utils/SpriteUtils.h" #include <vector> struct WhaleAsset { diff --git a/src/core/Aquarium.cpp b/src/core/Aquarium.cpp @@ -0,0 +1,423 @@ +#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); } diff --git a/src/core/Aquarium.h b/src/core/Aquarium.h @@ -0,0 +1,75 @@ +#pragma once +#include "../entities/Entity.h" +#include <memory> +#include <vector> + +extern int g_maxCells; + +class Aquarium { +private: + int width; + int height; + + struct Cell { + char ch = ' '; + char colorChar = 'k'; + bool bold = false; + bool operator==(const Cell &other) const { + return ch == other.ch && colorChar == other.colorChar && + bold == other.bold; + } + bool operator!=(const Cell &other) const { return !(*this == other); } + }; + + std::vector<std::vector<Cell>> currentFrame; + std::vector<std::vector<Cell>> previousFrame; + std::vector<std::unique_ptr<Entity>> entities; + size_t big_entity_index = 0; + void ensureBigEntityExists(); + bool entities_need_sorting = true; + static inline const char *colorLookup[256] = {nullptr}; + static inline bool colorLookupInitialized = false; + void drawDebugInfo(); + +public: + Aquarium(); + ~Aquarium(); + + static Aquarium &getInstance() { + static Aquarium instance; + return instance; + } + + [[nodiscard]] int getWidth() const { return width; } + [[nodiscard]] int getHeight() const { return height; } + + void addFish(); + void addBubble(float x, float y); + void addSeaweed(); + void addWaterline(); + void addCastle(); + void addShip(); + void addSeaMonster(); + void addWhale(); + void redraw(); + void initColors(); + void resize(); + void drawToFrame(int y, int x, const std::string &line, + const std::string &colorLine); + + // New termios-specific methods + int checkInput(); // Returns character code or -1 if no input + bool checkResize(); // Returns true if terminal was resized + +private: + void clearCurrentFrame(); + void renderToScreen(); + void ensureEntitiesSorted(); + void getTerminalSize(); + static void initColorLookup(); + + template <typename T, typename... Args> void addEntityImpl(Args &&...args) { + entities.emplace_back(std::make_unique<T>(std::forward<Args>(args)...)); + entities_need_sorting = true; + } +}; diff --git a/src/core/Config.cpp b/src/core/Config.cpp @@ -0,0 +1,175 @@ +#include "Config.h" +#include <cstdlib> +#include <cstring> +#include <getopt.h> +#include <iostream> + +AquariumConfig g_config; + +void printUsage(const char *program_name) { + std::cout << "Usage: " << program_name << " [OPTIONS]\n\n"; + std::cout << "Aquarium screensaver with customizable settings\n\n"; + std::cout << "OPTIONS:\n"; + std::cout << " -h, --help Show this help message\n"; + std::cout + << " -f, --fish COUNT Number of initial fish (default: auto)\n"; + std::cout << " -s, --seaweed COUNT Number of seaweed plants (default: " + "auto)\n"; + std::cout << " -b, --max-bubbles COUNT Maximum bubbles on screen " + "(default: 50)\n"; + std::cout << " -d, --delay MS Frame delay in milliseconds " + "(default: 100)\n"; + std::cout << " -r, --bubble-rate RATE Fish bubble spawn rate 1/N " + "(default: 10)\n"; + std::cout << " -m, --max-entities COUNT Maximum entities on screen " + "(default: 200)\n"; + std::cout << " --no-big-entities Disable ships, whales, and sea " + "monsters\n"; + std::cout << " --no-bubbles Disable bubble generation\n"; + std::cout << " --no-colors Disable colors (monochrome mode)\n"; + std::cout << " --no-bold Disable bold text\n"; + std::cout << " --show-fps Display FPS counter\n"; + std::cout << " --show-count Display entity count\n"; + std::cout << " --debug Enable debug mode\n"; + std::cout << "\nShort options can be combined: -f20 -s5 -d50\n"; + std::cout << "Long options accept = syntax: --fish=20 --delay=50\n"; + std::cout << "\nExamples:\n"; + std::cout << " " << program_name + << " -f20 -s5 -d50 # Fast aquarium with lots of fish\n"; + std::cout << " " << program_name + << " --fish=10 --no-colors # 10 fish, monochrome\n"; + std::cout << " " << program_name + << " --debug --show-fps # Debug mode with FPS\n"; +} + +bool parseArguments(int argc, char *argv[]) { + // Define long options + static const struct option long_options[] = { + {"help", no_argument, nullptr, 'h'}, + {"fish", required_argument, nullptr, 'f'}, + {"seaweed", required_argument, nullptr, 's'}, + {"max-bubbles", required_argument, nullptr, 'b'}, + {"delay", required_argument, nullptr, 'd'}, + {"bubble-rate", required_argument, nullptr, 'r'}, + {"max-entities", required_argument, nullptr, 'm'}, + {"no-big-entities", no_argument, nullptr, 'B'}, + {"no-bubbles", no_argument, nullptr, 'N'}, + {"no-colors", no_argument, nullptr, 'C'}, + {"no-bold", no_argument, nullptr, 'O'}, + {"show-fps", no_argument, nullptr, 'F'}, + {"show-count", no_argument, nullptr, 'S'}, + {"debug", no_argument, nullptr, 'D'}, + {nullptr, 0, nullptr, 0}}; + + // Short options string - : after letter means it takes an argument + const char *short_options = "hf:s:b:d:r:m:BNCOFSD"; + + int option; + int option_index = 0; + + // Reset getopt state (important for testing) + optind = 1; + + while ((option = getopt_long(argc, argv, short_options, long_options, + &option_index)) != -1) { + switch (option) { + case 'h': + printUsage(argv[0]); + return false; + + case 'f': + g_config.initial_fish_count = std::atoi(optarg); + if (g_config.initial_fish_count < -1) { + std::cerr << "Error: Fish count must be >= -1\n"; + return false; + } + break; + + case 's': + g_config.initial_seaweed_count = std::atoi(optarg); + if (g_config.initial_seaweed_count < -1) { + std::cerr << "Error: Seaweed count must be >= -1\n"; + return false; + } + break; + + case 'b': + g_config.max_bubbles = std::atoi(optarg); + if (g_config.max_bubbles < 0) { + std::cerr << "Error: Max bubbles must be >= 0\n"; + return false; + } + break; + + case 'd': + g_config.frame_delay_ms = std::atoi(optarg); + if (g_config.frame_delay_ms < 1) { + std::cerr << "Error: Frame delay must be >= 1ms\n"; + return false; + } + break; + + case 'r': + g_config.fish_bubble_rate = std::atoi(optarg); + if (g_config.fish_bubble_rate < 1) { + std::cerr << "Error: Bubble rate must be >= 1\n"; + return false; + } + break; + + case 'm': + g_config.max_entities = std::atoi(optarg); + if (g_config.max_entities < 1) { + std::cerr << "Error: Max entities must be >= 1\n"; + return false; + } + break; + + case 'B': + g_config.enable_big_entities = false; + break; + + case 'N': + g_config.enable_bubbles = false; + break; + + case 'C': + g_config.use_colors = false; + break; + + case 'O': + g_config.use_bold = false; + break; + + case 'F': + g_config.show_fps = true; + break; + + case 'S': + g_config.show_entity_count = true; + break; + + case 'D': + g_config.debug_mode = true; + break; + + case '?': + // getopt_long already printed an error message + std::cerr << "Use --help for usage information\n"; + return false; + + default: + std::cerr << "Error: Unexpected option character\n"; + return false; + } + } + + // Check for non-option arguments + if (optind < argc) { + std::cerr << "Error: Unexpected argument: " << argv[optind] << "\n"; + std::cerr << "Use --help for usage information\n"; + return false; + } + + return true; +} diff --git a/src/core/Config.h b/src/core/Config.h @@ -0,0 +1,33 @@ +#pragma once +#include <string> + +struct AquariumConfig { + // Entity spawn settings + int initial_fish_count = -1; // -1 means auto-calculate + int initial_seaweed_count = -1; // -1 means auto-calculate + int max_bubbles = 50; + + // Animation settings + int frame_delay_ms = 100; + bool enable_big_entities = true; + bool enable_bubbles = true; + + // Visual settings + bool use_colors = true; + bool use_bold = true; + + // Spawn rates (lower = more frequent) + int fish_bubble_rate = 10; // 1 in N chance per frame + int big_entity_spawn_delay = 100; // frames between big entities + + // Performance settings + bool optimize_rendering = true; + int max_entities = 200; + + // Debug settings + bool show_fps = false; + bool show_entity_count = false; + bool debug_mode = false; +}; + +extern AquariumConfig g_config; diff --git a/src/core/main.cpp b/src/core/main.cpp @@ -0,0 +1,115 @@ +#include "Aquarium.h" +#include "Config.h" +#include <chrono> +#include <iostream> +#include <thread> + +#ifdef __OpenBSD__ +#include <unistd.h> +#endif + +// Forward declaration +bool parseArguments(int argc, char *argv[]); + +int main(int argc, char *argv[]) { + // Parse command line arguments + if (!parseArguments(argc, argv)) { + return 1; // Exit if parsing failed or help was shown + } + + // Print configuration if debug mode is enabled + if (g_config.debug_mode) { + std::cout << "Configuration:\n"; + std::cout << " Fish count: " + << (g_config.initial_fish_count == -1 + ? "auto" + : std::to_string(g_config.initial_fish_count)) + << "\n"; + std::cout << " Seaweed count: " + << (g_config.initial_seaweed_count == -1 + ? "auto" + : std::to_string(g_config.initial_seaweed_count)) + << "\n"; + std::cout << " Max bubbles: " << g_config.max_bubbles << "\n"; + std::cout << " Frame delay: " << g_config.frame_delay_ms << "ms\n"; + std::cout << " Big entities: " + << (g_config.enable_big_entities ? "enabled" : "disabled") + << "\n"; + std::cout << " Bubbles: " + << (g_config.enable_bubbles ? "enabled" : "disabled") << "\n"; + std::cout << " Colors: " << (g_config.use_colors ? "enabled" : "disabled") + << "\n"; + std::cout << " Bold text: " << (g_config.use_bold ? "enabled" : "disabled") + << "\n"; + std::cout << " Max entities: " << g_config.max_entities << "\n"; + std::cout << "Press any key to continue...\n"; + std::cin.get(); + } + + Aquarium &aquarium = Aquarium::getInstance(); + +#ifdef __OpenBSD__ + if (pledge("stdio tty", NULL) == -1) { + perror("pledge"); + return 1; + } +#endif + + // Initialize the aquarium display + aquarium.resize(); // Setup initial entities + + // Variables for FPS calculation + auto last_time = std::chrono::steady_clock::now(); + int frame_count = 0; + double fps = 0.0; + + // Main game loop + while (true) { + auto frame_start = std::chrono::steady_clock::now(); + + int input = aquarium.checkInput(); + if (input == 'q' || input == 'Q' || input == 27) { // ESC key + break; + } + + // Toggle debug info with 'd' key + if (input == 'd' || input == 'D') { + g_config.show_fps = !g_config.show_fps; + g_config.show_entity_count = !g_config.show_entity_count; + } + + if (aquarium.checkResize()) { + aquarium.resize(); + } + + aquarium.redraw(); + + // Calculate FPS + if (g_config.show_fps) { + ++frame_count; + 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) { // Update FPS every second + fps = (frame_count * 1000.0) / elapsed; + frame_count = 0; + last_time = current_time; + } + } + + // Sleep for configured frame delay + auto frame_end = std::chrono::steady_clock::now(); + auto frame_duration = std::chrono::duration_cast<std::chrono::milliseconds>( + frame_end - frame_start); + auto sleep_duration = + std::chrono::milliseconds(g_config.frame_delay_ms) - frame_duration; + + if (sleep_duration > std::chrono::milliseconds(0)) { + std::this_thread::sleep_for(sleep_duration); + } + } + + return 0; +} diff --git a/src/Bubble.cpp b/src/entities/Bubble.cpp diff --git a/src/Bubble.h b/src/entities/Bubble.h diff --git a/src/entities/Castle.cpp b/src/entities/Castle.cpp @@ -0,0 +1,8 @@ +#include "Castle.h" +#include "../assets/CastleAssets.h" +#include "../core/Aquarium.h" + +Castle::Castle() + : Entity(Aquarium::getInstance().getWidth() - 32, + Aquarium::getInstance().getHeight() - 13), + image(getCastleAsset().image), mask(getCastleAsset().mask) {} diff --git a/src/Castle.h b/src/entities/Castle.h diff --git a/src/entities/Entity.cpp b/src/entities/Entity.cpp @@ -0,0 +1,63 @@ +#include "Entity.h" +#include "../core/Aquarium.h" + +void Entity::draw() const { + auto &aquarium = Aquarium::getInstance(); + + const auto &image = getImage(); + const auto &mask = getMask(); + const char default_color = getDefaultColor(); + + const int base_x = static_cast<int>(x); + const int base_y = static_cast<int>(y); + + for (size_t i = 0; i < image.size(); ++i) { + const std::string &row = image[i]; + const std::string &mask_row = (i < mask.size()) ? mask[i] : ""; + + // Build complete line at once instead of segments + std::string line; + std::string colors; + line.reserve(row.size()); + colors.reserve(row.size()); + + int start_x = base_x; + + for (size_t j = 0; j < row.size(); ++j) { + const char ch = row[j]; + + if (ch == '?') { + // Flush current line if not empty + if (!line.empty()) { + aquarium.drawToFrame(base_y + static_cast<int>(i), start_x, line, + colors); + start_x += static_cast<int>(line.size()) + 1; // +1 for the '?' skip + line.clear(); + colors.clear(); + } else { + ++start_x; // Just skip the '?' position + } + continue; + } + + line.push_back(ch); + + char color = default_color; + if (j < mask_row.size() && mask_row[j] != ' ') { + color = mask_row[j]; + } + colors.push_back(color); + } + + // Flush remaining line + if (!line.empty()) { + aquarium.drawToFrame(base_y + static_cast<int>(i), start_x, line, colors); + } + } +} + +bool Entity::shouldBeRemoved() const noexcept { + const auto &aquarium = Aquarium::getInstance(); + // unsigned nonsense + return x < -static_cast<int>(getWidth()) || x > aquarium.getWidth(); +} diff --git a/src/Entity.h b/src/entities/Entity.h diff --git a/src/entities/Fish.cpp b/src/entities/Fish.cpp @@ -0,0 +1,63 @@ +#include "Fish.h" +#include "../assets/FishAssets.h" +#include "../core/Aquarium.h" +#include "../utils/Random.h" +#include "../utils/defs.h" + +std::unordered_map<char, char> Fish::color_map; + +Fish::Fish() : Fish(getRandomAssetIndex()) {} + +Fish::Fish(int asset_index) + : Entity(asset_index % 2 == 0), + image(getFishAssetPairs()[asset_index].image), + mask(getFishAssetPairs()[asset_index].mask), + speed(Random::floatInRange(0.25f, 2.25f)) { + + const auto &aquarium = Aquarium::getInstance(); + y = Random::intInRange(static_cast<int>(image.size()) + 6, + aquarium.getHeight() - static_cast<int>(image.size())); + x = moving_right ? -this->getWidth() + : static_cast<float>(aquarium.getWidth()); + randomizeMask(); +} + +void Fish::randomizeMask() { + // Clear and rebuild color map with random colors each time + color_map.clear(); + color_map['4'] = 'W'; // White is always '4' for eyes + + // Assign random colors to digits 1-3, 5-9 + for (char digit = '1'; digit <= '9'; ++digit) { + if (digit != '4') { + color_map[digit] = AVAILABLE_COLORS[Random::intInRange( + 0, static_cast<int>(AVAILABLE_COLORS.size()) - 1)]; + } + } + + // Apply color mapping to mask + for (auto &line : mask) { + for (char &ch : line) { + if (auto it = color_map.find(ch); it != color_map.end()) { + ch = it->second; + } + } + } +} + +int Fish::getRandomAssetIndex() { + return Random::intInRange(0, + static_cast<int>(getFishAssetPairs().size()) - 1); +} + +void Fish::update() noexcept { x += moving_right ? speed : -speed; } + +std::unique_ptr<Entity> Fish::createReplacement() const { + return std::make_unique<Fish>(); +} + +int Fish::getPreferredLayer() const noexcept { return 10; } + +bool Fish::shouldSpawnBubble() const { + return Random::floatInRange(0, 1) < BUBBLE_SPAWN_CHANCE; +} diff --git a/src/entities/Fish.h b/src/entities/Fish.h @@ -0,0 +1,34 @@ +#pragma once +#include "../assets/FishAssets.h" +#include "Entity.h" +#include <array> +#include <unordered_map> + +class Fish : public Entity { +private: + static constexpr std::array<char, 12> AVAILABLE_COLORS = { + 'c', 'C', 'r', 'R', 'y', 'Y', 'b', 'B', 'g', 'G', 'm', 'M'}; + + const std::vector<std::string> &image; + std::vector<std::string> mask; + const float speed; + + static std::unordered_map<char, char> color_map; + + explicit Fish(int asset_index); + static int getRandomAssetIndex(); + void randomizeMask(); + +public: + Fish(); + + void update() noexcept override; + const std::vector<std::string> &getImage() const override { return image; } + const std::vector<std::string> &getMask() const override { return mask; } + char getDefaultColor() const noexcept override { return 'k'; } + + std::unique_ptr<Entity> createReplacement() const override; + int getPreferredLayer() const noexcept override; + + bool shouldSpawnBubble() const; +}; diff --git a/src/entities/SeaMonster.cpp b/src/entities/SeaMonster.cpp @@ -0,0 +1,44 @@ +#include "SeaMonster.h" +#include "../assets/SeaMonsterAssets.h" +#include "../core/Aquarium.h" +#include "../utils/Random.h" + +SeaMonster::SeaMonster() : SeaMonster(getRandomDirection()) {} + +SeaMonster::SeaMonster(int asset_index) + : Entity(asset_index == 0), + frames(getSeaMonsterAssets()[asset_index].frames), + mask(getSeaMonsterAssets()[asset_index].mask), speed(SEAMONSTER_SPEED) { + + const auto &aquarium = Aquarium::getInstance(); + y = WATER_SURFACE_OFFSET; + + // Use first frame for positioning calculations + const auto &first_frame = frames[0]; + if (moving_right) { + x = -static_cast<float>(first_frame[0].length()); + } else { + x = static_cast<float>(aquarium.getWidth()); + } + + current_image = first_frame; +} + +int SeaMonster::getRandomDirection() { return Random::intInRange(0, 1); } + +void SeaMonster::update() noexcept { + x += moving_right ? speed : -speed; + + ++animation_counter; + if (animation_counter >= ANIMATION_DELAY) { + current_frame_index = (current_frame_index + 1) % frames.size(); + animation_counter = 0; + } +} + +const std::vector<std::string> &SeaMonster::getImage() const { + current_image = frames[current_frame_index]; + return current_image; +} + +int SeaMonster::getPreferredLayer() const noexcept { return 8; } diff --git a/src/entities/SeaMonster.h b/src/entities/SeaMonster.h @@ -0,0 +1,32 @@ +#pragma once +#include "../assets/SeaMonsterAssets.h" +#include "Entity.h" + +class SeaMonster : public Entity { +private: + static constexpr float SEAMONSTER_SPEED = 0.8f; + static constexpr int WATER_SURFACE_OFFSET = 2; + + const std::vector<std::vector<std::string>> frames; + const std::vector<std::string> &mask; + const float speed; + + int current_frame_index = 0; + int animation_counter = 0; + mutable std::vector<std::string> current_image; + + static constexpr int ANIMATION_DELAY = 5; + + explicit SeaMonster(int asset_index); + static int getRandomDirection(); + +public: + SeaMonster(); + + void update() noexcept override; + const std::vector<std::string> &getImage() const override; + const std::vector<std::string> &getMask() const override { return mask; } + char getDefaultColor() const noexcept override { return 'G'; } + + int getPreferredLayer() const noexcept override; +}; diff --git a/src/entities/Seaweed.cpp b/src/entities/Seaweed.cpp @@ -0,0 +1,63 @@ +#include "Seaweed.h" +#include "../core/Aquarium.h" +#include "../utils/Random.h" +#include "../utils/defs.h" + +Seaweed::Seaweed() + : Entity(), + height(Random::intInRange(SEAWEED_MIN_HEIGHT, SEAWEED_MAX_HEIGHT)), + speed(Random::floatInRange(0.1f, 0.3f)), + lifetime(Random::intInRange(SEAWEED_MIN_LIFETIME, SEAWEED_MAX_LIFETIME)) { + + x = Random::intInRange(0, Aquarium::getInstance().getWidth()); + y = Aquarium::getInstance().getHeight() - height; + + current_image.resize(height); + current_mask.resize(height); + generateCurrentFrame(); +} + +void Seaweed::update() noexcept { + frame += speed; + if (frame >= 1.0f) { + pattern_flipped = !pattern_flipped; + frame -= 1.0f; + frame_dirty = true; // mark frame as needing regeneration + } + --lifetime; +} + +const std::vector<std::string> &Seaweed::getImage() const { + if (frame_dirty) { + generateCurrentFrame(); + frame_dirty = false; + } + return current_image; +} + +const std::vector<std::string> &Seaweed::getMask() const { + if (frame_dirty) { + generateCurrentFrame(); + frame_dirty = false; + } + return current_mask; +} + +void Seaweed::generateCurrentFrame() const { + for (size_t i = 0; i < height; ++i) { + const bool use_left = (i % 2 == 0) ^ pattern_flipped; + const char ch = use_left ? PATTERN_LEFT : PATTERN_RIGHT; + const int offset = use_left ? 0 : 1; + + current_image[i] = std::string(offset, ' ') + ch; + current_mask[i] = std::string(current_image[i].size(), 'g'); + } +} + +bool Seaweed::shouldBeRemoved() const noexcept { return lifetime == 0; } + +std::unique_ptr<Entity> Seaweed::createReplacement() const { + return std::make_unique<Seaweed>(); +} + +int Seaweed::getPreferredLayer() const noexcept { return 1; } diff --git a/src/Seaweed.h b/src/entities/Seaweed.h diff --git a/src/entities/Ship.cpp b/src/entities/Ship.cpp @@ -0,0 +1,25 @@ +#include "Ship.h" +#include "../assets/ShipAssets.h" +#include "../core/Aquarium.h" +#include "../utils/Random.h" + +Ship::Ship() : Ship(getRandomDirection()) {} + +Ship::Ship(int asset_index) + : Entity(asset_index == 0), image(getShipAssets()[asset_index].image), + mask(getShipAssets()[asset_index].mask), speed(SHIP_SPEED) { + + const auto &aquarium = Aquarium::getInstance(); + y = 0; + if (moving_right) { + x = -static_cast<float>(image[0].length()); + } else { + x = static_cast<float>(aquarium.getWidth()); + } +} + +int Ship::getRandomDirection() { return Random::intInRange(0, 1); } + +void Ship::update() noexcept { x += moving_right ? speed : -speed; } + +int Ship::getPreferredLayer() const noexcept { return 9; } diff --git a/src/entities/Ship.h b/src/entities/Ship.h @@ -0,0 +1,25 @@ +#pragma once +#include "../assets/ShipAssets.h" +#include "Entity.h" + +class Ship : public Entity { +private: + static constexpr float SHIP_SPEED = 1.0f; + + const std::vector<std::string> &image; + const std::vector<std::string> &mask; + const float speed; + + explicit Ship(int asset_index); + static int getRandomDirection(); + +public: + Ship(); + + void update() noexcept override; + const std::vector<std::string> &getImage() const override { return image; } + const std::vector<std::string> &getMask() const override { return mask; } + char getDefaultColor() const noexcept override { return 'W'; } + + int getPreferredLayer() const noexcept override; +}; diff --git a/src/entities/Waterline.cpp b/src/entities/Waterline.cpp @@ -0,0 +1,83 @@ +#include "Waterline.h" +#include "../core/Aquarium.h" +#include "../utils/Random.h" +#include "../utils/defs.h" + +Waterline::Waterline() : Entity(0, WATERLINE_Y) { + shape[0] = "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"; + shape[1] = "^^^^ ^^^ ^^^ ^^^ ^^^^ "; + shape[2] = "^^^^ ^^^^ ^^^ ^^ "; + shape[3] = "^^ ^^^^ ^^^ ^^^^^^ "; + + const size_t width = Aquarium::getInstance().getWidth(); + + for (size_t i = 0; i < NUM_WAVE_LAYERS; ++i) { + const std::string &original = shape[i]; + const size_t pattern_len = original.length(); + + // Calculate how many full patterns + remainder we need + const size_t full_patterns = width / pattern_len; + const size_t remainder = width % pattern_len; + + shape[i].reserve(width); + for (size_t p = 0; p < full_patterns; ++p) { + shape[i] += original; + } + if (remainder > 0) { + shape[i] += original.substr(0, remainder); + } + + // Create color line + colorLines[i].assign(shape[i].size(), WATERLINE_COLOR); + } + + // Initialize cache vectors + cached_image.assign(shape.begin(), shape.end()); + cached_mask.assign(colorLines.begin(), colorLines.end()); +} + +void Waterline::update() noexcept { + // Use cached probability calculations + static constexpr float thresholds[NUM_WAVE_LAYERS] = { + 0.0f, // Layer 0 never moves + 0.25f / WAVE_MOVE_CHANCE, 0.5f / WAVE_MOVE_CHANCE, + 0.75f / WAVE_MOVE_CHANCE}; + + for (size_t i = 1; i < NUM_WAVE_LAYERS; ++i) { + if (Random::floatInRange(0.0f, 1.0f) < thresholds[i]) { + if (Random::intInRange(0, 1) == 0) { + shiftStringLeft(shape[i]); + } else { + shiftStringRight(shape[i]); + } + } + } + + // Update cached vectors + std::copy(shape.begin(), shape.end(), cached_image.begin()); + std::copy(colorLines.begin(), colorLines.end(), cached_mask.begin()); +} + +void Waterline::shiftStringLeft(std::string &str) { + if (!str.empty()) { + char first = str.front(); + str.erase(0, 1); + str.push_back(first); + } +} + +void Waterline::shiftStringRight(std::string &str) { + if (!str.empty()) { + char last = str.back(); + str.pop_back(); + str.insert(0, 1, last); + } +} + +const std::vector<std::string> &Waterline::getImage() const { + return cached_image; +} + +const std::vector<std::string> &Waterline::getMask() const { + return cached_mask; +} diff --git a/src/Waterline.h b/src/entities/Waterline.h diff --git a/src/entities/Whale.cpp b/src/entities/Whale.cpp @@ -0,0 +1,48 @@ +#include "Whale.h" +#include "../assets/WhaleAssets.h" +#include "../core/Aquarium.h" +#include "../utils/Random.h" + +Whale::Whale() : Whale(getRandomDirection()) {} + +Whale::Whale(int asset_index) + : Entity((asset_index == 0)), frames(getWhaleAssets()[asset_index].frames), + mask(getWhaleAssets()[asset_index].mask), speed(WHALE_SPEED) { + + const auto &aquarium = Aquarium::getInstance(); + y = 0; + + // Use first frame for positioning calculations + const auto &first_frame = frames[0]; + if (moving_right) { + x = -static_cast<float>(first_frame[6].length()); + } else { + x = static_cast<float>(aquarium.getWidth()); + } + + current_image = first_frame; +} + +int Whale::getRandomDirection() { return Random::intInRange(0, 1); } + +void Whale::update() noexcept { + x += moving_right ? speed : -speed; + + ++animation_counter; + + // Use longer delay for first frame (no water spout) + int current_delay = + (current_frame_index == 0) ? FIRST_FRAME_PAUSE : ANIMATION_DELAY; + + if (animation_counter >= current_delay) { + current_frame_index = (current_frame_index + 1) % frames.size(); + animation_counter = 0; + } +} + +const std::vector<std::string> &Whale::getImage() const { + current_image = frames[current_frame_index]; + return current_image; +} + +int Whale::getPreferredLayer() const noexcept { return 8; } diff --git a/src/entities/Whale.h b/src/entities/Whale.h @@ -0,0 +1,31 @@ +#pragma once +#include "../assets/WhaleAssets.h" +#include "Entity.h" + +class Whale : public Entity { +private: + static constexpr float WHALE_SPEED = 1.0f; + + const std::vector<std::vector<std::string>> frames; + const std::vector<std::string> &mask; + const float speed; + + int current_frame_index = 0; + int animation_counter = 0; + mutable std::vector<std::string> current_image; + + static constexpr int ANIMATION_DELAY = 1; + static constexpr int FIRST_FRAME_PAUSE = 10; + + explicit Whale(int asset_index); + static int getRandomDirection(); + +public: + Whale(); + + void update() noexcept override; + const std::vector<std::string> &getImage() const override; + const std::vector<std::string> &getMask() const override { return mask; } + char getDefaultColor() const noexcept override { return 'B'; } + int getPreferredLayer() const noexcept override; +}; diff --git a/src/main.cpp b/src/main.cpp @@ -1,34 +0,0 @@ -#include "Aquarium.h" -#ifdef __OpenBSD__ -#include <unistd.h> -#endif - -int main() { - Aquarium &aquarium = Aquarium::getInstance(); - -#ifdef __OpenBSD__ - if (pledge("stdio tty", NULL) == -1) { - perror("pledge"); - return 1; - } -#endif - - // Initialize the aquarium display - aquarium.resize(); // Setup initial entities - - // Main game loop - while (true) { - int input = aquarium.checkInput(); - if (input == 'q' || input == 'Q' || input == 27) { // ESC key - break; - } - - if (aquarium.checkResize()) { - aquarium.resize(); - } - - aquarium.redraw(); - } - - return 0; -} diff --git a/src/Random.h b/src/utils/Random.h diff --git a/src/SpriteUtils.h b/src/utils/SpriteUtils.h diff --git a/src/defs.h b/src/utils/defs.h