diff --git a/2dgk_zad3/2dgk_zad3/2dgk_zad3.vcxproj b/2dgk_zad3/2dgk_zad3/2dgk_zad3.vcxproj index 2ea6dca..c0a36d3 100644 --- a/2dgk_zad3/2dgk_zad3/2dgk_zad3.vcxproj +++ b/2dgk_zad3/2dgk_zad3/2dgk_zad3.vcxproj @@ -169,6 +169,7 @@ + @@ -199,6 +200,7 @@ + diff --git a/2dgk_zad3/2dgk_zad3/2dgk_zad3.vcxproj.filters b/2dgk_zad3/2dgk_zad3/2dgk_zad3.vcxproj.filters index 8571c7c..74e4d9b 100644 --- a/2dgk_zad3/2dgk_zad3/2dgk_zad3.vcxproj.filters +++ b/2dgk_zad3/2dgk_zad3/2dgk_zad3.vcxproj.filters @@ -66,9 +66,13 @@ Source Files + + Source Files + + @@ -140,10 +144,39 @@ Header Files + + Header Files + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + + + + + + + + + \ No newline at end of file diff --git a/2dgk_zad3/2dgk_zad3/Constants.h b/2dgk_zad3/2dgk_zad3/Constants.h index 3de0b55..816a48e 100644 --- a/2dgk_zad3/2dgk_zad3/Constants.h +++ b/2dgk_zad3/2dgk_zad3/Constants.h @@ -1,7 +1,8 @@ #pragma once + namespace KapitanGame { namespace Constants { - constexpr const char* WINDOW_TITLE = "2DGK - Zadanie 2 (Lab 7-8)"; + constexpr const char* WINDOW_TITLE = "2DGK - Zadanie 3 (Lab 11-12)"; //Analog joystick dead zone constexpr int JOYSTICK_DEAD_ZONE = 8000; //Screen dimension constants @@ -13,8 +14,9 @@ namespace KapitanGame { constexpr int TILE_HEIGHT = 32; constexpr float SPEED = 200.f; constexpr float SMOOTH = 0.4f; - constexpr float JUMP_SPEED = 200.f; - constexpr float JUMP_SHORT_SPEED = 100.f; - constexpr float GRAVITY = 200.f; + constexpr float JUMP_SPEED = 300.f; + constexpr float GRAVITY = 450.f; + constexpr float MAX_JUMP_VELOCITY = 1000.f; + constexpr float MAX_GRAVITY = 1000.f; } } diff --git a/2dgk_zad3/2dgk_zad3/KCamera.cpp b/2dgk_zad3/2dgk_zad3/KCamera.cpp index f287972..3d719c6 100644 --- a/2dgk_zad3/2dgk_zad3/KCamera.cpp +++ b/2dgk_zad3/2dgk_zad3/KCamera.cpp @@ -36,7 +36,7 @@ namespace KapitanGame void KCamera::SetDebug(const SDL_FRect& map) { Viewport = { 0,0,Constants::SCREEN_WIDTH, Constants::SCREEN_HEIGHT }; - Scale = Constants::SCREEN_RATIO > 1.f ? Constants::SCREEN_HEIGHT * 1.f / map.h : Constants::SCREEN_WIDTH * 1.f / map.w; + Scale = std::min(Constants::SCREEN_HEIGHT * 1.f / map.h, Constants::SCREEN_WIDTH * 1.f / map.w); } const SDL_FRect& KCamera::GetPlayArea() const diff --git a/2dgk_zad3/2dgk_zad3/KCirclePawn.cpp b/2dgk_zad3/2dgk_zad3/KCirclePawn.cpp index 4e8a2dc..cdd80c6 100644 --- a/2dgk_zad3/2dgk_zad3/KCirclePawn.cpp +++ b/2dgk_zad3/2dgk_zad3/KCirclePawn.cpp @@ -3,7 +3,7 @@ namespace KapitanGame { KCirclePawn::KCirclePawn(const KVector2D& position, const KTexture& texture, - const std::shared_ptr& playerController) : KPawn(position, texture, playerController), + const std::shared_ptr& playerController, KSettings* settings) : KPawn(position, texture, playerController, settings), Collider(this, static_cast(texture.GetWidth())/2.0f) { } diff --git a/2dgk_zad3/2dgk_zad3/KCirclePawn.h b/2dgk_zad3/2dgk_zad3/KCirclePawn.h index 9ffac03..b454faf 100644 --- a/2dgk_zad3/2dgk_zad3/KCirclePawn.h +++ b/2dgk_zad3/2dgk_zad3/KCirclePawn.h @@ -8,7 +8,7 @@ namespace KapitanGame { public: KCirclePawn(const KVector2D& position, const KTexture& texture, - const std::shared_ptr& playerController); + const std::shared_ptr& playerController, KSettings* settings); const KCollider* GetCollider() const override; private: diff --git a/2dgk_zad3/2dgk_zad3/KFont.cpp b/2dgk_zad3/2dgk_zad3/KFont.cpp index 520fede..7bbb11d 100644 --- a/2dgk_zad3/2dgk_zad3/KFont.cpp +++ b/2dgk_zad3/2dgk_zad3/KFont.cpp @@ -48,7 +48,7 @@ namespace KapitanGame KTexture KFont::GetTextTexture(const std::string& text, const SDL_Color textColor, SDL_Renderer* renderer) const { KTexture texture; - SDL_Surface* textSurface = TTF_RenderText_Blended(Font, text.c_str(), textColor); + SDL_Surface* textSurface = TTF_RenderText_Solid(Font, text.c_str(), textColor); if (textSurface == nullptr) { printf("Unable to render text surface! SDL_ttf Error: %s\n", TTF_GetError()); diff --git a/2dgk_zad3/2dgk_zad3/KGame.cpp b/2dgk_zad3/2dgk_zad3/KGame.cpp index d8df2be..830be5a 100644 --- a/2dgk_zad3/2dgk_zad3/KGame.cpp +++ b/2dgk_zad3/2dgk_zad3/KGame.cpp @@ -29,6 +29,12 @@ namespace KapitanGame { { throw std::runtime_error(Utils::StringFormat("SDL could not initialize! SDL_Error: %s", SDL_GetError())); } + +#ifndef NDEBUG + SDL_LogSetAllPriority(SDL_LOG_PRIORITY_VERBOSE); +#endif + + //Set texture filtering to linear if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) { @@ -58,6 +64,7 @@ namespace KapitanGame { } //Initialize renderer color SDL_SetRenderDrawColor(Renderer, 0, 0xFF, 0, 0xFF); + SDL_Log("KGame initialized..."); } KGame::~KGame() { @@ -84,8 +91,8 @@ namespace KapitanGame { { bool success = true; Objects.clear(); - FreePositions.clear(); - /*std::ifstream levelFile; + Pawns.clear(); + std::ifstream levelFile; levelFile.open("assets/levels/level" + std::to_string(LvlCounter) + ".txt"); if (levelFile.fail()) { @@ -96,6 +103,7 @@ namespace KapitanGame { { int y = 0; float mapWidth = 0; + KVector2D startPosition{ 0.f, 0.f }; std::string line; while (std::getline(levelFile, line)) { if (mapWidth < static_cast(line.length() * Constants::TILE_WIDTH)) @@ -107,8 +115,8 @@ namespace KapitanGame { case '#': Objects.emplace(std::make_pair(static_cast(i), y), std::make_shared(position, Textures["wall.bmp"])); break; - case ' ': - FreePositions.emplace_back(position); + case 'P': + startPosition = position; break; default: break; @@ -119,19 +127,12 @@ namespace KapitanGame { const auto mapHeight = static_cast(y * Constants::TILE_HEIGHT); levelFile.close(); Map = { 0,0,mapWidth,mapHeight }; - }*/ - Map = { 0,0,Constants::SCREEN_WIDTH,Constants::SCREEN_HEIGHT }; + Pawns.emplace_back(std::make_shared(startPosition, Textures["P1.bmp"], PlayerControllers[static_cast(KPlayer::Player1)], &Settings)); + PlayerControllers[static_cast(KPlayer::Player1)]->Possess(Pawns.back().get()); + } return success; } - void KGame::LoadPlayerPawn() - { - Pawns.clear(); - KVector2D StartPosition{ 0.f, 0.f }; - Pawns.emplace_back(std::make_shared(StartPosition, Textures["P1.bmp"], PlayerControllers[static_cast(KPlayer::Player1)])); - PlayerControllers[static_cast(KPlayer::Player1)]->Possess(Pawns.back().get()); - } - bool KGame::LoadMedia() { //Loading success flag @@ -161,7 +162,7 @@ namespace KapitanGame { } Fonts.emplace("PressStart2P-Regular", KFont()); - if (!Fonts["PressStart2P-Regular"].LoadFromFile("assets/fonts/PressStart2P-Regular.ttf", 72)) + if (!Fonts["PressStart2P-Regular"].LoadFromFile("assets/fonts/PressStart2P-Regular.ttf", 8)) { printf("Failed to load PressStart2P-Regular font!\n"); Fonts.erase("PressStart2P-Regular"); @@ -213,11 +214,13 @@ namespace KapitanGame { KCamera debugCamera(Map); debugCamera.SetDebug(Map); - bool debug = false; + char devMode = 0; - LoadPlayerPawn(); camera.Update(Pawns, Map); + VelocityTextureDirty = true; + GravityTextureDirty = true; + Time = PreviousTime = SDL_GetTicks(); printf("\n"); @@ -246,11 +249,38 @@ namespace KapitanGame { if (Input.IsKeyboardButtonPressed(SDL_SCANCODE_F1)) { - debug = !debug; + if (++devMode > 2) devMode = 0; + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Changed devMode to %d\n", devMode); } - if (Input.IsKeyboardButtonPressed(SDL_SCANCODE_F2)) - { - CollisionEnabled = !CollisionEnabled; + if (devMode >= 1) { + if (Input.IsKeyboardButtonPressed(SDL_SCANCODE_F2)) + { + CollisionEnabled = !CollisionEnabled; + } + if (Input.IsKeyboardButtonHeld(SDL_SCANCODE_F3)) + { + Settings.JumpVelocity -= 0.1f; + if (Settings.JumpVelocity < 0) Settings.JumpVelocity = 0; + VelocityTextureDirty = true; + } + if (Input.IsKeyboardButtonHeld(SDL_SCANCODE_F4)) + { + Settings.JumpVelocity += 0.1f; + if (Settings.JumpVelocity > Constants::MAX_JUMP_VELOCITY) Settings.JumpVelocity = Constants::MAX_JUMP_VELOCITY; + VelocityTextureDirty = true; + } + if (Input.IsKeyboardButtonHeld(SDL_SCANCODE_F5)) + { + Settings.Gravity -= 0.1f; + if (Settings.Gravity < 0) Settings.Gravity = 0; + GravityTextureDirty = true; + } + if (Input.IsKeyboardButtonHeld(SDL_SCANCODE_F6)) + { + Settings.Gravity += 0.1f; + if (Settings.Gravity > Constants::MAX_GRAVITY) Settings.Gravity = Constants::MAX_GRAVITY; + GravityTextureDirty = true; + } } if (Playing) { @@ -279,13 +309,21 @@ namespace KapitanGame { camera.Update(Pawns, Map); } + if (VelocityTextureDirty) { + Textures.insert_or_assign("Text_Velocity", Fonts["PressStart2P-Regular"].GetTextTexture(Utils::StringFormat("Velocity: %f", Settings.JumpVelocity), { 0,0,0,0xFF }, Renderer)); + VelocityTextureDirty = false; + } + if (GravityTextureDirty) { + Textures.insert_or_assign("Text_Gravity", Fonts["PressStart2P-Regular"].GetTextTexture(Utils::StringFormat("Gravity: %f", Settings.Gravity), { 0,0,0,0xFF }, Renderer)); + GravityTextureDirty = false; + } + if (!Playing) { if (Time > RestartTick) { LoadLevel(); - LoadPlayerPawn(); debugCamera.SetDebug(Map); camera.Update(Pawns, Map); if (ShowWinner) @@ -308,12 +346,12 @@ namespace KapitanGame { SDL_SetRenderDrawColor(Renderer, 66, 135, 245, 0xFF); SDL_RenderClear(Renderer); - const auto& cameraInUse = debug ? debugCamera : camera; + const auto& cameraInUse = devMode >= 2 ? debugCamera : camera; if (Playing) { - for (const auto& object : Objects) - object.second->Render(Renderer, cameraInUse); + for (const auto& [tile, obj] : Objects) + obj->Render(Renderer, cameraInUse); for (const auto& pawn : Pawns) { pawn->Render(Renderer, cameraInUse); } @@ -329,8 +367,12 @@ namespace KapitanGame { Textures["Text_Winner"].Render(Renderer, Constants::SCREEN_WIDTH / 2.f - Textures["Text_Winner"].GetWidth() / 2.f, 25.f); } - Textures["Text_Score"].Render(Renderer, Constants::SCREEN_WIDTH / 2.f - Textures["Text_Score"].GetWidth() / 2.f, Constants::SCREEN_HEIGHT - 10.f - Textures["Text_Score"].GetHeight()); - if (debug) + //Textures["Text_Score"].Render(Renderer, Constants::SCREEN_WIDTH / 2.f - Textures["Text_Score"].GetWidth() / 2.f, Constants::SCREEN_HEIGHT - 10.f - Textures["Text_Score"].GetHeight()); + if (devMode >= 1) { + Textures["Text_Velocity"].Render(Renderer, 10.f, 25.f); + Textures["Text_Gravity"].Render(Renderer, 10.f, 30.f + Textures["Text_Velocity"].GetHeight()); + } + if (devMode >= 2) { SDL_SetRenderDrawColor(Renderer, 0x00, 0x00, 0x00, 0xFF); SDL_RenderDrawLineF(Renderer, 0, Constants::SCREEN_HEIGHT / 2.f, Constants::SCREEN_WIDTH, Constants::SCREEN_HEIGHT / 2.f); diff --git a/2dgk_zad3/2dgk_zad3/KGame.h b/2dgk_zad3/2dgk_zad3/KGame.h index bc2dc56..d54b48f 100644 --- a/2dgk_zad3/2dgk_zad3/KGame.h +++ b/2dgk_zad3/2dgk_zad3/KGame.h @@ -7,6 +7,7 @@ #include "KFont.h" #include "KInput.h" +#include "KSettings.h" #include "Utils.h" @@ -35,7 +36,6 @@ namespace KapitanGame { std::vector> PlayerControllers; std::unordered_map Textures; std::unordered_map Fonts; - std::vector FreePositions; KInput Input; uint32_t Time; uint32_t PreviousTime; @@ -43,9 +43,11 @@ namespace KapitanGame { bool CollisionEnabled = true; bool ShowWinner = false; std::weak_ptr Exit; + KSettings Settings; + bool VelocityTextureDirty; + bool GravityTextureDirty; bool LoadLevel(); - void LoadPlayerPawn(); bool LoadMedia(); int LvlCounter = 1; diff --git a/2dgk_zad3/2dgk_zad3/KPawn.cpp b/2dgk_zad3/2dgk_zad3/KPawn.cpp index 344df6c..c8cc29a 100644 --- a/2dgk_zad3/2dgk_zad3/KPawn.cpp +++ b/2dgk_zad3/2dgk_zad3/KPawn.cpp @@ -2,11 +2,11 @@ #include #include -#include #include "Constants.h" #include "KPlayerController.h" #include "KExit.h" +#include "KSettings.h" namespace KapitanGame { @@ -19,38 +19,42 @@ namespace KapitanGame for (const auto& [oX, oY] : std::initializer_list>{ {0, 0}, {-1,0}, {1,0}, {0,-1}, {0,1}, {-1,-1}, {1,1}, {1,-1}, {-1,1} }) { - try { - const auto& other = objects.at({ xPos + oX, yPos + oY }); - if (other->GetCollider() == nullptr) continue; - if (other->Id == Id) continue; - if (GetCollider()->IsCollision(other->GetCollider())) { - if (typeid(*other) == typeid(KExit)) { - if (const auto pc = PlayerController.lock()) { - //pc->NotifyWin(); - } - return; - } - const auto separationVector = GetCollider()->GetSeparationVector(other->GetCollider()); - - Position += separationVector; - if (separationVector.Y < 0) - { - Velocity.Y = 0; - Grounded = true; + auto it = objects.find({ xPos + oX, yPos + oY }); + if (it == objects.end()) continue; + const auto& other = it->second; + if (other->GetCollider() == nullptr) continue; + if (other->Id == Id) continue; + if (GetCollider()->IsCollision(other->GetCollider())) { + if (typeid(*other) == typeid(KExit)) { + if (const auto pc = PlayerController.lock()) { + //pc->NotifyWin(); } + return; + } + const auto separationVector = GetCollider()->GetSeparationVector(other->GetCollider()); + + Position += separationVector; + if (GetPosition().Y - GetCollider()->GetHeight() / 2.f >= other->GetPosition().Y + other->GetCollider()->GetHeight() / 2.f) + { + Velocity.Y = 0; + } + if (GetPosition().Y + GetCollider()->GetHeight() / 2.f <= other->GetPosition().Y - other->GetCollider()->GetHeight() / 2.f) + { + Velocity.Y = 0; + CanJump = true; + CanDoubleJump = true; + HasJumped = false; } - } - catch (std::out_of_range&) - { } } } void KPawn::MovementStep(const float& timeStep) { + const float gravityScale = Velocity.Y > 0.f ? 3.f : 1.f; Position.X += Velocity.X * timeStep; - Position.Y += Velocity.Y * timeStep + 1.f / 2.f * Constants::GRAVITY * timeStep * timeStep; - Velocity.Y += Constants::GRAVITY * timeStep; + Position.Y += Velocity.Y * timeStep + 1.f / 2.f * gravityScale * Settings->Gravity * timeStep * timeStep; + Velocity.Y += gravityScale * Settings->Gravity * timeStep; } void KPawn::CollisionDetectionWithMapStep(const SDL_FRect& map) @@ -60,7 +64,9 @@ namespace KapitanGame if (separationVector.Y < 0) { Velocity.Y = 0; - Grounded = true; + CanJump = true; + CanDoubleJump = true; + HasJumped = false; } } @@ -70,16 +76,26 @@ namespace KapitanGame } void KPawn::StopJump() { - if (!Grounded) { - if (Velocity.Y < -Constants::JUMP_SHORT_SPEED) - Velocity.Y = -Constants::JUMP_SHORT_SPEED; + if (!CanJump) { + if (Velocity.Y < -Settings->ShortJumpVelocity()) + Velocity.Y = -Settings->ShortJumpVelocity(); } } void KPawn::StartJump() { - if (Grounded) { - Grounded = false; - Velocity.Y = -Constants::JUMP_SPEED; + if (Velocity.Y != 0.f) + { + CanJump = false; + } + if (CanJump) { + CanJump = false; + HasJumped = true; + Velocity.Y = -Settings->JumpVelocity; + } + else if (HasJumped && CanDoubleJump) + { + CanDoubleJump = false; + Velocity.Y = -Settings->JumpVelocity; } } } diff --git a/2dgk_zad3/2dgk_zad3/KPawn.h b/2dgk_zad3/2dgk_zad3/KPawn.h index e37867e..f255102 100644 --- a/2dgk_zad3/2dgk_zad3/KPawn.h +++ b/2dgk_zad3/2dgk_zad3/KPawn.h @@ -7,17 +7,19 @@ namespace KapitanGame { + struct KSettings; class KPlayerController; class KPawn : public KObject { public: KPawn(const KVector2D& position, const KTexture& texture, - const std::shared_ptr& playerController) + const std::shared_ptr& playerController, KSettings* settings) : KObject(position, texture), - PlayerController(playerController) + PlayerController(playerController), CanJump(false), CanDoubleJump(false), HasJumped(true), Settings(settings) { } + void CollisionDetectionStep(const std::unordered_map, std::shared_ptr, Utils::PairHash>& objects); void MovementStep(const float& timeStep); void CollisionDetectionWithMapStep(const SDL_FRect& map); @@ -27,7 +29,10 @@ namespace KapitanGame private: const std::weak_ptr PlayerController; KVector2D Velocity{ 0.f, 0.f }; - bool Grounded; + bool CanJump; + bool CanDoubleJump; + bool HasJumped; + KSettings* Settings; }; } diff --git a/2dgk_zad3/2dgk_zad3/KRectPawn.cpp b/2dgk_zad3/2dgk_zad3/KRectPawn.cpp index 2ef8134..0794cf2 100644 --- a/2dgk_zad3/2dgk_zad3/KRectPawn.cpp +++ b/2dgk_zad3/2dgk_zad3/KRectPawn.cpp @@ -3,7 +3,7 @@ namespace KapitanGame { KRectPawn::KRectPawn(const KVector2D& position, const KTexture& texture, - const std::shared_ptr& playerController) : KPawn(position, texture, playerController), + const std::shared_ptr& playerController, KSettings* settings) : KPawn(position, texture, playerController, settings), Collider(this, texture.GetWidth(), texture.GetHeight()) { } diff --git a/2dgk_zad3/2dgk_zad3/KRectPawn.h b/2dgk_zad3/2dgk_zad3/KRectPawn.h index f2cf0c9..750a564 100644 --- a/2dgk_zad3/2dgk_zad3/KRectPawn.h +++ b/2dgk_zad3/2dgk_zad3/KRectPawn.h @@ -9,7 +9,7 @@ namespace KapitanGame { public: KRectPawn(const KVector2D& position, const KTexture& texture, - const std::shared_ptr& playerController); + const std::shared_ptr& playerController, KSettings* settings); const KCollider* GetCollider() const override; private: diff --git a/2dgk_zad3/2dgk_zad3/KSettings.cpp b/2dgk_zad3/2dgk_zad3/KSettings.cpp new file mode 100644 index 0000000..1597b37 --- /dev/null +++ b/2dgk_zad3/2dgk_zad3/KSettings.cpp @@ -0,0 +1,9 @@ +#include "KSettings.h" + +namespace KapitanGame +{ + float KSettings::ShortJumpVelocity() const + { + return JumpVelocity / 2.f; + } +} diff --git a/2dgk_zad3/2dgk_zad3/KSettings.h b/2dgk_zad3/2dgk_zad3/KSettings.h new file mode 100644 index 0000000..31b189d --- /dev/null +++ b/2dgk_zad3/2dgk_zad3/KSettings.h @@ -0,0 +1,14 @@ +#pragma once +#include "Constants.h" + +namespace KapitanGame +{ + struct KSettings + { + float Gravity = Constants::GRAVITY; + float JumpVelocity = Constants::JUMP_SPEED; + + [[nodiscard]] float ShortJumpVelocity() const; + }; +} + diff --git a/2dgk_zad3/2dgk_zad3/KTexture.cpp b/2dgk_zad3/2dgk_zad3/KTexture.cpp index 5de09e4..ac7798c 100644 --- a/2dgk_zad3/2dgk_zad3/KTexture.cpp +++ b/2dgk_zad3/2dgk_zad3/KTexture.cpp @@ -17,6 +17,7 @@ namespace KapitanGame { KTexture& KTexture::operator=(KTexture&& other) noexcept { if (this == &other) return *this; + Free(); Texture = other.Texture; Width = other.Width; Height = other.Height; diff --git a/2dgk_zad3/2dgk_zad3/assets/levels/level1.txt b/2dgk_zad3/2dgk_zad3/assets/levels/level1.txt index fee3295..6ecec00 100644 --- a/2dgk_zad3/2dgk_zad3/assets/levels/level1.txt +++ b/2dgk_zad3/2dgk_zad3/assets/levels/level1.txt @@ -1,33 +1,15 @@ -################################# -# # # # # # # -# ######### # # ### # # ### ### # -# # # # # # # # -### ### ### ### # ### # # # ##### -# # # # # # # # # # # -# # ### # # # # ####### # # ### # -# # # # # # # # # # # # -####### ### ### # ####### ### # # -# # # # # # # # # # -# # # ### ### # # # # ### # ##### -# # # # # # # # # # -# # ### # ### # # ### # # ### # # -# # # # # # # # # # # -# # # ### ### # ### ### ### # # # -# # # # # # # # # # # # -# ### ##### ##### # ######### ### -# # # # # # # -##### ### # ### ####### # ##### # -# # # # # # -# ######### # # ####### ##### ### -# # # # # # # # # -### ### ### ########### ### # ### -# # # # # # # -# # ### # ### # ##### ### ### # # -# # # # # # # # # -# # ##### ### ### ### # ### # ### -# # # # # # # # # # -# ##### # # ##### # # ##### ##### -# # # # # # # # # # # # # -# # ### ##### ####### # # # ### # -# # # # # # -################################# \ No newline at end of file + + + + + + # + + + + # ##### ## + ## ## + ## ## ## + P ## ## ## +################################################################## +################################################################## \ No newline at end of file