feat: add pushable box blocks

This commit is contained in:
Martin Prokoph
2025-01-21 15:17:47 +01:00
parent 3c609340f2
commit 543a23ce0a
9 changed files with 240 additions and 26 deletions

View File

@@ -22,6 +22,9 @@ public:
bool isNegative() {
return x < 0 || y < 0;
}
BlockPos add(int x, int y) {
return BlockPos(this->x + x, this->y + y);
}
BlockPos operator+(BlockPos offset) {
return BlockPos(this->getX() + offset.getX(), this->getY() + offset.getY());
}

View File

@@ -16,6 +16,7 @@ public:
Block GOAL = Block(Identifier("adventura", "goal"), 'O', Color::BRIGHT_GREEN, BlockSettingsBuilder().nonSolid().build());
Block WALL = Block(Identifier("adventura", "wall"), '0', Color::BRIGHT_BLACK, BlockSettingsBuilder().collidable().build());
Block SPIKE = Block(Identifier("adventura", "spike"), '^', Color::BRIGHT_RED, BlockSettingsBuilder().lethal().build());
Block BOX = Block(Identifier("adventura", "box"), 'x', Color::BRIGHT_CYAN, BlockSettingsBuilder().pushable().collidable().gravity().build());
BlockRegistry() {
registerBlock(AIR);
@@ -26,6 +27,7 @@ public:
registerBlock(GOAL);
registerBlock(WALL);
registerBlock(SPIKE);
registerBlock(BOX);
}
const Block getByEncoding(char encoding) {

View File

@@ -7,8 +7,11 @@ class BlockSettings {
bool hasCollision() {
return hasCollision_;
}
bool isMovable() {
return isMovable_;
bool hasGravity() {
return hasGravity_;
}
bool isPushable() {
return isPushable_;
}
bool isLethal() {
return isLethal_;
@@ -23,12 +26,15 @@ class BlockSettings {
void setSolid(bool isSolid) {
this->isSolid_ = isSolid;
}
void setMovable(bool isMovable) {
this->isMovable_ = isMovable;
void setPushable(bool isMovable) {
this->isPushable_ = isMovable;
}
void setCollision(bool hasCollision) {
this->hasCollision_ = hasCollision;
}
void setGravity(bool hasGravity) {
this->hasGravity_ = hasGravity;
}
void setLethal(bool isLethal) {
this->isLethal_ = isLethal;
}
@@ -41,11 +47,12 @@ class BlockSettings {
private:
bool isSolid_ = true;
bool isMovable_ = false;
bool isPushable_ = false;
bool isClimbableFromTop_ = false;
bool isClimbableFromBottom_ = false;
bool isLethal_ = false;
bool hasCollision_ = false;
bool hasGravity_ = false;
};
class BlockSettingsBuilder {
public:
@@ -53,14 +60,18 @@ class BlockSettingsBuilder {
blockSettings.setSolid(false);
return *this;
}
BlockSettingsBuilder movable() {
blockSettings.setMovable(true);
BlockSettingsBuilder pushable() {
blockSettings.setPushable(true);
return *this;
}
BlockSettingsBuilder collidable() {
blockSettings.setCollision(true);
return *this;
}
BlockSettingsBuilder gravity() {
blockSettings.setGravity(true);
return *this;
}
BlockSettingsBuilder lethal() {
blockSettings.setLethal(true);
return *this;

View File

@@ -1,6 +1,8 @@
#include <string>
#include <iostream>
#include <filesystem>
#include <algorithm>
#include "world.hpp"
#include "player.hpp"
#include "blockRegistry.hpp"
@@ -17,19 +19,40 @@ void jumpBackOneLine();
void printFile(string fileLocation, Color color);
bool startWorld(string worldFile);
/**
* Entry point of the program.
* If a world file is provided as an argument, play through that world.
* Otherwise, play through all worlds in the worlds directory.
* In case the player dies during gameplay, exit without printing the victory screen.
* If the player reaches the goal of the final level, print the victory screen and exit.
*/
int main(int argc, char *argv[]) {
if (argc > 1) {
if (!startWorld("./worlds/" + string(argv[1]))) return 0;
}
else {
for (const auto & entry : fs::directory_iterator("./worlds"))
if (!startWorld(entry.path())) return 0;
vector<string> worlds;
for (auto & entry : fs::directory_iterator("./worlds")) {
worlds.push_back(entry.path());
}
// We use this to sort the worlds alphabetically, so that the game progresses in the correct order.
std::sort( worlds.begin(), worlds.end(), [](string a, string b) {
return a < b;
});
for (const auto & world : worlds)
if (!startWorld(world)) return 0;
}
printFile("./screens/victory.txt", Color::BRIGHT_GREEN);
return 0;
}
/**
* Start a new world defined in the file at worldFile.
* If the player reaches the goal, return true.
* In case they die, print the death screen and return false.
* @return true if the player reached the goal, false in case of death
*/
bool startWorld(string worldFile) {
BlockRegistry blockRegistry = BlockRegistry();
World world = World(blockRegistry);
@@ -46,10 +69,25 @@ bool startWorld(string worldFile) {
if (!player.isAlive()) printFile("./screens/death.txt", Color::BRIGHT_RED);
return player.hasReachedGoal();
}
/**
* Move the console cursor up by one line.
* Used to overwrite the previous line.
*/
void jumpBackOneLine() {
std::cout << "\033[1A";
}
/**
* Redraws the game world and player state on the console.
* This function first moves the console cursor up by the number of lines
* equivalent to the world's height, effectively clearing previous output.
* It then calls the render function to display the current state of the world
* and the player.
*
* @param world Reference to the World object representing the game's world.
* @param player Reference to the Player object representing the player's state.
*/
void redraw(World &world, Player &player) {
//std::this_thread::sleep_for(std::chrono::seconds(1));
for (unsigned int y = 0; y <= world.getMaxY()+1; y++) {
@@ -58,13 +96,19 @@ void redraw(World &world, Player &player) {
render(world, player);
}
/**
* Renders the current state of the game world and player onto the console.
* It prints the world's blocks with their respective colors and encodings (characters).
* On positions that overlap with the player texture, the relevant character of the player's texture is printed instead.
*/
void render(World &world, Player &player) {
vector<vector<Block>> canvas = world.getFieldState();
vector<vector<char>> playerTexture = player.mapToWorldspace();
for (unsigned int y = 0; y <= world.getMaxY(); y++) {
for (unsigned int x = 0; x <= world.getMaxX(); x++) {
if (playerTexture.size() > y && playerTexture.at(y).size() > x && playerTexture.at(y).at(x) != ' ') {
if (!world.getBlockAt(BlockPos(x, y)).getSettings().isPushable()
&& playerTexture.size() > y && playerTexture.at(y).size() > x && playerTexture.at(y).at(x) != ' ') {
cout << Color::BRIGHT_YELLOW << playerTexture.at(y).at(x);
}
else if (canvas.size() > y && canvas.at(y).size() > x) {
@@ -75,6 +119,14 @@ void render(World &world, Player &player) {
cout << endl;
}
}
/**
* Prints the content of a file line by line onto the console,
* in the specified color.
* We use this to print our death and victory screens.
*
* @param fileLocation Path to the file to be printed.
* @param color Color to be used for the output.
*/
void printFile(string fileLocation, Color color) {
cout << color;
vector<string> file = readFileAsVector(fileLocation);

View File

@@ -5,6 +5,18 @@
bool tryWalk(World& world, Player& player, bool left);
bool tryGoDown(World& world, Player& player);
bool tryGoUp(World& world, Player& player);
bool tryPushBlock(BlockPos& blockPos, World& world, bool left);
/**
* Processes the player's input and attempts to move the player in the game world
* based on the input character. Supports moving up, left, down, or right
* using the keys 'w', 'a', 's', 'd' as well as their upper-case equivalents (useful in case caps lock is pressed by accident).
*
* @param lastChar The character input representing the player's movement command.
* @param world Reference to the World object representing the game's world.
* @param player Reference to the Player object representing the player's state.
* @return true if the player's position was successfully updated, false otherwise.
*/
bool onInput(char lastChar, World& world, Player& player) {
switch (lastChar) {
@@ -28,19 +40,44 @@ bool onInput(char lastChar, World& world, Player& player) {
default: return false;
}
}
/**
* Attempts to move the player one block to the left or right.
*
* Checks if the neighbour block to the player's feet is not a solid block and
* if so, moves the player there. In case that block solid and the block above it
* is not solid, moves the player on top of that block.
* Otherwise, returns false.
*
* @param world Reference to the World object representing the game's world.
* @param player Reference to the Player object representing the player's state.
* @param left Whether to move left (true) or right (false).
* @return true if the player's position was successfully updated, false otherwise.
*/
bool tryWalk(World& world, Player& player, bool left) {
BlockPos neighbourPosTorso = player.getPos()+(left ? BlockPos(-1, 0) : BlockPos(1, 0));
BlockPos neighbourPosFeet = player.getPos()+(left ? BlockPos(-1, 1) : BlockPos(1, 1));
tryPushBlock(neighbourPosFeet, world, left);
if (!world.getBlockAt(neighbourPosFeet).getSettings().hasCollision()) {
player.setPos(neighbourPosTorso);
return true;
}
else if (!left && world.getBlockAt(neighbourPosFeet).getSettings().hasCollision() && !world.getBlockAt(neighbourPosTorso).getSettings().isSolid()) {
else if (world.getBlockAt(neighbourPosFeet).getSettings().hasCollision() && !world.getBlockAt(neighbourPosTorso).getSettings().isSolid()) {
left ? player.move(-1, -1) : player.move(1, -1);
return true;
}
return false;
}
/**
* Attempts to move the player one block downwards.
*
* Checks if the block above the player's torso or the block above the player's feet is
* climbable from the top and if so, moves the player there.
* Otherwise, returns false.
*
* @param world Reference to the World object representing the game's world.
* @param player Reference to the Player object representing the player's state.
* @return true if the player's position was successfully updated, false otherwise.
*/
bool tryGoDown(World& world, Player& player) {
if (world.getBlockAt(player.getPos()+BlockPos(0, 2)).getSettings().isClimbableFromTop() || world.getBlockAt(player.getPos()+BlockPos(0, 3)).getSettings().isClimbableFromTop()) {
player.move(0, 1);
@@ -48,10 +85,35 @@ bool tryGoDown(World& world, Player& player) {
}
return false;
}
/**
* Attempts to move the player one block upwards.
*
* Checks if the block above the player's torso or the block above the player's head is
* climbable from the bottom and if so, moves the player there.
* Otherwise, returns false.
*
* @param world Reference to the World object representing the game's world.
* @param player Reference to the Player object representing the player's state.
* @return true if the player's position was successfully updated, false otherwise.
*/
bool tryGoUp(World& world, Player& player) {
if (world.getBlockAt(player.getPos()+BlockPos(0, 1)).getSettings().isClimbableFromBottom() || world.getBlockAt(player.getPos()+BlockPos(0, 2)).getSettings().isClimbableFromBottom()) {
player.move(0, -1);
return true;
}
return false;
}
bool tryPushBlock(BlockPos& blockPos, World& world, bool left) {
BlockPos neighbourBlockPos = blockPos+(left ? BlockPos(-1, 0) : BlockPos(1, 0));
if (world.getBlockAt(blockPos).getSettings().isPushable()) {
if (world.getBlockAt(neighbourBlockPos).getSettings().isPushable()) {
tryPushBlock(neighbourBlockPos, world, left);
}
if (world.getBlockAt(neighbourBlockPos) == world.getBlockRegistry().AIR) {
world.setBlockAt(neighbourBlockPos, world.getBlockAt(blockPos));
world.setBlockAt(blockPos, world.getBlockRegistry().AIR);
return true;
}
}
return false;
}

View File

@@ -11,7 +11,7 @@ public:
{' ', 'o', ' '},
{'/', '|', '\\'},
{'/', ' ', '\\'}
}}; // Player pos is at the bottom center
}}; // Player pos is at the center '|' char
}
BlockPos getPos() {
@@ -32,19 +32,19 @@ public:
if (world.getBlockAt(pos) == world.getBlockRegistry().GOAL) reachedGoal = true;
isFreeFalling = !world.getBlockAt(pos+BlockPos(0, 2)).getSettings().isSolid();
if (world.getBlockAt(pos.add(0, 2)) == world.getBlockRegistry().WATER) fallLength = 0;
isFreeFalling = !world.getBlockAt(pos.add(0, 2)).getSettings().isSolid();
if (isFreeFalling) {
fallLength += 1;
if (world.getBlockAt(pos+BlockPos(0, 2)) == world.getBlockRegistry().WATER) fallLength = 0;
move(BlockPos(0, 1));
move(0, 1);
}
else {
if (fallLength > 5) alive = false;
fallLength = 0;
}
if (world.getBlockAt(pos+BlockPos(0, 2)).getSettings().isLethal()) alive = false;
if (world.getBlockAt(pos.add(0, 2)).getSettings().isLethal()) alive = false;
}
bool isAlive() {
return alive;
@@ -75,7 +75,7 @@ public:
}
private:
World world;
World& world;
std::array<std::array<char, 3>, 3> playerTexture;
BlockPos pos = BlockPos(0, 0);
bool alive = true;

View File

@@ -10,10 +10,23 @@ using std::vector;
class World {
public:
/**
* Create a World object using the blocks defined in BlockRegistry.
*
* @param blockRegistry The BlockRegistry to use.
*/
World(BlockRegistry blockRegistry) {
this->blockRegistry = blockRegistry;
}
/**
* Load the world from the given text file.
* - The character '|' is the player's starting position.
* - The characters in the file are mapped to the corresponding blocks in the block registry.
* - All other characters are kept as purely visual decoration blocks.
*
* @param fileLocation The location of the file to load.
*/
void loadFromFile(string fileLocation) {
field = {};
vector<string> file = readFileAsVector(fileLocation);
@@ -27,14 +40,35 @@ public:
if (y > maxY) maxY = y;
}
}
/**
* Sets the block at the given position in the world.
*
* In case the position is outside the current bounds of the world, the world will be automatically be expanded.
* If the position is negative, an error will be logged.
*
* @param pos The position to set the block at.
* @param block The block to set at that position.
*/
void setBlockAt(BlockPos pos, Block block) {
if (pos.isNegative()) return;
if (pos.isNegative()) cout << "Tried to set block at negative position: (x: " << pos.getX() << ", y:" << pos.getY() << ")" << endl;
while (field.size() <= pos.getUnsignedY()) field.push_back({});
while (field[pos.getUnsignedY()].size() <= pos.getUnsignedX()) field[pos.getUnsignedY()].push_back(blockRegistry.AIR);
field[pos.getUnsignedY()][pos.getX()] = block;
if (block.getSettings().hasGravity() && containsPos(pos.add(0, 1)) && getBlockAt(pos.add(0, 1)) == blockRegistry.AIR) {
setBlockAt(pos.add(0, 1), block);
setBlockAt(pos, blockRegistry.AIR);
}
}
/**
* Get the block at the given position in the world.
*
* In position is outside the current bounds of the world, the AIR block will be returned.
*
* @param pos The position to get the block at.
* @return The block at that position.
*/
Block& getBlockAt(BlockPos pos) {
if (pos.getUnsignedY() < field.size() && pos.getUnsignedX() < field[pos.getUnsignedY()].size()) {
return field[pos.getY()][pos.getX()];
@@ -42,21 +76,57 @@ public:
//cout << "Out of bounds: " << pos.getX() << ", " << pos.getY() << endl;
return blockRegistry.AIR;
}
/**
* Checks whether the given position is within the bounds of the world, or not.
*
* @param pos The position to check.
* @return True if the position is non-negative and within the current bounds of the world, false otherwise.
*/
bool containsPos(BlockPos pos) {
return !pos.isNegative() && pos.getUnsignedY() < field.size();
}
/**
* @return The current state of the world as a 2D vector of blocks.
*/
vector<vector<Block>> getFieldState() {
return field;
}
/**
* Get the block registry for the world.
*
* @return The block registry containing all registered blocks.
*/
BlockRegistry getBlockRegistry() {
return blockRegistry;
}
/**
* Get the biggest X (horizontal) value in the world.
*
* @return The maximum X value.
*/
unsigned int getMaxX() {
return maxX;
}
/**
* Get the biggest Y (vertical) value in the world.
*
* @return The maximum Y value.
*/
unsigned int getMaxY() {
return maxY;
}
/**
* Get the starting position of the player in the world.
*
* @return The BlockPos representing the starting position in the current level.
*/
BlockPos getStartPos() {
return startPos;
}

View File

@@ -1,15 +1,15 @@
----------------------
00 <>
00 <++>
00 [+ +]
--------- [ ]
0 o 00------ O [ 7 ]
0 S /|\ 00 0 [ ]
00
00
00
---------
0 o 00------ O
0 S /|\ 00 0
0 / \ 00 0 ---------------
---------------- 0 H 0
0 H 0
0 H 0
0 H 0
0~~~~~~0---------0
00000000
00000000

14
worlds/4.txt Normal file
View File

@@ -0,0 +1,14 @@
o
S /|\
/ \
---
0
0 x x
0 -------------
0 H
H H
H H
H H O
~~~H H
------------------^^^^^----------------