mirror of
https://github.com/Motschen/Adventura.git
synced 2025-12-18 12:45:10 +01:00
feat: fully working on SDL3 :)
This commit is contained in:
@@ -152,7 +152,8 @@ else()
|
|||||||
)
|
)
|
||||||
endmacro()
|
endmacro()
|
||||||
copy_helper("assets/Inter-VariableFont.ttf")
|
copy_helper("assets/Inter-VariableFont.ttf")
|
||||||
copy_helper("assets/the_entertainer.ogg")
|
copy_helper("assets/MartianMono-VariableFont.ttf")
|
||||||
|
copy_helper("assets/sunset_on_the_beach.ogg")
|
||||||
copy_helper("assets/gs_tiger.svg")
|
copy_helper("assets/gs_tiger.svg")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|||||||
109
src/adventura.cpp
Normal file
109
src/adventura.cpp
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
#include <string>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include <thread>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
#include "world.hpp"
|
||||||
|
#include "player.hpp"
|
||||||
|
#include "blockRegistry.hpp"
|
||||||
|
#include "movementHandler.hpp"
|
||||||
|
#include "output.hpp"
|
||||||
|
|
||||||
|
using std::string;
|
||||||
|
using std::cout;
|
||||||
|
using std::endl;
|
||||||
|
|
||||||
|
bool startWorld(string worldFile);
|
||||||
|
bool parseArgs(int argc, char *argv[]);
|
||||||
|
|
||||||
|
static bool testMode = false;
|
||||||
|
static unsigned int worldIndex = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 start(int argc, char *argv[]) {
|
||||||
|
if (parseArgs(argc, argv)) return 0;
|
||||||
|
|
||||||
|
if (!testMode) {
|
||||||
|
printFile("./screens/start.txt", Color::BRIGHT_YELLOW); // Show the story introduction
|
||||||
|
waitForInput();
|
||||||
|
printGuide(); // Show the block guide
|
||||||
|
waitForInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load every world in order
|
||||||
|
for (const auto & world : getOrderedFileNames("./worlds/", ".txt"))
|
||||||
|
if (!startWorld(world)) return 0; // If the player dies, exit
|
||||||
|
|
||||||
|
// Print the victory screen once all levels have been completed
|
||||||
|
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);
|
||||||
|
|
||||||
|
world.loadFromFile(worldFile);
|
||||||
|
Player player = Player(world.getStartPos(), world);
|
||||||
|
render(world, player.mapToWorldspace());
|
||||||
|
|
||||||
|
inputLoop(player, world, testMode, worldIndex);
|
||||||
|
|
||||||
|
worldIndex++;
|
||||||
|
if (!player.isAlive()) printFile("./screens/death.txt", Color::BRIGHT_RED);
|
||||||
|
return player.hasReachedGoal();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses command-line arguments to set the game's configuration.
|
||||||
|
*
|
||||||
|
* Recognizes the following arguments:
|
||||||
|
* - "-h" or "--help": Displays the help screen and exits.
|
||||||
|
* - "-t" or "--test": Enables test mode, starting an automated playthrough.
|
||||||
|
* - "-l" or "--level <levelName>": Loads and plays only the specified level.
|
||||||
|
*
|
||||||
|
* If a custom action is specified, the function returns true to indicate
|
||||||
|
* immediate termination after executing the requested action.
|
||||||
|
*
|
||||||
|
* @param argc The number of command-line arguments.
|
||||||
|
* @param argv The array containing the command-line arguments.
|
||||||
|
* @return true if the program should exit after handling the arguments, false otherwise.
|
||||||
|
*/
|
||||||
|
bool parseArgs(int argc, char *argv[]) {
|
||||||
|
if (argc > 1) {
|
||||||
|
for (int i = 1; i < argc; i++) {
|
||||||
|
string arg = string(argv[i]); // Unsafe buffer usage warnings can be safely ignored, as we do check for the size
|
||||||
|
if (arg == "-h" || arg == "--help")
|
||||||
|
break;
|
||||||
|
else if (arg == "-t" || arg == "--test")
|
||||||
|
testMode = true;
|
||||||
|
|
||||||
|
else if ((arg == "-l" || arg == "--level") && argc > i + 1) {
|
||||||
|
if (!startWorld("./" + string(argv[i+1]))) // This warning can also be ignored, again – we do this in a safe way
|
||||||
|
return true; // Load only the specified world
|
||||||
|
else
|
||||||
|
printFile("./screens/completed_single_level.txt", Color::BRIGHT_GREEN);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!testMode) {
|
||||||
|
printFile("./screens/help.txt", Color::BRIGHT_BLUE); // Print help screen
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
BIN
src/assets/MartianMono-VariableFont.ttf
Normal file
BIN
src/assets/MartianMono-VariableFont.ttf
Normal file
Binary file not shown.
@@ -1,3 +1,3 @@
|
|||||||
"The Entertainer" Kevin MacLeod (incompetech.com)
|
"Sunset On The Beach" Kumiku
|
||||||
Licensed under Creative Commons: By Attribution 4.0 License
|
Licensed under CC0 1.0
|
||||||
http://creativecommons.org/licenses/by/4.0/
|
https://freemusicarchive.org/music/Komiku/Poupis_incredible_adventures_/Komiku_-_Poupis_incredible_adventures__-_55_Sunset_on_the_beach/
|
||||||
BIN
src/assets/sunset_on_the_beach.ogg
Normal file
BIN
src/assets/sunset_on_the_beach.ogg
Normal file
Binary file not shown.
Binary file not shown.
353
src/main.cpp
353
src/main.cpp
@@ -1,109 +1,284 @@
|
|||||||
#include <string>
|
#include <SDL3/SDL.h>
|
||||||
#include <iostream>
|
#include <SDL3/SDL_main.h>
|
||||||
|
#include <SDL3/SDL_init.h>
|
||||||
|
#include <SDL3_ttf/SDL_ttf.h>
|
||||||
|
#include <SDL3_mixer/SDL_mixer.h>
|
||||||
|
#include <SDL3_image/SDL_image.h>
|
||||||
|
#include <cmath>
|
||||||
|
#include <string_view>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
#include <thread>
|
#include "adventura.cpp"
|
||||||
#include <chrono>
|
|
||||||
|
|
||||||
#include "world.hpp"
|
constexpr uint32_t windowStartWidth = 1200;
|
||||||
#include "player.hpp"
|
constexpr uint32_t windowStartHeight = 800;
|
||||||
#include "blockRegistry.hpp"
|
|
||||||
#include "movementHandler.hpp"
|
|
||||||
#include "output.hpp"
|
|
||||||
|
|
||||||
using std::string;
|
void loadWorld(void* appstate, string worldFile);
|
||||||
using std::cout;
|
void resetWorld(void* appstate);
|
||||||
using std::endl;
|
void loadNextWorld(void* appstate);
|
||||||
|
|
||||||
bool startWorld(string worldFile);
|
struct AppContext {
|
||||||
bool parseArgs(int argc, char *argv[]);
|
SDL_Window* window;
|
||||||
|
SDL_Renderer* renderer;
|
||||||
|
SDL_Texture* messageTex, *imageTex;
|
||||||
|
SDL_FRect messageDest;
|
||||||
|
SDL_AudioDeviceID audioDevice;
|
||||||
|
Mix_Music* music;
|
||||||
|
SDL_AppResult app_quit = SDL_APP_CONTINUE;
|
||||||
|
Player* player;
|
||||||
|
World* world;
|
||||||
|
TTF_Font* font;
|
||||||
|
};
|
||||||
|
|
||||||
static bool testMode = false;
|
SDL_AppResult SDL_Fail(){
|
||||||
static unsigned int worldIndex = 2;
|
SDL_LogError(SDL_LOG_CATEGORY_CUSTOM, "Error %s", SDL_GetError());
|
||||||
|
return SDL_APP_FAILURE;
|
||||||
/**
|
|
||||||
* 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 (parseArgs(argc, argv)) return 0;
|
|
||||||
|
|
||||||
if (!testMode) {
|
|
||||||
printFile("./screens/start.txt", Color::BRIGHT_YELLOW); // Show the story introduction
|
|
||||||
waitForInput();
|
|
||||||
printGuide(); // Show the block guide
|
|
||||||
waitForInput();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load every world in order
|
SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[]) {
|
||||||
for (const auto & world : getOrderedFileNames("./worlds/", ".txt"))
|
// init the library, here we make a window so we only need the Video capabilities.
|
||||||
if (!startWorld(world)) return 0; // If the player dies, exit
|
if (not SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)){
|
||||||
|
return SDL_Fail();
|
||||||
// Print the victory screen once all levels have been completed
|
|
||||||
printFile("./screens/victory.txt", Color::BRIGHT_GREEN);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// init TTF
|
||||||
* Start a new world defined in the file at worldFile.
|
if (not TTF_Init()) {
|
||||||
* If the player reaches the goal, return true.
|
return SDL_Fail();
|
||||||
* In case they die, print the death screen and return false.
|
}
|
||||||
* @return true if the player reached the goal, false in case of death
|
|
||||||
*/
|
// create a window
|
||||||
bool startWorld(string worldFile) {
|
|
||||||
|
SDL_Window* window = SDL_CreateWindow("Adventura", windowStartWidth, windowStartHeight, SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY);
|
||||||
|
if (not window){
|
||||||
|
return SDL_Fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a renderer
|
||||||
|
SDL_Renderer* renderer = SDL_CreateRenderer(window, NULL);
|
||||||
|
if (not renderer){
|
||||||
|
return SDL_Fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the font
|
||||||
|
#if __ANDROID__
|
||||||
|
std::filesystem::path basePath = ""; // on Android we do not want to use basepath. Instead, assets are available at the root directory.
|
||||||
|
#else
|
||||||
|
auto basePathPtr = SDL_GetBasePath();
|
||||||
|
if (not basePathPtr){
|
||||||
|
return SDL_Fail();
|
||||||
|
}
|
||||||
|
const std::filesystem::path basePath = basePathPtr;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const auto fontPath = basePath / "assets/MartianMono-VariableFont.ttf";
|
||||||
|
TTF_Font* font = TTF_OpenFont(fontPath.string().c_str(), 36);
|
||||||
|
if (not font) {
|
||||||
|
return SDL_Fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
// render the font to a surface
|
||||||
|
const std::string_view text = "Hello SDL!";
|
||||||
|
SDL_Surface* surfaceMessage = TTF_RenderText_Solid(font, text.data(), text.length(), { 255,255,255 });
|
||||||
|
|
||||||
|
// make a texture from the surface
|
||||||
|
SDL_Texture* messageTex = SDL_CreateTextureFromSurface(renderer, surfaceMessage);
|
||||||
|
|
||||||
|
// we no longer need the font or the surface, so we can destroy those now.
|
||||||
|
//TTF_CloseFont(font);
|
||||||
|
SDL_DestroySurface(surfaceMessage);
|
||||||
|
|
||||||
|
// load the SVG
|
||||||
|
auto svg_surface = IMG_Load((basePath / "assets/gs_tiger.svg").string().c_str());
|
||||||
|
SDL_Texture* tex = SDL_CreateTextureFromSurface(renderer, svg_surface);
|
||||||
|
SDL_DestroySurface(svg_surface);
|
||||||
|
|
||||||
|
|
||||||
|
// get the on-screen dimensions of the text. this is necessary for rendering it
|
||||||
|
auto messageTexProps = SDL_GetTextureProperties(messageTex);
|
||||||
|
SDL_FRect text_rect{
|
||||||
|
.x = 0,
|
||||||
|
.y = 0,
|
||||||
|
.w = float(SDL_GetNumberProperty(messageTexProps, SDL_PROP_TEXTURE_WIDTH_NUMBER, 0)),
|
||||||
|
.h = float(SDL_GetNumberProperty(messageTexProps, SDL_PROP_TEXTURE_HEIGHT_NUMBER, 0))
|
||||||
|
};
|
||||||
|
|
||||||
|
// init SDL Mixer
|
||||||
|
auto audioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, NULL);
|
||||||
|
if (not audioDevice) {
|
||||||
|
return SDL_Fail();
|
||||||
|
}
|
||||||
|
if (not Mix_OpenAudio(audioDevice, NULL)) {
|
||||||
|
return SDL_Fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the music
|
||||||
|
auto musicPath = basePath / "assets/sunset_on_the_beach.ogg";
|
||||||
|
auto music = Mix_LoadMUS(musicPath.string().c_str());
|
||||||
|
if (not music) {
|
||||||
|
return SDL_Fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
// play the music (does not loop)
|
||||||
|
Mix_PlayMusic(music, 99999);
|
||||||
|
|
||||||
|
// print some information about the window
|
||||||
|
SDL_ShowWindow(window);
|
||||||
|
{
|
||||||
|
int width, height, bbwidth, bbheight;
|
||||||
|
SDL_GetWindowSize(window, &width, &height);
|
||||||
|
SDL_GetWindowSizeInPixels(window, &bbwidth, &bbheight);
|
||||||
|
SDL_Log("Window size: %ix%i", width, height);
|
||||||
|
SDL_Log("Backbuffer size: %ix%i", bbwidth, bbheight);
|
||||||
|
if (width != bbwidth){
|
||||||
|
SDL_Log("This is a highdpi environment.");
|
||||||
|
}
|
||||||
|
}
|
||||||
BlockRegistry blockRegistry = BlockRegistry();
|
BlockRegistry blockRegistry = BlockRegistry();
|
||||||
World world = World(blockRegistry);
|
World *world = new World(blockRegistry);
|
||||||
|
Player *player = new Player(world->getStartPos(), *world);
|
||||||
|
|
||||||
world.loadFromFile(worldFile);
|
// set up the application data
|
||||||
Player player = Player(world.getStartPos(), world);
|
*appstate = new AppContext{
|
||||||
render(world, player.mapToWorldspace());
|
.window = window,
|
||||||
|
.renderer = renderer,
|
||||||
|
.messageTex = messageTex,
|
||||||
|
.imageTex = tex,
|
||||||
|
.messageDest = text_rect,
|
||||||
|
.audioDevice = audioDevice,
|
||||||
|
.music = music,
|
||||||
|
.player = player,
|
||||||
|
.world = world,
|
||||||
|
.font = font
|
||||||
|
};
|
||||||
|
loadNextWorld(*appstate);
|
||||||
|
|
||||||
inputLoop(player, world, testMode, worldIndex);
|
SDL_SetRenderVSync(renderer, -1); // enable vysnc
|
||||||
|
|
||||||
worldIndex++;
|
SDL_Log("Application started successfully!");
|
||||||
if (!player.isAlive()) printFile("./screens/death.txt", Color::BRIGHT_RED);
|
|
||||||
return player.hasReachedGoal();
|
//start(argc, argv);
|
||||||
|
|
||||||
|
return SDL_APP_CONTINUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event* event) {
|
||||||
* Parses command-line arguments to set the game's configuration.
|
auto* app = (AppContext*)appstate;
|
||||||
*
|
|
||||||
* Recognizes the following arguments:
|
|
||||||
* - "-h" or "--help": Displays the help screen and exits.
|
|
||||||
* - "-t" or "--test": Enables test mode, starting an automated playthrough.
|
|
||||||
* - "-l" or "--level <levelName>": Loads and plays only the specified level.
|
|
||||||
*
|
|
||||||
* If a custom action is specified, the function returns true to indicate
|
|
||||||
* immediate termination after executing the requested action.
|
|
||||||
*
|
|
||||||
* @param argc The number of command-line arguments.
|
|
||||||
* @param argv The array containing the command-line arguments.
|
|
||||||
* @return true if the program should exit after handling the arguments, false otherwise.
|
|
||||||
*/
|
|
||||||
bool parseArgs(int argc, char *argv[]) {
|
|
||||||
if (argc > 1) {
|
|
||||||
for (int i = 1; i < argc; i++) {
|
|
||||||
string arg = string(argv[i]); // Unsafe buffer usage warnings can be safely ignored, as we do check for the size
|
|
||||||
if (arg == "-h" || arg == "--help")
|
|
||||||
break;
|
|
||||||
else if (arg == "-t" || arg == "--test")
|
|
||||||
testMode = true;
|
|
||||||
|
|
||||||
else if ((arg == "-l" || arg == "--level") && argc > i + 1) {
|
if (event->type == SDL_EVENT_KEY_DOWN) {
|
||||||
if (!startWorld("./" + string(argv[i+1]))) // This warning can also be ignored, again – we do this in a safe way
|
if (event->key.scancode == SDL_SCANCODE_W) {
|
||||||
return true; // Load only the specified world
|
onInput('w', *(app->world), *(app->player));
|
||||||
else
|
} else if (event->key.scancode == SDL_SCANCODE_A) {
|
||||||
printFile("./screens/completed_single_level.txt", Color::BRIGHT_GREEN);
|
onInput('a', *(app->world), *(app->player));
|
||||||
return true;
|
} else if (event->key.scancode == SDL_SCANCODE_S) {
|
||||||
|
onInput('s', *(app->world), *(app->player));
|
||||||
|
} else if (event->key.scancode == SDL_SCANCODE_D) {
|
||||||
|
onInput('d', *(app->world), *(app->player));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!testMode) {
|
|
||||||
printFile("./screens/help.txt", Color::BRIGHT_BLUE); // Print help screen
|
if ((*app->player).hasReachedGoal()) {
|
||||||
return true;
|
loadNextWorld(appstate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event->type == SDL_EVENT_QUIT) {
|
||||||
|
app->app_quit = SDL_APP_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SDL_APP_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_AppResult SDL_AppIterate(void *appstate) {
|
||||||
|
auto* app = (AppContext*)appstate;
|
||||||
|
|
||||||
|
// draw a color
|
||||||
|
auto time = SDL_GetTicks() / 1000.f;
|
||||||
|
auto red = (std::sin(time) + 1) / 2.0 * 255;
|
||||||
|
auto green = (std::sin(time / 2) + 1) / 2.0 * 255;
|
||||||
|
auto blue = (std::sin(time) * 2 + 1) / 2.0 * 255;
|
||||||
|
SDL_SetRenderDrawBlendMode(app->renderer, SDL_BLENDMODE_ADD);
|
||||||
|
SDL_SetRenderDrawColor(app->renderer, red, green, blue, 1);
|
||||||
|
SDL_SetRenderDrawColor(app->renderer, 0, 0, 0, 20);
|
||||||
|
|
||||||
|
SDL_RenderClear(app->renderer);
|
||||||
|
|
||||||
|
// Renderer uses the painter's algorithm to make the text appear above the image, we must render the image first.
|
||||||
|
//SDL_RenderTexture(app->renderer, app->imageTex, NULL, NULL);
|
||||||
|
vector<vector<char>> worldState = render(*(app->world), (*app->player).mapToWorldspace());
|
||||||
|
for (int i = 0; i < worldState.size(); i++) {
|
||||||
|
vector<char> line = worldState.at(i);
|
||||||
|
SDL_Surface* surfaceMessage = TTF_RenderText_Solid(app->font, line.data(), line.size(), { 255,255,255 });
|
||||||
|
|
||||||
|
// make a texture from the surface
|
||||||
|
SDL_Texture* messageTex = SDL_CreateTextureFromSurface(app->renderer, surfaceMessage);
|
||||||
|
|
||||||
|
auto messageTexProps = SDL_GetTextureProperties(messageTex);
|
||||||
|
SDL_FRect text_rect{
|
||||||
|
.x = 0,
|
||||||
|
.y = float(i * SDL_GetNumberProperty(messageTexProps, SDL_PROP_TEXTURE_HEIGHT_NUMBER, 0)*2),
|
||||||
|
.w = float(SDL_GetNumberProperty(messageTexProps, SDL_PROP_TEXTURE_WIDTH_NUMBER, 0))*2,
|
||||||
|
.h = float(SDL_GetNumberProperty(messageTexProps, SDL_PROP_TEXTURE_HEIGHT_NUMBER, 0))*2
|
||||||
|
};
|
||||||
|
SDL_DestroySurface(surfaceMessage);
|
||||||
|
SDL_RenderTexture(app->renderer, messageTex, NULL, &text_rect);
|
||||||
|
SDL_DestroyTexture(messageTex);
|
||||||
|
}
|
||||||
|
//SDL_RenderTexture(app->renderer, app->messageTex, NULL, &app->messageDest);
|
||||||
|
|
||||||
|
SDL_RenderPresent(app->renderer);
|
||||||
|
|
||||||
|
return app->app_quit;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDL_AppQuit(void* appstate, SDL_AppResult result) {
|
||||||
|
auto* app = (AppContext*)appstate;
|
||||||
|
if (app) {
|
||||||
|
SDL_DestroyRenderer(app->renderer);
|
||||||
|
SDL_DestroyWindow(app->window);
|
||||||
|
|
||||||
|
Mix_FadeOutMusic(1000); // prevent the music from abruptly ending.
|
||||||
|
Mix_FreeMusic(app->music); // this call blocks until the music has finished fading
|
||||||
|
Mix_CloseAudio();
|
||||||
|
SDL_CloseAudioDevice(app->audioDevice);
|
||||||
|
|
||||||
|
delete app;
|
||||||
|
}
|
||||||
|
TTF_Quit();
|
||||||
|
Mix_Quit();
|
||||||
|
|
||||||
|
SDL_Log("Application quit successfully!");
|
||||||
|
SDL_Quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
string currentWorld = "";
|
||||||
|
|
||||||
|
void loadWorld(void* appstate, string worldFile) {
|
||||||
|
auto* app = (AppContext*)appstate;
|
||||||
|
|
||||||
|
(*app->world).loadFromFile(worldFile);
|
||||||
|
(app -> player) = new Player((*app->world).getStartPos(), *app->world);
|
||||||
|
currentWorld = worldFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetWorld(void* appstate) {
|
||||||
|
auto* app = (AppContext*)appstate;
|
||||||
|
|
||||||
|
loadWorld(appstate, currentWorld);
|
||||||
|
}
|
||||||
|
void loadNextWorld(void* appstate) {
|
||||||
|
auto* app = (AppContext*)appstate;
|
||||||
|
|
||||||
|
vector<string> worldFiles = getOrderedFileNames("./worlds/", ".txt");
|
||||||
|
if (currentWorld == "") {
|
||||||
|
loadWorld(app, worldFiles.at(0));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int index = std::find(worldFiles.begin(), worldFiles.end(), currentWorld) - worldFiles.begin();
|
||||||
|
if (index + 1 < worldFiles.size()) {
|
||||||
|
loadWorld(appstate, worldFiles.at(index + 1));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
resetWorld(appstate);
|
||||||
|
// You won
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ static void tryBlockGravity(BlockPos& playerPos, World& world) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
static bool onInput(char lastChar, World& world, Player& player) {
|
static bool onInput(char lastChar, World& world, Player& player) {
|
||||||
|
if (player.isFreeFalling()) return false;
|
||||||
switch (lastChar) {
|
switch (lastChar) {
|
||||||
case ' ':
|
case ' ':
|
||||||
case 'w':
|
case 'w':
|
||||||
@@ -198,10 +199,10 @@ static void inputLoop(Player& player, World& world, bool testMode, unsigned int
|
|||||||
jumpBackOneLine();
|
jumpBackOneLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (char lastChar : currentInput) {
|
//for (char lastChar : currentInput) {
|
||||||
if (onInput(lastChar, world, player))
|
// if (onInput(lastChar, world, player))
|
||||||
redraw(world, player.mapToWorldspace());
|
// redraw(world, player.mapToWorldspace());
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
inputIndex = 0;
|
inputIndex = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
70
src/output.cpp
Normal file
70
src/output.cpp
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
#include <string>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include "output.hpp"
|
||||||
|
|
||||||
|
using std::string;
|
||||||
|
using std::cout;
|
||||||
|
using std::endl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the console cursor up by one line.
|
||||||
|
* Used to overwrite the previous line.
|
||||||
|
*/
|
||||||
|
static void jumpBackOneLine() {
|
||||||
|
std::cout << "\033[1A";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @param world Reference to the World object representing the current world.
|
||||||
|
* @param playerTexture Reference to the current Player texture.
|
||||||
|
*/
|
||||||
|
static vector<vector<char>> render(World &world, vector<vector<char>> playerTexture) {
|
||||||
|
vector<vector<Block>> canvas = world.getFieldState();
|
||||||
|
vector<vector<char>> out;
|
||||||
|
|
||||||
|
for (unsigned int y = 0; y <= world.getMaxY(); y++) {
|
||||||
|
vector<char> line;
|
||||||
|
for (unsigned int x = 0; x <= world.getMaxX(); x++) {
|
||||||
|
if (!world.getBlockAt(BlockPos(x, y)).getSettings().isPushable()
|
||||||
|
&& playerTexture.size() > y && playerTexture.at(y).size() > x && playerTexture.at(y).at(x) != ' ') {
|
||||||
|
line.push_back(playerTexture.at(y).at(x));
|
||||||
|
//<< Color::BRIGHT_YELLOW << playerTexture.at(y).at(x);
|
||||||
|
}
|
||||||
|
else if (canvas.size() > y && canvas.at(y).size() > x) {
|
||||||
|
line.push_back(canvas.at(y).at(x).getEncoding());
|
||||||
|
//cout << canvas.at(y).at(x).getColor() << canvas.at(y).at(x).getEncoding();
|
||||||
|
}
|
||||||
|
else line.push_back(' ');
|
||||||
|
}
|
||||||
|
out.push_back(line);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prints a guide for the player, explaining what each block in the game
|
||||||
|
* represents.
|
||||||
|
*/
|
||||||
|
static void printGuide() {
|
||||||
|
// We use a vector here instead of a map, because we want to keep this order
|
||||||
|
std::vector<std::pair<string, Color>> guide = {
|
||||||
|
{"- Plattform", Color::RESET},
|
||||||
|
{"H Leiter", Color::BRIGHT_MAGENTA},
|
||||||
|
{"S Start", Color::RESET},
|
||||||
|
{"O Ziel", Color::BRIGHT_GREEN},
|
||||||
|
{"0 Wand", Color::RESET},
|
||||||
|
{"^ Stacheln", Color::BRIGHT_RED},
|
||||||
|
{"~ Wasser", Color::BRIGHT_BLUE},
|
||||||
|
{"x Kiste", Color::BRIGHT_CYAN},
|
||||||
|
{"* Sand", Color::BRIGHT_YELLOW}
|
||||||
|
};
|
||||||
|
for (std::pair<string, Color> p : guide) {
|
||||||
|
cout << p.second << p.first << endl;
|
||||||
|
}
|
||||||
|
cout << endl << Color::RESET << "WASD + Enter -> Spiel starten" << endl;
|
||||||
|
}
|
||||||
@@ -1,19 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <string>
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
#include "world.hpp"
|
#include "world.hpp"
|
||||||
|
|
||||||
using std::string;
|
|
||||||
using std::cout;
|
|
||||||
using std::endl;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move the console cursor up by one line.
|
* Move the console cursor up by one line.
|
||||||
* Used to overwrite the previous line.
|
* Used to overwrite the previous line.
|
||||||
*/
|
*/
|
||||||
static void jumpBackOneLine() {
|
static void jumpBackOneLine() {
|
||||||
std::cout << "\033[1A";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,40 +17,27 @@ static void jumpBackOneLine() {
|
|||||||
* @param world Reference to the World object representing the current world.
|
* @param world Reference to the World object representing the current world.
|
||||||
* @param playerTexture Reference to the current Player texture.
|
* @param playerTexture Reference to the current Player texture.
|
||||||
*/
|
*/
|
||||||
static void render(World &world, vector<vector<char>> playerTexture) {
|
static vector<vector<char>> render(World &world, vector<vector<char>> playerTexture) {
|
||||||
vector<vector<Block>> canvas = world.getFieldState();
|
vector<vector<Block>> canvas = world.getFieldState();
|
||||||
|
vector<vector<char>> out;
|
||||||
|
|
||||||
for (unsigned int y = 0; y <= world.getMaxY(); y++) {
|
for (unsigned int y = 0; y <= world.getMaxY(); y++) {
|
||||||
|
vector<char> line;
|
||||||
for (unsigned int x = 0; x <= world.getMaxX(); x++) {
|
for (unsigned int x = 0; x <= world.getMaxX(); x++) {
|
||||||
if (!world.getBlockAt(BlockPos(x, y)).getSettings().isPushable()
|
if (!world.getBlockAt(BlockPos(x, y)).getSettings().isPushable()
|
||||||
&& playerTexture.size() > y && playerTexture.at(y).size() > x && playerTexture.at(y).at(x) != ' ') {
|
&& playerTexture.size() > y && playerTexture.at(y).size() > x && playerTexture.at(y).at(x) != ' ') {
|
||||||
cout << Color::BRIGHT_YELLOW << playerTexture.at(y).at(x);
|
line.push_back(playerTexture.at(y).at(x));
|
||||||
|
//cout << Color::BRIGHT_YELLOW << playerTexture.at(y).at(x);
|
||||||
}
|
}
|
||||||
else if (canvas.size() > y && canvas.at(y).size() > x) {
|
else if (canvas.size() > y && canvas.at(y).size() > x) {
|
||||||
cout << canvas.at(y).at(x).getColor() << canvas.at(y).at(x).getEncoding();
|
line.push_back(canvas.at(y).at(x).getEncoding());
|
||||||
|
//cout << canvas.at(y).at(x).getColor() << canvas.at(y).at(x).getEncoding();
|
||||||
}
|
}
|
||||||
else cout << ' ';
|
else line.push_back(' ');
|
||||||
}
|
}
|
||||||
cout << endl;
|
out.push_back(line);
|
||||||
}
|
}
|
||||||
}
|
return out;
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 current world.
|
|
||||||
* @param playerTexture Reference to the current Player texture.
|
|
||||||
*/
|
|
||||||
static void redraw(World &world, vector<vector<char>> playerTexture) {
|
|
||||||
for (unsigned int y = 0; y <= world.getMaxY(); y++) {
|
|
||||||
jumpBackOneLine();
|
|
||||||
}
|
|
||||||
render(world, playerTexture);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,20 +45,4 @@ static void redraw(World &world, vector<vector<char>> playerTexture) {
|
|||||||
* represents.
|
* represents.
|
||||||
*/
|
*/
|
||||||
static void printGuide() {
|
static void printGuide() {
|
||||||
// We use a vector here instead of a map, because we want to keep this order
|
|
||||||
std::vector<std::pair<string, Color>> guide = {
|
|
||||||
{"- Plattform", Color::RESET},
|
|
||||||
{"H Leiter", Color::BRIGHT_MAGENTA},
|
|
||||||
{"S Start", Color::RESET},
|
|
||||||
{"O Ziel", Color::BRIGHT_GREEN},
|
|
||||||
{"0 Wand", Color::RESET},
|
|
||||||
{"^ Stacheln", Color::BRIGHT_RED},
|
|
||||||
{"~ Wasser", Color::BRIGHT_BLUE},
|
|
||||||
{"x Kiste", Color::BRIGHT_CYAN},
|
|
||||||
{"* Sand", Color::BRIGHT_YELLOW}
|
|
||||||
};
|
|
||||||
for (std::pair<string, Color> p : guide) {
|
|
||||||
cout << p.second << p.first << endl;
|
|
||||||
}
|
|
||||||
cout << endl << Color::RESET << "WASD + Enter -> Spiel starten" << endl;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ public:
|
|||||||
setPos(pos + offset);
|
setPos(pos + offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the player's position and checks for any conditions that would update the state of the player.
|
* Updates the player's position and checks for any conditions that would update the state of the player.
|
||||||
*
|
*
|
||||||
@@ -65,13 +64,15 @@ public:
|
|||||||
|
|
||||||
if (world.getBlockAt(newPos.add(0, 2)) == world.getBlockRegistry().WATER) fallLength = 0;
|
if (world.getBlockAt(newPos.add(0, 2)) == world.getBlockRegistry().WATER) fallLength = 0;
|
||||||
|
|
||||||
isFreeFalling = !world.getBlockAt(newPos.add(0, 2)).getSettings().isSolid();
|
freeFalling = !world.getBlockAt(newPos.add(0, 2)).getSettings().isSolid();
|
||||||
if (isFreeFalling) {
|
if (freeFalling) {
|
||||||
fallLength += 1;
|
fallLength += 1;
|
||||||
if (fallLength > 2) playerTexture = FALLING_PLAYER_TEXTURE;
|
if (fallLength > 2) playerTexture = FALLING_PLAYER_TEXTURE;
|
||||||
redraw(world, this->mapToWorldspace());
|
SDL_AddTimer(100 / fallLength + 50, [](void *userdata, SDL_TimerID timerID, Uint32 interval) -> Uint32 {
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(100 / fallLength + 50));
|
Player *player = (Player *)userdata;
|
||||||
move(0, 1);
|
player->move(0, 1);
|
||||||
|
return 0;
|
||||||
|
}, this);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
playerTexture = REGULAR_PLAYER_TEXTURE;
|
playerTexture = REGULAR_PLAYER_TEXTURE;
|
||||||
@@ -87,7 +88,7 @@ public:
|
|||||||
*/
|
*/
|
||||||
void unalive() {
|
void unalive() {
|
||||||
playerTexture = DEAD_PLAYER_TEXTURE;
|
playerTexture = DEAD_PLAYER_TEXTURE;
|
||||||
redraw(world, this->mapToWorldspace());
|
//redraw(world, this->mapToWorldspace());
|
||||||
alive = false;
|
alive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +100,14 @@ public:
|
|||||||
bool isAlive() {
|
bool isAlive() {
|
||||||
return alive;
|
return alive;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Checks if the player is still alive.
|
||||||
|
*
|
||||||
|
* @return true if the player is alive, false otherwise.
|
||||||
|
*/
|
||||||
|
bool isFreeFalling() {
|
||||||
|
return freeFalling;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the player has reached the goal in the current world.
|
* Checks if the player has reached the goal in the current world.
|
||||||
@@ -141,7 +150,7 @@ private:
|
|||||||
std::array<std::array<char, 3>, 3> playerTexture;
|
std::array<std::array<char, 3>, 3> playerTexture;
|
||||||
BlockPos pos = BlockPos(0, 0);
|
BlockPos pos = BlockPos(0, 0);
|
||||||
bool alive = true;
|
bool alive = true;
|
||||||
bool isFreeFalling = false;
|
bool freeFalling = false;
|
||||||
bool reachedGoal = false;
|
bool reachedGoal = false;
|
||||||
int fallLength = 0;
|
int fallLength = 0;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user