commit 633070b960cc8776c7b7521b4709fa0ae1d70103
parent b0ea0d087b1218f73451036ddc1fa5177420f0c3
Author: amrfti <andrew@kloet.net>
Date: Fri, 23 May 2025 11:48:30 -0400
add ship, improve inheritance
Diffstat:
10 files changed, 207 insertions(+), 64 deletions(-)
diff --git a/src/Aquarium.cpp b/src/Aquarium.cpp
@@ -3,6 +3,7 @@
#include "defs.h"
#include <algorithm>
#include <iostream>
+#include <memory>
#include <ncurses.h>
int g_maxCells = 0;
@@ -72,6 +73,7 @@ void Aquarium::resize() {
addWaterline();
addCastle();
+ addShip();
for (int i = 0; i < width / 15; i++)
addSeaweed();
for (int i = 0; i < width * (height - 9) / 350; i++)
@@ -112,10 +114,13 @@ void Aquarium::redraw() {
}
int baseFishLayer = 10;
- for (auto it = fishes.begin(); it != fishes.end();) { // use an iterator
+ for (auto it = fishes.begin(); it != fishes.end();) {
auto &fish = *it;
- fish->draw(baseFishLayer +
+
+ static_cast<const Entity *>(fish.get())
+ ->draw(baseFishLayer +
static_cast<int>(std::distance(fishes.begin(), it)));
+
fish->update();
float fx = fish->getX();
@@ -123,17 +128,22 @@ void Aquarium::redraw() {
addBubble(fx, fish->getY());
}
- if (fx > width || fx < -30) {
- it = fishes.erase(it); // erase and update iterator
+ if (fish->isOffScreen()) {
+ it = fishes.erase(it);
addFish();
} else {
- ++it; // only increment if not erasing
+ ++it;
}
}
waterline->draw();
waterline->update();
+ static_cast<const Entity *>(ship.get())->draw(9);
+ if (ship->isOffScreen())
+ addShip();
+ ship->update();
+
applyBackBuffer();
}
@@ -151,6 +161,8 @@ 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());
diff --git a/src/Aquarium.h b/src/Aquarium.h
@@ -3,8 +3,10 @@
#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;
@@ -67,6 +69,7 @@ private:
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;
@@ -84,6 +87,7 @@ public:
void addSeaweed();
void addWaterline();
void addCastle();
+ void addShip();
void redraw();
void initColors();
void initColorLookup();
diff --git a/src/Entity.cpp b/src/Entity.cpp
@@ -0,0 +1,59 @@
+#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);
+ }
+ }
+}
diff --git a/src/Entity.h b/src/Entity.h
@@ -1,8 +1,5 @@
#pragma once
-#include <cctype>
-#include <ncurses.h>
#include <string>
-#include <unordered_map>
#include <vector>
struct AssetPair {
@@ -21,4 +18,12 @@ public:
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;
};
diff --git a/src/Fish.cpp b/src/Fish.cpp
@@ -14,12 +14,9 @@ Fish::Fish(int asset_index)
speed(Random::floatInRange(0.25f, 2.25f)),
moving_right(asset_index % 2 == 0) {
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());
-
randomizeMask();
}
@@ -27,7 +24,6 @@ void Fish::randomizeMask() {
// Clear and rebuild color map
color_map.clear();
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') {
@@ -35,7 +31,6 @@ void Fish::randomizeMask() {
0, static_cast<int>(AVAILABLE_COLORS.size()) - 1)];
}
}
-
// Apply color mapping to mask
for (auto &line : mask) {
for (char &ch : line) {
@@ -52,50 +47,13 @@ int Fish::getRandomAssetIndex() {
void Fish::update() noexcept { x += moving_right ? speed : -speed; }
-void Fish::draw(int layer) const {
- auto &aquarium = Aquarium::getInstance();
-
- // Pre-allocate strings to avoid repeated allocations
- std::string current_segment;
- std::string current_colors;
- current_segment.reserve(32); // Reserve reasonable capacity
- 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);
- current_colors.push_back((j < mask_row.size()) ? mask_row[j] : 'k');
- }
-
- // Flush remaining segment
- if (!current_segment.empty()) {
- aquarium.drawToBackBuffer(base_y + static_cast<int>(i), cursor_x, layer,
- current_segment, current_colors);
- }
+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;
}
}
diff --git a/src/Fish.h b/src/Fish.h
@@ -2,6 +2,7 @@
#include "Entity.h"
#include "assets/FishAssets.h"
#include <array>
+#include <unordered_map>
class Fish : public Entity {
private:
@@ -9,11 +10,10 @@ private:
'c', 'C', 'r', 'R', 'y', 'Y', 'b', 'B', 'g', 'G', 'm', 'M'};
const std::vector<std::string> ℑ
- std::vector<std::string> mask; // Copy needed for color randomization
+ std::vector<std::string> mask;
const float speed;
const bool moving_right;
- // Static color map to avoid recreation
static std::unordered_map<char, char> color_map;
explicit Fish(int asset_index);
@@ -22,6 +22,10 @@ private:
public:
Fish();
- void update() noexcept;
- void draw(int layer) const;
+
+ 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'; }
};
diff --git a/src/Seaweed.h b/src/Seaweed.h
@@ -3,11 +3,10 @@
class Seaweed {
private:
- const int y;
+ const size_t x, y;
static constexpr char PATTERN_LEFT = '(';
static constexpr char PATTERN_RIGHT = ')';
- const size_t x;
const size_t height;
const float speed;
diff --git a/src/Ship.cpp b/src/Ship.cpp
@@ -0,0 +1,39 @@
+#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;
+ }
+}
diff --git a/src/Ship.h b/src/Ship.h
@@ -0,0 +1,26 @@
+#pragma once
+#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();
+
+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'; }
+};
diff --git a/src/assets/ShipAssets.h b/src/assets/ShipAssets.h
@@ -0,0 +1,37 @@
+#pragma once
+#include "../Entity.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)",
+ }}};