diff --git a/in-progress-games/RetroFracture/README.md b/in-progress-games/RetroFracture/README.md new file mode 100644 index 00000000..51a81c7e --- /dev/null +++ b/in-progress-games/RetroFracture/README.md @@ -0,0 +1,143 @@ +# RetroFracture 🎮 + +*A Multi-Genre Arcade Adventure* + +RetroFracture is a multi-genre arcade game built in C++ using SplashKit, featuring 8-bit neon aesthetics and genre-switching gameplay. Each level represents a different genre, with the player adapting to new mechanics, controls, and challenges as they progress. + +![RetroFracture Banner](assets/envs/Apartment%20Hallway.png) + +## 🌟 Features + +### đŸŽ¯ Genre-Switching Gameplay + +- **Platformer**: Precision jumping and exploration +- **Run & Slash**: Melee combat with acquired weapons +- **Beat 'Em Up**: Combo-based boss battles [In Future] +- **Top-Down Shmup**: Vehicle combat [In Future] + +### 🎨 Visual Style + +- **8-bit pixel art** with vibrant neon palette +- **640×480 resolution** (arcade-perfect) +- **Smooth animations** and dynamic camera + +### đŸ•šī¸ Current Implementation + +- Explorable hallway level with multiple apartments +- **Apartment 101 interior** – fully playable platforming level +- Player physics and collision +- Interactable system (doors, prompts) +- Adaptive HUD + +## 🚀 Quick Start + +### Prerequisites + +- C++17 compatible compiler (GCC, Clang, or MSVC) +- [SplashKit SDK](https://splashkit.io/) installed + +### Installation & Building + +```bash +# Fork and clone the repository +git clone https://github.com/yourusername/RetroFracture.git +cd RetroFracture + +# Build with SplashKit +skm clang++ program.cpp src/*.cpp -o RetroFracture + +# Launch the game! +./RetroFracture +``` + +### Controls + +| Key | Action | +| ------- | --------------------- | +| **A/D** | Move left/right | +| **W** | Jump | +| **E** | Interact with objects | +| **ESC** | Quit game | +| **R** | Reset position | + +## 📁 Project Structure + +``` +RetroFracture/ +├── assets/ # All game assets +│ ├── envs/ # Backgrounds & environments +│ │ ├── Apartment Hallway.png +│ │ └── lvl1_platform.png +│ └── sprites/ # Player animations (Striker) +│ ├── striker/ # Striker character spritesheets +│ └── ui/ # UI elements +├── src/ # Source code +│ ├── game.cpp/.h # Main game controller +│ ├── player.cpp/.h # Player character & physics +│ ├── hallwaylevel.cpp/.h # Hallway level logic +│ ├── lvl1.cpp/.h # Apartment 101 level +│ ├── hud.cpp/.h # UI & display +│ └── interactable.cpp/.h # Interactive objects +├── include/ # External dependencies +│ └── splashkit -> (symlink) +├── program.cpp # Entry point +├── config.txt # Arcade machine config +└── README.md +``` + +### Architecture Overview + +- **Game Class**: Orchestrates levels, camera, and game state +- **Player Class**: Handles movement, animation, physics, and input +- **Level Classes**: Modular level implementations (`HallwayLevel`, `Lvl1`) +- **HUD Class**: Context-aware UI rendering +- **Interactable System**: Event-driven interactions (doors, prompts) + +### Adding New Levels + +1. Create `lvlX.cpp/.h` following the pattern of `lvl1` +2. Add the new level to the `Game` enum and instance variables +3. Implement transition methods in `game.cpp` +4. Add the level to update/draw switches + +## đŸ—ēī¸ Planned Roadmap + +### Campaign (8 Levels, 2 Levels per Act) + +``` +Act 1: +├── ✅ Apartment Hallway +├── 🔄 Apartment 101 (Level 1) +├── 📋 Apartment 102 (Level 2) +├── 📋 Apartment 103 (Level 3) +├── 📋 Apartment 104 (Level 4) +├── 📋 Apartment 105 (Level 5) +├── 📋 Apartment 106 (Level 6) +├── 📋 Apartment 107 & Boss +└── 📋 Rest of apartment complex + +Act 2: +└── To be decided +Act 3: +└── To be decided +Act 4: +└── To be decided +``` + +*The "Apartments" are the actually the long form game stage levels* + +### Coming Soon + +- Enemy AI and combat systems +- Character selection mechanics +- Music and sound implementation +- More apartment interiors (102–107) +- Lighting/shading effects + +## 🤝 Contributing + +Contributions are welcome! Please follow the existing code style and document complex logic with comments. For major changes, open an issue first to discuss what you would like to change. + +--- + +*Built with [SplashKit](https://splashkit.io/) – making game development accessible.* diff --git a/in-progress-games/RetroFracture/RetroFracture b/in-progress-games/RetroFracture/RetroFracture new file mode 100755 index 00000000..13476afd Binary files /dev/null and b/in-progress-games/RetroFracture/RetroFracture differ diff --git a/in-progress-games/RetroFracture/assets/envs/Apartment Hallway.png b/in-progress-games/RetroFracture/assets/envs/Apartment Hallway.png new file mode 100644 index 00000000..b691979c Binary files /dev/null and b/in-progress-games/RetroFracture/assets/envs/Apartment Hallway.png differ diff --git a/in-progress-games/RetroFracture/assets/envs/lvl1_platform.png b/in-progress-games/RetroFracture/assets/envs/lvl1_platform.png new file mode 100644 index 00000000..ef1beb83 Binary files /dev/null and b/in-progress-games/RetroFracture/assets/envs/lvl1_platform.png differ diff --git a/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerDash_strip.png b/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerDash_strip.png new file mode 100644 index 00000000..7e12deee Binary files /dev/null and b/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerDash_strip.png differ diff --git a/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerDeath_strip.png b/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerDeath_strip.png new file mode 100644 index 00000000..bff8e90a Binary files /dev/null and b/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerDeath_strip.png differ diff --git a/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerGetHit_strip.png b/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerGetHit_strip.png new file mode 100644 index 00000000..ce8d7521 Binary files /dev/null and b/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerGetHit_strip.png differ diff --git a/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerIdle_strip.png b/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerIdle_strip.png new file mode 100644 index 00000000..7b15a068 Binary files /dev/null and b/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerIdle_strip.png differ diff --git a/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerJump_strip.png b/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerJump_strip.png new file mode 100644 index 00000000..d157ce95 Binary files /dev/null and b/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerJump_strip.png differ diff --git a/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerRun_strip.png b/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerRun_strip.png new file mode 100644 index 00000000..56cfc22c Binary files /dev/null and b/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerRun_strip.png differ diff --git a/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerSlash_stripNoEffect.png b/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerSlash_stripNoEffect.png new file mode 100644 index 00000000..f8bb98ff Binary files /dev/null and b/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerSlash_stripNoEffect.png differ diff --git a/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerSlash_stripOnlyEffect.png b/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerSlash_stripOnlyEffect.png new file mode 100644 index 00000000..e4acf526 Binary files /dev/null and b/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerSlash_stripOnlyEffect.png differ diff --git a/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerSlash_stripWithEffect.png b/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerSlash_stripWithEffect.png new file mode 100644 index 00000000..16cf5ad7 Binary files /dev/null and b/in-progress-games/RetroFracture/assets/sprites/striker/spr_StrikerSlash_stripWithEffect.png differ diff --git a/in-progress-games/RetroFracture/assets/sprites/ui/preview.png b/in-progress-games/RetroFracture/assets/sprites/ui/preview.png new file mode 100644 index 00000000..6de40966 Binary files /dev/null and b/in-progress-games/RetroFracture/assets/sprites/ui/preview.png differ diff --git a/in-progress-games/RetroFracture/config.txt b/in-progress-games/RetroFracture/config.txt new file mode 100644 index 00000000..6f0e89fe --- /dev/null +++ b/in-progress-games/RetroFracture/config.txt @@ -0,0 +1,25 @@ +# PLEASE ONLY EDIT CONTENT AFTER THE "=" SIGN FOR EACH CONFIGURATION SETTING + +# The title of your game (eg. Super Mario) +title=RetroFracture + +# The name of the author (eg. Jane Doe) +author=Riveen Haggalla + +# The genre of your game (eg. Platformer) +genre=Multi-Genre Mashup + +# A Description of your game +description=8-bit arcade game that switches genres each level + +# A classification rating to advise the nature of the content (eg. MA 15+) +rating=PG + +# Programming language the game is written in (eg. C++) +language=C++ + +# Path to your game thumbnail image (eg. images/icon.png) +image=assets/sprites/ui/preview.png + +# Location of git repo (eg. https://github.com/studioant/arcade_game) +repository=https://github.com/RealH4D35/RetroFracture \ No newline at end of file diff --git a/in-progress-games/RetroFracture/include/splashkit b/in-progress-games/RetroFracture/include/splashkit new file mode 120000 index 00000000..296456d4 --- /dev/null +++ b/in-progress-games/RetroFracture/include/splashkit @@ -0,0 +1 @@ +/home/h4d35/.splashkit/clang++/include \ No newline at end of file diff --git a/in-progress-games/RetroFracture/program.cpp b/in-progress-games/RetroFracture/program.cpp new file mode 100644 index 00000000..5806eb68 --- /dev/null +++ b/in-progress-games/RetroFracture/program.cpp @@ -0,0 +1,21 @@ +#include "splashkit.h" +#include "src/game.h" + +/** + * Main entry point for RetroFracture game + * Sets up window for arcade machine compliance and launches the game + */ +int main() +{ + // Initialize game window with arcade machine specifications + open_window("RetroFracture", 800, 600); + + // Remove window border for arcade cabinet compliance + window_toggle_border("RetroFracture"); + + // Create and run the game instance + Game game; + game.run(); + + return 0; +} \ No newline at end of file diff --git a/in-progress-games/RetroFracture/src/game.cpp b/in-progress-games/RetroFracture/src/game.cpp new file mode 100644 index 00000000..c42cce3c --- /dev/null +++ b/in-progress-games/RetroFracture/src/game.cpp @@ -0,0 +1,290 @@ +#include "game.h" +#include + +Game::Game() + : quit_game(false), + current_level(LEVEL_HALLWAY), + camera_pos(point_at(0, 0)), + nearby_interaction_prompt(""), + show_hitbox(false) +{ + enter_hallway(); +} + +void Game::enter_hallway() +{ + current_level = LEVEL_HALLWAY; + hallway.load(); + + const float START_X = 1435.0f; + player.set_position(point_at(START_X, hallway.get_ground_level())); + player.set_grounded(true); + player.set_state(STATE_IDLE); + + // Disable advanced abilities in the hallway + player.enable_double_jump(false); + player.enable_dash(false); + + camera_pos = point_at(START_X - 400.0f, hallway.get_ground_level() - 300.0f); + if (camera_pos.x < hallway.get_world_left_boundary()) + camera_pos.x = hallway.get_world_left_boundary(); + if (camera_pos.y < -100.0f) camera_pos.y = -100.0f; + + nearby_interaction_prompt = ""; + write_line("Entered hallway level"); +} + +void Game::enter_apartment101() +{ + current_level = LEVEL_APARTMENT_101; + apartment101.load(); + apartment101.reset_door(); + + const float START_X = 100.0f; + player.set_position(point_at(START_X, apartment101.get_ground_level())); + player.set_grounded(true); + player.set_state(STATE_IDLE); + player.on_land(); // reset jump / dash status + + // Enable abilities for this level + player.enable_double_jump(true); + player.enable_dash(true); + + camera_pos = point_at(START_X - 400.0f, apartment101.get_ground_level() - 300.0f); + nearby_interaction_prompt = ""; + write_line("Entered Apartment 101 (Level 1)"); +} + +void Game::run() +{ + while (!quit_game && !quit_requested()) { + process_events(); + handle_input(); + update(); + draw(); + refresh_screen(60); + } +} + +void Game::update() +{ + player.update(); + update_camera(); + + nearby_interaction_prompt = ""; + + switch (current_level) { + case LEVEL_HALLWAY: + hallway.check_boundaries(player); + hallway.check_interactions(player); + { + Interactable* nearby = hallway.get_nearby_interactable(); + if (nearby && !nearby->get_is_triggered()) + nearby_interaction_prompt = "Press E to enter " + nearby->get_label(); + } + break; + + case LEVEL_APARTMENT_101: + apartment101.check_boundaries(player); + apartment101.check_interactions(player); + if (apartment101.is_door_triggered()) + nearby_interaction_prompt = "Press E to exit to hallway"; + break; + } +} + +void Game::update_camera() +{ + const float CAMERA_LERP_FACTOR = 0.1f; + point_2d player_pos = player.get_position(); + + float target_x = player_pos.x - 400.0f; + float target_y = player_pos.y - 300.0f; + + if (current_level == LEVEL_APARTMENT_101) { + const float APARTMENT_CAMERA_Y_OFFSET = -180.0f; + target_y += APARTMENT_CAMERA_Y_OFFSET; + } + + camera_pos.x += (target_x - camera_pos.x) * CAMERA_LERP_FACTOR; + camera_pos.y += (target_y - camera_pos.y) * CAMERA_LERP_FACTOR; + + if (current_level == LEVEL_HALLWAY) { + const float SCREEN_WIDTH = 800.0f; + float world_left = hallway.get_world_left_boundary(); + float world_right = hallway.get_world_right_boundary(); + + if (camera_pos.x < world_left) + camera_pos.x = world_left; + if (camera_pos.x > world_right - SCREEN_WIDTH) + camera_pos.x = world_right - SCREEN_WIDTH; + + float min_camera_y = hallway.get_ground_level() - 450.0f; + if (camera_pos.y < min_camera_y) camera_pos.y = min_camera_y; + } + + float max_camera_y = get_current_ground_level() - 200.0f; + if (camera_pos.y > max_camera_y) camera_pos.y = max_camera_y; + if (camera_pos.y < -100.0f) camera_pos.y = -100.0f; +} + +point_2d Game::world_to_screen(point_2d world_pos) const +{ + float background_y_offset = get_current_background_y_offset(); + return point_at(world_pos.x - camera_pos.x, + world_pos.y - camera_pos.y + background_y_offset); +} + +float Game::get_current_ground_level() const +{ + switch (current_level) { + case LEVEL_HALLWAY: return hallway.get_ground_level(); + case LEVEL_APARTMENT_101: return apartment101.get_ground_level(); + default: return 500.0f; + } +} + +float Game::get_current_background_y_offset() const +{ + switch (current_level) { + case LEVEL_HALLWAY: return hallway.get_y_offset(); + case LEVEL_APARTMENT_101: return 0.0f; + default: return 0.0f; + } +} + +void Game::draw() +{ + switch (current_level) { + case LEVEL_HALLWAY: + hallway.draw(camera_pos); + break; + case LEVEL_APARTMENT_101: + apartment101.draw(player, camera_pos); + break; + } + + point_2d screen_pos = world_to_screen(player.get_position()); + player.draw_at(screen_pos); + + if (show_hitbox) { + rectangle hitbox = player.get_bounding_box(); + point_2d tl = world_to_screen(point_at(hitbox.x, hitbox.y)); + point_2d tr = world_to_screen(point_at(hitbox.x + hitbox.width, hitbox.y)); + point_2d br = world_to_screen(point_at(hitbox.x + hitbox.width, hitbox.y + hitbox.height)); + point_2d bl = world_to_screen(point_at(hitbox.x, hitbox.y + hitbox.height)); + color hc = COLOR_RED; + draw_line(hc, tl.x, tl.y, tr.x, tr.y); + draw_line(hc, tr.x, tr.y, br.x, br.y); + draw_line(hc, br.x, br.y, bl.x, bl.y); + draw_line(hc, bl.x, bl.y, tl.x, tl.y); + } + + bool in_hallway = is_in_hallway(); + hud.draw_controls(in_hallway); + hud.draw_debug_info(player, camera_pos, in_hallway); + + std::string level_name = in_hallway ? "Apartment Hallway" : "Apartment 101"; + hud.draw_level_info(level_name, in_hallway); + hud.draw_interaction_prompt(nearby_interaction_prompt, in_hallway); +} + +void Game::handle_input() +{ + bool in_hallway = is_in_hallway(); + + // --- movement (A/D) --- + if (!player.get_is_dashing()) { + if (key_down(A_KEY)) { + player.move_left(); + } else if (key_down(D_KEY)) { + player.move_right(); + } else { + player.stop_moving(); + } + } + + // --- jump / double jump --- + if (key_typed(W_KEY)) player.jump(); + + // --- dash --- + if (key_typed(LEFT_SHIFT_KEY)) { + player.dash(); + } + + // --- interaction --- + if (key_typed(E_KEY)) { + if (in_hallway) { + Interactable* nearby = hallway.get_nearby_interactable(); + if (nearby && !nearby->get_is_triggered()) { + hallway.trigger_interaction(nearby->get_id()); + if (nearby->get_id() == "door1") enter_apartment101(); + else if (nearby->get_id() == "door2") write_line("Apartment 102 (coming soon!)"); + else if (nearby->get_id() == "door3") write_line("Apartment 103 (coming soon!)"); + else if (nearby->get_id() == "door5") write_line("Apartment 105 (coming soon!)"); + else if (nearby->get_id() == "door6") write_line("Apartment 106 (coming soon!)"); + else if (nearby->get_id() == "door7") write_line("Apartment 107 (coming soon!)"); + else if (nearby->get_id() == "hexit") write_line("Exit to lobby (coming soon!)"); + } + } else { + if (apartment101.is_door_triggered()) { + current_level = LEVEL_HALLWAY; + const float DOOR_X = 80.0f; + player.set_position(point_at(DOOR_X, hallway.get_ground_level())); + player.set_grounded(true); + player.set_state(STATE_IDLE); + camera_pos = point_at(DOOR_X - 400.0f, hallway.get_ground_level() - 300.0f); + if (camera_pos.x < hallway.get_world_left_boundary()) + camera_pos.x = hallway.get_world_left_boundary(); + if (camera_pos.y < -100.0f) camera_pos.y = -100.0f; + nearby_interaction_prompt = ""; + write_line("Exited to hallway at apartment 101 door"); + } + } + } + + if (key_down(ESCAPE_KEY)) { + if (!in_hallway) { + current_level = LEVEL_HALLWAY; + const float DOOR_X = 80.0f; + player.set_position(point_at(DOOR_X, hallway.get_ground_level())); + player.set_grounded(true); + player.set_state(STATE_IDLE); + camera_pos = point_at(DOOR_X - 400.0f, hallway.get_ground_level() - 300.0f); + if (camera_pos.x < hallway.get_world_left_boundary()) + camera_pos.x = hallway.get_world_left_boundary(); + if (camera_pos.y < -100.0f) camera_pos.y = -100.0f; + nearby_interaction_prompt = ""; + write_line("Returned to hallway at apartment 101 door (ESC)"); + } else { + quit_game = true; + } + } + + if (key_typed(R_KEY)) { + if (in_hallway) { + const float START_X = 1435.0f; + player.set_position(point_at(START_X, hallway.get_ground_level())); + player.set_grounded(true); + player.set_state(STATE_IDLE); + camera_pos = point_at(START_X - 400.0f, hallway.get_ground_level() - 300.0f); + if (camera_pos.x < hallway.get_world_left_boundary()) + camera_pos.x = hallway.get_world_left_boundary(); + if (camera_pos.y < -100.0f) camera_pos.y = -100.0f; + hallway.setup_interactables(); + } else { + const float START_X = 100.0f; + player.set_position(point_at(START_X, apartment101.get_ground_level())); + player.set_grounded(true); + player.set_state(STATE_IDLE); + camera_pos = point_at(START_X - 400.0f, apartment101.get_ground_level() - 300.0f); + player.on_land(); // reset abilities after reset + } + nearby_interaction_prompt = ""; + } + + if (key_typed(H_KEY)) { + show_hitbox = !show_hitbox; + write_line("Player hitbox: " + std::string(show_hitbox ? "ON" : "OFF")); + } +} \ No newline at end of file diff --git a/in-progress-games/RetroFracture/src/game.h b/in-progress-games/RetroFracture/src/game.h new file mode 100644 index 00000000..d3d0b2e9 --- /dev/null +++ b/in-progress-games/RetroFracture/src/game.h @@ -0,0 +1,48 @@ +#ifndef GAME_H +#define GAME_H + +#include "splashkit.h" +#include "player.h" +#include "hud.h" +#include "hallwaylevel.h" +#include "lvl1.h" + +class Game { +private: + Player player; + bool quit_game; + + enum CurrentLevel { + LEVEL_HALLWAY, + LEVEL_APARTMENT_101 + }; + CurrentLevel current_level; + point_2d camera_pos; + + HallwayLevel hallway; + Level1 apartment101; + + HUD hud; + std::string nearby_interaction_prompt; + bool show_hitbox; + +public: + Game(); + + void run(); + void update(); + void draw(); + void handle_input(); + + void enter_hallway(); + void enter_apartment101(); + + void update_camera(); + point_2d world_to_screen(point_2d world_pos) const; + + float get_current_ground_level() const; + float get_current_background_y_offset() const; + bool is_in_hallway() const { return current_level == LEVEL_HALLWAY; } +}; + +#endif \ No newline at end of file diff --git a/in-progress-games/RetroFracture/src/hallwaylevel.cpp b/in-progress-games/RetroFracture/src/hallwaylevel.cpp new file mode 100644 index 00000000..88c41a78 --- /dev/null +++ b/in-progress-games/RetroFracture/src/hallwaylevel.cpp @@ -0,0 +1,184 @@ +#include "hallwaylevel.h" +#include +#include + +HallwayLevel::HallwayLevel() + : original_width(0), original_height(0), + scaled_width(0), scaled_height(0), + scale_factor(0), y_offset(0), + world_left_boundary(0), world_right_boundary(0), + ground_level(0), ceiling_level(0), + image_offset_x(0), + nearby_interactable(nullptr), + show_boundaries(true) +{} + +void HallwayLevel::load() +{ + background = load_bitmap("hallway_bg", "assets/envs/Apartment Hallway.png"); + if (!background) { + write_line("ERROR: Could not load hallway background!"); + return; + } + + original_width = bitmap_width(background); + original_height = bitmap_height(background); + write_line("Loaded Apartment Hallway.png - Dimensions: " + + std::to_string(original_width) + "x" + std::to_string(original_height)); + + const float TARGET_HEIGHT = 450.0f; + scale_factor = TARGET_HEIGHT / original_height; + scaled_height = original_height * scale_factor; + scaled_width = original_width * scale_factor; + + // ---------- Explicit world boundaries (independent of image) ---------- + world_left_boundary = 10.0f; // you can change this + world_right_boundary = 1490.0f; // or set any value, e.g. 1400 + // --------------------------------------------------------------------- + + const float FLOOR_OFFSET = 150.0f * scale_factor; // pixels from image bottom to floor + ground_level = scaled_height - FLOOR_OFFSET; + ceiling_level = ground_level - 400.0f; // playable height + + image_offset_x = 125.0f; // left edge of image aligns with world_left_boundary + y_offset = (600.0f - scaled_height) / 2.0f; // vertical centering + + write_line("World boundaries: left=" + std::to_string(world_left_boundary) + + " right=" + std::to_string(world_right_boundary) + + " ground=" + std::to_string(ground_level) + + " ceiling=" + std::to_string(ceiling_level)); + + setup_interactables(); + write_line("Hallway level loaded successfully"); +} + +void HallwayLevel::unload() +{ + nearby_interactable = nullptr; + interactables.clear(); +} + +void HallwayLevel::update(Player& player) +{ + for (auto& i : interactables) i.update(); +} + +void HallwayLevel::draw(const point_2d& camera_pos) const +{ + clear_screen(COLOR_BLACK); + + // Draw background image using image_offset_x + float image_world_x = world_left_boundary + image_offset_x; + float draw_x = image_world_x - camera_pos.x; + float draw_y = y_offset - camera_pos.y; + drawing_options opts = option_scale_bmp(scale_factor, scale_factor); + draw_bitmap(background, draw_x, draw_y, opts); + + for (const auto& i : interactables) + i.draw(camera_pos, y_offset); + + if (show_boundaries) + draw_boundaries(camera_pos); +} + +void HallwayLevel::draw_boundaries(const point_2d& camera_pos) const +{ + auto wsx = [&](float world_x) { return world_x - camera_pos.x; }; + auto wsy = [&](float world_y) { return world_y - camera_pos.y + y_offset; }; + + color fl_ceil = COLOR_RED; + color wall = COLOR_GREEN; + + // Floor + float sf = wsy(ground_level); + draw_line(fl_ceil, 0, sf, 800, sf); + draw_text("FLOOR", fl_ceil, 10, sf - 20); + + // Ceiling + float sc = wsy(ceiling_level); + draw_line(fl_ceil, 0, sc, 800, sc); + draw_text("CEILING", fl_ceil, 10, sc + 5); + + // Left wall + float sl = wsx(world_left_boundary); + if (sl >= -50 && sl <= 850) { + draw_line(wall, sl, 0, sl, 600); + draw_text("LEFT WALL", wall, sl + 5, 100); + } + + // Right wall + float sr = wsx(world_right_boundary); + if (sr >= -50 && sr <= 850) { + draw_line(wall, sr, 0, sr, 600); + draw_text("RIGHT WALL", wall, sr - 80, 100); + } else { + draw_text("RIGHT WALL: " + std::to_string((int)world_right_boundary), wall, 600, 100); + } +} + +void HallwayLevel::check_boundaries(Player& player) +{ + point_2d pos = player.get_position(); + const float HALF_WIDTH = Player::HITBOX_WIDTH / 2.0f; + + // Left wall + const float LEFT_WALL = world_left_boundary + HALF_WIDTH; + if (pos.x < LEFT_WALL) pos.x = LEFT_WALL; + + // Right wall + const float RIGHT_WALL = world_right_boundary - HALF_WIDTH; + if (pos.x > RIGHT_WALL) pos.x = RIGHT_WALL; + + // Floor + if (pos.y > ground_level) { + pos.y = ground_level; + if (!player.get_is_grounded() && player.get_state() == STATE_FALL) + player.set_state(STATE_IDLE); + player.set_grounded(true); + player.on_land(); // <-- NEW: resets double jump & dash cooldown + } + + // Ceiling + if (pos.y < ceiling_level) { + pos.y = ceiling_level; + player.stop_vertical_movement(); + } + + player.set_position(pos); +} + +void HallwayLevel::check_interactions(const Player& player) +{ + nearby_interactable = nullptr; + for (auto& i : interactables) { + if (i.check_collision(player.get_position())) { + nearby_interactable = &i; + break; + } + } +} + +void HallwayLevel::setup_interactables() +{ + interactables.clear(); + nearby_interactable = nullptr; + + // Doors placed in world coordinates (relative to boundaries) + interactables.push_back(Interactable("door1", 90.0f, ground_level - 85.0f, 40.0f, 60.0f, "Apartment 101")); + interactables.push_back(Interactable("door2", 250.0f, ground_level - 85.0f, 40.0f, 60.0f, "Apartment 102")); + interactables.push_back(Interactable("door3", 400.0f, ground_level - 85.0f, 40.0f, 60.0f, "Apartment 103")); + interactables.push_back(Interactable("door5", 890.0f, ground_level - 85.0f, 40.0f, 60.0f, "Apartment 105")); + interactables.push_back(Interactable("door6", 1060.0f, ground_level - 85.0f, 40.0f, 60.0f, "Apartment 106")); + interactables.push_back(Interactable("door7", 1220.0f, ground_level - 85.0f, 40.0f, 60.0f, "Apartment 107")); + interactables.push_back(Interactable("hexit", 1445.0f, ground_level - 25.0f, 40.0f, 60.0f, "Exit To Lobby")); +} + +void HallwayLevel::trigger_interaction(const std::string& id) +{ + for (auto& i : interactables) { + if (i.get_id() == id && !i.get_is_triggered()) { + i.trigger(); + break; + } + } +} \ No newline at end of file diff --git a/in-progress-games/RetroFracture/src/hallwaylevel.h b/in-progress-games/RetroFracture/src/hallwaylevel.h new file mode 100644 index 00000000..7d0be45f --- /dev/null +++ b/in-progress-games/RetroFracture/src/hallwaylevel.h @@ -0,0 +1,58 @@ +#ifndef HALLWAYLEVEL_H +#define HALLWAYLEVEL_H + +#include "splashkit.h" +#include "player.h" +#include "interactable.h" +#include + +class HallwayLevel { +private: + bitmap background; + float original_width, original_height; + float scaled_width, scaled_height; + float scale_factor; + float y_offset; // vertical centering offset + + float world_left_boundary; // left wall X + float world_right_boundary; // right wall X + float ground_level; // floor Y + float ceiling_level; // ceiling Y + + float image_offset_x; // where the image's left edge sits relative to world_left + + std::vector interactables; + Interactable* nearby_interactable; + bool show_boundaries; + +public: + HallwayLevel(); + + void load(); + void unload(); + void update(Player& player); + void draw(const point_2d& camera_pos) const; + + void check_boundaries(Player& player); + void check_interactions(const Player& player); + + float get_ground_level() const { return ground_level; } + float get_y_offset() const { return y_offset; } + float get_world_left_boundary() const { return world_left_boundary; } + float get_world_right_boundary() const { return world_right_boundary; } + + Interactable* get_nearby_interactable() const { return nearby_interactable; } + const std::vector& get_interactables() const { return interactables; } + const bitmap& get_background() const { return background; } + + void set_show_boundaries(bool show) { show_boundaries = show; } + bool get_show_boundaries() const { return show_boundaries; } + + void setup_interactables(); + void trigger_interaction(const std::string& id); + +private: + void draw_boundaries(const point_2d& camera_pos) const; +}; + +#endif \ No newline at end of file diff --git a/in-progress-games/RetroFracture/src/hud.cpp b/in-progress-games/RetroFracture/src/hud.cpp new file mode 100644 index 00000000..831f0227 --- /dev/null +++ b/in-progress-games/RetroFracture/src/hud.cpp @@ -0,0 +1,101 @@ +#include "hud.h" +#include + +/** + * HUD constructor + * Initializes color scheme for different levels + */ +HUD::HUD() +{ + // Set default colors for text readability per level + hallway_text_color = COLOR_YELLOW; // Bright for dark hallway + apartment_text_color = COLOR_GREEN; // Dark for light apartment +} + +/** + * Get appropriate text color for current level + * @param in_hallway Whether player is in hallway level + * @return Color for text rendering + */ +color HUD::get_text_color(bool in_hallway) const +{ + return in_hallway ? hallway_text_color : apartment_text_color; +} + +/** + * Draw control instructions + * @param in_hallway Whether player is in hallway level + */ +void HUD::draw_controls(bool in_hallway) const +{ + color text_color = get_text_color(in_hallway); + + if (in_hallway) + { + draw_text("CONTROLS: A (Left), D (Right), W (Jump), E (Interact)", text_color, 10, 10); + draw_text("ESC to quit | R to reset", text_color, 10, 35); + } + else + { + draw_text("CONTROLS: A (Left), D (Right), W (Jump), E (Interact)", text_color, 10, 10); + draw_text("ESC to quit | R to reset", text_color, 10, 35); + } +} + +/** + * Draw debug information + * @param player Player for position data + * @param camera_pos Camera position + * @param in_hallway Whether player is in hallway level + */ +void HUD::draw_debug_info(const Player& player, const point_2d& camera_pos, bool in_hallway) const +{ + color text_color = get_text_color(in_hallway); + point_2d player_pos = player.get_position(); + + // Player world position + std::string world_text = "WORLD: (" + std::to_string((int)player_pos.x) + + ", " + std::to_string((int)player_pos.y) + ")"; + draw_text(world_text, text_color, 10, 65); + + // Camera position + std::string camera_text = "CAMERA: (" + std::to_string((int)camera_pos.x) + + ", " + std::to_string((int)camera_pos.y) + ")"; + draw_text(camera_text, text_color, 10, 90); +} + +/** + * Draw current level name + * @param level_name Name of current level + * @param in_hallway Whether player is in hallway level + */ +void HUD::draw_level_info(const std::string& level_name, bool in_hallway) const +{ + color text_color = get_text_color(in_hallway); + std::string level_text = "LEVEL: " + level_name; + draw_text(level_text, text_color, 10, 115); +} + +/** + * Draw interaction prompt if available + * @param prompt Text to display as prompt + * @param in_hallway Whether player is in hallway level + */ +void HUD::draw_interaction_prompt(const std::string& prompt, bool in_hallway) const +{ + if (!prompt.empty()) + { + // Use different colors based on level for better visibility + color prompt_color = in_hallway ? COLOR_CYAN : rgba_color(0, 150, 200, 255); + + // Position near bottom center with a background for readability + // Estimate text width - using fixed approximation since text_width might not be available + int text_width_estimate = prompt.length() * 8; // Approximate: 8 pixels per character + int x_pos = (800 - text_width_estimate) / 2; // Center horizontally + int y_pos = 570; // Near bottom + + // Draw with a slight shadow for better readability + draw_text(prompt, rgba_color(0, 0, 0, 200), x_pos + 1, y_pos + 1); + draw_text(prompt, prompt_color, x_pos, y_pos); + } +} \ No newline at end of file diff --git a/in-progress-games/RetroFracture/src/hud.h b/in-progress-games/RetroFracture/src/hud.h new file mode 100644 index 00000000..3cdb79be --- /dev/null +++ b/in-progress-games/RetroFracture/src/hud.h @@ -0,0 +1,34 @@ +#ifndef HUD_H +#define HUD_H + +#include "splashkit.h" +#include "player.h" + +/** + * Heads-Up Display class + * Manages rendering of UI elements with level-appropriate styling + */ +class HUD +{ +private: + color hallway_text_color; // Text color for hallway level + color apartment_text_color; // Text color for apartment level + +public: + HUD(); // Constructor + + // HUD rendering methods + void draw_controls(bool in_hallway) const; // Draw control instructions + void draw_debug_info(const Player& player, const point_2d& camera_pos, bool in_hallway) const; // Draw debug info + void draw_level_info(const std::string& level_name, bool in_hallway) const; // Draw level name + void draw_interaction_prompt(const std::string& prompt, bool in_hallway) const; // Draw interaction prompt + + // Color setters + void set_hallway_text_color(color c) { hallway_text_color = c; } + void set_apartment_text_color(color c) { apartment_text_color = c; } + + // Color getter based on current level + color get_text_color(bool in_hallway) const; +}; + +#endif \ No newline at end of file diff --git a/in-progress-games/RetroFracture/src/interactable.cpp b/in-progress-games/RetroFracture/src/interactable.cpp new file mode 100644 index 00000000..6882907c --- /dev/null +++ b/in-progress-games/RetroFracture/src/interactable.cpp @@ -0,0 +1,72 @@ +#include "interactable.h" +#include "player.h" +#include + +Interactable::Interactable(std::string id, float x, float y, float width, float height, std::string label) + : id(id), x(x), y(y), width(width), height(height), + is_active(true), is_triggered(false), label(label) +{} + +void Interactable::draw(point_2d camera_pos, float background_y_offset) const +{ + if (!is_active) return; + + float screen_x = x - camera_pos.x; + float screen_y = y - camera_pos.y + background_y_offset; + + if (is_triggered) { + draw_rectangle(COLOR_GREEN, screen_x, screen_y, width, height); + } else { + draw_rectangle(COLOR_YELLOW, screen_x, screen_y, width, height); + draw_rectangle(COLOR_BLACK, screen_x + 2, screen_y + 2, width - 4, height - 4); + } + + if (!label.empty() && !is_triggered) { + float text_width_estimate = label.length() * 8.0f; + float label_x = screen_x + (width - text_width_estimate) / 2; + draw_text(label, COLOR_WHITE, label_x, screen_y - 20); + } + + if (!is_triggered) { + float center_x = screen_x + width / 2 - 10; + float center_y = screen_y + height / 2 - 10; + draw_text("[E]", COLOR_CYAN, center_x, center_y); + } +} + +void Interactable::update() +{} + +bool Interactable::check_collision(point_2d player_pos) const +{ + if (!is_active || is_triggered) return false; + + const float HALF_WIDTH = Player::HITBOX_WIDTH / 2.0f; + const float HEIGHT = Player::HITBOX_HEIGHT; + + float player_left = player_pos.x - HALF_WIDTH; + float player_right = player_pos.x + HALF_WIDTH; + float player_top = player_pos.y - HEIGHT; + float player_bottom = player_pos.y; + + float interact_right = x + width; + float interact_bottom = y + height; + + return !(player_left > interact_right || + player_right < x || + player_top > interact_bottom || + player_bottom < y); +} + +void Interactable::trigger() +{ + if (!is_active || is_triggered) return; + is_triggered = true; + write_line("Interactable triggered: " + id + " (" + label + ")"); +} + +void Interactable::reset() +{ + is_triggered = false; + is_active = true; +} \ No newline at end of file diff --git a/in-progress-games/RetroFracture/src/interactable.h b/in-progress-games/RetroFracture/src/interactable.h new file mode 100644 index 00000000..6612a096 --- /dev/null +++ b/in-progress-games/RetroFracture/src/interactable.h @@ -0,0 +1,43 @@ +#ifndef INTERACTABLE_H +#define INTERACTABLE_H + +#include "splashkit.h" +#include + +/** + * Interactable object class + * Represents objects that player can interact with (doors, switches, etc.) + */ +class Interactable +{ +private: + std::string id; // Unique identifier + float x, y; // World position + float width, height; // Collision dimensions + bool is_active; // Whether object is active + bool is_triggered; // Whether interaction has occurred + std::string label; // Display label + +public: + // Constructor + Interactable(std::string id, float x, float y, float width, float height, std::string label = ""); + + // Rendering and update + void draw(point_2d camera_pos, float background_y_offset) const; // Draw object + void update(); // Update object state + + // Collision detection + bool check_collision(point_2d player_pos) const; // Check player proximity + + // Interaction handling + void trigger(); // Trigger interaction + void reset(); // Reset to initial state + + // Getters + std::string get_id() const { return id; } + bool get_is_active() const { return is_active; } + bool get_is_triggered() const { return is_triggered; } + std::string get_label() const { return label; } +}; + +#endif \ No newline at end of file diff --git a/in-progress-games/RetroFracture/src/lvl1.cpp b/in-progress-games/RetroFracture/src/lvl1.cpp new file mode 100644 index 00000000..e75bfb63 --- /dev/null +++ b/in-progress-games/RetroFracture/src/lvl1.cpp @@ -0,0 +1,235 @@ +#include "lvl1.h" +#include +#include +#include // for std::clamp if needed + +Level1::Level1() + : original_width(0), original_height(0), + scaled_width(0), scaled_height(0), + scale_factor(0), y_offset(0), + world_left_boundary(0), world_right_boundary(0), + ground_level(0), ceiling_level(0), + image_offset_x(0), + global_ground_level(1000.0f), // <-- moved before show_boundaries + exit_door("exit_door", 100.0f, 610.0f, 40.0f, 60.0f, "EXIT TO HALLWAY"), + door_triggered(false), + show_boundaries(true) +{ +} + +void Level1::load() +{ + background = load_bitmap("lvl1_bg", "assets/envs/lvl1_platform.png"); + if (!background) { + write_line("ERROR: Could not load apartment background!"); + return; + } + + original_width = bitmap_width(background); + original_height = bitmap_height(background); + write_line("Loaded lvl1_platform.png - Dimensions: " + + std::to_string(original_width) + "x" + std::to_string(original_height)); + + // Scale image to fit screen height + const float TARGET_HEIGHT = 600.0f; + scale_factor = TARGET_HEIGHT / original_height; + scaled_height = original_height * scale_factor; + scaled_width = original_width * scale_factor; + + // World boundaries + world_left_boundary = 0.0f; + world_right_boundary = scaled_width; // room width = image width + ceiling_level = 50.0f; + + // Image placement + image_offset_x = 335.0f; + y_offset = (600.0f - scaled_height) / 2.0f; // likely 0 + + // ---------- NEW: define the walkable ground polyline ---------- + ground_path.clear(); + // !! IMPORTANT – adapt these points to match YOUR lvl1_platform.png after scaling !! + // Points must be sorted by increasing x, each point is a spot on the TOP surface of the platform. + // Example (a flat floor with a slope in the middle): + ground_path.push_back(point_at(0, 445)); + ground_path.push_back(point_at(595, 445)); + ground_path.push_back(point_at(595, 375)); + ground_path.push_back(point_at(1020, 375)); + ground_path.push_back(point_at(1020, 600)); + ground_path.push_back(point_at(1235, 600)); + ground_path.push_back(point_at(1235, 390)); + ground_path.push_back(point_at(1550, 390)); + + // Fallback level for gaps (player will fall if x outside the path) + global_ground_level = 1500.0f; // way below screen, instant death later + + // ---------- NEW: define vertical interior walls ---------- + wall_segments.clear(); + // Example walls – place them where your image has unpassable vertical obstacles. + // (x, y_top, y_bottom) – y_top < y_bottom because y increases downward. + // Wall thickness is 10 pixels (half_width = 5). + wall_segments.push_back(WallSegment(595.0f, 375.0f, 600.0f)); + wall_segments.push_back(WallSegment(1020.0f, 375.0f, 600.0f)); + wall_segments.push_back(WallSegment(1235.0f, 390.0f, 600.0f)); + + // ---------- Ground level for camera ---------- + // Use the floor at the player start position (x = 100). + const float SPAWN_X = 100.0f; + ground_level = get_ground_y(SPAWN_X); + + // ---------- Exit door ---------- + float door_x = 100.0f; + float floor_at_door = get_ground_y(door_x); + exit_door = Interactable("exit_door", door_x, floor_at_door - 85.0f, 40.0f, 60.0f, "EXIT TO HALLWAY"); + + write_line("Apartment level loaded with polyline ground."); + write_line("Floor at door (x=100): " + std::to_string(floor_at_door)); +} + +// --- unload, update, draw unchanged except for boundary debug (see draw_boundaries) --- +void Level1::unload() {} +void Level1::update(Player& player) {} + +void Level1::draw(const Player& player, const point_2d& camera_pos) const +{ + clear_screen(COLOR_BLACK); + + // Background + float image_world_x = world_left_boundary + image_offset_x; + float draw_x = image_world_x - camera_pos.x; + float draw_y = y_offset - camera_pos.y; + drawing_options opts = option_scale_bmp(scale_factor, scale_factor); + draw_bitmap(background, draw_x, draw_y, opts); + + // Exit door + exit_door.draw(camera_pos, y_offset); + + // Debug: draw boundaries (now also shows polyline ground and walls) + if (show_boundaries) + draw_boundaries(camera_pos); +} + +void Level1::draw_boundaries(const point_2d& camera_pos) const +{ + auto wsx = [&](float world_x) { return world_x - camera_pos.x; }; + auto wsy = [&](float world_y) { return world_y - camera_pos.y + y_offset; }; + + color fl_ceil = COLOR_RED; + color wall = COLOR_GREEN; + + // Ceiling + float sc = wsy(ceiling_level); + draw_line(fl_ceil, 0, sc, 800, sc); + draw_text("CEILING", fl_ceil, 10, sc + 5); + + // Left / right world boundaries + float sl = wsx(world_left_boundary); + if (sl >= -50 && sl <= 850) { + draw_line(wall, sl, 0, sl, 600); + draw_text("LEFT WALL", wall, sl + 5, 100); + } + float sr = wsx(world_right_boundary); + if (sr >= -50 && sr <= 850) { + draw_line(wall, sr, 0, sr, 600); + draw_text("RIGHT WALL", wall, sr - 80, 100); + } + + // Draw polyline ground (debug) + if (ground_path.size() > 1) { + for (size_t i = 0; i < ground_path.size() - 1; ++i) { + point_2d p1 = point_at(wsx(ground_path[i].x), wsy(ground_path[i].y)); + point_2d p2 = point_at(wsx(ground_path[i+1].x), wsy(ground_path[i+1].y)); + draw_line(COLOR_YELLOW, p1.x, p1.y, p2.x, p2.y); + } + } + + // Draw vertical walls (debug) + for (const auto& w : wall_segments) { + float wx = wsx(w.x); + float wy1 = wsy(w.y_top); + float wy2 = wsy(w.y_bottom); + draw_line(COLOR_MAGENTA, wx, wy1, wx, wy2); + } +} + +// ---------- NEW: get_ground_y implementation ---------- +float Level1::get_ground_y(float x) const +{ + if (ground_path.size() < 2) + return global_ground_level; + + // Find the segment that contains x + for (size_t i = 0; i < ground_path.size() - 1; ++i) { + float x1 = ground_path[i].x; + float x2 = ground_path[i+1].x; + if (x >= x1 && x <= x2) { + float t = (x - x1) / (x2 - x1); + float y1 = ground_path[i].y; + float y2 = ground_path[i+1].y; + return y1 + t * (y2 - y1); + } + } + // x is outside the defined path + return global_ground_level; +} + +void Level1::check_boundaries(Player& player) +{ + point_2d pos = player.get_position(); + const float HALF_WIDTH = Player::HITBOX_WIDTH / 2.0f; + const float HITBOX_HEIGHT = Player::HITBOX_HEIGHT; + + // ---------- World left/right boundaries ---------- + const float LEFT_WALL = world_left_boundary + HALF_WIDTH; + const float RIGHT_WALL = world_right_boundary - HALF_WIDTH; + if (pos.x < LEFT_WALL) pos.x = LEFT_WALL; + if (pos.x > RIGHT_WALL) pos.x = RIGHT_WALL; + + // ---------- Floor (polyline) ---------- + float floor_y = get_ground_y(pos.x); + if (pos.y >= floor_y) + { + pos.y = floor_y; + if (!player.get_is_grounded() && player.get_state() == STATE_FALL) + player.set_state(STATE_IDLE); + player.set_grounded(true); + player.on_land(); // <-- NEW + } + // else: player is above floor – let gravity work, do not clamp. + + // ---------- Ceiling ---------- + if (pos.y < ceiling_level) + { + pos.y = ceiling_level; + player.stop_vertical_movement(); + } + + // ---------- NEW: interior vertical walls ---------- + float player_left = pos.x - HALF_WIDTH; + float player_right = pos.x + HALF_WIDTH; + float player_top = pos.y - HITBOX_HEIGHT; + float player_bottom = pos.y; + + for (const auto& w : wall_segments) + { + float wall_left = w.x - w.half_width; + float wall_right = w.x + w.half_width; + if (player_bottom <= w.y_top || player_top >= w.y_bottom) + continue; + if (player_right > wall_left && player_left < wall_right) + { + if (pos.x < w.x) + pos.x = wall_left - HALF_WIDTH; + else + pos.x = wall_right + HALF_WIDTH; + player_left = pos.x - HALF_WIDTH; + player_right = pos.x + HALF_WIDTH; + } + } + + player.set_position(pos); +} + +void Level1::check_interactions(const Player& player) +{ + door_triggered = exit_door.check_collision(player.get_position()); +} \ No newline at end of file diff --git a/in-progress-games/RetroFracture/src/lvl1.h b/in-progress-games/RetroFracture/src/lvl1.h new file mode 100644 index 00000000..53f23d53 --- /dev/null +++ b/in-progress-games/RetroFracture/src/lvl1.h @@ -0,0 +1,79 @@ +#ifndef LVL1_H +#define LVL1_H + +#include "splashkit.h" +#include "player.h" +#include "interactable.h" +#include + +/** + * A thin vertical wall obstacle. + * - x: world x coordinate of the wall centre + * - y_top, y_bottom: vertical range (y_top is the top, y_bottom is the bottom; note y increases downward) + * - half_width: half thickness (default 5 pixels) + */ +struct WallSegment +{ + float x; + float y_top; + float y_bottom; + float half_width; + WallSegment(float x, float y_top, float y_bottom, float hw = 5.0f) + : x(x), y_top(y_top), y_bottom(y_bottom), half_width(hw) {} +}; + +class Level1 { +private: + bitmap background; + float original_width, original_height; + float scaled_width, scaled_height; + float scale_factor; + float y_offset; + + float world_left_boundary; + float world_right_boundary; + float ground_level; // representative floor height (for camera) + float ceiling_level; + + float image_offset_x; + + // --- NEW: polyline ground data --- + std::vector ground_path; // sorted by x, no duplicate x + float global_ground_level; // fallback when x is outside the path + float get_ground_y(float x) const; // returns floor y at any x + + // --- NEW: vertical interior walls --- + std::vector wall_segments; + + Interactable exit_door; + bool door_triggered; + bool show_boundaries; + +public: + Level1(); + + void load(); + void unload(); + + void update(Player& player); + void draw(const Player& player, const point_2d& camera_pos) const; + + void check_boundaries(Player& player); + void check_interactions(const Player& player); + + float get_ground_level() const { return ground_level; } + float get_y_offset() const { return y_offset; } + float get_world_left_boundary() const { return world_left_boundary; } + float get_world_right_boundary() const { return world_right_boundary; } + + bool is_door_triggered() const { return door_triggered; } + void reset_door() { door_triggered = false; } + + void set_show_boundaries(bool show) { show_boundaries = show; } + bool get_show_boundaries() const { return show_boundaries; } + +private: + void draw_boundaries(const point_2d& camera_pos) const; +}; + +#endif \ No newline at end of file diff --git a/in-progress-games/RetroFracture/src/player.cpp b/in-progress-games/RetroFracture/src/player.cpp new file mode 100644 index 00000000..46870f09 --- /dev/null +++ b/in-progress-games/RetroFracture/src/player.cpp @@ -0,0 +1,239 @@ +#include "player.h" + +Player::Player() + : position(point_at(0, 0)), + speed(5.0f), + jump_force(-12.0f), + gravity(1.0f), + vertical_velocity(0), + is_grounded(true), + facing_right(true), + current_state(STATE_IDLE), + current_frame_index(0), + animation_timer(0), + current_animation(nullptr), + double_jump_enabled(false), + double_jump_available(false), + dash_enabled(false), + is_dashing(false), + dash_direction(1), + dash_speed(15.0f), + dash_frames(10), // lasts ~0.17s at 60fps + dash_timer(0), + dash_cooldown(30), // half second cooldown + dash_cooldown_timer(0) +{ + setup_animations(); + set_state(STATE_IDLE); +} + +rectangle Player::get_bounding_box() const +{ + float left = position.x - HITBOX_WIDTH / 2.0f; + float top = position.y - HITBOX_HEIGHT; + return rectangle_from(left, top, HITBOX_WIDTH, HITBOX_HEIGHT); +} + +void Player::setup_animations() +{ + load_animation("assets/sprites/striker/spr_StrikerIdle_strip.png", + STATE_IDLE, 8, 96, 96, 10, true); + load_animation("assets/sprites/striker/spr_StrikerRun_strip.png", + STATE_RUN, 8, 96, 96, 6, true); + load_animation("assets/sprites/striker/spr_StrikerJump_strip.png", + STATE_JUMP, 12, 96, 96, 8, false); + load_animation("assets/sprites/striker/spr_StrikerJump_strip.png", + STATE_FALL, 12, 96, 96, 8, false); + load_animation("assets/sprites/striker/spr_StrikerDash_strip.png", + STATE_DASH, 8, 96, 96, 6, false); + write_line("Player animations loaded successfully"); +} + +void Player::load_animation(const std::string& filename, PlayerState state, + int frame_count, int frame_width, int frame_height, + int speed, bool loops) +{ + Animation anim; + std::string bitmap_name = "anim_" + std::to_string(state); + anim.anim_bitmap = load_bitmap(bitmap_name, filename); + anim.frame_count = frame_count; + anim.frame_width = frame_width; + anim.frame_height = frame_height; + anim.animation_speed = speed; + anim.loops = loops; + + switch(state) { + case STATE_IDLE: anim.name = "Idle"; break; + case STATE_RUN: anim.name = "Run"; break; + case STATE_JUMP: anim.name = "Jump"; break; + case STATE_FALL: anim.name = "Fall"; break; + case STATE_DASH: anim.name = "Dash"; break; + default: anim.name = "Unknown"; + } + animations[state] = anim; +} + +void Player::update() +{ + // --- gravity & vertical movement --- + if (!is_grounded && !is_dashing) { // no gravity during dash (optional) + vertical_velocity += gravity; + position.y += vertical_velocity; + if (vertical_velocity > 0 && current_state == STATE_JUMP) + set_state(STATE_FALL); + } + + // --- dash logic --- + if (is_dashing) { + // move horizontally + position.x += dash_direction * dash_speed; + dash_timer--; + if (dash_timer <= 0) { + is_dashing = false; + dash_cooldown_timer = dash_cooldown; + // revert to appropriate state + if (is_grounded) + set_state(STATE_IDLE); + else + set_state(STATE_FALL); + } + } else { + // cooldown tick + if (dash_cooldown_timer > 0) + dash_cooldown_timer--; + } + + update_animation(); +} + +void Player::stop_vertical_movement() +{ + vertical_velocity = 0; +} + +void Player::update_animation() +{ + if (!current_animation) return; + animation_timer++; + if (animation_timer >= current_animation->animation_speed) { + animation_timer = 0; + current_frame_index++; + if (current_frame_index >= current_animation->frame_count) { + if (current_animation->loops) + current_frame_index = 0; + else { + current_frame_index = current_animation->frame_count - 1; + if (current_state == STATE_JUMP && !is_grounded) + set_state(STATE_FALL); + } + } + } +} + +void Player::set_state(PlayerState new_state) +{ + if (current_state == new_state && current_animation != nullptr) return; + current_state = new_state; + if (animations.find(new_state) != animations.end()) { + current_animation = &animations[new_state]; + current_frame_index = 0; + animation_timer = 0; + if (new_state == STATE_FALL) + current_frame_index = 6; + } +} + +void Player::draw() +{ + draw_at(position); +} + +void Player::draw_at(point_2d screen_position) +{ + if (!current_animation) return; + + int frame_x = current_frame_index * current_animation->frame_width; + rectangle frame_rect = rectangle_from(frame_x, 0, + current_animation->frame_width, + current_animation->frame_height); + float draw_x = screen_position.x - current_animation->frame_width / 2; + float draw_y = screen_position.y - current_animation->frame_height; + + drawing_options opts = option_part_bmp(frame_rect); + if (!facing_right) + opts = option_scale_bmp(-1.0, 1.0, opts); + draw_bitmap(current_animation->anim_bitmap, draw_x, draw_y, opts); +} + +// ---------- movement ---------- +void Player::move_left() +{ + position.x -= speed; + facing_right = false; + if (is_grounded && current_state != STATE_JUMP && current_state != STATE_FALL) + set_state(STATE_RUN); +} + +void Player::move_right() +{ + position.x += speed; + facing_right = true; + if (is_grounded && current_state != STATE_JUMP && current_state != STATE_FALL) + set_state(STATE_RUN); +} + +void Player::jump() +{ + if (is_grounded) { + vertical_velocity = jump_force; + is_grounded = false; + double_jump_available = double_jump_enabled; // reset double jump + set_state(STATE_JUMP); + } + else if (double_jump_enabled && double_jump_available && !is_dashing) { + // second jump (allowed in air, but not during dash to avoid exploits) + vertical_velocity = jump_force; + double_jump_available = false; + set_state(STATE_JUMP); + } +} + +void Player::stop_moving() +{ + if (is_grounded && current_state == STATE_RUN) + set_state(STATE_IDLE); +} + +// ---------- new abilities ---------- +void Player::dash() +{ + if (!dash_enabled || is_dashing || dash_cooldown_timer > 0) + return; + + is_dashing = true; + dash_timer = dash_frames; + dash_direction = facing_right ? 1 : -1; + set_state(STATE_DASH); +} + +void Player::on_land() +{ + double_jump_available = double_jump_enabled; + dash_cooldown_timer = 0; // dash instantly recharges on landing +} + +void Player::enable_double_jump(bool enable) +{ + double_jump_enabled = enable; + if (!enable) double_jump_available = false; +} + +void Player::enable_dash(bool enable) +{ + dash_enabled = enable; + if (!enable) { + is_dashing = false; + dash_timer = 0; + dash_cooldown_timer = 0; + } +} \ No newline at end of file diff --git a/in-progress-games/RetroFracture/src/player.h b/in-progress-games/RetroFracture/src/player.h new file mode 100644 index 00000000..6e8917f1 --- /dev/null +++ b/in-progress-games/RetroFracture/src/player.h @@ -0,0 +1,102 @@ +#ifndef PLAYER_H +#define PLAYER_H + +#include "splashkit.h" +#include +#include + +enum PlayerState { + STATE_IDLE, + STATE_RUN, + STATE_JUMP, + STATE_FALL, + STATE_DASH // new +}; + +struct Animation { + bitmap anim_bitmap; + int frame_count; + int frame_width; + int frame_height; + int animation_speed; + bool loops; + std::string name; +}; + +class Player { +public: + static constexpr float HITBOX_WIDTH = 60.0f; + static constexpr float HITBOX_HEIGHT = 90.0f; + + Player(); + + void update(); + void draw(); + void draw_at(point_2d screen_position); + void set_state(PlayerState new_state); + PlayerState get_state() const { return current_state; } + + void move_left(); + void move_right(); + void jump(); + void stop_moving(); + void stop_vertical_movement(); + + // --- new abilities --- + void dash(); + void on_land(); // call when player touches ground + void enable_double_jump(bool enable); + void enable_dash(bool enable); + bool get_is_dashing() const { return is_dashing; } + // -------------------- + + point_2d get_position() const { return position; } + void set_position(point_2d new_pos) { position = new_pos; } + bool get_is_grounded() const { return is_grounded; } + void set_grounded(bool grounded) { is_grounded = grounded; } + bool get_facing_right() const { return facing_right; } + + rectangle get_bounding_box() const; + + int get_current_frame_index() const { return current_frame_index; } + std::string get_current_animation_name() const { + return current_animation ? current_animation->name : "None"; + } + +private: + point_2d position; + float speed; + float jump_force; + float gravity; + float vertical_velocity; + bool is_grounded; + bool facing_right; + + PlayerState current_state; + int current_frame_index; + int animation_timer; + std::map animations; + Animation* current_animation; + + // --- double jump --- + bool double_jump_enabled; + bool double_jump_available; // true when grounded, consumed by second jump + + // --- dash --- + bool dash_enabled; + bool is_dashing; + int dash_direction; // 1 = right, -1 = left + float dash_speed; + int dash_frames; + int dash_timer; + int dash_cooldown; + int dash_cooldown_timer; + + void setup_animations(); + void load_animation(const std::string& filename, PlayerState state, + int frame_count, int frame_width, int frame_height, + int speed, bool loops); + void update_animation(); +}; + +#endif \ No newline at end of file