mirror of
https://github.com/Motschen/Adventura.git
synced 2025-12-13 02:25:08 +01:00
feat: add pushable box blocks
This commit is contained in:
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
58
src/main.cpp
58
src/main.cpp
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
14
worlds/3.txt
14
worlds/3.txt
@@ -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
14
worlds/4.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
o
|
||||
S /|\
|
||||
/ \
|
||||
---
|
||||
0
|
||||
0 x x
|
||||
0 -------------
|
||||
0 H
|
||||
H H
|
||||
H H
|
||||
H H O
|
||||
~~~H H
|
||||
------------------^^^^^----------------
|
||||
Reference in New Issue
Block a user