Compare commits
30 Commits
b4e08ff28d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e15b5db7ff | ||
|
|
78d6209818 | ||
| 75e147f511 | |||
|
|
2fe0538857 | ||
|
|
f236af2709 | ||
|
|
16d8858841 | ||
|
|
8342e60547 | ||
|
|
b36242c78d | ||
| 4f44c2fc34 | |||
|
|
36e6c3786f | ||
|
|
2f44398664 | ||
|
|
d93d351d38 | ||
| b4f1826464 | |||
| 8302503333 | |||
| 5bae9a07af | |||
| 348b687bd9 | |||
| 1670bccc4d | |||
| cda06a5169 | |||
| 9c631d3d9a | |||
| 2eead14b8e | |||
| d350044cb2 | |||
|
|
c54089ae19 | ||
| 5706c90f87 | |||
| f4ec53cbbe | |||
| 7bd10b454d | |||
| b95b8ba5bf | |||
| bbe2fa3553 | |||
| 90ae6678c5 | |||
| c04d34132a | |||
| 593bbd6786 |
40
Makefile
40
Makefile
@@ -1,44 +1,36 @@
|
||||
# Compiler and flags
|
||||
CXX = g++
|
||||
CXXFLAGS = -std=c++17 -Wall -Wextra -O3 --function-sections -fdata-sections -flto -march=native -ffast-math
|
||||
LDFLAGS = -lncurses -ltinfo -Wl,--gc-sections -flto -Wl,-O2
|
||||
|
||||
# Directories
|
||||
CXX = c++
|
||||
CXXFLAGS = -Wall -Wextra -O3 -Isrc
|
||||
LDFLAGS = -static
|
||||
SRC_DIR = src
|
||||
OBJ_DIR = build
|
||||
BIN_DIR = bin
|
||||
|
||||
# File extensions
|
||||
SRC_EXT = cpp
|
||||
OBJ_EXT = o
|
||||
|
||||
# Find all source files and generate object files
|
||||
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))
|
||||
|
||||
# Output executable
|
||||
EXEC = $(BIN_DIR)/fissh
|
||||
|
||||
# Default target: build everything
|
||||
# Get all unique subdirectories for build structure
|
||||
OBJ_DIRS = $(sort $(dir $(OBJECTS)))
|
||||
|
||||
all: $(EXEC)
|
||||
|
||||
# Rule to link the object files into the final executable
|
||||
$(EXEC): $(OBJECTS)
|
||||
@mkdir -p $(BIN_DIR) # Make sure the bin dir exists
|
||||
@mkdir -p $(BIN_DIR)
|
||||
$(CXX) $(OBJECTS) -o $(EXEC) $(LDFLAGS)
|
||||
|
||||
# Rule to compile .cpp files into .o files
|
||||
$(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 up the build directory
|
||||
clean:
|
||||
rm -rf $(OBJ_DIR) $(BIN_DIR)
|
||||
|
||||
# Run the program
|
||||
run: $(EXEC)
|
||||
./$(EXEC)
|
||||
install: $(EXEC)
|
||||
install -m 755 $(EXEC) /usr/local/bin/
|
||||
|
||||
# Phony targets
|
||||
.PHONY: all clean run
|
||||
.PHONY: all clean install
|
||||
|
||||
275
src/Aquarium.cpp
275
src/Aquarium.cpp
@@ -1,275 +0,0 @@
|
||||
#include "Aquarium.h"
|
||||
#include "Random.h"
|
||||
#include "defs.h"
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <ncurses.h>
|
||||
|
||||
int g_maxCells = 0;
|
||||
Aquarium::Aquarium() {
|
||||
initscr();
|
||||
noecho();
|
||||
cbreak();
|
||||
nodelay(stdscr, TRUE);
|
||||
curs_set(0);
|
||||
initColors();
|
||||
initColorLookup();
|
||||
timeout(100);
|
||||
getmaxyx(stdscr, height, width);
|
||||
frontBuffer.assign(height, std::vector<Cell>(width));
|
||||
backBuffer.assign(height, std::vector<Cell>(width));
|
||||
layeredMap.assign(height, std::vector<LayeredCell>(width));
|
||||
}
|
||||
|
||||
void Aquarium::initColors() {
|
||||
if (has_colors()) {
|
||||
start_color();
|
||||
init_pair(1, COLOR_RED, COLOR_BLACK); // 'r'
|
||||
init_pair(2, COLOR_GREEN, COLOR_BLACK); // 'g'
|
||||
init_pair(3, COLOR_YELLOW, COLOR_BLACK); // 'y'
|
||||
init_pair(4, COLOR_BLUE, COLOR_BLACK); // 'b'
|
||||
init_pair(5, COLOR_MAGENTA, COLOR_BLACK); // 'm'
|
||||
init_pair(6, COLOR_CYAN, COLOR_BLACK); // 'c'
|
||||
init_pair(7, COLOR_WHITE, COLOR_BLACK); // 'w'
|
||||
init_pair(8, COLOR_BLACK, COLOR_BLACK); // 'k'
|
||||
}
|
||||
}
|
||||
|
||||
short colorLookup[256];
|
||||
void Aquarium::initColorLookup() {
|
||||
for (int i = 0; i < 256; ++i)
|
||||
colorLookup[i] = 8; // default to black
|
||||
|
||||
colorLookup['r'] = 1;
|
||||
colorLookup['g'] = 2;
|
||||
colorLookup['y'] = 3;
|
||||
colorLookup['b'] = 4;
|
||||
colorLookup['m'] = 5;
|
||||
colorLookup['c'] = 6;
|
||||
colorLookup['w'] = 7;
|
||||
colorLookup['k'] = 8;
|
||||
}
|
||||
|
||||
void Aquarium::resize() {
|
||||
clear();
|
||||
getmaxyx(stdscr, height, width);
|
||||
|
||||
if (g_maxCells && height * width > g_maxCells) {
|
||||
endwin(); // Cleanly shut down ncurses
|
||||
std::cerr << "Error: Terminal too large. Maximum allowed area is "
|
||||
<< g_maxCells << " cells, but current size is "
|
||||
<< (height * width) << ".\n";
|
||||
std::exit(1);
|
||||
}
|
||||
|
||||
frontBuffer.assign(height, std::vector<Cell>(width));
|
||||
backBuffer.assign(height, std::vector<Cell>(width));
|
||||
layeredMap.assign(height, std::vector<LayeredCell>(width));
|
||||
|
||||
fishes.clear();
|
||||
bubbles.clear();
|
||||
seaweeds.clear();
|
||||
|
||||
addWaterline();
|
||||
addCastle();
|
||||
addShip();
|
||||
for (int i = 0; i < width / 15; i++)
|
||||
addSeaweed();
|
||||
for (int i = 0; i < width * (height - 9) / 350; i++)
|
||||
addFish();
|
||||
}
|
||||
|
||||
Aquarium::~Aquarium() { endwin(); }
|
||||
|
||||
void Aquarium::redraw() {
|
||||
clearBackBuffer();
|
||||
|
||||
for (auto &row : layeredMap)
|
||||
for (auto &cell : row)
|
||||
cell.layers.clear();
|
||||
|
||||
for (auto it = bubbles.begin(); it != bubbles.end();) {
|
||||
auto &bubble = *it;
|
||||
bubble->draw();
|
||||
bubble->update();
|
||||
if (bubble->isOutOfWater())
|
||||
it = bubbles.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
|
||||
castle->draw();
|
||||
|
||||
for (auto it = seaweeds.begin(); it != seaweeds.end();) {
|
||||
auto &seaweed = *it;
|
||||
seaweed->draw();
|
||||
seaweed->update();
|
||||
if (seaweed->getLifetime() < 0) {
|
||||
it = seaweeds.erase(it);
|
||||
addSeaweed();
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
int baseFishLayer = 10;
|
||||
for (auto it = fishes.begin(); it != fishes.end();) {
|
||||
auto &fish = *it;
|
||||
|
||||
static_cast<const Entity *>(fish.get())
|
||||
->draw(baseFishLayer +
|
||||
static_cast<int>(std::distance(fishes.begin(), it)));
|
||||
|
||||
fish->update();
|
||||
|
||||
float fx = fish->getX();
|
||||
if (Random::floatInRange(0, 1) < BUBBLE_SPAWN_CHANCE) {
|
||||
addBubble(fx, fish->getY());
|
||||
}
|
||||
|
||||
if (fish->isOffScreen()) {
|
||||
it = fishes.erase(it);
|
||||
addFish();
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
waterline->draw();
|
||||
waterline->update();
|
||||
|
||||
static_cast<const Entity *>(ship.get())->draw(9);
|
||||
if (ship->isOffScreen())
|
||||
addShip();
|
||||
ship->update();
|
||||
|
||||
applyBackBuffer();
|
||||
}
|
||||
|
||||
void Aquarium::addBubble(size_t x, size_t y) {
|
||||
bubbles.emplace_back(std::make_unique<Bubble>(x, y));
|
||||
}
|
||||
|
||||
void Aquarium::addWaterline() { waterline = std::make_unique<Waterline>(); };
|
||||
|
||||
void Aquarium::addSeaweed() {
|
||||
seaweeds.emplace_back(std::make_unique<Seaweed>());
|
||||
}
|
||||
|
||||
void Aquarium::addCastle() { castle = std::make_unique<Castle>(); }
|
||||
|
||||
void Aquarium::addFish() { fishes.emplace_back(std::make_unique<Fish>()); }
|
||||
|
||||
void Aquarium::addShip() { ship = std::make_unique<Ship>(); }
|
||||
|
||||
void Aquarium::clearBackBuffer() {
|
||||
for (auto &row : backBuffer)
|
||||
std::fill(row.begin(), row.end(), Cell());
|
||||
}
|
||||
|
||||
inline char fastToLower(char c) { return (c >= 'A' && c <= 'Z') ? c + 32 : c; }
|
||||
|
||||
inline bool fastIsUpper(char c) { return (c >= 'A' && c <= 'Z'); }
|
||||
|
||||
void Aquarium::drawToBackBuffer(int y, int x, int layer,
|
||||
const std::string &line,
|
||||
const std::string &colorLine) {
|
||||
if (y < 0 || y >= height)
|
||||
return;
|
||||
|
||||
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 = fastIsUpper(static_cast<unsigned char>(colorChar));
|
||||
|
||||
Cell cell{
|
||||
ch,
|
||||
static_cast<char>(fastToLower(static_cast<unsigned char>(colorChar))),
|
||||
isBold};
|
||||
|
||||
auto &cellLayers = layeredMap[y][cx].layers;
|
||||
|
||||
bool replaced = false;
|
||||
for (auto &p : cellLayers) {
|
||||
if (p.first == layer) {
|
||||
p.second = cell;
|
||||
replaced = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!replaced) {
|
||||
cellLayers.emplace_back(layer, cell);
|
||||
}
|
||||
|
||||
// Set back buffer to topmost layer (assume highest layer is last)
|
||||
if (!cellLayers.empty()) {
|
||||
backBuffer[y][cx] = cellLayers.back().second;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Aquarium::removeFromBackBuffer(int y, int x, int layer,
|
||||
const std::string &line) {
|
||||
for (size_t j = 0; j < line.size(); ++j) {
|
||||
int cx = x + static_cast<int>(j);
|
||||
if (y < 0 || y >= height || cx < 0 || cx >= width)
|
||||
continue;
|
||||
|
||||
auto &cellLayers = layeredMap[y][cx].layers;
|
||||
|
||||
auto it = std::find_if(cellLayers.begin(), cellLayers.end(),
|
||||
[layer](const auto &p) { return p.first == layer; });
|
||||
|
||||
if (it != cellLayers.end())
|
||||
cellLayers.erase(it);
|
||||
|
||||
if (!cellLayers.empty())
|
||||
backBuffer[y][cx] = cellLayers.back().second;
|
||||
else
|
||||
backBuffer[y][cx] = Cell(); // Clear
|
||||
}
|
||||
}
|
||||
|
||||
void Aquarium::applyBackBuffer() {
|
||||
for (int y = 0; y < height; ++y) {
|
||||
std::string rowStr;
|
||||
int lastPair = -1;
|
||||
bool lastBold = false;
|
||||
|
||||
move(y, 0);
|
||||
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const Cell &newCell = backBuffer[y][x];
|
||||
Cell &oldCell = frontBuffer[y][x];
|
||||
|
||||
if (newCell != oldCell) {
|
||||
oldCell = newCell;
|
||||
|
||||
int pairId = colorLookup[static_cast<unsigned char>(newCell.colorChar)];
|
||||
bool bold = newCell.bold;
|
||||
|
||||
// Change attr if needed
|
||||
if (pairId != lastPair || bold != lastBold) {
|
||||
attrset(COLOR_PAIR(pairId) | (bold ? A_BOLD : A_NORMAL));
|
||||
lastPair = pairId;
|
||||
lastBold = bold;
|
||||
}
|
||||
|
||||
addch(newCell.ch);
|
||||
} else {
|
||||
// Still move the cursor to stay aligned
|
||||
move(y, x + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refresh();
|
||||
}
|
||||
100
src/Aquarium.h
100
src/Aquarium.h
@@ -1,100 +0,0 @@
|
||||
#pragma once
|
||||
#include "Bubble.h"
|
||||
#include "Castle.h"
|
||||
#include "Fish.h"
|
||||
#include "Seaweed.h"
|
||||
#include "Ship.h"
|
||||
#include "Waterline.h"
|
||||
#include <memory>
|
||||
#include <ncurses.h>
|
||||
#include <vector>
|
||||
|
||||
extern int g_maxCells;
|
||||
class Aquarium {
|
||||
private:
|
||||
int width;
|
||||
int height;
|
||||
|
||||
struct Cell {
|
||||
char ch = ' ';
|
||||
char colorChar = 'k'; // Default to black
|
||||
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); }
|
||||
};
|
||||
|
||||
struct LayeredCell {
|
||||
std::vector<std::pair<int, Cell>> layers; // Sorted by layer (ascending)
|
||||
};
|
||||
|
||||
std::vector<std::vector<Cell>> frontBuffer;
|
||||
std::vector<std::vector<Cell>> backBuffer;
|
||||
std::vector<std::vector<LayeredCell>> layeredMap;
|
||||
|
||||
inline static const std::unordered_map<char, int> colorCharToPair = {
|
||||
{'r', 1}, // Red
|
||||
{'g', 2}, // Green
|
||||
{'y', 3}, // Yellow
|
||||
{'b', 4}, // Blue
|
||||
{'m', 5}, // Magenta
|
||||
{'c', 6}, // Cyan
|
||||
{'w', 7}, // White
|
||||
{'k', 8} // Black
|
||||
};
|
||||
|
||||
inline void applyColorAttr(char colorChar, bool enable) const {
|
||||
bool bold = std::isupper(colorChar);
|
||||
char lowerChar = std::tolower(static_cast<unsigned char>(colorChar));
|
||||
|
||||
auto it = colorCharToPair.find(lowerChar);
|
||||
if (it != colorCharToPair.end()) {
|
||||
int colorPairId = it->second;
|
||||
if (enable) {
|
||||
attron(COLOR_PAIR(colorPairId));
|
||||
if (bold)
|
||||
attron(A_BOLD);
|
||||
} else {
|
||||
attroff(COLOR_PAIR(colorPairId));
|
||||
if (bold)
|
||||
attroff(A_BOLD);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<Fish>> fishes;
|
||||
std::vector<std::unique_ptr<Bubble>> bubbles;
|
||||
std::vector<std::unique_ptr<Seaweed>> seaweeds;
|
||||
std::unique_ptr<Ship> ship;
|
||||
std::unique_ptr<Waterline> waterline;
|
||||
std::unique_ptr<Castle> castle;
|
||||
|
||||
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(size_t x, size_t y);
|
||||
void addSeaweed();
|
||||
void addWaterline();
|
||||
void addCastle();
|
||||
void addShip();
|
||||
void redraw();
|
||||
void initColors();
|
||||
void initColorLookup();
|
||||
void resize();
|
||||
void clearBackBuffer();
|
||||
void drawToBackBuffer(int y, int x, int layer, const std::string &line,
|
||||
const std::string &colorLine);
|
||||
void removeFromBackBuffer(int y, int x, int layer, const std::string &line);
|
||||
void applyBackBuffer();
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
#include "Bubble.h"
|
||||
#include "Aquarium.h"
|
||||
#include <ncurses.h>
|
||||
|
||||
Bubble::Bubble(size_t x, size_t y) : x(x), y(y) {}
|
||||
|
||||
void Bubble::update() {
|
||||
--y;
|
||||
++lifetime;
|
||||
}
|
||||
|
||||
void Bubble::draw() const {
|
||||
static const std::string colorString(1, BUBBLE_COLOR);
|
||||
// Clamp frame index
|
||||
int frameIndex = std::min(lifetime / FRAMES_PER_ANIMATION, MAX_FRAME_INDEX);
|
||||
Aquarium::getInstance().drawToBackBuffer(y, x, 0, BUBBLE_FRAMES[frameIndex],
|
||||
colorString);
|
||||
}
|
||||
21
src/Bubble.h
21
src/Bubble.h
@@ -1,21 +0,0 @@
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
class Bubble {
|
||||
private:
|
||||
static constexpr const char *BUBBLE_FRAMES[3] = {".", "o", "O"};
|
||||
static constexpr int FRAMES_PER_ANIMATION = 9;
|
||||
static constexpr int MAX_FRAME_INDEX = 2;
|
||||
static constexpr char BUBBLE_COLOR = 'c';
|
||||
size_t x, y;
|
||||
int lifetime = 0;
|
||||
|
||||
public:
|
||||
Bubble(size_t x, size_t y);
|
||||
|
||||
bool isOutOfWater() const { return y < 5; }
|
||||
|
||||
void update();
|
||||
void draw() const;
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
#include "Castle.h"
|
||||
#include "Aquarium.h"
|
||||
#include "assets/CastleAssets.h"
|
||||
|
||||
Castle::Castle()
|
||||
: x(Aquarium::getInstance().getWidth() - 32),
|
||||
y(Aquarium::getInstance().getHeight() - 13), image(castleAsset.image),
|
||||
mask(castleAsset.mask) {}
|
||||
|
||||
void Castle::draw() const {
|
||||
auto &aquarium = Aquarium::getInstance();
|
||||
|
||||
for (size_t i = 0; i < image.size(); ++i) {
|
||||
std::string currentLine;
|
||||
std::string colorLine;
|
||||
currentLine.reserve(image[i].size());
|
||||
colorLine.reserve(image[i].size());
|
||||
|
||||
// Iterate over characters in the current line
|
||||
for (size_t j = 0; j < image[i].size(); ++j) {
|
||||
char ch = image[i][j];
|
||||
|
||||
char colorChar = 'K'; // default to black
|
||||
if (i < mask.size() && j < mask[i].size() && mask[i][j] != ' ')
|
||||
colorChar = mask[i][j];
|
||||
|
||||
// Store the character and color
|
||||
currentLine += ch;
|
||||
colorLine += colorChar;
|
||||
}
|
||||
|
||||
aquarium.drawToBackBuffer(y + i, x, 0, currentLine, colorLine);
|
||||
}
|
||||
}
|
||||
14
src/Castle.h
14
src/Castle.h
@@ -1,14 +0,0 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class Castle {
|
||||
private:
|
||||
const size_t x, y;
|
||||
const std::vector<std::string> image;
|
||||
const std::vector<std::string> mask;
|
||||
|
||||
public:
|
||||
Castle();
|
||||
void draw() const;
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
#include "Entity.h"
|
||||
#include "Aquarium.h"
|
||||
|
||||
void Entity::draw(int layer) const {
|
||||
auto &aquarium = Aquarium::getInstance();
|
||||
|
||||
const auto &image = getImage();
|
||||
const auto &mask = getMask();
|
||||
const char default_color = getDefaultColor();
|
||||
|
||||
std::string current_segment;
|
||||
std::string current_colors;
|
||||
current_segment.reserve(32);
|
||||
current_colors.reserve(32);
|
||||
|
||||
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] : "";
|
||||
|
||||
int cursor_x = base_x;
|
||||
current_segment.clear();
|
||||
current_colors.clear();
|
||||
|
||||
for (size_t j = 0; j < row.size(); ++j) {
|
||||
const char ch = row[j];
|
||||
|
||||
if (ch == '?') {
|
||||
// Flush current segment if not empty
|
||||
if (!current_segment.empty()) {
|
||||
aquarium.drawToBackBuffer(base_y + static_cast<int>(i), cursor_x,
|
||||
layer, current_segment, current_colors);
|
||||
cursor_x += static_cast<int>(current_segment.size());
|
||||
current_segment.clear();
|
||||
current_colors.clear();
|
||||
}
|
||||
++cursor_x; // Skip transparent character
|
||||
continue;
|
||||
}
|
||||
|
||||
current_segment.push_back(ch);
|
||||
|
||||
// Use mask color if available, otherwise use default color for spaces
|
||||
char color = default_color;
|
||||
if (j < mask_row.size()) {
|
||||
color = (mask_row[j] == ' ') ? default_color : mask_row[j];
|
||||
}
|
||||
current_colors.push_back(color);
|
||||
}
|
||||
|
||||
// Flush remaining segment
|
||||
if (!current_segment.empty()) {
|
||||
aquarium.drawToBackBuffer(base_y + static_cast<int>(i), cursor_x, layer,
|
||||
current_segment, current_colors);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/Entity.h
29
src/Entity.h
@@ -1,29 +0,0 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct AssetPair {
|
||||
std::vector<std::string> image;
|
||||
std::vector<std::string> mask;
|
||||
};
|
||||
|
||||
class Entity {
|
||||
protected:
|
||||
float x;
|
||||
float y;
|
||||
|
||||
public:
|
||||
Entity() : x(0.0f), y(0.0f) {}
|
||||
virtual ~Entity() = default;
|
||||
|
||||
float getX() const noexcept { return x; }
|
||||
float getY() const noexcept { return y; }
|
||||
|
||||
virtual void update() noexcept = 0;
|
||||
virtual bool isOffScreen() const noexcept = 0;
|
||||
virtual const std::vector<std::string> &getImage() const = 0;
|
||||
virtual const std::vector<std::string> &getMask() const = 0;
|
||||
virtual char getDefaultColor() const noexcept = 0;
|
||||
|
||||
void draw(int layer) const;
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
#include "Seaweed.h"
|
||||
#include "Aquarium.h"
|
||||
#include "Random.h"
|
||||
#include "defs.h"
|
||||
#include <ncurses.h>
|
||||
|
||||
Seaweed::Seaweed()
|
||||
: x(Random::intInRange(0, Aquarium::getInstance().getWidth())),
|
||||
y(Aquarium::getInstance().getHeight()),
|
||||
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)) {
|
||||
}
|
||||
|
||||
void Seaweed::update() noexcept {
|
||||
frame += speed;
|
||||
if (frame >= 1.0f) {
|
||||
pattern_flipped = !pattern_flipped;
|
||||
frame -= 1.0f;
|
||||
}
|
||||
--lifetime;
|
||||
}
|
||||
|
||||
void Seaweed::draw() const {
|
||||
auto &aquarium = Aquarium::getInstance();
|
||||
|
||||
std::string line(1, '\0');
|
||||
std::string colorLine(1, 'g');
|
||||
|
||||
for (size_t i = 0; i < height; ++i) {
|
||||
// Determine character and position for this segment
|
||||
const bool use_left = (i % 2 == 0) ^ pattern_flipped;
|
||||
const char ch = use_left ? PATTERN_LEFT : PATTERN_RIGHT;
|
||||
const int drawX = static_cast<int>(x) + (use_left ? 0 : 1);
|
||||
const int drawY = y - static_cast<int>(i);
|
||||
|
||||
line[0] = ch;
|
||||
aquarium.drawToBackBuffer(drawY, drawX, 0, line, colorLine);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
|
||||
class Seaweed {
|
||||
private:
|
||||
const size_t x, y;
|
||||
static constexpr char PATTERN_LEFT = '(';
|
||||
static constexpr char PATTERN_RIGHT = ')';
|
||||
|
||||
const size_t height;
|
||||
const float speed;
|
||||
|
||||
float frame = 0.0f;
|
||||
size_t lifetime;
|
||||
bool pattern_flipped = false;
|
||||
|
||||
public:
|
||||
Seaweed();
|
||||
|
||||
size_t getLifetime() const noexcept { return lifetime; }
|
||||
void update() noexcept;
|
||||
void draw() const;
|
||||
};
|
||||
39
src/Ship.cpp
39
src/Ship.cpp
@@ -1,39 +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(), image(pirateShipAssets[asset_index].image),
|
||||
mask(pirateShipAssets[asset_index].mask), speed(SHIP_SPEED),
|
||||
moving_right(asset_index == 0) {
|
||||
const auto &aquarium = Aquarium::getInstance();
|
||||
// Ships move along the water surface (top of aquarium)
|
||||
y = WATER_SURFACE_OFFSET;
|
||||
// Start off-screen on appropriate side
|
||||
if (moving_right) {
|
||||
x = -static_cast<float>(
|
||||
image[0].length()); // Start completely off left side
|
||||
} else {
|
||||
x = static_cast<float>(aquarium.getWidth()); // Start off right side
|
||||
}
|
||||
}
|
||||
|
||||
int Ship::getRandomDirection() {
|
||||
return Random::intInRange(0, 1); // 0 = right, 1 = left
|
||||
}
|
||||
|
||||
void Ship::update() noexcept { x += moving_right ? speed : -speed; }
|
||||
|
||||
bool Ship::isOffScreen() const noexcept {
|
||||
const auto &aquarium = Aquarium::getInstance();
|
||||
if (moving_right) {
|
||||
// Ship is off screen when its left edge is past the right border
|
||||
return x > static_cast<float>(aquarium.getWidth());
|
||||
} else {
|
||||
// Ship is off screen when its right edge is past the left border
|
||||
return (x + static_cast<float>(image[0].length())) < 0;
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
#include "Waterline.h"
|
||||
#include "Aquarium.h"
|
||||
#include "Random.h"
|
||||
#include "defs.h"
|
||||
#include <algorithm>
|
||||
|
||||
Waterline::Waterline() : x(0), y(WATERLINE_Y) {
|
||||
shape = {
|
||||
"~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~", "^^^^ ^^^ ^^^ ^^^ ^^^^ ",
|
||||
"^^^^ ^^^^ ^^^ ^^ ", "^^ ^^^^ ^^^ ^^^^^^ "};
|
||||
|
||||
const size_t width = Aquarium::getInstance().getWidth();
|
||||
for (auto &line : shape) {
|
||||
const std::string original = line;
|
||||
while (line.size() < width) {
|
||||
line += original;
|
||||
}
|
||||
colorLines.emplace_back(line.size(), WATERLINE_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
void Waterline::draw() const {
|
||||
for (size_t i = 0; i < shape.size(); ++i) {
|
||||
Aquarium::getInstance().drawToBackBuffer(y + static_cast<int>(i), x, 0,
|
||||
shape[i], colorLines[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void Waterline::update() {
|
||||
// Skip the first line (index 0) as it's static
|
||||
for (size_t i = 1; i < shape.size(); ++i) {
|
||||
// Probability increases with depth (higher index = more movement)
|
||||
float movementChance =
|
||||
static_cast<float>(i) / static_cast<float>(shape.size());
|
||||
float threshold = movementChance / WAVE_MOVE_CHANCE;
|
||||
|
||||
if (Random::floatInRange(0.0f, 1.0f) < threshold) {
|
||||
int direction = Random::intInRange(0, 1) == 0 ? -1 : 1;
|
||||
shiftString(shape[i], direction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Waterline::shiftString(std::string &str, int direction) {
|
||||
if (direction > 0) {
|
||||
std::rotate(str.rbegin(), str.rbegin() + 1, str.rend());
|
||||
} else {
|
||||
std::rotate(str.begin(), str.begin() + 1, str.end());
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class Waterline {
|
||||
private:
|
||||
static constexpr int WATERLINE_Y = 5;
|
||||
static constexpr char WATERLINE_COLOR = 'c';
|
||||
|
||||
size_t x, y;
|
||||
std::vector<std::string> shape;
|
||||
std::vector<std::string> colorLines;
|
||||
|
||||
void shiftString(std::string &str, int direction);
|
||||
void initializeShape();
|
||||
|
||||
public:
|
||||
Waterline();
|
||||
void draw() const;
|
||||
void update();
|
||||
};
|
||||
@@ -8,7 +8,9 @@ struct CastleAsset {
|
||||
std::vector<std::string> mask;
|
||||
};
|
||||
|
||||
inline CastleAsset castleAsset = {{
|
||||
inline const CastleAsset& getCastleAsset() {
|
||||
static const CastleAsset castleAsset = {
|
||||
{
|
||||
R"( T~~)",
|
||||
R"( |)",
|
||||
R"( /^\)",
|
||||
@@ -22,7 +24,7 @@ inline CastleAsset castleAsset = {{
|
||||
R"( |= []- |- /| |\ |=_ =[] |)",
|
||||
R"( |- =_ | =| | | | |- = - |)",
|
||||
R"( |_______|__|_|_|_|__|_______|)"},
|
||||
{
|
||||
{
|
||||
R"( RR)",
|
||||
R"()",
|
||||
R"( yyy)",
|
||||
@@ -35,4 +37,7 @@ inline CastleAsset castleAsset = {{
|
||||
R"( yyy)",
|
||||
R"( yy yy)",
|
||||
R"( y y y y)",
|
||||
R"( yyyyyyy)"}};
|
||||
R"( yyyyyyy)"}
|
||||
};
|
||||
return castleAsset;
|
||||
}
|
||||
|
||||
@@ -1,235 +1,136 @@
|
||||
#pragma once
|
||||
|
||||
#include "../Entity.h"
|
||||
#include "../entities/Entity.h"
|
||||
#include "../utils/SpriteUtils.h"
|
||||
#include <vector>
|
||||
|
||||
inline std::vector<AssetPair> fishAssetPairs = {
|
||||
{
|
||||
/*
|
||||
Mask Definitions:
|
||||
1: body
|
||||
2: dorsal fin
|
||||
3: flippers
|
||||
4: eye
|
||||
5: mouth
|
||||
6: tailfin
|
||||
7: gills
|
||||
*/
|
||||
|
||||
inline const std::vector<AssetPair>& getFishAssets() {
|
||||
static const std::vector<AssetPair> fishAssets = {
|
||||
{
|
||||
{
|
||||
R"(???\)",
|
||||
R"(??/ \)",
|
||||
R"(>=_('>)",
|
||||
R"(??\_/)",
|
||||
R"(???/)"
|
||||
},
|
||||
R"(???\??)",
|
||||
R"(??/ \?)",
|
||||
R"(>=_('>)",
|
||||
R"(??\_/?)",
|
||||
R"(???/??)"},
|
||||
{
|
||||
R"( 1)",
|
||||
R"( 1 1)",
|
||||
R"( 1 )",
|
||||
R"( 1 1 )",
|
||||
R"(663745)",
|
||||
R"( 111)",
|
||||
R"( 3)"
|
||||
}
|
||||
},
|
||||
{
|
||||
{
|
||||
R"(??/)",
|
||||
R"(?/ \)",
|
||||
R"(<')_=<)",
|
||||
R"(?\_/)",
|
||||
R"(??\)"
|
||||
R"( 111 )",
|
||||
R"( 3 )"}},
|
||||
{
|
||||
{
|
||||
R"(?????,????)",
|
||||
R"(?????}\???)",
|
||||
R"(\??.' `\?)",
|
||||
R"(}}< ( 6>)",
|
||||
R"(/??`, .'?)",
|
||||
R"(?????}/???)",
|
||||
R"(?????'????)"},
|
||||
{
|
||||
R"( 2 )",
|
||||
R"( 22 )",
|
||||
R"(6 11 11 )",
|
||||
R"(661 7 45)",
|
||||
R"(6 11 11 )",
|
||||
R"( 33 )",
|
||||
R"( 3 )"}},
|
||||
{
|
||||
{
|
||||
R"(???????,--,_????)",
|
||||
R"(__????_\.---'-.?)",
|
||||
R"(\ '.-" // o\)",
|
||||
R"(/_.'-._ \\ /)",
|
||||
R"(???????`"--(/"`?)"},
|
||||
{
|
||||
R"( 22222 )",
|
||||
R"(66 121111211 )",
|
||||
R"(6 6111 77 41)",
|
||||
R"(6661111 77 1)",
|
||||
R"( 11113311 )"}},
|
||||
{
|
||||
{
|
||||
R"(??__?)",
|
||||
R"(><_'>)",
|
||||
R"(???'?)"},
|
||||
{
|
||||
R"( 11 )",
|
||||
R"(61145)",
|
||||
R"( 3 )"}},
|
||||
{
|
||||
{
|
||||
R"(????????_.-`\??????)",
|
||||
R"(?????-:`_..,_\?????)",
|
||||
R"(('-..:-` , '-.,?)",
|
||||
R"(?} _ ;':( o :)",
|
||||
R"((.-`/'-.,__'` _.-`?)",
|
||||
R"(???`'-.,/??//`?????)"},
|
||||
{
|
||||
R"( 22222 )",
|
||||
R"( 222111112 )",
|
||||
R"(66661111 7 1111 )",
|
||||
R"( 6 1 7777 4 1)",
|
||||
R"(6666211111177 1111 )",
|
||||
R"( 222222 333 )"}},
|
||||
{
|
||||
{
|
||||
R"(????????/\??????)",
|
||||
R"(????????\.\_????)",
|
||||
R"(\'-,.:-` '-,?)",
|
||||
R"( ) _ (>( o <)",
|
||||
R"(/.-`?':._ _.-`?)",
|
||||
R"(??????;/?``?????)",
|
||||
},
|
||||
{
|
||||
R"( 2)",
|
||||
R"( 111)",
|
||||
R"(547366)",
|
||||
R"( 111)",
|
||||
R"( 3)"
|
||||
}
|
||||
},
|
||||
{
|
||||
{
|
||||
R"(?????,)",
|
||||
R"(?????}\)",
|
||||
R"(\??.' `\)",
|
||||
R"(}}< ( 6>)",
|
||||
R"(/??`, .')",
|
||||
R"(?????}/)",
|
||||
R"(?????')"
|
||||
},
|
||||
{
|
||||
R"( 2)",
|
||||
R"( 22)",
|
||||
R"(6 11 11)",
|
||||
R"(661 7 45)",
|
||||
R"(6 11 11)",
|
||||
R"( 33)",
|
||||
R"( 3)"
|
||||
}
|
||||
},
|
||||
{
|
||||
{
|
||||
R"(????,)",
|
||||
R"(???/{)",
|
||||
R"(?/' `. /)",
|
||||
R"(<6 ) >{{)",
|
||||
R"(?`. ,' \)",
|
||||
R"(???\{)",
|
||||
R"(????`)"
|
||||
},
|
||||
{
|
||||
R"( 2)",
|
||||
R"( 22)",
|
||||
R"( 11 11 6)",
|
||||
R"(54 7 166)",
|
||||
R"( 11 11 6)",
|
||||
R"( 33)",
|
||||
R"( 3)"
|
||||
}
|
||||
},
|
||||
{
|
||||
{
|
||||
R"(???????,--,_)",
|
||||
R"(__????_\.---'-.)",
|
||||
R"(\ '.-" // o\)",
|
||||
R"(/_.'-._ \\ /)",
|
||||
R"(???????`"--(/"`)"
|
||||
},
|
||||
{
|
||||
R"( 22222)",
|
||||
R"(66 121111211)",
|
||||
R"(6 6111 77 41)",
|
||||
R"(6661111 77 1)",
|
||||
R"( 11113311)"
|
||||
}
|
||||
},
|
||||
{
|
||||
{
|
||||
R"(????_,--,)",
|
||||
R"(?.-'---./_????__)",
|
||||
R"(/o \\ "-.' /)",
|
||||
R"(\ // _.-'._\)",
|
||||
R"(?`"\)--"`)"
|
||||
},
|
||||
{
|
||||
R"( 22222)",
|
||||
R"( 112111121 66)",
|
||||
R"(14 77 1116 6)",
|
||||
R"(1 77 1111666)",
|
||||
R"( 11331111)"
|
||||
}
|
||||
},
|
||||
{
|
||||
{
|
||||
R"(??__)",
|
||||
R"(><_'>)",
|
||||
R"(???')"
|
||||
},
|
||||
{
|
||||
R"( 11)",
|
||||
R"(61145)",
|
||||
R"( 3)"
|
||||
}
|
||||
},
|
||||
{
|
||||
{
|
||||
R"(?__)",
|
||||
R"(<'_><)",
|
||||
R"(?`)"
|
||||
},
|
||||
{
|
||||
R"( 11)",
|
||||
R"(54116)",
|
||||
R"( 3)"
|
||||
}
|
||||
},
|
||||
{
|
||||
{
|
||||
R"(????????_.-`\)",
|
||||
R"(?????-:`_..,_\)",
|
||||
R"(('-..:-` , '-.,)",
|
||||
R"(?} _ ;':( o :)",
|
||||
R"((.-`/'-.,__'` _.-`)",
|
||||
R"(???`'-.,/??//`)"
|
||||
},
|
||||
{
|
||||
R"( 22222)",
|
||||
R"( 222111112)",
|
||||
R"(66661111 7 1111)",
|
||||
R"( 6 1 7777 4 1)",
|
||||
R"(6666211111177 1111)",
|
||||
R"( 222222 333)"
|
||||
}
|
||||
},
|
||||
R"( 22 )",
|
||||
R"( 2121 )",
|
||||
R"(66661111 111 )",
|
||||
R"( 6 1 777 4 1)",
|
||||
R"(6666 1111 1111 )",
|
||||
R"( 22 33 )",}},
|
||||
{
|
||||
{
|
||||
R"(_?????????_.*"\??????)",
|
||||
R"(\'-._..-*` `'*-.??)",
|
||||
R"(?) , (( o >)",
|
||||
R"(/.`"*--.__)_.`_.-*`??)"},
|
||||
{
|
||||
R"(6 11222 )",
|
||||
R"(6661111111 11111 )",
|
||||
R"( 6 3 77 4 1)",
|
||||
R"(6661111111311311111 )",}},
|
||||
{
|
||||
R"(??????/`-._)",
|
||||
R"(?????/_,.._`:-)",
|
||||
R"(?,.-' , `-:..-'))",
|
||||
R"(: o ):'; _ {)",
|
||||
R"(?`-._ `'__,.-'\`-.))",
|
||||
R"(?????`\\??\,.-'`)"
|
||||
},
|
||||
{
|
||||
R"( 22222)",
|
||||
R"( 211111222)",
|
||||
R"( 1111 7 11116666)",
|
||||
R"(1 4 7777 1 6)",
|
||||
R"( 1111 7711111126666)",
|
||||
R"( 333 222222)"
|
||||
}
|
||||
},
|
||||
{
|
||||
{
|
||||
R"(????????/\)",
|
||||
R"(????????\.\_)",
|
||||
R"(\'-,.:-` '-,)",
|
||||
R"( ) _ (>( o <)",
|
||||
R"(/.-`?':._ _.-`)",
|
||||
R"(??????;/?``)",
|
||||
},
|
||||
{
|
||||
R"( 22)",
|
||||
R"( 2121)",
|
||||
R"(66661111 111)",
|
||||
R"( 6 1 777 4 1)",
|
||||
R"(6666 1111 1111)",
|
||||
R"( 22 33)",
|
||||
}
|
||||
},
|
||||
{
|
||||
{
|
||||
R"(??????/\)",
|
||||
R"(????_/./)",
|
||||
R"(?,-' `-:.,-'/)",
|
||||
R"(> o )<) _ ()",
|
||||
R"(?`-._ _.:'?`-.\)",
|
||||
R"(?????``?\;)",
|
||||
},
|
||||
{
|
||||
R"( 22)",
|
||||
R"( 1212)",
|
||||
R"( 111 11116666)",
|
||||
R"(1 4 777 1 6)",
|
||||
R"( 1111 1111 6666)",
|
||||
R"( 33 22)",
|
||||
}
|
||||
},
|
||||
{
|
||||
{
|
||||
R"(_?????????_.*"\)",
|
||||
R"(\'-._..-*` `'*-.)",
|
||||
R"(?) , (( o >)",
|
||||
R"(/.`"*--.__)_.`_.-*`)"
|
||||
},
|
||||
{
|
||||
R"(6 11222)",
|
||||
R"(6661111111 11111)",
|
||||
R"( 6 3 77 4 1)",
|
||||
R"(6661111111311311111)",
|
||||
}
|
||||
},
|
||||
{
|
||||
{
|
||||
R"(??????/"*._?????????_)",
|
||||
R"(??.-*'` `*-.._.-'/)",
|
||||
R"(< o )) , ()",
|
||||
R"(??`*-._`._(__.--*"`.\)",
|
||||
},
|
||||
{
|
||||
R"( 22211 6)",
|
||||
R"( 11111 1111111666)",
|
||||
R"(1 4 77 3 6)",
|
||||
R"( 1111131131111111666)",
|
||||
},
|
||||
}
|
||||
};
|
||||
{
|
||||
R"(???????,-.?????)",
|
||||
R"(__ ????) \.????)",
|
||||
R"(\ \_.!` '-.?)",
|
||||
R"(?> _ |<( O <)",
|
||||
R"(/_/?';,,....-`?)",
|
||||
},
|
||||
{
|
||||
R"( 222 )",
|
||||
R"(66 2 22 )",
|
||||
R"(6 61111 111 )",
|
||||
R"( 6 1 777 4 5)",
|
||||
R"(666 '333111111 )",}}
|
||||
};
|
||||
return fishAssets;
|
||||
}
|
||||
|
||||
inline const std::vector<AssetPair>& getFishAssetPairs() {
|
||||
static const std::vector<AssetPair> fishAssetPairs =
|
||||
createBidirectionalAssets(getFishAssets());
|
||||
return fishAssetPairs;
|
||||
}
|
||||
|
||||
38
src/assets/SeaMonsterAssets.h
Normal file
38
src/assets/SeaMonsterAssets.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
#include "../entities/Entity.h"
|
||||
#include "../utils/SpriteUtils.h"
|
||||
#include <vector>
|
||||
|
||||
struct SeaMonsterAsset {
|
||||
std::vector<std::vector<std::string>> frames;
|
||||
std::vector<std::string> mask;
|
||||
};
|
||||
|
||||
inline const SeaMonsterAsset& getSeaMonster() {
|
||||
static const SeaMonsterAsset seaMonster = {
|
||||
{
|
||||
{
|
||||
R"(?????????_???_?????????????????????_???_???????_a_a???)",
|
||||
R"(???????_{.`=`.}_??????_???_??????_{.`=`.}_????{/ ''\_?)",
|
||||
R"(?_????{.' _ '.}????{.`'`.}????{.' _ '.}??{| ._oo))",
|
||||
R"({ \??{/ .'?'. \}??{/ .-. \}??{/ .'?'. \}?{/ |????)"},
|
||||
{
|
||||
R"(??????????????????????_???_????????????????????_a_a???)",
|
||||
R"(??_??????_???_??????_{.`=`.}_??????_???_??????{/ ''\_?)",
|
||||
R"(?{ \????{.`'`.}????{.' _ '.}????{.`'`.}????{| ._oo))",
|
||||
R"(??\ \??{/ .-. \}??{/ .'?'. \}??{/ .-. \}???{/ |????)"}},
|
||||
{
|
||||
R"( W W )",
|
||||
R"()",
|
||||
R"()",
|
||||
R"()"
|
||||
}
|
||||
};
|
||||
return seaMonster;
|
||||
}
|
||||
|
||||
inline const std::vector<SeaMonsterAsset>& getSeaMonsterAssets() {
|
||||
static const std::vector<SeaMonsterAsset> seaMonsterAssets =
|
||||
createBidirectionalFramedAssets(getSeaMonster());
|
||||
return seaMonsterAssets;
|
||||
}
|
||||
@@ -1,37 +1,29 @@
|
||||
#pragma once
|
||||
#include "../Entity.h"
|
||||
#include "../entities/Entity.h"
|
||||
#include "../utils/SpriteUtils.h"
|
||||
#include <vector>
|
||||
|
||||
inline std::vector<AssetPair> pirateShipAssets = {
|
||||
{{
|
||||
R"( | | |)",
|
||||
R"( )_) )_) )_))",
|
||||
R"( )___))___))___)\)",
|
||||
R"( )____)____)_____)\\)",
|
||||
R"(_____|____|____|____\\\__)",
|
||||
R"(\ /)",
|
||||
},
|
||||
{
|
||||
R"( y y y)",
|
||||
R"()",
|
||||
R"( w)",
|
||||
R"( ww)",
|
||||
R"(yyyyyyyyyyyyyyyyyyyywwwyy)",
|
||||
R"(y y)",
|
||||
}},
|
||||
{{
|
||||
R"( | | |)",
|
||||
R"( (_( (_( (_()",
|
||||
R"( /(___((___((___()",
|
||||
R"( //(_____(____(____()",
|
||||
R"(__///____|____|____|_____)",
|
||||
R"( \ /)",
|
||||
},
|
||||
{
|
||||
R"( y y y)",
|
||||
R"()",
|
||||
R"( w)",
|
||||
R"( ww)",
|
||||
R"(yywwwyyyyyyyyyyyyyyyyyyyy)",
|
||||
R"( y y)",
|
||||
}}};
|
||||
inline const AssetPair &getShip() {
|
||||
static const AssetPair ship = {
|
||||
{
|
||||
R"( | | | )",
|
||||
R"( )_) )_) )_) )",
|
||||
R"( )___))___))___)\ )",
|
||||
R"( )____)____)_____)\\ )",
|
||||
R"(_____|____|____|____\\\__)",
|
||||
R"(\ /????)"},
|
||||
{
|
||||
R"( y y y )",
|
||||
R"()",
|
||||
R"( w )",
|
||||
R"( ww )",
|
||||
R"(yyyyyyyyyyyyyyyyyyyywwwyy)",
|
||||
R"(y y )"}};
|
||||
return ship;
|
||||
}
|
||||
|
||||
inline const std::vector<AssetPair> &getShipAssets() {
|
||||
static const std::vector<AssetPair> shipAssets =
|
||||
createBidirectionalAssets({getShip()});
|
||||
return shipAssets;
|
||||
}
|
||||
|
||||
104
src/assets/WhaleAssets.h
Normal file
104
src/assets/WhaleAssets.h
Normal file
@@ -0,0 +1,104 @@
|
||||
#pragma once
|
||||
#include "../entities/Entity.h"
|
||||
#include "../utils/SpriteUtils.h"
|
||||
#include <vector>
|
||||
|
||||
struct WhaleAsset {
|
||||
std::vector<std::vector<std::string>> frames;
|
||||
std::vector<std::string> mask;
|
||||
};
|
||||
|
||||
inline const WhaleAsset& getWhale() {
|
||||
static const WhaleAsset whale = {
|
||||
{
|
||||
{
|
||||
R"( )",
|
||||
R"()",
|
||||
R"()",
|
||||
R"( .-----. )",
|
||||
R"( .' `. )",
|
||||
R"(,????/ (o) \?)",
|
||||
R"(\`._/ ,__))"
|
||||
},
|
||||
{
|
||||
R"( )",
|
||||
R"()",
|
||||
R"( : )",
|
||||
R"( .-----. )",
|
||||
R"( .' `. )",
|
||||
R"(,????/ (o) \?)",
|
||||
R"(\`._/ ,__))"
|
||||
},
|
||||
{
|
||||
R"( )",
|
||||
R"( : )",
|
||||
R"( : )",
|
||||
R"( .-----. )",
|
||||
R"( .' `. )",
|
||||
R"(,????/ (o) \?)",
|
||||
R"(\`._/ ,__))"
|
||||
},
|
||||
{
|
||||
R"( . . )",
|
||||
R"( -:- )",
|
||||
R"( : )",
|
||||
R"( .-----. )",
|
||||
R"( .' `. )",
|
||||
R"(,????/ (o) \?)",
|
||||
R"(\`._/ ,__))"
|
||||
},
|
||||
{
|
||||
R"( . . )",
|
||||
R"( .-.-. )",
|
||||
R"( : )",
|
||||
R"( .-----. )",
|
||||
R"( .' `. )",
|
||||
R"(,????/ (o) \?)",
|
||||
R"(\`._/ ,__))"
|
||||
},
|
||||
{
|
||||
R"( . . )",
|
||||
R"( '.-:-.' )",
|
||||
R"( ' : ' )",
|
||||
R"( .-----. )",
|
||||
R"( .' `. )",
|
||||
R"(,????/ (o) \?)",
|
||||
R"(\`._/ ,__))"
|
||||
},
|
||||
{
|
||||
R"( )",
|
||||
R"( .- -. )",
|
||||
R"( ; : ; )",
|
||||
R"( .-----. )",
|
||||
R"( .' `. )",
|
||||
R"(,????/ (o) \?)",
|
||||
R"(\`._/ ,__))"
|
||||
},
|
||||
{
|
||||
R"( )",
|
||||
R"( )",
|
||||
R"( ; ; )",
|
||||
R"( .-----. )",
|
||||
R"( .' `. )",
|
||||
R"(,????/ (o) \?)",
|
||||
R"(\`._/ ,__))"
|
||||
}
|
||||
},
|
||||
{
|
||||
R"( C C )",
|
||||
R"( CCCCCCC )",
|
||||
R"( C C C )",
|
||||
R"()",
|
||||
R"()",
|
||||
R"( W )",
|
||||
R"()"
|
||||
}
|
||||
};
|
||||
return whale;
|
||||
}
|
||||
|
||||
inline const std::vector<WhaleAsset>& getWhaleAssets() {
|
||||
static const std::vector<WhaleAsset> whaleAssets =
|
||||
createBidirectionalFramedAssets(getWhale());
|
||||
return whaleAssets;
|
||||
}
|
||||
423
src/core/Aquarium.cpp
Normal file
423
src/core/Aquarium.cpp
Normal file
@@ -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); }
|
||||
75
src/core/Aquarium.h
Normal file
75
src/core/Aquarium.h
Normal file
@@ -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;
|
||||
}
|
||||
};
|
||||
175
src/core/Config.cpp
Normal file
175
src/core/Config.cpp
Normal file
@@ -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;
|
||||
}
|
||||
33
src/core/Config.h
Normal file
33
src/core/Config.h
Normal file
@@ -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;
|
||||
115
src/core/main.cpp
Normal file
115
src/core/main.cpp
Normal file
@@ -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;
|
||||
}
|
||||
27
src/entities/Bubble.cpp
Normal file
27
src/entities/Bubble.cpp
Normal file
@@ -0,0 +1,27 @@
|
||||
#include "Bubble.h"
|
||||
|
||||
Bubble::Bubble(float x, float y) : Entity(x, y) {
|
||||
current_image.resize(1);
|
||||
current_mask.resize(1);
|
||||
updateFrame();
|
||||
}
|
||||
|
||||
void Bubble::update() noexcept {
|
||||
--y;
|
||||
++lifetime;
|
||||
updateFrame();
|
||||
}
|
||||
|
||||
void Bubble::updateFrame() {
|
||||
int frameIndex = std::min(lifetime / FRAMES_PER_ANIMATION, MAX_FRAME_INDEX);
|
||||
current_image[0] = BUBBLE_FRAMES[frameIndex];
|
||||
current_mask[0] = BUBBLE_COLOR;
|
||||
}
|
||||
|
||||
// Waterline collision
|
||||
bool Bubble::shouldBeRemoved() const noexcept { return y < 10; }
|
||||
|
||||
// Bubbles don't create replacements
|
||||
std::unique_ptr<Entity> Bubble::createReplacement() const { return nullptr; }
|
||||
|
||||
int Bubble::getPreferredLayer() const noexcept { return 5; }
|
||||
33
src/entities/Bubble.h
Normal file
33
src/entities/Bubble.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
#include "Entity.h"
|
||||
#include <string>
|
||||
|
||||
class Bubble : public Entity {
|
||||
private:
|
||||
static constexpr const char *BUBBLE_FRAMES[3] = {".", "o", "O"};
|
||||
static constexpr int FRAMES_PER_ANIMATION = 9;
|
||||
static constexpr int MAX_FRAME_INDEX = 2;
|
||||
static constexpr char BUBBLE_COLOR = 'c';
|
||||
|
||||
int lifetime = 0;
|
||||
std::vector<std::string> current_image;
|
||||
std::vector<std::string> current_mask;
|
||||
|
||||
void updateFrame();
|
||||
|
||||
public:
|
||||
Bubble(float x, float y);
|
||||
|
||||
void update() noexcept override;
|
||||
const std::vector<std::string> &getImage() const override {
|
||||
return current_image;
|
||||
}
|
||||
const std::vector<std::string> &getMask() const override {
|
||||
return current_mask;
|
||||
}
|
||||
char getDefaultColor() const noexcept override { return BUBBLE_COLOR; }
|
||||
|
||||
bool shouldBeRemoved() const noexcept override;
|
||||
std::unique_ptr<Entity> createReplacement() const override;
|
||||
int getPreferredLayer() const noexcept override;
|
||||
};
|
||||
8
src/entities/Castle.cpp
Normal file
8
src/entities/Castle.cpp
Normal file
@@ -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) {}
|
||||
19
src/entities/Castle.h
Normal file
19
src/entities/Castle.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
#include "Entity.h"
|
||||
|
||||
class Castle : public Entity {
|
||||
private:
|
||||
const std::vector<std::string> image;
|
||||
const std::vector<std::string> mask;
|
||||
|
||||
public:
|
||||
Castle();
|
||||
|
||||
void update() noexcept override {} // Castle doesn't move
|
||||
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 { return nullptr; }
|
||||
int getPreferredLayer() const noexcept override { return 0; }
|
||||
};
|
||||
63
src/entities/Entity.cpp
Normal file
63
src/entities/Entity.cpp
Normal file
@@ -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();
|
||||
}
|
||||
42
src/entities/Entity.h
Normal file
42
src/entities/Entity.h
Normal file
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct AssetPair {
|
||||
std::vector<std::string> image;
|
||||
std::vector<std::string> mask;
|
||||
};
|
||||
|
||||
class Entity {
|
||||
protected:
|
||||
const bool moving_right;
|
||||
float x;
|
||||
float y;
|
||||
static inline size_t next_id = 0;
|
||||
const size_t entity_id;
|
||||
virtual const std::vector<std::string> &getImage() const = 0;
|
||||
virtual const std::vector<std::string> &getMask() const = 0;
|
||||
virtual char getDefaultColor() const noexcept = 0;
|
||||
|
||||
public:
|
||||
Entity() : moving_right(false), x(0.0f), y(0.0f), entity_id(++next_id) {}
|
||||
Entity(bool moving_right)
|
||||
: moving_right(moving_right), x(0.0f), y(0.0f), entity_id(++next_id) {}
|
||||
Entity(float init_x, float init_y)
|
||||
: moving_right(false), x(init_x), y(init_y), entity_id(++next_id) {}
|
||||
virtual ~Entity() = default;
|
||||
|
||||
float getX() const noexcept { return x; }
|
||||
float getY() const noexcept { return y; }
|
||||
virtual int getWidth() const noexcept { return getImage()[0].length(); }
|
||||
size_t getId() const noexcept { return entity_id; }
|
||||
|
||||
virtual void update() noexcept = 0;
|
||||
|
||||
virtual bool shouldBeRemoved() const noexcept;
|
||||
virtual std::unique_ptr<Entity> createReplacement() const { return nullptr; }
|
||||
virtual int getPreferredLayer() const noexcept = 0;
|
||||
|
||||
void draw() const;
|
||||
};
|
||||
@@ -1,29 +1,32 @@
|
||||
#include "Fish.h"
|
||||
#include "Aquarium.h"
|
||||
#include "Random.h"
|
||||
#include "assets/FishAssets.h"
|
||||
#include <ncurses.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(), image(fishAssetPairs[asset_index].image),
|
||||
mask(fishAssetPairs[asset_index].mask),
|
||||
speed(Random::floatInRange(0.25f, 2.25f)),
|
||||
moving_right(asset_index % 2 == 0) {
|
||||
: 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 ? -20.0f : static_cast<float>(aquarium.getWidth());
|
||||
x = moving_right ? -this->getWidth()
|
||||
: static_cast<float>(aquarium.getWidth());
|
||||
randomizeMask();
|
||||
}
|
||||
|
||||
void Fish::randomizeMask() {
|
||||
// Clear and rebuild color map
|
||||
// Clear and rebuild color map with random colors each time
|
||||
color_map.clear();
|
||||
color_map['4'] = 'W'; // White is always '4'
|
||||
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') {
|
||||
@@ -31,6 +34,7 @@ void Fish::randomizeMask() {
|
||||
0, static_cast<int>(AVAILABLE_COLORS.size()) - 1)];
|
||||
}
|
||||
}
|
||||
|
||||
// Apply color mapping to mask
|
||||
for (auto &line : mask) {
|
||||
for (char &ch : line) {
|
||||
@@ -42,18 +46,18 @@ void Fish::randomizeMask() {
|
||||
}
|
||||
|
||||
int Fish::getRandomAssetIndex() {
|
||||
return Random::intInRange(0, static_cast<int>(fishAssetPairs.size()) - 1);
|
||||
return Random::intInRange(0,
|
||||
static_cast<int>(getFishAssetPairs().size()) - 1);
|
||||
}
|
||||
|
||||
void Fish::update() noexcept { x += moving_right ? speed : -speed; }
|
||||
|
||||
bool Fish::isOffScreen() const noexcept {
|
||||
const auto &aquarium = Aquarium::getInstance();
|
||||
if (moving_right) {
|
||||
// Fish is off screen when its left edge is past the right border
|
||||
return x > static_cast<float>(aquarium.getWidth());
|
||||
} else {
|
||||
// Fish is off screen when its right edge is past the left border
|
||||
return (x + static_cast<float>(image[0].length())) < 0;
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
#include "../assets/FishAssets.h"
|
||||
#include "Entity.h"
|
||||
#include "assets/FishAssets.h"
|
||||
#include <array>
|
||||
#include <unordered_map>
|
||||
|
||||
@@ -12,7 +12,6 @@ private:
|
||||
const std::vector<std::string> ℑ
|
||||
std::vector<std::string> mask;
|
||||
const float speed;
|
||||
const bool moving_right;
|
||||
|
||||
static std::unordered_map<char, char> color_map;
|
||||
|
||||
@@ -24,8 +23,12 @@ public:
|
||||
Fish();
|
||||
|
||||
void update() noexcept override;
|
||||
bool isOffScreen() const 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;
|
||||
};
|
||||
44
src/entities/SeaMonster.cpp
Normal file
44
src/entities/SeaMonster.cpp
Normal file
@@ -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; }
|
||||
32
src/entities/SeaMonster.h
Normal file
32
src/entities/SeaMonster.h
Normal file
@@ -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;
|
||||
};
|
||||
63
src/entities/Seaweed.cpp
Normal file
63
src/entities/Seaweed.cpp
Normal file
@@ -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; }
|
||||
33
src/entities/Seaweed.h
Normal file
33
src/entities/Seaweed.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
#include "Entity.h"
|
||||
|
||||
class Seaweed : public Entity {
|
||||
private:
|
||||
static constexpr char PATTERN_LEFT = '(';
|
||||
static constexpr char PATTERN_RIGHT = ')';
|
||||
|
||||
const size_t height;
|
||||
const float speed;
|
||||
|
||||
float frame = 0.0f;
|
||||
size_t lifetime;
|
||||
bool pattern_flipped = false;
|
||||
mutable bool frame_dirty = true;
|
||||
|
||||
mutable std::vector<std::string> current_image;
|
||||
mutable std::vector<std::string> current_mask;
|
||||
|
||||
void generateCurrentFrame() const;
|
||||
|
||||
public:
|
||||
Seaweed();
|
||||
|
||||
void update() noexcept override;
|
||||
const std::vector<std::string> &getImage() const override;
|
||||
const std::vector<std::string> &getMask() const override;
|
||||
char getDefaultColor() const noexcept override { return 'g'; }
|
||||
|
||||
bool shouldBeRemoved() const noexcept override;
|
||||
std::unique_ptr<Entity> createReplacement() const override;
|
||||
int getPreferredLayer() const noexcept override;
|
||||
};
|
||||
25
src/entities/Ship.cpp
Normal file
25
src/entities/Ship.cpp
Normal file
@@ -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; }
|
||||
@@ -1,16 +1,14 @@
|
||||
#pragma once
|
||||
#include "../assets/ShipAssets.h"
|
||||
#include "Entity.h"
|
||||
#include "assets/ShipAssets.h"
|
||||
|
||||
class Ship : public Entity {
|
||||
private:
|
||||
static constexpr float SHIP_SPEED = 1.0f;
|
||||
static constexpr int WATER_SURFACE_OFFSET = 0;
|
||||
|
||||
const std::vector<std::string> ℑ
|
||||
const std::vector<std::string> &mask;
|
||||
const float speed;
|
||||
const bool moving_right;
|
||||
|
||||
explicit Ship(int asset_index);
|
||||
static int getRandomDirection();
|
||||
@@ -19,8 +17,9 @@ public:
|
||||
Ship();
|
||||
|
||||
void update() noexcept override;
|
||||
bool isOffScreen() const 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;
|
||||
};
|
||||
83
src/entities/Waterline.cpp
Normal file
83
src/entities/Waterline.cpp
Normal file
@@ -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;
|
||||
}
|
||||
34
src/entities/Waterline.h
Normal file
34
src/entities/Waterline.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
#include "Entity.h"
|
||||
#include <array>
|
||||
|
||||
class Waterline : public Entity {
|
||||
private:
|
||||
static constexpr int WATERLINE_Y = 5;
|
||||
static constexpr char WATERLINE_COLOR = 'c';
|
||||
static constexpr size_t NUM_WAVE_LAYERS = 4;
|
||||
|
||||
// Use arrays instead of vectors for fixed-size data
|
||||
std::array<std::string, NUM_WAVE_LAYERS> shape;
|
||||
std::array<std::string, NUM_WAVE_LAYERS> colorLines;
|
||||
|
||||
// Pre-compute shift operations
|
||||
void shiftStringLeft(std::string &str);
|
||||
void shiftStringRight(std::string &str);
|
||||
|
||||
public:
|
||||
Waterline();
|
||||
|
||||
void update() noexcept override;
|
||||
const std::vector<std::string> &getImage() const override;
|
||||
const std::vector<std::string> &getMask() const override;
|
||||
char getDefaultColor() const noexcept override { return WATERLINE_COLOR; }
|
||||
|
||||
std::unique_ptr<Entity> createReplacement() const override { return nullptr; }
|
||||
int getPreferredLayer() const noexcept override { return 0; }
|
||||
|
||||
private:
|
||||
// Cache vectors to avoid allocation each frame
|
||||
mutable std::vector<std::string> cached_image;
|
||||
mutable std::vector<std::string> cached_mask;
|
||||
};
|
||||
48
src/entities/Whale.cpp
Normal file
48
src/entities/Whale.cpp
Normal file
@@ -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; }
|
||||
31
src/entities/Whale.h
Normal file
31
src/entities/Whale.h
Normal file
@@ -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;
|
||||
};
|
||||
32
src/main.cpp
32
src/main.cpp
@@ -1,32 +0,0 @@
|
||||
#include "Aquarium.h"
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <unistd.h>
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
int opt;
|
||||
while ((opt = getopt(argc, argv, "r:")) != -1) {
|
||||
switch (opt) {
|
||||
case 'r':
|
||||
g_maxCells = std::atoi(optarg);
|
||||
break;
|
||||
default:
|
||||
fprintf(stderr, "Usage: %s [-r max_cells]\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
Aquarium &aquarium = Aquarium::getInstance();
|
||||
aquarium.resize();
|
||||
|
||||
while (true) {
|
||||
aquarium.redraw();
|
||||
int ch = getch();
|
||||
if (ch == 'q')
|
||||
break;
|
||||
if (ch == 'r')
|
||||
aquarium.resize();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
80
src/utils/SpriteUtils.h
Normal file
80
src/utils/SpriteUtils.h
Normal file
@@ -0,0 +1,80 @@
|
||||
#pragma once
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
struct AssetPair;
|
||||
|
||||
// Character mapping for directional elements
|
||||
const std::unordered_map<char, char> charFlipMap = {
|
||||
{'(', ')'}, {')', '('}, {'[', ']'}, {']', '['}, {'{', '}'},
|
||||
{'}', '{'}, {'<', '>'}, {'>', '<'}, {'/', '\\'}, {'\\', '/'}};
|
||||
|
||||
inline char mirrorChar(char c) {
|
||||
auto it = charFlipMap.find(c);
|
||||
return (it != charFlipMap.end()) ? it->second : c;
|
||||
}
|
||||
|
||||
inline std::string mirrorRow(std::string row) {
|
||||
std::reverse(row.begin(), row.end());
|
||||
for (char &c : row) {
|
||||
c = mirrorChar(c);
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
// Mirror an entire sprite (vector of strings)
|
||||
inline std::vector<std::string>
|
||||
mirrorSprite(const std::vector<std::string> &sprite) {
|
||||
std::vector<std::string> mirrored;
|
||||
mirrored.reserve(sprite.size());
|
||||
for (const auto &row : sprite) {
|
||||
mirrored.push_back(mirrorRow(row));
|
||||
}
|
||||
return mirrored;
|
||||
}
|
||||
|
||||
inline AssetPair mirrorAssetPair(const AssetPair &asset) {
|
||||
return {mirrorSprite(asset.image), mirrorSprite(asset.mask)};
|
||||
}
|
||||
|
||||
// Create bidirectional assets from simple AssetPair vector
|
||||
inline std::vector<AssetPair>
|
||||
createBidirectionalAssets(const std::vector<AssetPair> &rightFacingAssets) {
|
||||
std::vector<AssetPair> result;
|
||||
result.reserve(rightFacingAssets.size() * 2);
|
||||
|
||||
for (const auto &asset : rightFacingAssets) {
|
||||
result.push_back(asset); // Right-facing
|
||||
result.push_back(mirrorAssetPair(asset)); // Left-facing (mirrored)
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Generic template for any asset type with frames and mask
|
||||
template <typename AssetType>
|
||||
AssetType mirrorFramedAsset(const AssetType &asset) {
|
||||
AssetType mirrored;
|
||||
|
||||
// Mirror all frames
|
||||
for (const auto &frame : asset.frames) {
|
||||
mirrored.frames.push_back(mirrorSprite(frame));
|
||||
}
|
||||
|
||||
// Mirror mask
|
||||
mirrored.mask = mirrorSprite(asset.mask);
|
||||
|
||||
return mirrored;
|
||||
}
|
||||
|
||||
// Create bidirectional assets from single framed asset
|
||||
template <typename AssetType>
|
||||
std::vector<AssetType>
|
||||
createBidirectionalFramedAssets(const AssetType &rightFacingAsset) {
|
||||
return {
|
||||
rightFacingAsset, // [0] = Right-facing
|
||||
mirrorFramedAsset(rightFacingAsset) // [1] = Left-facing (mirrored)
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user