REBASE AWESOME
This commit is contained in:
4
Makefile
4
Makefile
@@ -1,7 +1,7 @@
|
|||||||
# Compiler and flags
|
# Compiler and flags
|
||||||
CXX = g++
|
CXX = g++
|
||||||
CXXFLAGS = -std=c++17 -Wall -Wextra -O3 --function-sections -fdata-sections -flto -march=native -ffast-math
|
CXXFLAGS = -std=c++17 -Wall -Wextra -O3
|
||||||
LDFLAGS = -lncurses -ltinfo -Wl,--gc-sections -flto -Wl,-O2
|
LDFLAGS = -lncurses -ltinfo
|
||||||
|
|
||||||
# Directories
|
# Directories
|
||||||
SRC_DIR = src
|
SRC_DIR = src
|
||||||
|
|||||||
329
src/Aquarium.cpp
329
src/Aquarium.cpp
@@ -1,12 +1,15 @@
|
|||||||
#include "Aquarium.h"
|
#include "Aquarium.h"
|
||||||
#include "Random.h"
|
#include "Bubble.h"
|
||||||
#include "defs.h"
|
#include "Castle.h"
|
||||||
|
#include "Fish.h"
|
||||||
|
#include "Seaweed.h"
|
||||||
|
#include "Ship.h"
|
||||||
|
#include "Waterline.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <memory>
|
|
||||||
#include <ncurses.h>
|
|
||||||
|
|
||||||
int g_maxCells = 0;
|
int g_maxCells = 0;
|
||||||
|
|
||||||
Aquarium::Aquarium() {
|
Aquarium::Aquarium() {
|
||||||
initscr();
|
initscr();
|
||||||
noecho();
|
noecho();
|
||||||
@@ -14,41 +17,86 @@ Aquarium::Aquarium() {
|
|||||||
nodelay(stdscr, TRUE);
|
nodelay(stdscr, TRUE);
|
||||||
curs_set(0);
|
curs_set(0);
|
||||||
initColors();
|
initColors();
|
||||||
initColorLookup();
|
|
||||||
timeout(100);
|
timeout(100);
|
||||||
getmaxyx(stdscr, height, width);
|
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() {
|
currentFrame.assign(height, std::vector<Cell>(width));
|
||||||
if (has_colors()) {
|
previousFrame.assign(height, std::vector<Cell>(width));
|
||||||
start_color();
|
|
||||||
init_pair(1, COLOR_RED, COLOR_BLACK); // 'r'
|
if (!colorLookupInitialized) {
|
||||||
init_pair(2, COLOR_GREEN, COLOR_BLACK); // 'g'
|
initColorLookup();
|
||||||
init_pair(3, COLOR_YELLOW, COLOR_BLACK); // 'y'
|
colorLookupInitialized = true;
|
||||||
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::ensureEntitiesSorted() {
|
||||||
void Aquarium::initColorLookup() {
|
if (entities_need_sorting) {
|
||||||
for (int i = 0; i < 256; ++i)
|
std::sort(entities.begin(), entities.end(),
|
||||||
colorLookup[i] = 8; // default to black
|
[](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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
colorLookup['r'] = 1;
|
void Aquarium::redraw() {
|
||||||
colorLookup['g'] = 2;
|
clearCurrentFrame();
|
||||||
colorLookup['y'] = 3;
|
|
||||||
colorLookup['b'] = 4;
|
std::vector<std::unique_ptr<Entity>> newEntities;
|
||||||
colorLookup['m'] = 5;
|
bool entities_modified = false;
|
||||||
colorLookup['c'] = 6;
|
|
||||||
colorLookup['w'] = 7;
|
// Update and check for removal/replacement
|
||||||
colorLookup['k'] = 8;
|
for (auto it = entities.begin(); it != entities.end();) {
|
||||||
|
auto &entity = *it;
|
||||||
|
entity->update();
|
||||||
|
|
||||||
|
// Handle fish bubble spawning
|
||||||
|
if (auto *fish = dynamic_cast<Fish *>(entity.get())) {
|
||||||
|
if (fish->shouldSpawnBubble()) {
|
||||||
|
newEntities.emplace_back(
|
||||||
|
std::make_unique<Bubble>(fish->getX(), fish->getY()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity->shouldBeRemoved()) {
|
||||||
|
auto replacement = entity->createReplacement();
|
||||||
|
if (replacement) {
|
||||||
|
*it = std::move(replacement);
|
||||||
|
entities_modified = true;
|
||||||
|
++it;
|
||||||
|
} else {
|
||||||
|
it = entities.erase(it);
|
||||||
|
entities_modified = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new entities if any
|
||||||
|
if (!newEntities.empty()) {
|
||||||
|
for (auto &newEntity : newEntities) {
|
||||||
|
entities.emplace_back(std::move(newEntity));
|
||||||
|
}
|
||||||
|
entities_modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only sort if entities were modified
|
||||||
|
if (entities_modified) {
|
||||||
|
entities_need_sorting = true;
|
||||||
|
}
|
||||||
|
ensureEntitiesSorted();
|
||||||
|
|
||||||
|
// Draw all entities
|
||||||
|
for (const auto &entity : entities) {
|
||||||
|
entity->draw(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderToScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Aquarium::resize() {
|
void Aquarium::resize() {
|
||||||
@@ -56,20 +104,18 @@ void Aquarium::resize() {
|
|||||||
getmaxyx(stdscr, height, width);
|
getmaxyx(stdscr, height, width);
|
||||||
|
|
||||||
if (g_maxCells && height * width > g_maxCells) {
|
if (g_maxCells && height * width > g_maxCells) {
|
||||||
endwin(); // Cleanly shut down ncurses
|
endwin();
|
||||||
std::cerr << "Error: Terminal too large. Maximum allowed area is "
|
std::cerr << "Error: Terminal too large. Maximum allowed area is "
|
||||||
<< g_maxCells << " cells, but current size is "
|
<< g_maxCells << " cells, but current size is "
|
||||||
<< (height * width) << ".\n";
|
<< (height * width) << ".\n";
|
||||||
std::exit(1);
|
std::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
frontBuffer.assign(height, std::vector<Cell>(width));
|
currentFrame.assign(height, std::vector<Cell>(width));
|
||||||
backBuffer.assign(height, std::vector<Cell>(width));
|
previousFrame.assign(height, std::vector<Cell>(width));
|
||||||
layeredMap.assign(height, std::vector<LayeredCell>(width));
|
|
||||||
|
|
||||||
fishes.clear();
|
entities.clear();
|
||||||
bubbles.clear();
|
entities_need_sorting = true;
|
||||||
seaweeds.clear();
|
|
||||||
|
|
||||||
addWaterline();
|
addWaterline();
|
||||||
addCastle();
|
addCastle();
|
||||||
@@ -80,100 +126,20 @@ void Aquarium::resize() {
|
|||||||
addFish();
|
addFish();
|
||||||
}
|
}
|
||||||
|
|
||||||
Aquarium::~Aquarium() { endwin(); }
|
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::redraw() {
|
void Aquarium::clearCurrentFrame() {
|
||||||
clearBackBuffer();
|
for (auto &row : currentFrame) {
|
||||||
|
|
||||||
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());
|
std::fill(row.begin(), row.end(), Cell());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline char fastToLower(char c) { return (c >= 'A' && c <= 'Z') ? c + 32 : c; }
|
void Aquarium::drawToFrame(int y, int x, const std::string &line,
|
||||||
|
|
||||||
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) {
|
const std::string &colorLine) {
|
||||||
if (y < 0 || y >= height)
|
if (y < 0 || y >= height)
|
||||||
return;
|
return;
|
||||||
@@ -187,89 +153,58 @@ void Aquarium::drawToBackBuffer(int y, int x, int layer,
|
|||||||
|
|
||||||
const char ch = line[j];
|
const char ch = line[j];
|
||||||
const char colorChar = colorLine[j];
|
const char colorChar = colorLine[j];
|
||||||
const bool isBold = fastIsUpper(static_cast<unsigned char>(colorChar));
|
const bool isBold = (colorChar >= 'A' && colorChar <= 'Z');
|
||||||
|
|
||||||
Cell cell{
|
currentFrame[y][cx] = {
|
||||||
ch,
|
ch, static_cast<char>(isBold ? colorChar + 32 : colorChar), isBold};
|
||||||
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,
|
void Aquarium::initColorLookup() {
|
||||||
const std::string &line) {
|
for (int i = 0; i < 256; ++i)
|
||||||
for (size_t j = 0; j < line.size(); ++j) {
|
colorLookup[i] = 8; // Default black
|
||||||
int cx = x + static_cast<int>(j);
|
colorLookup['r'] = 1;
|
||||||
if (y < 0 || y >= height || cx < 0 || cx >= width)
|
colorLookup['g'] = 2;
|
||||||
continue;
|
colorLookup['y'] = 3;
|
||||||
|
colorLookup['b'] = 4;
|
||||||
auto &cellLayers = layeredMap[y][cx].layers;
|
colorLookup['m'] = 5;
|
||||||
|
colorLookup['c'] = 6;
|
||||||
auto it = std::find_if(cellLayers.begin(), cellLayers.end(),
|
colorLookup['w'] = 7;
|
||||||
[layer](const auto &p) { return p.first == layer; });
|
colorLookup['k'] = 8;
|
||||||
|
|
||||||
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() {
|
void Aquarium::renderToScreen() {
|
||||||
for (int y = 0; y < height; ++y) {
|
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) {
|
for (int x = 0; x < width; ++x) {
|
||||||
const Cell &newCell = backBuffer[y][x];
|
const Cell &newCell = currentFrame[y][x];
|
||||||
Cell &oldCell = frontBuffer[y][x];
|
Cell &oldCell = previousFrame[y][x];
|
||||||
|
|
||||||
if (newCell != oldCell) {
|
if (newCell != oldCell) {
|
||||||
oldCell = newCell;
|
oldCell = newCell;
|
||||||
|
move(y, x);
|
||||||
|
|
||||||
int pairId = colorLookup[static_cast<unsigned char>(newCell.colorChar)];
|
int colorPair =
|
||||||
bool bold = newCell.bold;
|
colorLookup[static_cast<unsigned char>(newCell.colorChar)];
|
||||||
|
attrset(COLOR_PAIR(colorPair) | (newCell.bold ? A_BOLD : A_NORMAL));
|
||||||
// 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);
|
addch(newCell.ch);
|
||||||
} else {
|
|
||||||
// Still move the cursor to stay aligned
|
|
||||||
move(y, x + 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Aquarium::initColors() {
|
||||||
|
if (has_colors()) {
|
||||||
|
start_color();
|
||||||
|
init_pair(1, COLOR_RED, COLOR_BLACK);
|
||||||
|
init_pair(2, COLOR_GREEN, COLOR_BLACK);
|
||||||
|
init_pair(3, COLOR_YELLOW, COLOR_BLACK);
|
||||||
|
init_pair(4, COLOR_BLUE, COLOR_BLACK);
|
||||||
|
init_pair(5, COLOR_MAGENTA, COLOR_BLACK);
|
||||||
|
init_pair(6, COLOR_CYAN, COLOR_BLACK);
|
||||||
|
init_pair(7, COLOR_WHITE, COLOR_BLACK);
|
||||||
|
init_pair(8, COLOR_BLACK, COLOR_BLACK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Aquarium::~Aquarium() { endwin(); }
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "Bubble.h"
|
#include "Entity.h"
|
||||||
#include "Castle.h"
|
|
||||||
#include "Fish.h"
|
|
||||||
#include "Seaweed.h"
|
|
||||||
#include "Ship.h"
|
|
||||||
#include "Waterline.h"
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <ncurses.h>
|
#include <ncurses.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
extern int g_maxCells;
|
extern int g_maxCells;
|
||||||
|
|
||||||
class Aquarium {
|
class Aquarium {
|
||||||
private:
|
private:
|
||||||
int width;
|
int width;
|
||||||
@@ -17,84 +13,57 @@ private:
|
|||||||
|
|
||||||
struct Cell {
|
struct Cell {
|
||||||
char ch = ' ';
|
char ch = ' ';
|
||||||
char colorChar = 'k'; // Default to black
|
char colorChar = 'k';
|
||||||
bool bold = false;
|
bool bold = false;
|
||||||
|
|
||||||
bool operator==(const Cell &other) const {
|
bool operator==(const Cell &other) const {
|
||||||
return ch == other.ch && colorChar == other.colorChar &&
|
return ch == other.ch && colorChar == other.colorChar &&
|
||||||
bold == other.bold;
|
bold == other.bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool operator!=(const Cell &other) const { return !(*this == other); }
|
bool operator!=(const Cell &other) const { return !(*this == other); }
|
||||||
};
|
};
|
||||||
|
|
||||||
struct LayeredCell {
|
std::vector<std::vector<Cell>> currentFrame;
|
||||||
std::vector<std::pair<int, Cell>> layers; // Sorted by layer (ascending)
|
std::vector<std::vector<Cell>> previousFrame;
|
||||||
};
|
std::vector<std::unique_ptr<Entity>> entities;
|
||||||
|
|
||||||
std::vector<std::vector<Cell>> frontBuffer;
|
bool entities_need_sorting = true;
|
||||||
std::vector<std::vector<Cell>> backBuffer;
|
static inline short colorLookup[256] = {0};
|
||||||
std::vector<std::vector<LayeredCell>> layeredMap;
|
static inline bool colorLookupInitialized = false;
|
||||||
|
|
||||||
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:
|
public:
|
||||||
Aquarium();
|
Aquarium();
|
||||||
~Aquarium();
|
~Aquarium();
|
||||||
|
|
||||||
static Aquarium &getInstance() {
|
static Aquarium &getInstance() {
|
||||||
static Aquarium instance;
|
static Aquarium instance;
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] int getWidth() const { return width; }
|
[[nodiscard]] int getWidth() const { return width; }
|
||||||
[[nodiscard]] int getHeight() const { return height; }
|
[[nodiscard]] int getHeight() const { return height; }
|
||||||
|
|
||||||
void addFish();
|
void addFish();
|
||||||
void addBubble(size_t x, size_t y);
|
void addBubble(float x, float y);
|
||||||
void addSeaweed();
|
void addSeaweed();
|
||||||
void addWaterline();
|
void addWaterline();
|
||||||
void addCastle();
|
void addCastle();
|
||||||
void addShip();
|
void addShip();
|
||||||
|
|
||||||
void redraw();
|
void redraw();
|
||||||
void initColors();
|
void initColors();
|
||||||
void initColorLookup();
|
|
||||||
void resize();
|
void resize();
|
||||||
void clearBackBuffer();
|
void drawToFrame(int y, int x, const std::string &line,
|
||||||
void drawToBackBuffer(int y, int x, int layer, const std::string &line,
|
|
||||||
const std::string &colorLine);
|
const std::string &colorLine);
|
||||||
void removeFromBackBuffer(int y, int x, int layer, const std::string &line);
|
|
||||||
void applyBackBuffer();
|
private:
|
||||||
|
void clearCurrentFrame();
|
||||||
|
void renderToScreen();
|
||||||
|
void ensureEntitiesSorted();
|
||||||
|
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;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
#include "Bubble.h"
|
#include "Bubble.h"
|
||||||
#include "Aquarium.h"
|
|
||||||
#include <ncurses.h>
|
|
||||||
|
|
||||||
Bubble::Bubble(size_t x, size_t y) : x(x), y(y) {}
|
Bubble::Bubble(float x, float y) : Entity(x, y) {
|
||||||
|
current_image.resize(1);
|
||||||
|
current_mask.resize(1);
|
||||||
|
updateFrame();
|
||||||
|
}
|
||||||
|
|
||||||
void Bubble::update() {
|
void Bubble::update() noexcept {
|
||||||
--y;
|
--y;
|
||||||
++lifetime;
|
++lifetime;
|
||||||
|
updateFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Bubble::draw() const {
|
void Bubble::updateFrame() {
|
||||||
static const std::string colorString(1, BUBBLE_COLOR);
|
|
||||||
// Clamp frame index
|
|
||||||
int frameIndex = std::min(lifetime / FRAMES_PER_ANIMATION, MAX_FRAME_INDEX);
|
int frameIndex = std::min(lifetime / FRAMES_PER_ANIMATION, MAX_FRAME_INDEX);
|
||||||
Aquarium::getInstance().drawToBackBuffer(y, x, 0, BUBBLE_FRAMES[frameIndex],
|
current_image[0] = BUBBLE_FRAMES[frameIndex];
|
||||||
colorString);
|
current_mask[0] = BUBBLE_COLOR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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; }
|
||||||
|
|||||||
26
src/Bubble.h
26
src/Bubble.h
@@ -1,21 +1,33 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <cstddef>
|
#include "Entity.h"
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
class Bubble {
|
class Bubble : public Entity {
|
||||||
private:
|
private:
|
||||||
static constexpr const char *BUBBLE_FRAMES[3] = {".", "o", "O"};
|
static constexpr const char *BUBBLE_FRAMES[3] = {".", "o", "O"};
|
||||||
static constexpr int FRAMES_PER_ANIMATION = 9;
|
static constexpr int FRAMES_PER_ANIMATION = 9;
|
||||||
static constexpr int MAX_FRAME_INDEX = 2;
|
static constexpr int MAX_FRAME_INDEX = 2;
|
||||||
static constexpr char BUBBLE_COLOR = 'c';
|
static constexpr char BUBBLE_COLOR = 'c';
|
||||||
size_t x, y;
|
|
||||||
int lifetime = 0;
|
int lifetime = 0;
|
||||||
|
std::vector<std::string> current_image;
|
||||||
|
std::vector<std::string> current_mask;
|
||||||
|
|
||||||
|
void updateFrame();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
Bubble(size_t x, size_t y);
|
Bubble(float x, float y);
|
||||||
|
|
||||||
bool isOutOfWater() const { return y < 5; }
|
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; }
|
||||||
|
|
||||||
void update();
|
bool shouldBeRemoved() const noexcept override;
|
||||||
void draw() const;
|
std::unique_ptr<Entity> createReplacement() const override;
|
||||||
|
int getPreferredLayer() const noexcept override;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,32 +3,6 @@
|
|||||||
#include "assets/CastleAssets.h"
|
#include "assets/CastleAssets.h"
|
||||||
|
|
||||||
Castle::Castle()
|
Castle::Castle()
|
||||||
: x(Aquarium::getInstance().getWidth() - 32),
|
: Entity(Aquarium::getInstance().getWidth() - 32,
|
||||||
y(Aquarium::getInstance().getHeight() - 13), image(castleAsset.image),
|
Aquarium::getInstance().getHeight() - 13),
|
||||||
mask(castleAsset.mask) {}
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
16
src/Castle.h
16
src/Castle.h
@@ -1,14 +1,20 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <string>
|
#include "Entity.h"
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
class Castle {
|
class Castle : public Entity {
|
||||||
private:
|
private:
|
||||||
const size_t x, y;
|
|
||||||
const std::vector<std::string> image;
|
const std::vector<std::string> image;
|
||||||
const std::vector<std::string> mask;
|
const std::vector<std::string> mask;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
Castle();
|
Castle();
|
||||||
void draw() const;
|
|
||||||
|
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'; }
|
||||||
|
|
||||||
|
bool shouldBeRemoved() const noexcept override { return false; }
|
||||||
|
std::unique_ptr<Entity> createReplacement() const override { return nullptr; }
|
||||||
|
int getPreferredLayer() const noexcept override { return 0; }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,11 +8,6 @@ void Entity::draw(int layer) const {
|
|||||||
const auto &mask = getMask();
|
const auto &mask = getMask();
|
||||||
const char default_color = getDefaultColor();
|
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_x = static_cast<int>(x);
|
||||||
const int base_y = static_cast<int>(y);
|
const int base_y = static_cast<int>(y);
|
||||||
|
|
||||||
@@ -20,40 +15,43 @@ void Entity::draw(int layer) const {
|
|||||||
const std::string &row = image[i];
|
const std::string &row = image[i];
|
||||||
const std::string &mask_row = (i < mask.size()) ? mask[i] : "";
|
const std::string &mask_row = (i < mask.size()) ? mask[i] : "";
|
||||||
|
|
||||||
int cursor_x = base_x;
|
// Build complete line at once instead of segments
|
||||||
current_segment.clear();
|
std::string line;
|
||||||
current_colors.clear();
|
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) {
|
for (size_t j = 0; j < row.size(); ++j) {
|
||||||
const char ch = row[j];
|
const char ch = row[j];
|
||||||
|
|
||||||
if (ch == '?') {
|
if (ch == '?') {
|
||||||
// Flush current segment if not empty
|
// Flush current line if not empty
|
||||||
if (!current_segment.empty()) {
|
if (!line.empty()) {
|
||||||
aquarium.drawToBackBuffer(base_y + static_cast<int>(i), cursor_x,
|
aquarium.drawToFrame(base_y + static_cast<int>(i), start_x, line,
|
||||||
layer, current_segment, current_colors);
|
colors);
|
||||||
cursor_x += static_cast<int>(current_segment.size());
|
start_x += static_cast<int>(line.size()) + 1; // +1 for the '?' skip
|
||||||
current_segment.clear();
|
line.clear();
|
||||||
current_colors.clear();
|
colors.clear();
|
||||||
|
} else {
|
||||||
|
++start_x; // Just skip the '?' position
|
||||||
}
|
}
|
||||||
++cursor_x; // Skip transparent character
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
current_segment.push_back(ch);
|
line.push_back(ch);
|
||||||
|
|
||||||
// Use mask color if available, otherwise use default color for spaces
|
|
||||||
char color = default_color;
|
char color = default_color;
|
||||||
if (j < mask_row.size()) {
|
if (j < mask_row.size() && mask_row[j] != ' ') {
|
||||||
color = (mask_row[j] == ' ') ? default_color : mask_row[j];
|
color = mask_row[j];
|
||||||
}
|
}
|
||||||
current_colors.push_back(color);
|
colors.push_back(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush remaining segment
|
// Flush remaining line
|
||||||
if (!current_segment.empty()) {
|
if (!line.empty()) {
|
||||||
aquarium.drawToBackBuffer(base_y + static_cast<int>(i), cursor_x, layer,
|
aquarium.drawToFrame(base_y + static_cast<int>(i), start_x, line, colors);
|
||||||
current_segment, current_colors);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
src/Entity.h
15
src/Entity.h
@@ -1,4 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -11,19 +12,27 @@ class Entity {
|
|||||||
protected:
|
protected:
|
||||||
float x;
|
float x;
|
||||||
float y;
|
float y;
|
||||||
|
static inline size_t next_id = 0;
|
||||||
|
const size_t entity_id;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
Entity() : x(0.0f), y(0.0f) {}
|
Entity() : x(0.0f), y(0.0f), entity_id(++next_id) {}
|
||||||
|
Entity(float init_x, float init_y)
|
||||||
|
: x(init_x), y(init_y), entity_id(++next_id) {}
|
||||||
virtual ~Entity() = default;
|
virtual ~Entity() = default;
|
||||||
|
|
||||||
float getX() const noexcept { return x; }
|
float getX() const noexcept { return x; }
|
||||||
float getY() const noexcept { return y; }
|
float getY() const noexcept { return y; }
|
||||||
|
size_t getId() const noexcept { return entity_id; }
|
||||||
|
|
||||||
virtual void update() noexcept = 0;
|
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> &getImage() const = 0;
|
||||||
virtual const std::vector<std::string> &getMask() const = 0;
|
virtual const std::vector<std::string> &getMask() const = 0;
|
||||||
virtual char getDefaultColor() const noexcept = 0;
|
virtual char getDefaultColor() const noexcept = 0;
|
||||||
|
|
||||||
void draw(int layer) const;
|
virtual bool shouldBeRemoved() const noexcept = 0;
|
||||||
|
virtual std::unique_ptr<Entity> createReplacement() const = 0;
|
||||||
|
virtual int getPreferredLayer() const noexcept = 0;
|
||||||
|
|
||||||
|
void draw(int) const;
|
||||||
};
|
};
|
||||||
|
|||||||
60
src/Fish.cpp
60
src/Fish.cpp
@@ -2,9 +2,26 @@
|
|||||||
#include "Aquarium.h"
|
#include "Aquarium.h"
|
||||||
#include "Random.h"
|
#include "Random.h"
|
||||||
#include "assets/FishAssets.h"
|
#include "assets/FishAssets.h"
|
||||||
#include <ncurses.h>
|
#include "defs.h"
|
||||||
|
|
||||||
std::unordered_map<char, char> Fish::color_map;
|
std::array<std::unordered_map<char, char>, 20> Fish::color_map_cache;
|
||||||
|
bool Fish::color_maps_initialized = false;
|
||||||
|
|
||||||
|
// Pre-generate 20 different color mappings at startup
|
||||||
|
void Fish::initializeColorMaps() {
|
||||||
|
for (auto &color_map : color_map_cache) {
|
||||||
|
color_map.clear();
|
||||||
|
color_map['4'] = 'W'; // White is always '4'
|
||||||
|
|
||||||
|
for (char digit = '1'; digit <= '9'; ++digit) {
|
||||||
|
if (digit != '4') {
|
||||||
|
color_map[digit] = AVAILABLE_COLORS[Random::intInRange(
|
||||||
|
0, static_cast<int>(AVAILABLE_COLORS.size()) - 1)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
color_maps_initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
Fish::Fish() : Fish(getRandomAssetIndex()) {}
|
Fish::Fish() : Fish(getRandomAssetIndex()) {}
|
||||||
|
|
||||||
@@ -13,28 +30,25 @@ Fish::Fish(int asset_index)
|
|||||||
mask(fishAssetPairs[asset_index].mask),
|
mask(fishAssetPairs[asset_index].mask),
|
||||||
speed(Random::floatInRange(0.25f, 2.25f)),
|
speed(Random::floatInRange(0.25f, 2.25f)),
|
||||||
moving_right(asset_index % 2 == 0) {
|
moving_right(asset_index % 2 == 0) {
|
||||||
|
|
||||||
|
if (!color_maps_initialized) {
|
||||||
|
initializeColorMaps();
|
||||||
|
}
|
||||||
|
|
||||||
const auto &aquarium = Aquarium::getInstance();
|
const auto &aquarium = Aquarium::getInstance();
|
||||||
y = Random::intInRange(static_cast<int>(image.size()) + 6,
|
y = Random::intInRange(static_cast<int>(image.size()) + 6,
|
||||||
aquarium.getHeight() - static_cast<int>(image.size()));
|
aquarium.getHeight() - static_cast<int>(image.size()));
|
||||||
x = moving_right ? -20.0f : static_cast<float>(aquarium.getWidth());
|
x = moving_right ? -20.0f : static_cast<float>(aquarium.getWidth());
|
||||||
randomizeMask();
|
applyRandomColorMapping();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Fish::randomizeMask() {
|
// Pick one of the pre-generated color mappings
|
||||||
// Clear and rebuild color map
|
void Fish::applyRandomColorMapping() {
|
||||||
color_map.clear();
|
const auto &selected_map = color_map_cache[Random::intInRange(0, 9)];
|
||||||
color_map['4'] = 'W'; // White is always '4'
|
|
||||||
// Assign random colors to digits 1-3, 5-9
|
|
||||||
for (char digit = '1'; digit <= '9'; ++digit) {
|
|
||||||
if (digit != '4') {
|
|
||||||
color_map[digit] = AVAILABLE_COLORS[Random::intInRange(
|
|
||||||
0, static_cast<int>(AVAILABLE_COLORS.size()) - 1)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Apply color mapping to mask
|
|
||||||
for (auto &line : mask) {
|
for (auto &line : mask) {
|
||||||
for (char &ch : line) {
|
for (char &ch : line) {
|
||||||
if (auto it = color_map.find(ch); it != color_map.end()) {
|
if (auto it = selected_map.find(ch); it != selected_map.end()) {
|
||||||
ch = it->second;
|
ch = it->second;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,13 +61,21 @@ int Fish::getRandomAssetIndex() {
|
|||||||
|
|
||||||
void Fish::update() noexcept { x += moving_right ? speed : -speed; }
|
void Fish::update() noexcept { x += moving_right ? speed : -speed; }
|
||||||
|
|
||||||
bool Fish::isOffScreen() const noexcept {
|
bool Fish::shouldBeRemoved() const noexcept {
|
||||||
const auto &aquarium = Aquarium::getInstance();
|
const auto &aquarium = Aquarium::getInstance();
|
||||||
if (moving_right) {
|
if (moving_right) {
|
||||||
// Fish is off screen when its left edge is past the right border
|
|
||||||
return x > static_cast<float>(aquarium.getWidth());
|
return x > static_cast<float>(aquarium.getWidth());
|
||||||
} else {
|
} else {
|
||||||
// Fish is off screen when its right edge is past the left border
|
|
||||||
return (x + static_cast<float>(image[0].length())) < 0;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
13
src/Fish.h
13
src/Fish.h
@@ -14,18 +14,25 @@ private:
|
|||||||
const float speed;
|
const float speed;
|
||||||
const bool moving_right;
|
const bool moving_right;
|
||||||
|
|
||||||
static std::unordered_map<char, char> color_map;
|
static std::array<std::unordered_map<char, char>, 20> color_map_cache;
|
||||||
|
static bool color_maps_initialized;
|
||||||
|
static void initializeColorMaps();
|
||||||
|
|
||||||
explicit Fish(int asset_index);
|
explicit Fish(int asset_index);
|
||||||
static int getRandomAssetIndex();
|
static int getRandomAssetIndex();
|
||||||
void randomizeMask();
|
void applyRandomColorMapping();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
Fish();
|
Fish();
|
||||||
|
|
||||||
void update() noexcept override;
|
void update() noexcept override;
|
||||||
bool isOffScreen() const noexcept override;
|
|
||||||
const std::vector<std::string> &getImage() const override { return image; }
|
const std::vector<std::string> &getImage() const override { return image; }
|
||||||
const std::vector<std::string> &getMask() const override { return mask; }
|
const std::vector<std::string> &getMask() const override { return mask; }
|
||||||
char getDefaultColor() const noexcept override { return 'k'; }
|
char getDefaultColor() const noexcept override { return 'k'; }
|
||||||
|
|
||||||
|
bool shouldBeRemoved() const noexcept override;
|
||||||
|
std::unique_ptr<Entity> createReplacement() const override;
|
||||||
|
int getPreferredLayer() const noexcept override;
|
||||||
|
|
||||||
|
bool shouldSpawnBubble() const;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,14 +2,19 @@
|
|||||||
#include "Aquarium.h"
|
#include "Aquarium.h"
|
||||||
#include "Random.h"
|
#include "Random.h"
|
||||||
#include "defs.h"
|
#include "defs.h"
|
||||||
#include <ncurses.h>
|
|
||||||
|
|
||||||
Seaweed::Seaweed()
|
Seaweed::Seaweed()
|
||||||
: x(Random::intInRange(0, Aquarium::getInstance().getWidth())),
|
: Entity(),
|
||||||
y(Aquarium::getInstance().getHeight()),
|
|
||||||
height(Random::intInRange(SEAWEED_MIN_HEIGHT, SEAWEED_MAX_HEIGHT)),
|
height(Random::intInRange(SEAWEED_MIN_HEIGHT, SEAWEED_MAX_HEIGHT)),
|
||||||
speed(Random::floatInRange(0.1f, 0.3f)),
|
speed(Random::floatInRange(0.1f, 0.3f)),
|
||||||
lifetime(Random::intInRange(SEAWEED_MIN_LIFETIME, SEAWEED_MAX_LIFETIME)) {
|
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 {
|
void Seaweed::update() noexcept {
|
||||||
@@ -17,24 +22,42 @@ void Seaweed::update() noexcept {
|
|||||||
if (frame >= 1.0f) {
|
if (frame >= 1.0f) {
|
||||||
pattern_flipped = !pattern_flipped;
|
pattern_flipped = !pattern_flipped;
|
||||||
frame -= 1.0f;
|
frame -= 1.0f;
|
||||||
|
frame_dirty = true; // mark frame as needing regeneration
|
||||||
}
|
}
|
||||||
--lifetime;
|
--lifetime;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Seaweed::draw() const {
|
const std::vector<std::string> &Seaweed::getImage() const {
|
||||||
auto &aquarium = Aquarium::getInstance();
|
if (frame_dirty) {
|
||||||
|
generateCurrentFrame();
|
||||||
|
frame_dirty = false;
|
||||||
|
}
|
||||||
|
return current_image;
|
||||||
|
}
|
||||||
|
|
||||||
std::string line(1, '\0');
|
const std::vector<std::string> &Seaweed::getMask() const {
|
||||||
std::string colorLine(1, 'g');
|
if (frame_dirty) {
|
||||||
|
generateCurrentFrame();
|
||||||
|
frame_dirty = false;
|
||||||
|
}
|
||||||
|
return current_mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Seaweed::generateCurrentFrame() const {
|
||||||
for (size_t i = 0; i < height; ++i) {
|
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 bool use_left = (i % 2 == 0) ^ pattern_flipped;
|
||||||
const char ch = use_left ? PATTERN_LEFT : PATTERN_RIGHT;
|
const char ch = use_left ? PATTERN_LEFT : PATTERN_RIGHT;
|
||||||
const int drawX = static_cast<int>(x) + (use_left ? 0 : 1);
|
const int offset = use_left ? 0 : 1;
|
||||||
const int drawY = y - static_cast<int>(i);
|
|
||||||
|
|
||||||
line[0] = ch;
|
current_image[i] = std::string(offset, ' ') + ch;
|
||||||
aquarium.drawToBackBuffer(drawY, drawX, 0, line, colorLine);
|
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; }
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <cstddef>
|
#include "Entity.h"
|
||||||
|
|
||||||
class Seaweed {
|
class Seaweed : public Entity {
|
||||||
private:
|
private:
|
||||||
const size_t x, y;
|
|
||||||
static constexpr char PATTERN_LEFT = '(';
|
static constexpr char PATTERN_LEFT = '(';
|
||||||
static constexpr char PATTERN_RIGHT = ')';
|
static constexpr char PATTERN_RIGHT = ')';
|
||||||
|
|
||||||
@@ -13,11 +12,22 @@ private:
|
|||||||
float frame = 0.0f;
|
float frame = 0.0f;
|
||||||
size_t lifetime;
|
size_t lifetime;
|
||||||
bool pattern_flipped = false;
|
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:
|
public:
|
||||||
Seaweed();
|
Seaweed();
|
||||||
|
|
||||||
size_t getLifetime() const noexcept { return lifetime; }
|
void update() noexcept override;
|
||||||
void update() noexcept;
|
const std::vector<std::string> &getImage() const override;
|
||||||
void draw() const;
|
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;
|
||||||
};
|
};
|
||||||
|
|||||||
24
src/Ship.cpp
24
src/Ship.cpp
@@ -9,31 +9,33 @@ Ship::Ship(int asset_index)
|
|||||||
: Entity(), image(pirateShipAssets[asset_index].image),
|
: Entity(), image(pirateShipAssets[asset_index].image),
|
||||||
mask(pirateShipAssets[asset_index].mask), speed(SHIP_SPEED),
|
mask(pirateShipAssets[asset_index].mask), speed(SHIP_SPEED),
|
||||||
moving_right(asset_index == 0) {
|
moving_right(asset_index == 0) {
|
||||||
|
|
||||||
const auto &aquarium = Aquarium::getInstance();
|
const auto &aquarium = Aquarium::getInstance();
|
||||||
// Ships move along the water surface (top of aquarium)
|
|
||||||
y = WATER_SURFACE_OFFSET;
|
y = WATER_SURFACE_OFFSET;
|
||||||
// Start off-screen on appropriate side
|
|
||||||
if (moving_right) {
|
if (moving_right) {
|
||||||
x = -static_cast<float>(
|
x = -static_cast<float>(image[0].length());
|
||||||
image[0].length()); // Start completely off left side
|
|
||||||
} else {
|
} else {
|
||||||
x = static_cast<float>(aquarium.getWidth()); // Start off right side
|
x = static_cast<float>(aquarium.getWidth());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int Ship::getRandomDirection() {
|
int Ship::getRandomDirection() { return Random::intInRange(0, 1); }
|
||||||
return Random::intInRange(0, 1); // 0 = right, 1 = left
|
|
||||||
}
|
|
||||||
|
|
||||||
void Ship::update() noexcept { x += moving_right ? speed : -speed; }
|
void Ship::update() noexcept { x += moving_right ? speed : -speed; }
|
||||||
|
|
||||||
bool Ship::isOffScreen() const noexcept {
|
bool Ship::shouldBeRemoved() const noexcept {
|
||||||
const auto &aquarium = Aquarium::getInstance();
|
const auto &aquarium = Aquarium::getInstance();
|
||||||
if (moving_right) {
|
if (moving_right) {
|
||||||
// Ship is off screen when its left edge is past the right border
|
|
||||||
return x > static_cast<float>(aquarium.getWidth());
|
return x > static_cast<float>(aquarium.getWidth());
|
||||||
} else {
|
} else {
|
||||||
// Ship is off screen when its right edge is past the left border
|
|
||||||
return (x + static_cast<float>(image[0].length())) < 0;
|
return (x + static_cast<float>(image[0].length())) < 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Entity> Ship::createReplacement() const {
|
||||||
|
return std::make_unique<Ship>();
|
||||||
|
}
|
||||||
|
|
||||||
|
int Ship::getPreferredLayer() const noexcept {
|
||||||
|
return 9; // Ships on layer 9
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,8 +19,11 @@ public:
|
|||||||
Ship();
|
Ship();
|
||||||
|
|
||||||
void update() noexcept override;
|
void update() noexcept override;
|
||||||
bool isOffScreen() const noexcept override;
|
|
||||||
const std::vector<std::string> &getImage() const override { return image; }
|
const std::vector<std::string> &getImage() const override { return image; }
|
||||||
const std::vector<std::string> &getMask() const override { return mask; }
|
const std::vector<std::string> &getMask() const override { return mask; }
|
||||||
char getDefaultColor() const noexcept override { return 'W'; }
|
char getDefaultColor() const noexcept override { return 'W'; }
|
||||||
|
|
||||||
|
bool shouldBeRemoved() const noexcept override;
|
||||||
|
std::unique_ptr<Entity> createReplacement() const override;
|
||||||
|
int getPreferredLayer() const noexcept override;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,49 +2,82 @@
|
|||||||
#include "Aquarium.h"
|
#include "Aquarium.h"
|
||||||
#include "Random.h"
|
#include "Random.h"
|
||||||
#include "defs.h"
|
#include "defs.h"
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
Waterline::Waterline() : x(0), y(WATERLINE_Y) {
|
Waterline::Waterline() : Entity(0, WATERLINE_Y) {
|
||||||
shape = {
|
shape[0] = "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~";
|
||||||
"~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~", "^^^^ ^^^ ^^^ ^^^ ^^^^ ",
|
shape[1] = "^^^^ ^^^ ^^^ ^^^ ^^^^ ";
|
||||||
"^^^^ ^^^^ ^^^ ^^ ", "^^ ^^^^ ^^^ ^^^^^^ "};
|
shape[2] = "^^^^ ^^^^ ^^^ ^^ ";
|
||||||
|
shape[3] = "^^ ^^^^ ^^^ ^^^^^^ ";
|
||||||
|
|
||||||
const size_t width = Aquarium::getInstance().getWidth();
|
const size_t width = Aquarium::getInstance().getWidth();
|
||||||
for (auto &line : shape) {
|
|
||||||
const std::string original = line;
|
for (size_t i = 0; i < NUM_WAVE_LAYERS; ++i) {
|
||||||
while (line.size() < width) {
|
const std::string &original = shape[i];
|
||||||
line += original;
|
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;
|
||||||
}
|
}
|
||||||
colorLines.emplace_back(line.size(), WATERLINE_COLOR);
|
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::draw() const {
|
void Waterline::update() noexcept {
|
||||||
for (size_t i = 0; i < shape.size(); ++i) {
|
// Use cached probability calculations
|
||||||
Aquarium::getInstance().drawToBackBuffer(y + static_cast<int>(i), x, 0,
|
static constexpr float thresholds[NUM_WAVE_LAYERS] = {
|
||||||
shape[i], colorLines[i]);
|
0.0f, // Layer 0 never moves
|
||||||
}
|
0.25f / WAVE_MOVE_CHANCE, 0.5f / WAVE_MOVE_CHANCE,
|
||||||
}
|
0.75f / WAVE_MOVE_CHANCE};
|
||||||
|
|
||||||
void Waterline::update() {
|
for (size_t i = 1; i < NUM_WAVE_LAYERS; ++i) {
|
||||||
// Skip the first line (index 0) as it's static
|
if (Random::floatInRange(0.0f, 1.0f) < thresholds[i]) {
|
||||||
for (size_t i = 1; i < shape.size(); ++i) {
|
if (Random::intInRange(0, 1) == 0) {
|
||||||
// Probability increases with depth (higher index = more movement)
|
shiftStringLeft(shape[i]);
|
||||||
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 {
|
} else {
|
||||||
std::rotate(str.begin(), str.begin() + 1, str.end());
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,21 +1,35 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <string>
|
#include "Entity.h"
|
||||||
#include <vector>
|
#include <array>
|
||||||
|
|
||||||
class Waterline {
|
class Waterline : public Entity {
|
||||||
private:
|
private:
|
||||||
static constexpr int WATERLINE_Y = 5;
|
static constexpr int WATERLINE_Y = 5;
|
||||||
static constexpr char WATERLINE_COLOR = 'c';
|
static constexpr char WATERLINE_COLOR = 'c';
|
||||||
|
static constexpr size_t NUM_WAVE_LAYERS = 4;
|
||||||
|
|
||||||
size_t x, y;
|
// Use arrays instead of vectors for fixed-size data
|
||||||
std::vector<std::string> shape;
|
std::array<std::string, NUM_WAVE_LAYERS> shape;
|
||||||
std::vector<std::string> colorLines;
|
std::array<std::string, NUM_WAVE_LAYERS> colorLines;
|
||||||
|
|
||||||
void shiftString(std::string &str, int direction);
|
// Pre-compute shift operations
|
||||||
void initializeShape();
|
void shiftStringLeft(std::string &str);
|
||||||
|
void shiftStringRight(std::string &str);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
Waterline();
|
Waterline();
|
||||||
void draw() const;
|
|
||||||
void update();
|
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; }
|
||||||
|
|
||||||
|
bool shouldBeRemoved() const noexcept override { return false; }
|
||||||
|
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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,31 +8,15 @@ struct CastleAsset {
|
|||||||
std::vector<std::string> mask;
|
std::vector<std::string> mask;
|
||||||
};
|
};
|
||||||
|
|
||||||
inline CastleAsset castleAsset = {{
|
inline CastleAsset castleAsset = {
|
||||||
R"( T~~)",
|
{R"( T~~)", R"( |)", R"( /^\)",
|
||||||
R"( |)",
|
R"( / \)", R"( _ _ _ / \ _ _ _)",
|
||||||
R"( /^\)",
|
R"([ ]_[ ]_[ ]/ _ _ \[ ]_[ ]_[ ])", R"(|_=__-_ =_|_[ ]_[ ]_|_=-___-__|)",
|
||||||
R"( / \)",
|
R"( | _- = | =_ = _ |= _= |)", R"( |= -[] |- = _ = |_-=_[] |)",
|
||||||
R"( _ _ _ / \ _ _ _)",
|
R"( | =_ |= - ___ | =_ = |)", R"( |= []- |- /| |\ |=_ =[] |)",
|
||||||
R"([ ]_[ ]_[ ]/ _ _ \[ ]_[ ]_[ ])",
|
R"( |- =_ | =| | | | |- = - |)", R"( |_______|__|_|_|_|__|_______|)"},
|
||||||
R"(|_=__-_ =_|_[ ]_[ ]_|_=-___-__|)",
|
{R"( RR)", R"()", R"( yyy)",
|
||||||
R"( | _- = | =_ = _ |= _= |)",
|
R"( y y)", R"( y y)",
|
||||||
R"( |= -[] |- = _ = |_-=_[] |)",
|
R"( y y)", R"()", R"()", R"()", R"( yyy)",
|
||||||
R"( | =_ |= - ___ | =_ = |)",
|
R"( yy yy)", R"( y y y y)",
|
||||||
R"( |= []- |- /| |\ |=_ =[] |)",
|
|
||||||
R"( |- =_ | =| | | | |- = - |)",
|
|
||||||
R"( |_______|__|_|_|_|__|_______|)"},
|
|
||||||
{
|
|
||||||
R"( RR)",
|
|
||||||
R"()",
|
|
||||||
R"( yyy)",
|
|
||||||
R"( y y)",
|
|
||||||
R"( y y)",
|
|
||||||
R"( y y)",
|
|
||||||
R"()",
|
|
||||||
R"()",
|
|
||||||
R"()",
|
|
||||||
R"( yyy)",
|
|
||||||
R"( yy yy)",
|
|
||||||
R"( y y y y)",
|
|
||||||
R"( yyyyyyy)"}};
|
R"( yyyyyyy)"}};
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ inline std::vector<AssetPair> pirateShipAssets = {
|
|||||||
R"( /(___((___((___()",
|
R"( /(___((___((___()",
|
||||||
R"( //(_____(____(____()",
|
R"( //(_____(____(____()",
|
||||||
R"(__///____|____|____|_____)",
|
R"(__///____|____|____|_____)",
|
||||||
R"( \ /)",
|
R"(????\ /)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
R"( y y y)",
|
R"( y y y)",
|
||||||
|
|||||||
Reference in New Issue
Block a user