From 4fe945eb009ae0ba5733a65baa4d33613f514e36 Mon Sep 17 00:00:00 2001 From: erwan Date: Sun, 1 Feb 2026 23:53:25 +0100 Subject: [PATCH 1/7] feat: files architecture refactor --- CMakeLists.txt | 101 +- engine/CMakeLists.txt | 19 + engine/README.md | 805 ++++++++++ engine/include/Animation/AnimationModule.hpp | 67 + engine/include/Animation/AnimationTypes.hpp | 115 ++ engine/include/Animation/IAnimation.hpp | 68 + engine/include/Audio/IAudio.hpp | 72 + engine/include/Audio/SFMLAudio.hpp | 63 + engine/include/Core/ColorFilter.hpp | 20 + engine/include/Core/Engine.hpp | 117 ++ engine/include/Core/InputMapping.hpp | 22 + engine/include/Core/Logger.hpp | 111 ++ engine/include/Core/Module.hpp | 39 + engine/include/Core/ModuleLoader.hpp | 62 + engine/include/Core/Platform.hpp | 95 ++ engine/include/ECS/AnimationSystem.hpp | 33 + engine/include/ECS/AudioSystem.hpp | 28 + engine/include/ECS/BlackOrbSystem.hpp | 28 + engine/include/ECS/BossAttackSystem.hpp | 39 + engine/include/ECS/BossSystem.hpp | 27 + .../ECS/BulletCollisionResponseSystem.hpp | 36 + .../include/ECS/CollisionDetectionSystem.hpp | 42 + engine/include/ECS/Component.hpp | 644 ++++++++ engine/include/ECS/Components/Clickable.hpp | 29 + engine/include/ECS/Components/TextLabel.hpp | 25 + engine/include/ECS/EffectFactory.hpp | 115 ++ engine/include/ECS/EnemyFactory.hpp | 26 + engine/include/ECS/EnemySystem.hpp | 34 + engine/include/ECS/Entity.hpp | 11 + engine/include/ECS/ForcePodSystem.hpp | 25 + engine/include/ECS/HealthSystem.hpp | 22 + engine/include/ECS/ISystem.hpp | 22 + engine/include/ECS/InputSystem.hpp | 20 + engine/include/ECS/LevelLoader.hpp | 189 +++ engine/include/ECS/MenuSystem.hpp | 32 + engine/include/ECS/MineSystem.hpp | 31 + engine/include/ECS/MovementSystem.hpp | 20 + .../ECS/ObstacleCollisionResponseSystem.hpp | 27 + .../ECS/PlayerCollisionResponseSystem.hpp | 38 + engine/include/ECS/PlayerFactory.hpp | 24 + engine/include/ECS/PlayerSystem.hpp | 28 + engine/include/ECS/PowerUpCollisionSystem.hpp | 34 + engine/include/ECS/PowerUpFactory.hpp | 59 + engine/include/ECS/PowerUpSpawnSystem.hpp | 48 + engine/include/ECS/Registry.hpp | 232 +++ engine/include/ECS/RenderingSystem.hpp | 23 + engine/include/ECS/ScoreSystem.hpp | 18 + engine/include/ECS/ScrollingSystem.hpp | 20 + engine/include/ECS/ShieldSystem.hpp | 25 + engine/include/ECS/ShootingSystem.hpp | 32 + engine/include/ECS/SparseArray.hpp | 184 +++ engine/include/ECS/TextRenderingSystem.hpp | 21 + engine/include/ECS/ThirdBulletSystem.hpp | 28 + engine/include/Math/Types.hpp | 30 + engine/include/Physics/IPhysics.hpp | 52 + engine/include/Renderer/IRenderer.hpp | 178 +++ engine/include/Renderer/SFMLRenderer.hpp | 105 ++ engine/src/Animation/AnimationModule.cpp | 254 +++ engine/src/Animation/CMakeLists.txt | 31 + engine/src/Audio/CMakeLists.txt | 89 ++ engine/src/Audio/SFMLAudio.cpp | 219 +++ engine/src/Core/CMakeLists.txt | 44 + engine/src/Core/ColorFilter.cpp | 39 + engine/src/Core/Engine.cpp | 168 ++ engine/src/Core/InputMapping.cpp | 38 + engine/src/Core/ModuleLoader.cpp | 200 +++ engine/src/ECS/AnimationSystem.cpp | 195 +++ engine/src/ECS/AudioSystem.cpp | 66 + engine/src/ECS/BlackOrbSystem.cpp | 110 ++ engine/src/ECS/BossAttackSystem.cpp | 398 +++++ engine/src/ECS/BossSystem.cpp | 104 ++ .../src/ECS/BulletCollisionResponseSystem.cpp | 189 +++ engine/src/ECS/CMakeLists.txt | 129 ++ engine/src/ECS/CollisionDetectionSystem.cpp | 155 ++ engine/src/ECS/EffectFactory.cpp | 410 +++++ engine/src/ECS/EnemyFactory.cpp | 111 ++ engine/src/ECS/EnemySystem.cpp | 119 ++ engine/src/ECS/ForcePodSystem.cpp | 62 + engine/src/ECS/HealthSystem.cpp | 45 + engine/src/ECS/InputSystem.cpp | 61 + engine/src/ECS/LevelLoader.cpp | 578 +++++++ engine/src/ECS/MenuSystem.cpp | 73 + engine/src/ECS/MineSystem.cpp | 101 ++ engine/src/ECS/MovementSystem.cpp | 38 + .../ECS/ObstacleCollisionResponseSystem.cpp | 149 ++ .../src/ECS/PlayerCollisionResponseSystem.cpp | 324 ++++ engine/src/ECS/PlayerFactory.cpp | 95 ++ engine/src/ECS/PlayerSystem.cpp | 56 + engine/src/ECS/PowerUpCollisionSystem.cpp | 76 + engine/src/ECS/PowerUpFactory.cpp | 265 ++++ engine/src/ECS/PowerUpSpawnSystem.cpp | 83 + engine/src/ECS/Registry.cpp | 80 + engine/src/ECS/RenderingSystem.cpp | 73 + engine/src/ECS/ScoreSystem.cpp | 61 + engine/src/ECS/ScrollingSystem.cpp | 115 ++ engine/src/ECS/ShieldSystem.cpp | 75 + engine/src/ECS/ShootingSystem.cpp | 276 ++++ engine/src/ECS/TextRenderingSystem.cpp | 43 + engine/src/ECS/ThirdBulletSystem.cpp | 73 + engine/src/Renderer/CMakeLists.txt | 114 ++ engine/src/Renderer/SFMLRenderer.cpp | 541 +++++++ engine/src/main.cpp | 27 + games/breakout/README.md | 33 + .../include/BallAccelerationSystem.hpp | 23 + .../include/BreakoutPhysicsSystem.hpp | 26 + .../breakout/include/BreakoutRenderSystem.hpp | 22 + games/breakout/src/BallAccelerationSystem.cpp | 45 + games/breakout/src/BreakoutPhysicsSystem.cpp | 245 +++ games/breakout/src/BreakoutRenderSystem.cpp | 72 + games/breakout/src/main.cpp | 301 ++++ games/rtype/client/include/EditorState.hpp | 85 + games/rtype/client/include/GameState.hpp | 392 +++++ .../rtype/client/include/GameStateMachine.hpp | 199 +++ games/rtype/client/include/LobbyState.hpp | 93 ++ games/rtype/client/include/MenuState.hpp | 81 + games/rtype/client/include/ResultsState.hpp | 77 + games/rtype/client/include/RoomListState.hpp | 88 ++ games/rtype/client/include/SettingsState.hpp | 185 +++ .../include/editor/EditorAssetLibrary.hpp | 52 + .../include/editor/EditorCanvasManager.hpp | 43 + .../include/editor/EditorColliderManager.hpp | 47 + .../client/include/editor/EditorConstants.hpp | 102 ++ .../client/include/editor/EditorDrawing.hpp | 35 + .../include/editor/EditorEntityManager.hpp | 67 + .../include/editor/EditorFileManager.hpp | 57 + .../client/include/editor/EditorGeometry.hpp | 52 + .../include/editor/EditorInputHandler.hpp | 31 + .../include/editor/EditorPropertyManager.hpp | 53 + .../client/include/editor/EditorTypes.hpp | 116 ++ .../client/include/editor/EditorUIManager.hpp | 96 ++ games/rtype/client/main.cpp | 96 ++ games/rtype/client/src/EditorState.cpp | 342 +++++ games/rtype/client/src/GameState.cpp | 14 + games/rtype/client/src/LobbyState.cpp | 10 + games/rtype/client/src/MenuState.cpp | 316 ++++ games/rtype/client/src/ResultsState.cpp | 267 ++++ games/rtype/client/src/SettingsState.cpp | 1365 +++++++++++++++++ .../client/src/editor/EditorAssetLibrary.cpp | 119 ++ .../client/src/editor/EditorCanvasManager.cpp | 116 ++ .../src/editor/EditorColliderManager.cpp | 189 +++ .../rtype/client/src/editor/EditorDrawing.cpp | 79 + .../client/src/editor/EditorEntityManager.cpp | 334 ++++ .../client/src/editor/EditorFileManager.cpp | 269 ++++ .../client/src/editor/EditorInputHandler.cpp | 41 + .../src/editor/EditorPropertyManager.cpp | 219 +++ .../client/src/editor/EditorUIManager.cpp | 433 ++++++ .../client/src/game/GameStateHelpers.cpp | 513 +++++++ games/rtype/client/src/game/GameStateInit.cpp | 782 ++++++++++ .../client/src/game/GameStateNetwork.cpp | 1059 +++++++++++++ .../client/src/game/GameStateTransition.cpp | 303 ++++ games/rtype/client/src/game/GameStateUI.cpp | 605 ++++++++ .../rtype/client/src/game/GameStateUpdate.cpp | 800 ++++++++++ .../rtype/client/src/lobby/LobbyStateInit.cpp | 203 +++ .../client/src/lobby/LobbyStateUpdate.cpp | 253 +++ games/rtype/client/src/room/RoomListState.cpp | 358 +++++ games/rtype/server/main.cpp | 81 + 156 files changed, 22858 insertions(+), 38 deletions(-) create mode 100644 engine/CMakeLists.txt create mode 100644 engine/README.md create mode 100644 engine/include/Animation/AnimationModule.hpp create mode 100644 engine/include/Animation/AnimationTypes.hpp create mode 100644 engine/include/Animation/IAnimation.hpp create mode 100644 engine/include/Audio/IAudio.hpp create mode 100644 engine/include/Audio/SFMLAudio.hpp create mode 100644 engine/include/Core/ColorFilter.hpp create mode 100644 engine/include/Core/Engine.hpp create mode 100644 engine/include/Core/InputMapping.hpp create mode 100644 engine/include/Core/Logger.hpp create mode 100644 engine/include/Core/Module.hpp create mode 100644 engine/include/Core/ModuleLoader.hpp create mode 100644 engine/include/Core/Platform.hpp create mode 100644 engine/include/ECS/AnimationSystem.hpp create mode 100644 engine/include/ECS/AudioSystem.hpp create mode 100644 engine/include/ECS/BlackOrbSystem.hpp create mode 100644 engine/include/ECS/BossAttackSystem.hpp create mode 100644 engine/include/ECS/BossSystem.hpp create mode 100644 engine/include/ECS/BulletCollisionResponseSystem.hpp create mode 100644 engine/include/ECS/CollisionDetectionSystem.hpp create mode 100644 engine/include/ECS/Component.hpp create mode 100644 engine/include/ECS/Components/Clickable.hpp create mode 100644 engine/include/ECS/Components/TextLabel.hpp create mode 100644 engine/include/ECS/EffectFactory.hpp create mode 100644 engine/include/ECS/EnemyFactory.hpp create mode 100644 engine/include/ECS/EnemySystem.hpp create mode 100644 engine/include/ECS/Entity.hpp create mode 100644 engine/include/ECS/ForcePodSystem.hpp create mode 100644 engine/include/ECS/HealthSystem.hpp create mode 100644 engine/include/ECS/ISystem.hpp create mode 100644 engine/include/ECS/InputSystem.hpp create mode 100644 engine/include/ECS/LevelLoader.hpp create mode 100644 engine/include/ECS/MenuSystem.hpp create mode 100644 engine/include/ECS/MineSystem.hpp create mode 100644 engine/include/ECS/MovementSystem.hpp create mode 100644 engine/include/ECS/ObstacleCollisionResponseSystem.hpp create mode 100644 engine/include/ECS/PlayerCollisionResponseSystem.hpp create mode 100644 engine/include/ECS/PlayerFactory.hpp create mode 100644 engine/include/ECS/PlayerSystem.hpp create mode 100644 engine/include/ECS/PowerUpCollisionSystem.hpp create mode 100644 engine/include/ECS/PowerUpFactory.hpp create mode 100644 engine/include/ECS/PowerUpSpawnSystem.hpp create mode 100644 engine/include/ECS/Registry.hpp create mode 100644 engine/include/ECS/RenderingSystem.hpp create mode 100644 engine/include/ECS/ScoreSystem.hpp create mode 100644 engine/include/ECS/ScrollingSystem.hpp create mode 100644 engine/include/ECS/ShieldSystem.hpp create mode 100644 engine/include/ECS/ShootingSystem.hpp create mode 100644 engine/include/ECS/SparseArray.hpp create mode 100644 engine/include/ECS/TextRenderingSystem.hpp create mode 100644 engine/include/ECS/ThirdBulletSystem.hpp create mode 100644 engine/include/Math/Types.hpp create mode 100644 engine/include/Physics/IPhysics.hpp create mode 100644 engine/include/Renderer/IRenderer.hpp create mode 100644 engine/include/Renderer/SFMLRenderer.hpp create mode 100644 engine/src/Animation/AnimationModule.cpp create mode 100644 engine/src/Animation/CMakeLists.txt create mode 100644 engine/src/Audio/CMakeLists.txt create mode 100644 engine/src/Audio/SFMLAudio.cpp create mode 100644 engine/src/Core/CMakeLists.txt create mode 100644 engine/src/Core/ColorFilter.cpp create mode 100644 engine/src/Core/Engine.cpp create mode 100644 engine/src/Core/InputMapping.cpp create mode 100644 engine/src/Core/ModuleLoader.cpp create mode 100644 engine/src/ECS/AnimationSystem.cpp create mode 100644 engine/src/ECS/AudioSystem.cpp create mode 100644 engine/src/ECS/BlackOrbSystem.cpp create mode 100644 engine/src/ECS/BossAttackSystem.cpp create mode 100644 engine/src/ECS/BossSystem.cpp create mode 100644 engine/src/ECS/BulletCollisionResponseSystem.cpp create mode 100644 engine/src/ECS/CMakeLists.txt create mode 100644 engine/src/ECS/CollisionDetectionSystem.cpp create mode 100644 engine/src/ECS/EffectFactory.cpp create mode 100644 engine/src/ECS/EnemyFactory.cpp create mode 100644 engine/src/ECS/EnemySystem.cpp create mode 100644 engine/src/ECS/ForcePodSystem.cpp create mode 100644 engine/src/ECS/HealthSystem.cpp create mode 100644 engine/src/ECS/InputSystem.cpp create mode 100644 engine/src/ECS/LevelLoader.cpp create mode 100644 engine/src/ECS/MenuSystem.cpp create mode 100644 engine/src/ECS/MineSystem.cpp create mode 100644 engine/src/ECS/MovementSystem.cpp create mode 100644 engine/src/ECS/ObstacleCollisionResponseSystem.cpp create mode 100644 engine/src/ECS/PlayerCollisionResponseSystem.cpp create mode 100644 engine/src/ECS/PlayerFactory.cpp create mode 100644 engine/src/ECS/PlayerSystem.cpp create mode 100644 engine/src/ECS/PowerUpCollisionSystem.cpp create mode 100644 engine/src/ECS/PowerUpFactory.cpp create mode 100644 engine/src/ECS/PowerUpSpawnSystem.cpp create mode 100644 engine/src/ECS/Registry.cpp create mode 100644 engine/src/ECS/RenderingSystem.cpp create mode 100644 engine/src/ECS/ScoreSystem.cpp create mode 100644 engine/src/ECS/ScrollingSystem.cpp create mode 100644 engine/src/ECS/ShieldSystem.cpp create mode 100644 engine/src/ECS/ShootingSystem.cpp create mode 100644 engine/src/ECS/TextRenderingSystem.cpp create mode 100644 engine/src/ECS/ThirdBulletSystem.cpp create mode 100644 engine/src/Renderer/CMakeLists.txt create mode 100644 engine/src/Renderer/SFMLRenderer.cpp create mode 100644 engine/src/main.cpp create mode 100644 games/breakout/README.md create mode 100644 games/breakout/include/BallAccelerationSystem.hpp create mode 100644 games/breakout/include/BreakoutPhysicsSystem.hpp create mode 100644 games/breakout/include/BreakoutRenderSystem.hpp create mode 100644 games/breakout/src/BallAccelerationSystem.cpp create mode 100644 games/breakout/src/BreakoutPhysicsSystem.cpp create mode 100644 games/breakout/src/BreakoutRenderSystem.cpp create mode 100644 games/breakout/src/main.cpp create mode 100644 games/rtype/client/include/EditorState.hpp create mode 100644 games/rtype/client/include/GameState.hpp create mode 100644 games/rtype/client/include/GameStateMachine.hpp create mode 100644 games/rtype/client/include/LobbyState.hpp create mode 100644 games/rtype/client/include/MenuState.hpp create mode 100644 games/rtype/client/include/ResultsState.hpp create mode 100644 games/rtype/client/include/RoomListState.hpp create mode 100644 games/rtype/client/include/SettingsState.hpp create mode 100644 games/rtype/client/include/editor/EditorAssetLibrary.hpp create mode 100644 games/rtype/client/include/editor/EditorCanvasManager.hpp create mode 100644 games/rtype/client/include/editor/EditorColliderManager.hpp create mode 100644 games/rtype/client/include/editor/EditorConstants.hpp create mode 100644 games/rtype/client/include/editor/EditorDrawing.hpp create mode 100644 games/rtype/client/include/editor/EditorEntityManager.hpp create mode 100644 games/rtype/client/include/editor/EditorFileManager.hpp create mode 100644 games/rtype/client/include/editor/EditorGeometry.hpp create mode 100644 games/rtype/client/include/editor/EditorInputHandler.hpp create mode 100644 games/rtype/client/include/editor/EditorPropertyManager.hpp create mode 100644 games/rtype/client/include/editor/EditorTypes.hpp create mode 100644 games/rtype/client/include/editor/EditorUIManager.hpp create mode 100644 games/rtype/client/main.cpp create mode 100644 games/rtype/client/src/EditorState.cpp create mode 100644 games/rtype/client/src/GameState.cpp create mode 100644 games/rtype/client/src/LobbyState.cpp create mode 100644 games/rtype/client/src/MenuState.cpp create mode 100644 games/rtype/client/src/ResultsState.cpp create mode 100644 games/rtype/client/src/SettingsState.cpp create mode 100644 games/rtype/client/src/editor/EditorAssetLibrary.cpp create mode 100644 games/rtype/client/src/editor/EditorCanvasManager.cpp create mode 100644 games/rtype/client/src/editor/EditorColliderManager.cpp create mode 100644 games/rtype/client/src/editor/EditorDrawing.cpp create mode 100644 games/rtype/client/src/editor/EditorEntityManager.cpp create mode 100644 games/rtype/client/src/editor/EditorFileManager.cpp create mode 100644 games/rtype/client/src/editor/EditorInputHandler.cpp create mode 100644 games/rtype/client/src/editor/EditorPropertyManager.cpp create mode 100644 games/rtype/client/src/editor/EditorUIManager.cpp create mode 100644 games/rtype/client/src/game/GameStateHelpers.cpp create mode 100644 games/rtype/client/src/game/GameStateInit.cpp create mode 100644 games/rtype/client/src/game/GameStateNetwork.cpp create mode 100644 games/rtype/client/src/game/GameStateTransition.cpp create mode 100644 games/rtype/client/src/game/GameStateUI.cpp create mode 100644 games/rtype/client/src/game/GameStateUpdate.cpp create mode 100644 games/rtype/client/src/lobby/LobbyStateInit.cpp create mode 100644 games/rtype/client/src/lobby/LobbyStateUpdate.cpp create mode 100644 games/rtype/client/src/room/RoomListState.cpp create mode 100644 games/rtype/server/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 01e9510..06bb9dd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ if(NOT DEFINED CMAKE_TOOLCHAIN_FILE) include(cmake/vcpkg.cmake) endif() -project(RType VERSION 1.0.0 LANGUAGES CXX) +project(RTypeEngine VERSION 2.0.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -24,43 +24,59 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) +# Copy assets to build directory add_custom_target(copy_assets ALL COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/assets ${CMAKE_BINARY_DIR}/assets ) +# ============================================================================= +# Dependencies +# ============================================================================= find_package(asio CONFIG REQUIRED) find_package(nlohmann_json CONFIG REQUIRED) find_package(Threads REQUIRED) -add_subdirectory(libs/engine) +# ============================================================================= +# Engine Libraries (generic, reusable by any game) +# ============================================================================= +add_subdirectory(engine) + +# ============================================================================= +# Network Libraries (also generic) +# ============================================================================= add_subdirectory(libs/network) -add_executable(r-type_server server/main.cpp) +# ============================================================================= +# Games +# ============================================================================= + +# R-Type Game +add_executable(r-type_server games/rtype/server/main.cpp) target_link_libraries(r-type_server PRIVATE rtype_network rtype_asio_network) add_executable(r-type_client - client/main.cpp - client/src/MenuState.cpp - client/src/SettingsState.cpp - client/src/room/RoomListState.cpp - client/src/LobbyState.cpp - client/src/GameState.cpp - client/src/ResultsState.cpp - client/src/EditorState.cpp - client/src/editor/EditorCanvasManager.cpp - client/src/editor/EditorAssetLibrary.cpp - client/src/editor/EditorUIManager.cpp - client/src/editor/EditorEntityManager.cpp - client/src/editor/EditorFileManager.cpp - client/src/editor/EditorInputHandler.cpp - client/src/editor/EditorDrawing.cpp - client/src/editor/EditorPropertyManager.cpp - client/src/editor/EditorColliderManager.cpp + games/rtype/client/main.cpp + games/rtype/client/src/MenuState.cpp + games/rtype/client/src/SettingsState.cpp + games/rtype/client/src/room/RoomListState.cpp + games/rtype/client/src/LobbyState.cpp + games/rtype/client/src/GameState.cpp + games/rtype/client/src/ResultsState.cpp + games/rtype/client/src/EditorState.cpp + games/rtype/client/src/editor/EditorCanvasManager.cpp + games/rtype/client/src/editor/EditorAssetLibrary.cpp + games/rtype/client/src/editor/EditorUIManager.cpp + games/rtype/client/src/editor/EditorEntityManager.cpp + games/rtype/client/src/editor/EditorFileManager.cpp + games/rtype/client/src/editor/EditorInputHandler.cpp + games/rtype/client/src/editor/EditorDrawing.cpp + games/rtype/client/src/editor/EditorPropertyManager.cpp + games/rtype/client/src/editor/EditorColliderManager.cpp ) target_include_directories(r-type_client PRIVATE - ${CMAKE_SOURCE_DIR}/client/include + ${CMAKE_SOURCE_DIR}/games/rtype/client/include ) target_link_libraries(r-type_client PRIVATE rtype_network @@ -71,11 +87,36 @@ target_link_libraries(r-type_client PRIVATE rtype_sfml_audio ) +# Breakout Game (demo of engine reusability) +if(TARGET rtype_sfml_renderer) + add_executable(breakout + games/breakout/src/main.cpp + games/breakout/src/BreakoutPhysicsSystem.cpp + games/breakout/src/BreakoutRenderSystem.cpp + games/breakout/src/BallAccelerationSystem.cpp + ) + target_include_directories(breakout PRIVATE + ${CMAKE_SOURCE_DIR}/games/breakout/include + ) + target_link_libraries(breakout PRIVATE + rtype_core + rtype_ecs_core + rtype_sfml_renderer + ) + message(STATUS "breakout will be built (engine reusability demo)") +endif() + +# Metroidvania Game (NEW - to be developed) +# add_subdirectory(games/metroidvania) + +# ============================================================================= +# Tests +# ============================================================================= add_executable(test_lobby tests/test_lobby.cpp) target_link_libraries(test_lobby PRIVATE rtype_network rtype_asio_network) add_executable(test_sparse_array tests/test_sparse_array.cpp) -target_link_libraries(test_sparse_array PRIVATE rtype_ecs) +target_link_libraries(test_sparse_array PRIVATE rtype_ecs_core) add_executable(test_udp_protocol tests/test_udp_protocol.cpp) target_link_libraries(test_udp_protocol PRIVATE rtype_network rtype_asio_network) @@ -113,20 +154,4 @@ if(TARGET rtype_sfml_renderer) rtype_sfml_renderer ) message(STATUS "test_background will be built") - - add_executable(breakout - breakout/src/main.cpp - breakout/src/BreakoutPhysicsSystem.cpp - breakout/src/BreakoutRenderSystem.cpp - breakout/src/BallAccelerationSystem.cpp - ) - target_include_directories(breakout PRIVATE - ${CMAKE_SOURCE_DIR}/breakout/include - ) - target_link_libraries(breakout PRIVATE - rtype_core - rtype_ecs - rtype_sfml_renderer - ) - message(STATUS "breakout will be built (second game demo)") endif() diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt new file mode 100644 index 0000000..23bfad3 --- /dev/null +++ b/engine/CMakeLists.txt @@ -0,0 +1,19 @@ +# Engine CMakeLists.txt - Generic Game Engine +# +# This builds the generic engine libraries that can be used by any game. +# Game-specific code belongs in games// + +# Core subsystem (Logger, Engine, Module system) +add_subdirectory(src/Core) + +# ECS subsystem (split into generic and rtype-specific) +add_subdirectory(src/ECS) + +# Renderer plugin (SFML implementation) +add_subdirectory(src/Renderer) + +# Audio plugin (SFML implementation) +add_subdirectory(src/Audio) + +# Animation system +add_subdirectory(src/Animation) diff --git a/engine/README.md b/engine/README.md new file mode 100644 index 0000000..0fd256e --- /dev/null +++ b/engine/README.md @@ -0,0 +1,805 @@ +# R-Type Engine & ECS - Developer Documentation + +## Table of Contents + +- [Overview](#overview) +- [Architecture](#architecture) +- [Core Engine](#core-engine) +- [Module System](#module-system) +- [ECS Fundamentals](#ecs-fundamentals) +- [Registry API](#registry-api) +- [Components](#components) +- [Systems](#systems) +- [SparseArray](#sparsearray) +- [Quick Start Guide](#quick-start-guide) +- [Best Practices](#best-practices) + +--- + +## Overview + +The R-Type Engine is a modular, data-oriented game engine built with C++17. It features: + +- **Plugin-based architecture**: Load modules dynamically at runtime +- **Entity Component System (ECS)**: Data-oriented design for efficient game object management +- **Cross-platform**: Works on Linux, macOS, and Windows +- **Type-safe**: Template-based component and system registration + +### Project Structure + +``` +libs/engine/ +├── include/ +│ ├── Core/ +│ │ ├── Engine.hpp # Main engine coordinator +│ │ ├── Module.hpp # IModule interface +│ │ ├── ModuleLoader.hpp # Dynamic plugin loader +│ │ └── Logger.hpp # Logging utility +│ └── ECS/ +│ ├── Entity.hpp # Entity type (uint32_t) +│ ├── Component.hpp # All component definitions +│ ├── SparseArray.hpp # Component storage container +│ ├── Registry.hpp # ECS registry +│ ├── ISystem.hpp # System interface +│ └── *System.hpp # Game logic systems +└── src/ + ├── Core/ + └── ECS/ +``` + +--- + +## Architecture + +The engine follows a layered architecture: + +``` +┌─────────────────────────────────────┐ +│ GAME APPLICATION │ +│ (Uses engine as static library) │ +└─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ ENGINE CORE │ +│ ┌──────────┐ ┌─────────────────┐ │ +│ │ Engine │ │ Registry │ │ +│ │ │ │ (ECS Manager) │ │ +│ └──────────┘ └─────────────────┘ │ +│ ┌──────────┐ ┌─────────────────┐ │ +│ │ Module │ │ Systems │ │ +│ │ Loader │ │ (Game Logic) │ │ +│ └──────────┘ └─────────────────┘ │ +└─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ MODULES / PLUGINS │ +│ ┌──────────┐ ┌─────────┐ ┌──────┐ │ +│ │ Renderer │ │ Physics │ │Audio │ │ +│ └──────────┘ └─────────┘ └──────┘ │ +└─────────────────────────────────────┘ +``` + +--- + +## Core Engine + +The `Engine` class is the central coordinator that manages modules, plugins, and the ECS registry. + +### Basic Usage + +```cpp +#include +#include + +int main() { + // Create engine with default config + RType::Core::EngineConfig config; + config.pluginPath = "./plugins"; + + auto engine = std::make_unique(config); + + // Load plugins (optional) + engine->LoadPlugin("plugins/libSFMLRenderer.so"); + + // Register built-in modules + engine->RegisterModule(std::make_unique()); + + // Initialize all modules (in priority order) + if (!engine->Initialize()) { + RType::Core::Logger::Error("Engine initialization failed"); + return 1; + } + + // Access the ECS registry + auto& registry = engine->GetRegistry(); + + // Register systems + engine->RegisterSystem(std::make_unique()); + + // Game loop + while (running) { + float deltaTime = getDeltaTime(); + engine->UpdateSystems(deltaTime); + // ... render, etc. + } + + // Shutdown (modules shutdown in reverse priority order) + engine->Shutdown(); + return 0; +} +``` + +### Engine API + +| Method | Description | +|--------|-------------| +| `Initialize()` | Initialize all registered modules (returns false on failure) | +| `Shutdown()` | Shutdown all modules in reverse priority order | +| `LoadPlugin(path)` | Load a plugin from shared library (.so/.dll/.dylib) | +| `UnloadPlugin(name)` | Unload a plugin by name | +| `RegisterModule(module)` | Register a built-in module | +| `GetModule()` | Get module by type (searches built-in and plugins) | +| `GetModuleByName(name)` | Get module by name string | +| `GetRegistry()` | Access the ECS registry | +| `RegisterSystem(system)` | Register an ECS system | +| `UpdateSystems(deltaTime)` | Update all registered systems | + +--- + +## Module System + +Modules are the building blocks of the engine. They can be built-in (compiled with engine) or plugins (loaded dynamically). + +### IModule Interface + +All modules must implement `IModule`: + +```cpp +class IModule { +public: + virtual ~IModule() = default; + virtual const char* GetName() const = 0; + virtual ModulePriority GetPriority() const = 0; + virtual bool Initialize(Engine* engine) = 0; + virtual void Shutdown() = 0; + virtual void Update(float deltaTime) = 0; + virtual bool ShouldUpdateInRenderThread() const { return false; } + virtual bool IsOverridable() const { return true; } +}; +``` + +### Module Priority + +Modules initialize in priority order (lowest value first): + +| Priority | Value | Use Case | +|----------|-------|----------| +| `Critical` | 0 | Core systems that others depend on | +| `High` | 1 | Rendering, Physics | +| `Normal` | 2 | Audio, Input | +| `Low` | 3 | Non-essential features | + +**Shutdown occurs in reverse priority order.** + +### Creating a Module + +```cpp +// MyModule.hpp +#pragma once +#include +#include + +class MyModule : public RType::Core::IModule { +public: + const char* GetName() const override { return "MyModule"; } + + RType::Core::ModulePriority GetPriority() const override { + return RType::Core::ModulePriority::Normal; + } + + bool Initialize(RType::Core::Engine* engine) override { + m_engine = engine; + RType::Core::Logger::Info("MyModule initialized"); + return true; + } + + void Shutdown() override { + RType::Core::Logger::Info("MyModule shutdown"); + } + + void Update(float deltaTime) override { + // Per-frame update + } + +private: + RType::Core::Engine* m_engine = nullptr; +}; +``` + +### Plugin Export Functions + +Plugins must export these C functions: + +```cpp +extern "C" { + RTYPE_MODULE_EXPORT RType::Core::IModule* CreateModule() { + return new MyModule(); + } + + RTYPE_MODULE_EXPORT void DestroyModule(RType::Core::IModule* module) { + delete module; + } +} +``` + +The `extern "C"` prevents C++ name mangling, allowing `dlsym()`/`GetProcAddress()` to find the functions. + +--- + +## ECS Fundamentals + +The Entity Component System provides a data-oriented approach to game object management. + +### Core Concepts + +- **Entity**: A unique identifier (`uint32_t`). Represents a game object. +- **Component**: Plain data struct attached to entities. Contains no logic. +- **System**: Logic that processes entities with specific component combinations. +- **Registry**: Central manager that stores entities and their components. + +### Entity + +```cpp +using Entity = uint32_t; +constexpr Entity NULL_ENTITY = 0; +``` + +Entities are just IDs. They have no data or behavior themselves. + +--- + +## Registry API + +The `Registry` manages all entities and components. + +### Entity Management + +```cpp +// Create entity +Entity player = registry.CreateEntity(); + +// Check if entity exists +if (registry.IsEntityAlive(player)) { + // Entity is valid +} + +// Destroy entity (removes all components) +registry.DestroyEntity(player); + +// Get total entity count +size_t count = registry.GetEntityCount(); +``` + +### Component Management + +```cpp +// Add component (by copy) +registry.AddComponent(player, RType::ECS::Position{100.0f, 200.0f}); + +// Add component (by move) +auto velocity = RType::ECS::Velocity{5.0f, 0.0f}; +registry.AddComponent(player, std::move(velocity)); + +// Add component (default constructed) +registry.AddComponent(player); + +// Get component (non-const) +auto& position = registry.GetComponent(player); +position.x = 150.0f; + +// Get component (const) +const auto& health = registry.GetComponent(player); + +// Check if entity has component +if (registry.HasComponent(player)) { + // Safe to access +} + +// Remove component +registry.RemoveComponent(player); + +// Get all entities with a component +auto entities = registry.GetEntitiesWithComponent(); +for (Entity e : entities) { + // Process entities with Position +} +``` + +### Error Handling + +- `GetComponent(entity)` throws `std::runtime_error` if component doesn't exist +- `AddComponent(entity)` throws if entity is `NULL_ENTITY` or doesn't exist +- Always check with `HasComponent()` before accessing + +**Best Practice:** +```cpp +if (registry.HasComponent(entity)) { + auto& pos = registry.GetComponent(entity); + // Safe to use +} +``` + +--- + +## Components + +Components are plain data structs that inherit from `IComponent`. They contain **no logic**. + +### Built-in Components + +The engine provides many built-in components: + +```cpp +// Transform +struct Position { float x, y; }; +struct Velocity { float dx, dy; }; + +// Rendering +struct Drawable { + Renderer::SpriteId spriteId; + Math::Vector2 scale{1.0f, 1.0f}; + float rotation = 0.0f; + int layer = 0; +}; + +// Gameplay +struct Health { int current, max; }; +struct Player { uint8_t playerNumber; uint64_t playerHash; }; +struct Enemy { EnemyType type; uint32_t id; }; +struct Bullet { Entity owner; }; +struct PowerUp { PowerUpType type; uint32_t id; }; + +// Physics +struct BoxCollider { float width, height; }; +struct CircleCollider { float radius; }; +struct CollisionLayer { uint16_t layer; uint16_t mask; }; + +// And many more... +``` + +### Creating Custom Components + +```cpp +// In Component.hpp or your own header +struct MyCustomComponent : public RType::ECS::IComponent { + int value = 0; + std::string name; + + MyCustomComponent() = default; + MyCustomComponent(int v, const std::string& n) + : value(v), name(n) {} +}; + +// Usage +registry.AddComponent(entity, MyCustomComponent{42, "test"}); +``` + +### Component Guidelines + +- ✅ Use plain structs with public members +- ✅ Provide default constructor +- ✅ Keep components small and focused +- ✅ No virtual functions (unless inheriting from IComponent) +- ❌ Don't put logic in components +- ❌ Don't store pointers to other entities (use `Entity` IDs instead) + +--- + +## Systems + +Systems contain the game logic. They process entities with specific component combinations. + +### ISystem Interface + +```cpp +class ISystem { +public: + virtual ~ISystem() = default; + virtual void Update(Registry& registry, float deltaTime) = 0; + virtual const char* GetName() const = 0; + virtual bool Initialize(Registry& registry) { return true; } + virtual void Shutdown() {} +}; +``` + +### Creating a System + +```cpp +// MovementSystem.hpp +#pragma once +#include "ECS/ISystem.hpp" + +namespace RType::ECS { + class MovementSystem : public ISystem { + public: + void Update(Registry& registry, float deltaTime) override; + const char* GetName() const override { return "MovementSystem"; } + }; +} + +// MovementSystem.cpp +#include "ECS/MovementSystem.hpp" +#include "ECS/Component.hpp" + +void MovementSystem::Update(Registry& registry, float deltaTime) { + // Get all entities with Velocity + auto entities = registry.GetEntitiesWithComponent(); + + for (Entity entity : entities) { + // Check if entity also has Position + if (!registry.HasComponent(entity)) { + continue; + } + + // Update position based on velocity + auto& position = registry.GetComponent(entity); + const auto& velocity = registry.GetComponent(entity); + + position.x += velocity.dx * deltaTime; + position.y += velocity.dy * deltaTime; + } +} +``` + +### Registering Systems + +```cpp +// In your game initialization +auto engine = std::make_unique(); + +engine->RegisterSystem(std::make_unique()); +engine->RegisterSystem(std::make_unique()); +engine->RegisterSystem(std::make_unique()); + +engine->Initialize(); + +// In game loop +while (running) { + float deltaTime = getDeltaTime(); + engine->UpdateSystems(deltaTime); // Updates all systems +} +``` + +### System Patterns + +**Query Multiple Components:** +```cpp +void MySystem::Update(Registry& registry, float deltaTime) { + // Get entities with first component + auto entities = registry.GetEntitiesWithComponent(); + + for (Entity e : entities) { + // Check for additional components + if (registry.HasComponent(e) && + registry.HasComponent(e)) { + + auto& a = registry.GetComponent(e); + auto& b = registry.GetComponent(e); + auto& c = registry.GetComponent(e); + + // Process... + } + } +} +``` + +**Create/Modify Entities:** +```cpp +void SpawnSystem::Update(Registry& registry, float deltaTime) { + if (shouldSpawn) { + Entity enemy = registry.CreateEntity(); + registry.AddComponent(enemy, Position{100.0f, 50.0f}); + registry.AddComponent(enemy, Velocity{0.0f, 50.0f}); + registry.AddComponent(enemy, Enemy{EnemyType::BASIC, nextId++}); + } +} +``` + +--- + +## SparseArray + +`SparseArray` is the internal container used by the Registry to store components efficiently. + +### Concept + +A sparse array is a vector where the **index directly corresponds to an Entity ID**. This provides: +- **O(1)** direct access by entity ID +- **Cache-friendly** sequential iteration +- **Holes allowed** (indices can be empty via `std::nullopt`) + +### Structure + +```cpp +template +class SparseArray { + std::vector> _data; + // Index = Entity ID +}; +``` + +### Visual Example + +``` +Entities: 0, 2, 5 have Position components + +Index: 0 1 2 3 4 5 + ┌────┬────┬────┬────┬────┬────┐ +Data: │{x,y}│null│{x,y}│null│null│{x,y}│ + └────┴────┴────┴────┴────┴────┘ +``` + +### Usage + +```cpp +SparseArray positions; + +// Insert component at entity ID 5 +positions.insert_at(5, Position{10.0f, 20.0f}); + +// Access directly +auto& pos_opt = positions[5]; +if (pos_opt.has_value()) { + Position& pos = pos_opt.value(); + pos.x = 100.0f; +} + +// Emplace (constructs in-place, more efficient) +positions.emplace_at(7, 30.0f, 40.0f); + +// Erase +positions.erase(5); + +// Iterate +for (size_t i = 0; i < positions.size(); ++i) { + if (positions[i].has_value()) { + Entity e = static_cast(i); + Position& pos = positions[i].value(); + // Process... + } +} +``` + +--- + +## Quick Start Guide + +### 1. Create an Entity with Components + +```cpp +#include +#include + +auto& registry = engine->GetRegistry(); + +// Create player entity +Entity player = registry.CreateEntity(); + +// Add components +registry.AddComponent(player, RType::ECS::Position{400.0f, 300.0f}); +registry.AddComponent(player, RType::ECS::Velocity{0.0f, 0.0f}); +registry.AddComponent(player, RType::ECS::Health{100, 100}); +registry.AddComponent(player, RType::ECS::Player{1, 0x1234, true}); +``` + +### 2. Create a System + +```cpp +// MySystem.hpp +class MySystem : public RType::ECS::ISystem { +public: + void Update(RType::ECS::Registry& registry, float deltaTime) override { + auto entities = registry.GetEntitiesWithComponent(); + for (Entity e : entities) { + auto& pos = registry.GetComponent(e); + // Update logic... + } + } + const char* GetName() const override { return "MySystem"; } +}; +``` + +### 3. Register and Run + +```cpp +engine->RegisterSystem(std::make_unique()); +engine->Initialize(); + +// Game loop +while (running) { + engine->UpdateSystems(deltaTime); +} +``` + +### 4. Complete Example + +```cpp +#include +#include +#include + +int main() { + auto engine = std::make_unique(); + + // Register systems + engine->RegisterSystem(std::make_unique()); + + // Initialize + engine->Initialize(); + auto& registry = engine->GetRegistry(); + + // Create entities + Entity player = registry.CreateEntity(); + registry.AddComponent(player, RType::ECS::Position{100.0f, 200.0f}); + registry.AddComponent(player, RType::ECS::Velocity{50.0f, 0.0f}); + + // Game loop + float deltaTime = 0.016f; // ~60 FPS + for (int i = 0; i < 100; ++i) { + engine->UpdateSystems(deltaTime); + + // Check updated position + const auto& pos = registry.GetComponent(player); + RType::Core::Logger::Info("Player position: ({}, {})", pos.x, pos.y); + } + + engine->Shutdown(); + return 0; +} +``` + +--- + +## Best Practices + +### Component Design + +1. **Keep components small and focused** + ```cpp + // ✅ Good: Single responsibility + struct Position { float x, y; }; + struct Velocity { float dx, dy; }; + + // ❌ Bad: Too much data in one component + struct Transform { float x, y, dx, dy, rotation, scale; }; + ``` + +2. **Use Entity IDs, not pointers** + ```cpp + // ✅ Good + struct Bullet { Entity owner; }; + + // ❌ Bad + struct Bullet { Entity* owner; }; + ``` + +3. **Provide sensible defaults** + ```cpp + struct Health { + int current = 100; + int max = 100; + }; + ``` + +### System Design + +1. **Process entities efficiently** + ```cpp + // ✅ Good: Get entities with most selective component first + auto entities = registry.GetEntitiesWithComponent(); + for (Entity e : entities) { + if (registry.HasComponent(e)) { + // Process... + } + } + ``` + +2. **Don't modify component pools during iteration** + ```cpp + // ❌ Bad: Modifying while iterating + for (Entity e : entities) { + registry.DestroyEntity(e); // Can cause issues + } + + // ✅ Good: Collect first, then modify + std::vector toDestroy; + for (Entity e : entities) { + if (shouldDestroy) { + toDestroy.push_back(e); + } + } + for (Entity e : toDestroy) { + registry.DestroyEntity(e); + } + ``` + +3. **Use const references when possible** + ```cpp + // ✅ Good + const auto& velocity = registry.GetComponent(entity); + + // Only use non-const when modifying + auto& position = registry.GetComponent(entity); + ``` + +### Performance Tips + +1. **Batch component access**: Get all entities once, then iterate +2. **Cache component references**: Don't call `GetComponent()` multiple times per entity +3. **Use `HasComponent()` before `GetComponent()`** to avoid exceptions +4. **Consider component layout**: Group frequently accessed components together + +### Error Handling + +```cpp +// Always check before accessing +if (registry.HasComponent(entity)) { + auto& health = registry.GetComponent(entity); + health.current -= damage; +} + +// Or use try-catch for critical paths +try { + auto& pos = registry.GetComponent(entity); + // Use pos... +} catch (const std::runtime_error& e) { + Logger::Error("Failed to get Position: {}", e.what()); +} +``` + +--- + +## API Reference Summary + +### RType::Core::Engine + +- `Initialize()` → `bool` +- `Shutdown()` → `void` +- `LoadPlugin(path)` → `IModule*` +- `RegisterModule(module)` → `void` +- `GetModule()` → `T*` +- `GetRegistry()` → `Registry&` +- `RegisterSystem(system)` → `void` +- `UpdateSystems(deltaTime)` → `void` + +### RType::ECS::Registry + +- `CreateEntity()` → `Entity` +- `DestroyEntity(entity)` → `void` +- `IsEntityAlive(entity)` → `bool` +- `AddComponent(entity, component)` → `T&` +- `GetComponent(entity)` → `T&` / `const T&` +- `HasComponent(entity)` → `bool` +- `RemoveComponent(entity)` → `void` +- `GetEntitiesWithComponent()` → `std::vector` +- `GetEntityCount()` → `size_t` + +### RType::Core::Logger + +- `Logger::Debug(format, ...)` +- `Logger::Info(format, ...)` +- `Logger::Warning(format, ...)` +- `Logger::Error(format, ...)` +- `Logger::Critical(format, ...)` + +--- + +## Additional Resources + +- Check `CODING_STYLE.md` for code style guidelines +- Review existing systems in `libs/engine/include/ECS/*System.hpp` for examples +- Examine component definitions in `libs/engine/include/ECS/Component.hpp` + +--- \ No newline at end of file diff --git a/engine/include/Animation/AnimationModule.hpp b/engine/include/Animation/AnimationModule.hpp new file mode 100644 index 0000000..7635af7 --- /dev/null +++ b/engine/include/Animation/AnimationModule.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include "Animation/IAnimation.hpp" + +namespace Animation { + + class AnimationModule : public IAnimation { + public: + AnimationModule(); + ~AnimationModule() override; + + const char* GetName() const override { return "AnimationModule"; } + RType::Core::ModulePriority GetPriority() const override { + return RType::Core::ModulePriority::Normal; + } + bool Initialize(RType::Core::Engine* engine) override; + void Shutdown() override; + void Update(float deltaTime) override; + + // Clip Management + AnimationClipId CreateClip(const AnimationClipConfig& config) override; + AnimationClipId CreateClipFromGrid(const std::string& name, + const std::string& texturePath, + const GridLayout& layout, + bool looping) override; + AnimationClipId LoadClipFromJson(const std::string& path) override; + void DestroyClip(AnimationClipId clipId) override; + const AnimationClipConfig* GetClipConfig(AnimationClipId clipId) const override; + + // Clip Introspection + float GetClipDuration(AnimationClipId clipId) const override; + std::size_t GetClipFrameCount(AnimationClipId clipId) const override; + bool IsClipValid(AnimationClipId clipId) const override; + + // Frame Calculation + FrameDef GetFrameAtTime(AnimationClipId clipId, + float time, + bool looping) const override; + std::size_t GetFrameIndexAtTime(AnimationClipId clipId, + float time, + bool looping) const override; + + // Bulk Loading + void LoadAnimationsFromManifest(const std::string& manifestPath) override; + void UnloadAll() override; + + // Utility + AnimationClipId GetClipByName(const std::string& name) const override; + + private: + // Internal storage + std::unordered_map m_clips; + std::unordered_map m_clipNameToId; + + // ID generation + AnimationClipId m_nextClipId = 1; + + // Engine reference + RType::Core::Engine* m_engine = nullptr; + + // Helper functions + float CalculateClipDuration(const AnimationClipConfig& config) const; + }; + +} diff --git a/engine/include/Animation/AnimationTypes.hpp b/engine/include/Animation/AnimationTypes.hpp new file mode 100644 index 0000000..b2eecb0 --- /dev/null +++ b/engine/include/Animation/AnimationTypes.hpp @@ -0,0 +1,115 @@ +#pragma once + +#include +#include +#include +#include +#include "Math/Types.hpp" + +namespace Animation { + + using AnimationClipId = std::uint32_t; + using AnimationGraphId = std::uint32_t; + using AnimationStateId = std::uint32_t; + + constexpr AnimationClipId INVALID_CLIP_ID = 0; + constexpr AnimationGraphId INVALID_GRAPH_ID = 0; + constexpr AnimationStateId INVALID_STATE_ID = 0; + + struct FrameDef { + Math::Rectangle region{}; + float duration = 0.1f; + std::string eventName; + + FrameDef() = default; + FrameDef(const Math::Rectangle& reg, float dur, const std::string& event = "") + : region(reg), duration(dur), eventName(event) {} + }; + + struct AnimationClipConfig { + std::string name; + std::string texturePath; + std::vector frames; + bool looping = false; + float playbackSpeed = 1.0f; + + AnimationClipConfig() = default; + }; + + struct GridLayout { + std::uint32_t columns = 1; + std::uint32_t rows = 1; + std::uint32_t startFrame = 0; + std::uint32_t frameCount = 0; + float frameWidth = 0.0f; + float frameHeight = 0.0f; + float defaultDuration = 0.1f; + + GridLayout() = default; + }; + + enum class CompareOp : std::uint8_t { + GREATER = 0, + GREATER_EQUAL = 1, + LESS = 2, + LESS_EQUAL = 3, + EQUAL = 4, + NOT_EQUAL = 5 + }; + + struct TransitionDef { + AnimationStateId targetState = INVALID_STATE_ID; + std::string conditionParam; + CompareOp compareOp = CompareOp::GREATER_EQUAL; + float conditionValue = 1.0f; + float blendDuration = 0.0f; + bool hasExitTime = false; + float exitTimeNormalized = 1.0f; + int priority = 0; + + TransitionDef() = default; + }; + + struct AnimationStateDef { + std::string name; + AnimationClipId clipId = INVALID_CLIP_ID; + std::vector transitions; + float speed = 1.0f; + bool looping = true; + + AnimationStateDef() = default; + }; + + struct AnimationGraphDef { + std::string name; + std::vector states; + AnimationStateId defaultStateId = INVALID_STATE_ID; + std::vector parameterNames; + + AnimationGraphDef() = default; + }; + + struct PlaybackOptions { + float speed = 1.0f; + bool reversed = false; + float startTimeNormalized = 0.0f; + bool destroyOnComplete = false; + + PlaybackOptions() = default; + }; + + using AnimationEventCallback = std::function; + + enum class EffectType : std::uint8_t { + EXPLOSION_SMALL = 0, + EXPLOSION_LARGE = 1, + BULLET_IMPACT = 2, + HIT_MARKER = 3, + POWER_UP_COLLECT = 4, + BOSS_PHASE_TRANSITION = 5, + SPAWN_EFFECT = 6, + DEATH_EFFECT = 7, + CUSTOM = 255 + }; + +} diff --git a/engine/include/Animation/IAnimation.hpp b/engine/include/Animation/IAnimation.hpp new file mode 100644 index 0000000..a19e23e --- /dev/null +++ b/engine/include/Animation/IAnimation.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include "Core/Module.hpp" +#include "Animation/AnimationTypes.hpp" + +namespace Animation { + + class IAnimation : public RType::Core::IModule { + public: + ~IAnimation() override = default; + + const char* GetName() const override = 0; + RType::Core::ModulePriority GetPriority() const override = 0; + bool Initialize(RType::Core::Engine* engine) override = 0; + void Shutdown() override = 0; + void Update(float deltaTime) override = 0; + + // Create a clip from explicit configuration + virtual AnimationClipId CreateClip(const AnimationClipConfig& config) = 0; + + // Create a clip from a grid layout (uniform sprite sheet) + virtual AnimationClipId CreateClipFromGrid(const std::string& name, + const std::string& texturePath, + const GridLayout& layout, + bool looping = false) = 0; + + // Load a clip definition from a JSON file + virtual AnimationClipId LoadClipFromJson(const std::string& path) = 0; + + // Destroy a clip and release its resources + virtual void DestroyClip(AnimationClipId clipId) = 0; + + // Get the configuration of a clip (nullptr if not found) + virtual const AnimationClipConfig* GetClipConfig(AnimationClipId clipId) const = 0; + + // Get total duration of a clip in seconds + virtual float GetClipDuration(AnimationClipId clipId) const = 0; + + // Get number of frames in a clip + virtual std::size_t GetClipFrameCount(AnimationClipId clipId) const = 0; + + // Check if a clip exists and is valid + virtual bool IsClipValid(AnimationClipId clipId) const = 0; + + // Get the frame definition at a given time + // Returns the frame data including texture region + virtual FrameDef GetFrameAtTime(AnimationClipId clipId, + float time, + bool looping) const = 0; + + // Get the frame index at a given time + virtual std::size_t GetFrameIndexAtTime(AnimationClipId clipId, + float time, + bool looping) const = 0; + + // Load multiple animations from a manifest JSON file + virtual void LoadAnimationsFromManifest(const std::string& manifestPath) = 0; + + // Unload all loaded animations + virtual void UnloadAll() = 0; + + // Get a clip ID by name (returns INVALID_CLIP_ID if not found) + virtual AnimationClipId GetClipByName(const std::string& name) const = 0; + }; + +} diff --git a/engine/include/Audio/IAudio.hpp b/engine/include/Audio/IAudio.hpp new file mode 100644 index 0000000..82fe93a --- /dev/null +++ b/engine/include/Audio/IAudio.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include "Core/Module.hpp" +#include "Math/Types.hpp" + +namespace Audio { + + using SoundId = std::uint32_t; + using MusicId = std::uint32_t; + + constexpr SoundId INVALID_SOUND_ID = 0; + constexpr MusicId INVALID_MUSIC_ID = 0; + + struct AudioConfig { + std::uint32_t sampleRate = 44100; + std::uint32_t channelCount = 2; + float masterVolume = 1.0f; + std::uint32_t streamingBufferSize = 4096; + }; + + struct PlaybackOptions { + float volume = 1.0f; + float pitch = 1.0f; + float pan = 0.0f; + bool loop = false; + }; + + struct ListenerProperties { + Math::Vector2 position{0.0f, 0.0f}; + Math::Vector2 forward{0.0f, -1.0f}; + Math::Vector2 velocity{0.0f, 0.0f}; + }; + + class IAudio : public RType::Core::IModule { + public: + ~IAudio() override = default; + + const char* GetName() const override = 0; + RType::Core::ModulePriority GetPriority() const override = 0; + bool Initialize(RType::Core::Engine* engine) override = 0; + void Shutdown() override = 0; + void Update(float deltaTime) override = 0; + + virtual bool ConfigureDevice(const AudioConfig& config) = 0; + + virtual SoundId LoadSound(const std::string& path) = 0; + virtual void UnloadSound(SoundId soundId) = 0; + + virtual MusicId LoadMusic(const std::string& path) = 0; + virtual void UnloadMusic(MusicId musicId) = 0; + + virtual void PlaySound(SoundId soundId, + const PlaybackOptions& options = PlaybackOptions{}) = 0; + virtual void StopSound(SoundId soundId) = 0; + + virtual void PlayMusic(MusicId musicId, + const PlaybackOptions& options = PlaybackOptions{}) = 0; + virtual void StopMusic(MusicId musicId) = 0; + + virtual void PauseAll() = 0; + virtual void ResumeAll() = 0; + virtual void StopAll() = 0; + + virtual void SetMasterVolume(float volume) = 0; + virtual float GetMasterVolume() const = 0; + + virtual void SetListener(const ListenerProperties& listener) = 0; + }; + +} diff --git a/engine/include/Audio/SFMLAudio.hpp b/engine/include/Audio/SFMLAudio.hpp new file mode 100644 index 0000000..bc77b1f --- /dev/null +++ b/engine/include/Audio/SFMLAudio.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include "Audio/IAudio.hpp" + +#include +#include +#include +#include + +namespace Audio { + + class SFMLAudio : public IAudio { + public: + SFMLAudio() = default; + ~SFMLAudio() override = default; + + const char* GetName() const override { return "SFMLAudio"; } + RType::Core::ModulePriority GetPriority() const override { return RType::Core::ModulePriority::Normal; } + bool Initialize(RType::Core::Engine* /*engine*/) override { return true; } + void Shutdown() override; + void Update(float deltaTime) override; + + bool ConfigureDevice(const AudioConfig& config) override; + + SoundId LoadSound(const std::string& path) override; + void UnloadSound(SoundId soundId) override; + + MusicId LoadMusic(const std::string& path) override; + void UnloadMusic(MusicId musicId) override; + + void PlaySound(SoundId soundId, const PlaybackOptions& options = PlaybackOptions{}) override; + void StopSound(SoundId soundId) override; + + void PlayMusic(MusicId musicId, const PlaybackOptions& options = PlaybackOptions{}) override; + void StopMusic(MusicId musicId) override; + + void PauseAll() override; + void ResumeAll() override; + void StopAll() override; + + void SetMasterVolume(float volume) override; + float GetMasterVolume() const override; + + void SetListener(const ListenerProperties& listener) override; + + private: + void CleanupStoppedSounds(); + void UpdateAllVolumes(); + + float m_masterVolume = 1.0f; + SoundId m_nextSoundId = 1; + MusicId m_nextMusicId = 1; + + std::unordered_map m_soundBuffers; + std::list m_activeSounds; + std::unordered_map m_soundOriginalVolumes; + + std::unordered_map> m_music; + std::unordered_map m_musicOriginalVolumes; + }; + +} + diff --git a/engine/include/Core/ColorFilter.hpp b/engine/include/Core/ColorFilter.hpp new file mode 100644 index 0000000..e04b6c3 --- /dev/null +++ b/engine/include/Core/ColorFilter.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "Math/Types.hpp" + +namespace RType { + namespace Core { + + class ColorFilter { + public: + static bool IsColourBlindModeEnabled(); + static void SetColourBlindMode(bool enabled); + static Math::Color ApplyColourBlindFilter(const Math::Color& color); + + private: + static bool s_colourBlindMode; + }; + + } +} + diff --git a/engine/include/Core/Engine.hpp b/engine/include/Core/Engine.hpp new file mode 100644 index 0000000..e01682f --- /dev/null +++ b/engine/include/Core/Engine.hpp @@ -0,0 +1,117 @@ +#pragma once + +#include "Module.hpp" +#include "ModuleLoader.hpp" +#include "Logger.hpp" +#include "../ECS/Registry.hpp" +#include "../ECS/ISystem.hpp" +#include +#include +#include +#include +#include + +namespace RType { + + namespace Core { + + struct EngineConfig { + std::string pluginPath = "./plugins"; + }; + + class Engine { + public: + explicit Engine(const EngineConfig& config = EngineConfig{}); + ~Engine(); + + Engine(const Engine&) = delete; + Engine& operator=(const Engine&) = delete; + + bool Initialize(); + void Shutdown(); + + IModule* LoadPlugin(const std::string& pluginPath); + bool UnloadPlugin(const std::string& pluginName); + + template + void RegisterModule(std::unique_ptr module); + + template + T* GetModule(); + + IModule* GetModuleByName(const std::string& name); + std::vector GetAllModules() const; + + ECS::Registry& GetRegistry() { return m_registry; } + const ECS::Registry& GetRegistry() const { return m_registry; } + + template + void RegisterSystem(std::unique_ptr system); + void UpdateSystems(float deltaTime); + private: + void SortModulesByPriority(); + bool InitializeModules(); + void ShutdownModules(); + void InitializeSystems(); + void ShutdownSystems(); + + EngineConfig m_config; + ModuleLoader m_moduleLoader; + std::unordered_map> m_builtinModules; + std::vector m_sortedModules; + ECS::Registry m_registry; + std::vector> m_systems; + bool m_initialized{false}; + }; + + template + void Engine::RegisterModule(std::unique_ptr module) { + static_assert(std::is_base_of::value, "T must derive from IModule"); + + if (!module) { + Logger::Error("Cannot register null module"); + return; + } + + std::type_index typeId = std::type_index(typeid(T)); + Logger::Info("Registering module '{}'", module->GetName()); + m_builtinModules[typeId] = std::move(module); + } + + template + T* Engine::GetModule() { + static_assert(std::is_base_of::value, "T must derive from IModule"); + + std::type_index typeId = std::type_index(typeid(T)); + + auto it = m_builtinModules.find(typeId); + if (it != m_builtinModules.end()) { + return static_cast(it->second.get()); + } + + for (IModule* module : m_moduleLoader.GetAllPlugins()) { + T* casted = dynamic_cast(module); + if (casted) { + return casted; + } + } + + return nullptr; + } + + template + void Engine::RegisterSystem(std::unique_ptr system) { + static_assert(std::is_base_of::value, "T must derive from ISystem"); + + if (!system) { + Logger::Error("Cannot register null system"); + return; + } + + Logger::Info("Registering system '{}'", system->GetName()); + m_systems.push_back(std::move(system)); + } + + } + +} diff --git a/engine/include/Core/InputMapping.hpp b/engine/include/Core/InputMapping.hpp new file mode 100644 index 0000000..6f32994 --- /dev/null +++ b/engine/include/Core/InputMapping.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "../Renderer/IRenderer.hpp" +#include +#include + +namespace RType { + namespace Core { + + class InputMapping { + public: + static Renderer::Key GetKey(const std::string& action); + static void SetKey(const std::string& action, Renderer::Key key); + static void LoadDefaults(); + + private: + static std::unordered_map s_mappings; + }; + + } +} + diff --git a/engine/include/Core/Logger.hpp b/engine/include/Core/Logger.hpp new file mode 100644 index 0000000..1c1f8ea --- /dev/null +++ b/engine/include/Core/Logger.hpp @@ -0,0 +1,111 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace RType { + + namespace Core { + + enum class LogLevel { Debug = 0, + Info = 1, + Warning = 2, + Error = 3, + Critical = 4 }; + + class Logger { + public: + static void SetLogLevel(LogLevel level) { s_logLevel = level; } + + template + static void Debug(const std::string& format, Args&&... args) { + Log(LogLevel::Debug, format, std::forward(args)...); + } + + template + static void Info(const std::string& format, Args&&... args) { + Log(LogLevel::Info, format, std::forward(args)...); + } + + template + static void Warning(const std::string& format, Args&&... args) { + Log(LogLevel::Warning, format, std::forward(args)...); + } + + template + static void Error(const std::string& format, Args&&... args) { + Log(LogLevel::Error, format, std::forward(args)...); + } + + template + static void Critical(const std::string& format, Args&&... args) { + Log(LogLevel::Critical, format, std::forward(args)...); + } + private: + static inline LogLevel s_logLevel = LogLevel::Debug; + + template + static void Log(LogLevel level, const std::string& format, Args&&... args) { + if (level < s_logLevel) { + return; + } + + std::string message = FormatString(format, std::forward(args)...); + std::string levelStr = GetLevelString(level); + std::string timestamp = GetTimestamp(); + + std::ostream& stream = (level >= LogLevel::Error) ? std::cerr : std::cout; + stream << "[" << timestamp << "] [" << levelStr << "] " << message << std::endl; + } + + static std::string GetLevelString(LogLevel level) { + switch (level) { + case LogLevel::Debug: + return "DEBUG"; + case LogLevel::Info: + return "INFO"; + case LogLevel::Warning: + return "WARN"; + case LogLevel::Error: + return "ERROR"; + case LogLevel::Critical: + return "CRITICAL"; + default: + return "UNKNOWN"; + } + } + + static std::string GetTimestamp() { + auto now = std::time(nullptr); + auto tm = *std::localtime(&now); + std::ostringstream oss; + oss << std::put_time(&tm, "%H:%M:%S"); + return oss.str(); + } + + template + static std::string FormatArg(const T& arg) { + std::ostringstream oss; + oss << arg; + return oss.str(); + } + + static std::string FormatString(const std::string& format) { return format; } + + template + static std::string FormatString(const std::string& format, T&& first, Args&&... rest) { + std::string result = format; + size_t pos = result.find("{}"); + if (pos != std::string::npos) { + result.replace(pos, 2, FormatArg(first)); + } + return FormatString(result, std::forward(rest)...); + } + }; + + } + +} diff --git a/engine/include/Core/Module.hpp b/engine/include/Core/Module.hpp new file mode 100644 index 0000000..4a7b733 --- /dev/null +++ b/engine/include/Core/Module.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include + +namespace RType { + + namespace Core { + + class Engine; + + enum class ModulePriority { Critical = 0, + High = 1, + Normal = 2, + Low = 3 }; + + class IModule { + public: + virtual ~IModule() = default; + virtual const char* GetName() const = 0; + virtual ModulePriority GetPriority() const = 0; + virtual bool Initialize(Engine* engine) = 0; + virtual void Shutdown() = 0; + virtual void Update(float deltaTime) = 0; + virtual bool ShouldUpdateInRenderThread() const { return false; } + virtual bool IsOverridable() const { return true; } + }; + + using CreateModuleFunc = IModule* (*)(); + using DestroyModuleFunc = void (*)(IModule*); + + } + +} + +#ifdef _WIN32 +#define RTYPE_MODULE_EXPORT __declspec(dllexport) +#else +#define RTYPE_MODULE_EXPORT __attribute__((visibility("default"))) +#endif diff --git a/engine/include/Core/ModuleLoader.hpp b/engine/include/Core/ModuleLoader.hpp new file mode 100644 index 0000000..f8bd232 --- /dev/null +++ b/engine/include/Core/ModuleLoader.hpp @@ -0,0 +1,62 @@ +#pragma once + +#define RTYPE_INCLUDE_WINDOWS_H +#include "Platform.hpp" +#include "Module.hpp" +#include "Logger.hpp" +#include +#include +#include +#include +#include + +#ifdef _WIN32 +using LibraryHandle = HMODULE; +#else +#include +using LibraryHandle = void*; +#endif + +namespace RType { + + namespace Core { + + struct PluginInfo { + std::string name; + std::string path; + LibraryHandle handle; + IModule* module; + DestroyModuleFunc destroyFunc; + }; + + class ModuleLoader { + public: + ModuleLoader() = default; + ~ModuleLoader(); + + ModuleLoader(const ModuleLoader&) = delete; + ModuleLoader& operator=(const ModuleLoader&) = delete; + + ModuleLoader(ModuleLoader&&) = default; + ModuleLoader& operator=(ModuleLoader&&) = default; + + IModule* LoadPlugin(const std::string& pluginPath); + bool UnloadPlugin(const std::string& pluginName); + void UnloadAllPlugins(); + IModule* GetPlugin(const std::string& pluginName); + std::vector GetAllPlugins() const; + bool IsPluginLoaded(const std::string& pluginName) const; + size_t GetPluginCount() const { return m_loadedPlugins.size(); } + private: + LibraryHandle LoadLibraryFromPath(const std::string& path); + void FreeLibraryHandle(LibraryHandle handle); + void* GetFunction(LibraryHandle handle, const std::string& name); + std::string GetLastErrorMessage(); + std::string ExtractPluginName(const std::string& path); + + std::unordered_map m_loadedPlugins; + }; + + } + +} diff --git a/engine/include/Core/Platform.hpp b/engine/include/Core/Platform.hpp new file mode 100644 index 0000000..ff4c073 --- /dev/null +++ b/engine/include/Core/Platform.hpp @@ -0,0 +1,95 @@ +#pragma once +#ifdef _WIN32 + + +#ifndef NOMINMAX +#define NOMINMAX +#endif + + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif + + +#ifdef RTYPE_INCLUDE_WINDOWS_H +#include + + +#ifdef CreateWindow +#undef CreateWindow +#endif + +#ifdef DrawText +#undef DrawText +#endif + +#ifdef PlaySound +#undef PlaySound +#endif + +#ifdef LoadLibrary +#undef LoadLibrary +#endif + +#ifdef FreeLibrary +#undef FreeLibrary +#endif + +#ifdef GetMessage +#undef GetMessage +#endif + +#ifdef SendMessage +#undef SendMessage +#endif + +#ifdef PostMessage +#undef PostMessage +#endif + +#ifdef GetObject +#undef GetObject +#endif + +#ifdef RGB +#undef RGB +#endif + +#ifdef TRANSPARENT +#undef TRANSPARENT +#endif + +#ifdef max +#undef max +#endif + +#ifdef min +#undef min +#endif + +#endif // RTYPE_INCLUDE_WINDOWS_H + +#endif // _WIN32 + +#include + +namespace RType { + namespace Core { + namespace Platform { +#ifdef _WIN32 + constexpr const char* PLUGIN_EXTENSION = ".dll"; +#elif defined(__APPLE__) + constexpr const char* PLUGIN_EXTENSION = ".dylib"; +#else + constexpr const char* PLUGIN_EXTENSION = ".so"; +#endif + inline std::string GetPluginPath(const std::string& pluginName) { + return pluginName + PLUGIN_EXTENSION; + } + inline std::string GetPluginPathFromBin(const std::string& pluginName) { + return "../lib/" + pluginName + PLUGIN_EXTENSION; + } + } + } +} diff --git a/engine/include/ECS/AnimationSystem.hpp b/engine/include/ECS/AnimationSystem.hpp new file mode 100644 index 0000000..f5129e6 --- /dev/null +++ b/engine/include/ECS/AnimationSystem.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include "ISystem.hpp" +#include "Animation/IAnimation.hpp" +#include + +namespace RType { +namespace ECS { + + class AnimationSystem : public ISystem { + public: + explicit AnimationSystem(Animation::IAnimation* animation); + ~AnimationSystem() override = default; + + const char* GetName() const override { return "AnimationSystem"; } + void Update(Registry& registry, float deltaTime) override; + + void SetAnimationBackend(Animation::IAnimation* animation) { m_animation = animation; } + Animation::IAnimation* GetAnimationBackend() const { return m_animation; } + + private: + void UpdateSpriteAnimations(Registry& registry, float deltaTime); + void UpdateVisualEffects(Registry& registry, float deltaTime); + void UpdateFloatingTexts(Registry& registry, float deltaTime); + void UpdatePowerUpGlow(Registry& registry, float deltaTime); + void CleanupCompletedAnimations(Registry& registry); + + Animation::IAnimation* m_animation; + std::vector m_entitiesToDestroy; + }; + +} +} diff --git a/engine/include/ECS/AudioSystem.hpp b/engine/include/ECS/AudioSystem.hpp new file mode 100644 index 0000000..aa080ad --- /dev/null +++ b/engine/include/ECS/AudioSystem.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "ISystem.hpp" +#include "../Audio/IAudio.hpp" + +namespace RType { + namespace ECS { + + class AudioSystem : public ISystem { + public: + explicit AudioSystem(Audio::IAudio* audio) + : m_audio(audio) {} + + ~AudioSystem() override = default; + + const char* GetName() const override { return "AudioSystem"; } + + void Update(Registry& registry, float deltaTime) override; + + void SetAudioBackend(Audio::IAudio* audio) { m_audio = audio; } + + private: + Audio::IAudio* m_audio; + }; + + } +} + diff --git a/engine/include/ECS/BlackOrbSystem.hpp b/engine/include/ECS/BlackOrbSystem.hpp new file mode 100644 index 0000000..c9f71c1 --- /dev/null +++ b/engine/include/ECS/BlackOrbSystem.hpp @@ -0,0 +1,28 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** BlackOrbSystem - Handles black orb attraction and proximity damage +*/ + +#pragma once + +#include "ISystem.hpp" +#include "Registry.hpp" +#include + +namespace RType { + namespace ECS { + + class BlackOrbSystem : public ISystem { + public: + BlackOrbSystem() = default; + ~BlackOrbSystem() override = default; + + const char* GetName() const override { return "BlackOrbSystem"; } + + void Update(Registry& registry, float deltaTime) override; + }; + + } +} diff --git a/engine/include/ECS/BossAttackSystem.hpp b/engine/include/ECS/BossAttackSystem.hpp new file mode 100644 index 0000000..8cef9ee --- /dev/null +++ b/engine/include/ECS/BossAttackSystem.hpp @@ -0,0 +1,39 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** BossAttackSystem - Handles boss attack patterns +*/ + +#pragma once + +#include "ISystem.hpp" +#include "Registry.hpp" +#include + +namespace RType { + namespace ECS { + + class BossAttackSystem : public ISystem { + public: + BossAttackSystem() = default; + ~BossAttackSystem() override = default; + + const char* GetName() const override { return "BossAttackSystem"; } + + void Update(Registry& registry, float deltaTime) override; + + private: + void CreateFanSpray(Registry& registry, Entity bossEntity, float bossX, float bossY); + void CreateBossBullet(Registry& registry, float x, float y, float angle, float speed); + void CreateBlackOrb(Registry& registry, Entity bossEntity, float bossX, float bossY); + void CreateThirdBullet(Registry& registry, Entity bossEntity, float bossX, float bossY); + + void CreateAnimatedOrb(Registry& registry, Entity bossEntity, float bossX, float bossY); + void CreateSecondAttackSpray(Registry& registry, Entity bossEntity, float bossX, float bossY); + void CreateContinuousFire(Registry& registry, Entity bossEntity, float bossX, float bossY); + void CreateMine(Registry& registry, Entity bossEntity, float bossX, float bossY); + }; + + } +} diff --git a/engine/include/ECS/BossSystem.hpp b/engine/include/ECS/BossSystem.hpp new file mode 100644 index 0000000..ba492a7 --- /dev/null +++ b/engine/include/ECS/BossSystem.hpp @@ -0,0 +1,27 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** BossSystem - Handles boss behavior +*/ + +#pragma once + +#include "ISystem.hpp" +#include "Registry.hpp" + +namespace RType { + namespace ECS { + + class BossSystem : public ISystem { + public: + BossSystem() = default; + ~BossSystem() override = default; + + const char* GetName() const override { return "BossSystem"; } + + void Update(Registry& registry, float deltaTime) override; + }; + + } +} diff --git a/engine/include/ECS/BulletCollisionResponseSystem.hpp b/engine/include/ECS/BulletCollisionResponseSystem.hpp new file mode 100644 index 0000000..0a09137 --- /dev/null +++ b/engine/include/ECS/BulletCollisionResponseSystem.hpp @@ -0,0 +1,36 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** BulletCollisionResponseSystem - Handles bullet collision responses +*/ + +#pragma once + +#include "ISystem.hpp" +#include "Registry.hpp" +#include "Component.hpp" + +namespace RType { + namespace ECS { + + class EffectFactory; + + class BulletCollisionResponseSystem : public ISystem { + public: + BulletCollisionResponseSystem() = default; + explicit BulletCollisionResponseSystem(EffectFactory* effectFactory) + : m_effectFactory(effectFactory) {} + ~BulletCollisionResponseSystem() override = default; + + const char* GetName() const override { return "BulletCollisionResponseSystem"; } + void Update(Registry& registry, float deltaTime) override; + + void SetEffectFactory(EffectFactory* effectFactory) { m_effectFactory = effectFactory; } + + private: + EffectFactory* m_effectFactory = nullptr; + }; + + } +} diff --git a/engine/include/ECS/CollisionDetectionSystem.hpp b/engine/include/ECS/CollisionDetectionSystem.hpp new file mode 100644 index 0000000..0919416 --- /dev/null +++ b/engine/include/ECS/CollisionDetectionSystem.hpp @@ -0,0 +1,42 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** CollisionDetectionSystem - Unified collision detection system +*/ + +#pragma once + +#include "ISystem.hpp" +#include "Registry.hpp" +#include "Component.hpp" + +namespace RType { + namespace ECS { + + class CollisionDetectionSystem : public ISystem { + public: + CollisionDetectionSystem() = default; + ~CollisionDetectionSystem() override = default; + + const char* GetName() const override { return "CollisionDetectionSystem"; } + void Update(Registry& registry, float deltaTime) override; + + static bool CheckCollision(Registry& registry, Entity a, Entity b); + private: + void ClearCollisionEvents(Registry& registry); + std::vector GetCollidableEntities(Registry& registry); + bool ShouldCollide(Registry& registry, Entity a, Entity b); + + static bool CheckCircleCircle(float x1, float y1, float r1, + float x2, float y2, float r2); + + static bool CheckAABB(float x1, float y1, float w1, float h1, + float x2, float y2, float w2, float h2); + + static bool CheckCircleAABB(float cx, float cy, float radius, + float bx, float by, float bw, float bh); + }; + + } +} diff --git a/engine/include/ECS/Component.hpp b/engine/include/ECS/Component.hpp new file mode 100644 index 0000000..7920583 --- /dev/null +++ b/engine/include/ECS/Component.hpp @@ -0,0 +1,644 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "Renderer/IRenderer.hpp" +#include "Math/Types.hpp" +#include "Entity.hpp" +#include "Audio/IAudio.hpp" +#include "Animation/AnimationTypes.hpp" + +namespace RType { + + namespace ECS { + using ComponentID = std::type_index; + + struct IComponent { + virtual ~IComponent() = default; + }; + + struct Position : public IComponent { + float x = 0.0f; + float y = 0.0f; + + Position() = default; + Position(float x, float y) + : x(x), y(y) {} + }; + + struct Velocity : public IComponent { + float dx = 0.0f; + float dy = 0.0f; + + Velocity() = default; + Velocity(float dx, float dy) + : dx(dx), dy(dy) {} + }; + + struct Drawable : public IComponent { + Renderer::SpriteId spriteId = Renderer::INVALID_SPRITE_ID; + Math::Vector2 scale{1.0f, 1.0f}; + float rotation = 0.0f; + Math::Vector2 origin{0.0f, 0.0f}; + Math::Color tint{1.0f, 1.0f, 1.0f, 1.0f}; + int layer = 0; + + Drawable() = default; + Drawable(Renderer::SpriteId sprite, int renderLayer = 0) + : spriteId(sprite), layer(renderLayer) {} + }; + + struct NetworkPlayer : public IComponent { + uint8_t playerNumber = 0; + uint64_t playerHash = 0; + char name[32] = {}; + bool ready = false; + + NetworkPlayer() = default; + NetworkPlayer(uint8_t num, uint64_t hash, const char* playerName, bool isReady = false) + : playerNumber(num), playerHash(hash), ready(isReady) { + if (playerName) { + std::strncpy(name, playerName, 31); + name[31] = '\0'; + } + } + }; + + struct BoxCollider : public IComponent { + float width = 0.0f; + float height = 0.0f; + + BoxCollider() = default; + BoxCollider(float width, float height) + : width(width), height(height) {} + }; + + struct Controllable : public IComponent { + float speed = 200.0f; + + Controllable() = default; + Controllable(float moveSpeed) + : speed(moveSpeed) {} + }; + + struct Player : public IComponent { + uint8_t playerNumber = 0; + uint64_t playerHash = 0; + bool isLocalPlayer = false; + uint8_t lives = 3; + + Player() = default; + Player(uint8_t number, uint64_t hash, bool local = false, uint8_t startLives = 3) + : playerNumber(number), playerHash(hash), isLocalPlayer(local), lives(startLives) {} + }; + + enum class EnemyType : uint8_t { + BASIC = 0, + FAST = 1, + TANK = 2, + BOSS = 3, + FORMATION = 4 + }; + + struct Enemy : public IComponent { + EnemyType type = EnemyType::BASIC; + uint32_t id = 0; + + Enemy() = default; + Enemy(EnemyType enemyType, uint32_t enemyId = 0) + : type(enemyType), id(enemyId) {} + }; + + struct Boss : public IComponent { + uint8_t bossId = 1; + + Boss() = default; + Boss(uint8_t id) : bossId(id) {} + }; + + struct BossKilled : public IComponent { + Entity bossEntity; + int levelNumber; + float timeSinceDeath = 0.0f; + + BossKilled() = default; + BossKilled(Entity boss, int level) + : bossEntity(boss), levelNumber(level) {} + }; + + enum class BossAttackPattern { + IDLE = 0, + // Boss 1 + FAN_SPRAY = 1, + DIRECT_SHOT = 2, + CIRCLE = 3, + BLACK_ORB = 4, + THIRD_BULLET = 5, + // Boss 2 + SPIRAL_WAVE = 6, + ANIMATED_ORB = 7, + LASER_BEAM = 8 + }; + + struct BossAttack : public IComponent { + float attackCooldown = 3.0f; + float timeSinceLastAttack = 0.0f; + BossAttackPattern currentPattern = BossAttackPattern::FAN_SPRAY; + + BossAttack() = default; + BossAttack(float cooldown) : attackCooldown(cooldown) {} + }; + + struct BossBullet : public IComponent { + BossBullet() = default; + }; + + struct WaveAttack : public IComponent { + WaveAttack() = default; + }; + + struct SecondAttack : public IComponent { + SecondAttack() = default; + }; + + struct FireBullet : public IComponent { + FireBullet() = default; + }; + + struct Mine : public IComponent { + float proximityRadius = 80.0f; + float explosionRadius = 100.0f; + float lifeTime = 10.0f; + float timer = 0.0f; + bool isExploding = false; + float explosionTimer = 0.0f; + + Mine() = default; + Mine(float proximity, float explosion, float life) + : proximityRadius(proximity), explosionRadius(explosion), lifeTime(life) {} + }; + + struct BossMovementPattern : public IComponent { + float timer = 0.0f; + float amplitudeY = 200.0f; + float amplitudeX = 80.0f; + float frequencyY = 0.5f; + float frequencyX = 0.3f; + float centerY = 0.0f; + float centerX = 0.0f; + + BossMovementPattern() = default; + BossMovementPattern(float ampY, float ampX, float freqY, float freqX, float centerYPos, float centerXPos) + : amplitudeY(ampY), amplitudeX(ampX), frequencyY(freqY), frequencyX(freqX), centerY(centerYPos), centerX(centerXPos) {} + }; + + struct BlackOrb : public IComponent { + float attractionRadius = 200.0f; + float absorptionRadius = 30.0f; + float attractionForce = 500.0f; + bool isActive = true; + + BlackOrb() = default; + BlackOrb(float attraction, float absorption, float force) + : attractionRadius(attraction), absorptionRadius(absorption), attractionForce(force) {} + }; + + struct ProximityDamage : public IComponent { + float damageRadius = 120.0f; + float damageAmount = 1.0f; + float tickRate = 0.5f; + float timeSinceDamage = 0.0f; + + ProximityDamage() = default; + ProximityDamage(float radius, float damage, float rate) + : damageRadius(radius), damageAmount(damage), tickRate(rate) {} + }; + + struct DamageFlash : public IComponent { + float duration = 0.1f; + float timeRemaining = 0.0f; + bool isActive = false; + + DamageFlash() = default; + DamageFlash(float flashDuration) : duration(flashDuration) {} + + void Trigger() { + isActive = true; + timeRemaining = duration; + } + }; + + struct ThirdBullet : public IComponent { + float spawnInterval = 0.3f; + float timeSinceSpawn = 0.0f; + int damage = 50; + bool isActive = true; + + ThirdBullet() = default; + ThirdBullet(float interval, int dmg) + : spawnInterval(interval), damage(dmg) {} + }; + + struct Health : public IComponent { + int current = 100; + int max = 100; + + Health() = default; + Health(int maxHealth) + : current(maxHealth), max(maxHealth) {} + Health(int currentHealth, int maxHealth) + : current(currentHealth), max(maxHealth) {} + }; + + struct ScoreValue : public IComponent { + uint32_t points = 100; + + ScoreValue() = default; + ScoreValue(uint32_t scorePoints) + : points(scorePoints) {} + }; + + struct ScoreTimer : public IComponent { + float elapsed = 0.0f; + + ScoreTimer() = default; + ScoreTimer(float startElapsed) + : elapsed(startElapsed) {} + }; + + struct Damage : public IComponent { + int amount = 10; + + Damage() = default; + Damage(int damageAmount) + : amount(damageAmount) {} + }; + + struct EnemyKilled : public IComponent { + uint32_t enemyId = 0; + Entity killedBy = NULL_ENTITY; + + EnemyKilled() = default; + EnemyKilled(uint32_t id, Entity killer = NULL_ENTITY) + : enemyId(id), killedBy(killer) {} + }; + + struct Bullet : public IComponent { + Entity owner = NULL_ENTITY; + + Bullet() = default; + Bullet(Entity shooter) + : owner(shooter) {} + }; + + struct Shooter : public IComponent { + float fireRate = 0.2f; + float cooldown = 0.0f; + float offsetX = 50.0f; + float offsetY = 20.0f; + + Shooter() = default; + Shooter(float rate, float oX = 50.0f, float oY = 20.0f) : fireRate(rate), offsetX(oX), offsetY(oY) {} + }; + + struct ShootCommand : public IComponent { + bool wantsToShoot = false; + + ShootCommand() = default; + ShootCommand(bool shoot) : wantsToShoot(shoot) {} + }; + + struct Scrollable : public IComponent { + float speed = -100.0f; + + Scrollable() = default; + Scrollable(float scrollSpeed) : speed(scrollSpeed) {} + }; + + struct Obstacle : public IComponent { + bool blocking = true; + + Obstacle() = default; + Obstacle(bool isBlocking) : blocking(isBlocking) {} + }; + + struct ObstacleVisual : public IComponent { + }; + + // Stable network identifier for server->client entity mirroring. + // IMPORTANT: This must live on the entity as a component, because raw ECS entity IDs are recycled. + // If we instead map "ECS entity id -> network id" in a hash map, then destroying and reusing an + // ECS id in the same tick can cause a new entity to inherit the old network id (type confusion). + struct NetworkId : public IComponent { + uint32_t id = 0; + + NetworkId() = default; + explicit NetworkId(uint32_t networkId) : id(networkId) {} + }; + + struct ObstacleMetadata : public IComponent { + uint32_t uniqueId = 0; + Entity visualEntity = NULL_ENTITY; + float offsetX = 0.0f; + float offsetY = 0.0f; + + ObstacleMetadata() = default; + ObstacleMetadata(uint32_t id, + Entity visual = NULL_ENTITY, + float offsetX = 0.0f, + float offsetY = 0.0f) + : uniqueId(id), + visualEntity(visual), + offsetX(offsetX), + offsetY(offsetY) {} + }; + + struct Invincibility : public IComponent { + float remainingTime = 0.0f; + + Invincibility() = default; + Invincibility(float duration) : remainingTime(duration) {} + }; + + struct CollisionLayer : public IComponent { + uint16_t layer = 0; // What layer this entity is on + uint16_t mask = 0xFFFF; // Which layers this entity collides with + + CollisionLayer() = default; + CollisionLayer(uint16_t l, uint16_t m) : layer(l), mask(m) {} + }; + + // masks for collision layers + namespace CollisionLayers { + constexpr uint16_t NONE = 0; + constexpr uint16_t PLAYER = 1 << 0; // 0x0001 + constexpr uint16_t ENEMY = 1 << 1; // 0x0002 + constexpr uint16_t PLAYER_BULLET = 1 << 2; // 0x0004 + constexpr uint16_t ENEMY_BULLET = 1 << 3; // 0x0008 + constexpr uint16_t OBSTACLE = 1 << 4; // 0x0010 + constexpr uint16_t POWERUP = 1 << 5; // 0x0020 + constexpr uint16_t ALL = 0xFFFF; + } + + struct CircleCollider : public IComponent { + float radius = 0.0f; + + CircleCollider() = default; + CircleCollider(float r) : radius(r) {} + }; + + struct CollisionEvent : public IComponent { + Entity other = NULL_ENTITY; + + CollisionEvent() = default; + CollisionEvent(Entity e) : other(e) {} + }; + + // Powerup system components + enum class PowerUpType : uint8_t { + FIRE_RATE_BOOST = 0, + SPREAD_SHOT = 1, + LASER_BEAM = 2, + FORCE_POD = 3, + SPEED_BOOST = 4, + SHIELD = 5 + }; + + struct PowerUp : public IComponent { + PowerUpType type = PowerUpType::FIRE_RATE_BOOST; + uint32_t id = 0; + + PowerUp() = default; + PowerUp(PowerUpType powerupType, uint32_t powerupId = 0) + : type(powerupType), id(powerupId) {} + }; + + struct ActivePowerUps : public IComponent { + bool hasFireRateBoost = false; + bool hasSpreadShot = false; + bool hasLaserBeam = false; + bool hasShield = false; + float speedMultiplier = 1.0f; + + ActivePowerUps() = default; + }; + + enum class WeaponType : uint8_t { + STANDARD = 0, + SPREAD = 1, + LASER = 2 + }; + + struct WeaponSlot : public IComponent { + WeaponType type = WeaponType::STANDARD; + float fireRate = 0.2f; + float cooldown = 0.0f; + int damage = 25; + bool enabled = true; + + WeaponSlot() = default; + WeaponSlot(WeaponType weaponType, float rate, int dmg) + : type(weaponType), fireRate(rate), damage(dmg) {} + }; + + struct ForcePod : public IComponent { + Entity owner = NULL_ENTITY; + float offsetX = -60.0f; + float offsetY = 0.0f; + bool isAttached = true; + + ForcePod() = default; + ForcePod(Entity ownerEntity, float oX = -60.0f, float oY = 0.0f) + : owner(ownerEntity), offsetX(oX), offsetY(oY) {} + }; + + struct Shield : public IComponent { + float duration = 0.0f; // 0 = permanent (until death) + float timeRemaining = 0.0f; + + Shield() = default; + Shield(float dur = 0.0f) : duration(dur), timeRemaining(dur) {} + }; + + struct SoundEffect : public IComponent { + Audio::SoundId soundId = Audio::INVALID_SOUND_ID; + float volume = 1.0f; + float pitch = 1.0f; + float pan = 0.0f; + bool loop = false; + bool positional = false; + + SoundEffect() = default; + SoundEffect(Audio::SoundId id, float vol = 1.0f) + : soundId(id), volume(vol) {} + }; + + struct MusicEffect : public IComponent { + Audio::MusicId musicId = Audio::INVALID_MUSIC_ID; + bool play = true; + bool stop = false; + float volume = 1.0f; + float pitch = 1.0f; + bool loop = true; + + MusicEffect() = default; + explicit MusicEffect(Audio::MusicId id) + : musicId(id) {} + }; + + struct SpriteAnimation : public IComponent { + Animation::AnimationClipId clipId = Animation::INVALID_CLIP_ID; + float currentTime = 0.0f; + float playbackSpeed = 1.0f; + bool playing = true; + bool looping = false; + bool destroyOnComplete = false; + + std::size_t currentFrameIndex = 0; + Math::Rectangle currentRegion{}; + + SpriteAnimation() = default; + SpriteAnimation(Animation::AnimationClipId clip, bool loop = false, float speed = 1.0f) + : clipId(clip), playbackSpeed(speed), looping(loop) {} + }; + + struct AnimationStateMachine : public IComponent { + Animation::AnimationGraphId graphId = Animation::INVALID_GRAPH_ID; + Animation::AnimationStateId currentState = Animation::INVALID_STATE_ID; + Animation::AnimationStateId previousState = Animation::INVALID_STATE_ID; + float stateTime = 0.0f; + float blendFactor = 0.0f; + float blendDuration = 0.0f; + bool isTransitioning = false; + + static constexpr std::size_t MAX_PARAMS = 8; + std::array parameters{}; + std::array, MAX_PARAMS> parameterNames{}; + std::size_t parameterCount = 0; + + AnimationStateMachine() = default; + explicit AnimationStateMachine(Animation::AnimationGraphId graph) + : graphId(graph) {} + + void SetParameter(const char* name, float value) { + for (std::size_t i = 0; i < parameterCount; ++i) { + if (std::strncmp(parameterNames[i].data(), name, 31) == 0) { + parameters[i] = value; + return; + } + } + if (parameterCount < MAX_PARAMS) { + std::strncpy(parameterNames[parameterCount].data(), name, 31); + parameterNames[parameterCount][31] = '\0'; + parameters[parameterCount] = value; + parameterCount++; + } + } + + float GetParameter(const char* name) const { + for (std::size_t i = 0; i < parameterCount; ++i) { + if (std::strncmp(parameterNames[i].data(), name, 31) == 0) { + return parameters[i]; + } + } + return 0.0f; + } + }; + + struct AnimatedSprite : public IComponent { + bool needsUpdate = true; + + AnimatedSprite() = default; + }; + + struct VisualEffect : public IComponent { + Animation::EffectType type = Animation::EffectType::EXPLOSION_SMALL; + float lifetime = 0.0f; + float maxLifetime = 1.0f; + Entity owner = NULL_ENTITY; + float offsetX = 0.0f; + float offsetY = 0.0f; + + VisualEffect() = default; + VisualEffect(Animation::EffectType t, float duration) + : type(t), maxLifetime(duration) {} + VisualEffect(Animation::EffectType t, float duration, Entity ownerEntity, float offX, float offY) + : type(t), maxLifetime(duration), owner(ownerEntity), offsetX(offX), offsetY(offY) {} + }; + + struct FloatingText : public IComponent { + char text[32] = {}; + float lifetime = 0.0f; + float maxLifetime = 1.5f; + float velocityY = -50.0f; + float fadeStartTime = 0.5f; + Math::Color color{1.0f, 1.0f, 1.0f, 1.0f}; + + FloatingText() = default; + FloatingText(const char* txt, float duration, const Math::Color& col) + : maxLifetime(duration), color(col) { + if (txt) { + std::strncpy(text, txt, 31); + text[31] = '\0'; + } + } + }; + + struct AnimationEvents : public IComponent { + static constexpr std::size_t MAX_EVENTS = 4; + std::array, MAX_EVENTS> eventNames{}; + std::size_t eventCount = 0; + + AnimationEvents() = default; + + void PushEvent(const char* name) { + if (eventCount < MAX_EVENTS && name) { + std::strncpy(eventNames[eventCount].data(), name, 31); + eventNames[eventCount][31] = '\0'; + eventCount++; + } + } + + void Clear() { eventCount = 0; } + + bool HasEvent(const char* name) const { + for (std::size_t i = 0; i < eventCount; ++i) { + if (std::strncmp(eventNames[i].data(), name, 31) == 0) { + return true; + } + } + return false; + } + }; + + struct AnimationLayer : public IComponent { + Animation::AnimationClipId clipId = Animation::INVALID_CLIP_ID; + float currentTime = 0.0f; + float weight = 1.0f; + float playbackSpeed = 1.0f; + bool additive = false; + int layerIndex = 0; + + AnimationLayer() = default; + AnimationLayer(Animation::AnimationClipId clip, int layer, float w = 1.0f) + : clipId(clip), weight(w), layerIndex(layer) {} + }; + + struct PowerUpGlow : public IComponent { + float time = 0.0f; + float pulseSpeed = 2.0f; + float minAlpha = 0.7f; + float maxAlpha = 1.0f; + float baseScale = 2.5f; + float scalePulse = 0.08f; + + PowerUpGlow() = default; + }; + } + +} diff --git a/engine/include/ECS/Components/Clickable.hpp b/engine/include/ECS/Components/Clickable.hpp new file mode 100644 index 0000000..1556bc3 --- /dev/null +++ b/engine/include/ECS/Components/Clickable.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "ECS/Component.hpp" +#include + +namespace RType { + namespace ECS { + + enum class ButtonState { + Idle, + Hover, + Active + }; + + struct Clickable : public IComponent { + float width = 0.0f; + float height = 0.0f; + + ButtonState state = ButtonState::Idle; + + int actionId = 0; + + Clickable() = default; + Clickable(float w, float h, int action) + : width(w), height(h), actionId(action) {} + }; + + } +} diff --git a/engine/include/ECS/Components/TextLabel.hpp b/engine/include/ECS/Components/TextLabel.hpp new file mode 100644 index 0000000..2f6af54 --- /dev/null +++ b/engine/include/ECS/Components/TextLabel.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "ECS/Component.hpp" +#include "Math/Types.hpp" +#include + +namespace RType { + namespace ECS { + + struct TextLabel : public IComponent { + std::string text; + Renderer::FontId fontId = 0; + unsigned int characterSize = 24; + Math::Color color{1.0f, 1.0f, 1.0f, 1.0f}; + + float offsetX = 0.0f; + float offsetY = 0.0f; + bool centered = false; + + TextLabel() = default; + TextLabel(const std::string& t, Renderer::FontId font, unsigned int size = 24) + : text(t), fontId(font), characterSize(size) {} + }; + } +} diff --git a/engine/include/ECS/EffectFactory.hpp b/engine/include/ECS/EffectFactory.hpp new file mode 100644 index 0000000..e4a3b7d --- /dev/null +++ b/engine/include/ECS/EffectFactory.hpp @@ -0,0 +1,115 @@ +#pragma once + +#include "Entity.hpp" +#include "Animation/AnimationTypes.hpp" +#include "Renderer/IRenderer.hpp" +#include "Math/Types.hpp" + +namespace RType { +namespace ECS { + + class Registry; + + struct EffectConfig { + Animation::AnimationClipId explosionSmall = Animation::INVALID_CLIP_ID; + Animation::AnimationClipId explosionLarge = Animation::INVALID_CLIP_ID; + Animation::AnimationClipId bulletImpact = Animation::INVALID_CLIP_ID; + Animation::AnimationClipId powerUpCollect = Animation::INVALID_CLIP_ID; + Animation::AnimationClipId bossPhaseTransition = Animation::INVALID_CLIP_ID; + Animation::AnimationClipId spawnEffect = Animation::INVALID_CLIP_ID; + Animation::AnimationClipId deathEffect = Animation::INVALID_CLIP_ID; + Animation::AnimationClipId shootingAnimation = Animation::INVALID_CLIP_ID; + Animation::AnimationClipId forcePodAnimation = Animation::INVALID_CLIP_ID; + Animation::AnimationClipId beamAnimation = Animation::INVALID_CLIP_ID; + Animation::AnimationClipId hitAnimation = Animation::INVALID_CLIP_ID; + + Renderer::FontId damageFont = Renderer::INVALID_FONT_ID; + Renderer::TextureId effectsTexture = Renderer::INVALID_TEXTURE_ID; + Renderer::SpriteId effectsSprite = Renderer::INVALID_SPRITE_ID; + Renderer::TextureId shootingTexture = Renderer::INVALID_TEXTURE_ID; + Renderer::SpriteId shootingSprite = Renderer::INVALID_SPRITE_ID; + Renderer::TextureId forcePodTexture = Renderer::INVALID_TEXTURE_ID; + Renderer::SpriteId forcePodSprite = Renderer::INVALID_SPRITE_ID; + Renderer::TextureId beamTexture = Renderer::INVALID_TEXTURE_ID; + Renderer::SpriteId beamSprite = Renderer::INVALID_SPRITE_ID; + Renderer::TextureId hitTexture = Renderer::INVALID_TEXTURE_ID; + Renderer::SpriteId hitSprite = Renderer::INVALID_SPRITE_ID; + Math::Rectangle forcePodFirstFrameRegion{}; + Math::Rectangle shootingFirstFrameRegion{}; + Math::Rectangle explosionFirstFrameRegion{}; + Math::Rectangle beamFirstFrameRegion{}; + Math::Rectangle hitFirstFrameRegion{}; + + EffectConfig() = default; + }; + + class EffectFactory { + public: + explicit EffectFactory(const EffectConfig& config); + ~EffectFactory() = default; + + void SetConfig(const EffectConfig& config) { m_config = config; } + const EffectConfig& GetConfig() const { return m_config; } + + Entity CreateExplosionSmall(Registry& registry, float x, float y); + + Entity CreateExplosionLarge(Registry& registry, float x, float y); + + Entity CreateExplosion(Registry& registry, + float x, float y, + Animation::AnimationClipId clipId, + float scale = 1.0f, + int layer = 100); + + Entity CreateBulletImpact(Registry& registry, float x, float y); + + Entity CreateDamageNumber(Registry& registry, + float x, float y, + int damage, + const Math::Color& color = {1.0f, 0.3f, 0.3f, 1.0f}); + + Entity CreateScorePopup(Registry& registry, + float x, float y, + int score, + const Math::Color& color = {1.0f, 1.0f, 0.0f, 1.0f}); + + Entity CreateFloatingText(Registry& registry, + float x, float y, + const char* text, + const Math::Color& color, + float duration = 1.5f); + + Entity CreatePowerUpEffect(Registry& registry, float x, float y); + + Entity CreateBossTransitionEffect(Registry& registry, float x, float y); + + Entity CreateSpawnEffect(Registry& registry, float x, float y); + + Entity CreateDeathEffect(Registry& registry, float x, float y); + + Entity CreateShootingEffect(Registry& registry, float x, float y, Entity owner = NULL_ENTITY); + + Entity CreateBeam(Registry& registry, float x, float y, Entity owner, float chargeTime, float screenWidth = 1280.0f, float beamHeight = 64.0f); + + Entity CreateHitEffect(Registry& registry, float x, float y); + + void CreateHitMarker(Registry& registry, Entity target, int damage); + + void CreateEnemyDeathEffect(Registry& registry, + float x, float y, + int scoreValue); + + private: + EffectConfig m_config; + + Entity CreateBaseEffect(Registry& registry, + float x, float y, + Animation::EffectType type, + float duration, + Entity owner = NULL_ENTITY, + float offsetX = 0.0f, + float offsetY = 0.0f); + }; + +} +} diff --git a/engine/include/ECS/EnemyFactory.hpp b/engine/include/ECS/EnemyFactory.hpp new file mode 100644 index 0000000..8410294 --- /dev/null +++ b/engine/include/ECS/EnemyFactory.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "Registry.hpp" +#include "Component.hpp" +#include "Renderer/IRenderer.hpp" +#include "Math/Types.hpp" +#include + +namespace RType { + + namespace ECS { + + class EnemyFactory { + public: + static Entity CreateEnemy(Registry& registry, EnemyType type, float startX, float startY, + Renderer::IRenderer* renderer); + + static float GetEnemySpeed(EnemyType type); + static int GetEnemyHealth(EnemyType type); + static int GetEnemyDamage(EnemyType type); + static uint32_t GetEnemyScore(EnemyType type); + }; + + } + +} diff --git a/engine/include/ECS/EnemySystem.hpp b/engine/include/ECS/EnemySystem.hpp new file mode 100644 index 0000000..f392589 --- /dev/null +++ b/engine/include/ECS/EnemySystem.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "ISystem.hpp" +#include "Renderer/IRenderer.hpp" +#include + +namespace RType { + + namespace ECS { + + class EnemySystem : public ISystem { + public: + explicit EnemySystem(Renderer::IRenderer* renderer = nullptr, float screenWidth = 1280.0f, + float screenHeight = 720.0f); + ~EnemySystem() override = default; + + void Update(Registry& registry, float deltaTime) override; + const char* GetName() const override { return "EnemySystem"; } + + void SpawnRandomEnemy(Registry& registry); + static void DestroyEnemiesOffScreen(Registry& registry, float screenWidth); + static void ApplyMovementPattern(Registry& registry, Entity enemy, float /* deltaTime */); + private: + Renderer::IRenderer* m_renderer; + float m_screenWidth; + float m_screenHeight; + float m_spawnTimer = 0.0f; + float m_spawnInterval = 3.0f; + std::mt19937 m_rng; + }; + + } + +} diff --git a/engine/include/ECS/Entity.hpp b/engine/include/ECS/Entity.hpp new file mode 100644 index 0000000..c40d5ae --- /dev/null +++ b/engine/include/ECS/Entity.hpp @@ -0,0 +1,11 @@ +#pragma once +#include + +namespace RType { + + namespace ECS { + using Entity = uint32_t; + constexpr Entity NULL_ENTITY = 0; + } + +} \ No newline at end of file diff --git a/engine/include/ECS/ForcePodSystem.hpp b/engine/include/ECS/ForcePodSystem.hpp new file mode 100644 index 0000000..c827392 --- /dev/null +++ b/engine/include/ECS/ForcePodSystem.hpp @@ -0,0 +1,25 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** ForcePodSystem +*/ + +#pragma once + +#include "ISystem.hpp" + +namespace RType { + namespace ECS { + + class ForcePodSystem : public ISystem { + public: + ForcePodSystem() = default; + ~ForcePodSystem() override = default; + + void Update(Registry& registry, float deltaTime) override; + const char* GetName() const override { return "ForcePodSystem"; } + }; + + } +} diff --git a/engine/include/ECS/HealthSystem.hpp b/engine/include/ECS/HealthSystem.hpp new file mode 100644 index 0000000..1944983 --- /dev/null +++ b/engine/include/ECS/HealthSystem.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "ISystem.hpp" + +namespace RType { + + namespace ECS { + + class HealthSystem : public ISystem { + public: + HealthSystem() = default; + ~HealthSystem() override = default; + + void Update(Registry& registry, float deltaTime) override; + const char* GetName() const override { return "HealthSystem"; } + private: + void checkAndDestroyDeadEntities(Registry& registry); + }; + + } + +} diff --git a/engine/include/ECS/ISystem.hpp b/engine/include/ECS/ISystem.hpp new file mode 100644 index 0000000..9b722dd --- /dev/null +++ b/engine/include/ECS/ISystem.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "Registry.hpp" +#include + +namespace RType { + + namespace ECS { + + class ISystem { + public: + virtual ~ISystem() = default; + + virtual void Update(Registry& registry, float deltaTime) = 0; + virtual const char* GetName() const = 0; + virtual bool Initialize(Registry& /* registry */) { return true; } + virtual void Shutdown() {} + }; + + } + +} diff --git a/engine/include/ECS/InputSystem.hpp b/engine/include/ECS/InputSystem.hpp new file mode 100644 index 0000000..28f46e4 --- /dev/null +++ b/engine/include/ECS/InputSystem.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "ISystem.hpp" +#include "Renderer/IRenderer.hpp" + +namespace RType { + namespace ECS { + + class InputSystem : public ISystem { + public: + InputSystem(Renderer::IRenderer* renderer); + + const char* GetName() const override { return "InputSystem"; } + void Update(Registry& registry, float deltaTime) override; + private: + Renderer::IRenderer* m_renderer; + }; + + } +} \ No newline at end of file diff --git a/engine/include/ECS/LevelLoader.hpp b/engine/include/ECS/LevelLoader.hpp new file mode 100644 index 0000000..cb4c96d --- /dev/null +++ b/engine/include/ECS/LevelLoader.hpp @@ -0,0 +1,189 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** LevelLoader - JSON-based level loading system +*/ + +#pragma once + +#include "Registry.hpp" +#include "Component.hpp" +#include "Renderer/IRenderer.hpp" +#include +#include +#include +#include + +namespace RType { + + namespace ECS { + + struct FontDef { + std::string path; + uint32_t size = 16; + }; + + struct ColliderDef { + float x = 0.0f; + float y = 0.0f; + float width = 0.0f; + float height = 0.0f; + }; + + struct ObstacleDef { + std::string texture; + float x = 0.0f; + float y = 0.0f; + float scaleWidth = 1200.0f; + float scaleHeight = 720.0f; + float scrollSpeed = -150.0f; + int layer = 1; + std::vector colliders; + }; + + struct EnemyDef { + std::string type = "BASIC"; + float x = 0.0f; + float y = 0.0f; + }; + + struct BossDef { + std::string texture; + float x = 0.0f; + float y = 0.0f; + float width = 200.0f; + float height = 200.0f; + int health = 1000; + float scrollSpeed = -300.0f; + int attackPattern = 1; + uint8_t bossId = 1; + }; + + struct PlayerSpawnDef { + float x = 100.0f; + float y = 360.0f; + }; + + struct BackgroundDef { + std::string texture; + float scrollSpeed = -150.0f; + int copies = 3; + int layer = -100; + }; + + struct PlayerConfig { + float movementSpeed = 200.0f; + float fireRate = 0.2f; + float bulletOffsetX = 50.0f; + float bulletOffsetY = 25.0f; + int maxHealth = 100; + }; + + struct LevelConfig { + float screenWidth = 1280.0f; + float screenHeight = 720.0f; + float powerUpSpawnInterval = 5.0f; + PlayerConfig playerDefaults; + }; + + struct LevelData { + std::string name; + + // Asset paths + std::unordered_map textures; + std::unordered_map fonts; + + // Level configuration + LevelConfig config; + + // Level elements + BackgroundDef background; + std::vector obstacles; + std::vector enemies; + std::vector playerSpawns; + std::optional boss; + }; + + struct LoadedAssets { + std::unordered_map textures; + std::unordered_map sprites; + std::unordered_map fonts; + }; + + struct CreatedEntities { + std::vector backgrounds; + std::vector obstacleVisuals; + std::vector obstacleColliders; + std::vector enemies; + Entity boss = NULL_ENTITY; + }; + + class LevelLoader { + public: + static LevelData LoadFromFile(const std::string& path); + static LevelData LoadFromString(const std::string& jsonString); + + static std::string SerializeToString(const LevelData& level); + static void SaveToFile(const LevelData& level, const std::string& path); + + static LoadedAssets LoadAssets( + const LevelData& level, + Renderer::IRenderer* renderer); + + static CreatedEntities CreateEntities( + Registry& registry, + const LevelData& level, + const LoadedAssets& assets, + Renderer::IRenderer* renderer); + + static CreatedEntities CreateServerEntities( + Registry& registry, + const LevelData& level); + + static const std::vector& GetPlayerSpawns(const LevelData& level); + private: + static EnemyType ParseEnemyType(const std::string& typeStr); + + static void CreateBackgrounds( + Registry& registry, + const BackgroundDef& background, + const LoadedAssets& assets, + Renderer::IRenderer* renderer, + CreatedEntities& entities); + + static void CreateObstacles( + Registry& registry, + const std::vector& obstacles, + const LoadedAssets& assets, + Renderer::IRenderer* renderer, + CreatedEntities& entities, + uint32_t& obstacleIdCounter); + + static void CreateEnemies( + Registry& registry, + const std::vector& enemies, + const LoadedAssets& assets, + Renderer::IRenderer* renderer, + CreatedEntities& entities); + + static void CreateServerObstacles( + Registry& registry, + const std::vector& obstacles, + CreatedEntities& entities, + uint32_t& obstacleIdCounter); + + static void CreateServerEnemies( + Registry& registry, + const std::vector& enemies, + CreatedEntities& entities); + + static void CreateServerBoss( + Registry& registry, + const std::optional& boss, + CreatedEntities& entities); + }; + + } + +} diff --git a/engine/include/ECS/MenuSystem.hpp b/engine/include/ECS/MenuSystem.hpp new file mode 100644 index 0000000..93530d8 --- /dev/null +++ b/engine/include/ECS/MenuSystem.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "ISystem.hpp" +#include "Renderer/IRenderer.hpp" +#include + +namespace RType { + namespace ECS { + + class MenuSystem : public ISystem { + public: + using ActionCallback = std::function; + + explicit MenuSystem(Renderer::IRenderer* renderer); + ~MenuSystem() override = default; + + void Update(Registry& registry, float deltaTime) override; + const char* GetName() const override { + return "MenuSystem"; + } + + void SetActionCallback(ActionCallback callback) { + m_callback = callback; + } + private: + Renderer::IRenderer* m_renderer; + ActionCallback m_callback; + bool m_wasMouseDown = false; + }; + + } +} diff --git a/engine/include/ECS/MineSystem.hpp b/engine/include/ECS/MineSystem.hpp new file mode 100644 index 0000000..5873b85 --- /dev/null +++ b/engine/include/ECS/MineSystem.hpp @@ -0,0 +1,31 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** MineSystem - Handles mine proximity detection and explosions +*/ + +#pragma once + +#include "ISystem.hpp" +#include "Registry.hpp" +#include + +namespace RType { + namespace ECS { + + class MineSystem : public ISystem { + public: + MineSystem() = default; + ~MineSystem() override = default; + + const char* GetName() const override { return "MineSystem"; } + + void Update(Registry& registry, float deltaTime) override; + + private: + float Distance(float x1, float y1, float x2, float y2); + }; + + } +} diff --git a/engine/include/ECS/MovementSystem.hpp b/engine/include/ECS/MovementSystem.hpp new file mode 100644 index 0000000..e306d27 --- /dev/null +++ b/engine/include/ECS/MovementSystem.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "ISystem.hpp" + +namespace RType { + + namespace ECS { + + class MovementSystem : public ISystem { + public: + MovementSystem() = default; + ~MovementSystem() override = default; + + void Update(Registry& registry, float deltaTime) override; + const char* GetName() const override { return "MovementSystem"; } + }; + + } + +} diff --git a/engine/include/ECS/ObstacleCollisionResponseSystem.hpp b/engine/include/ECS/ObstacleCollisionResponseSystem.hpp new file mode 100644 index 0000000..080f588 --- /dev/null +++ b/engine/include/ECS/ObstacleCollisionResponseSystem.hpp @@ -0,0 +1,27 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** ObstacleCollisionResponseSystem - Handles obstacle collision responses +*/ + +#pragma once + +#include "ISystem.hpp" +#include "Registry.hpp" +#include "Component.hpp" + +namespace RType { + namespace ECS { + + class ObstacleCollisionResponseSystem : public ISystem { + public: + ObstacleCollisionResponseSystem() = default; + ~ObstacleCollisionResponseSystem() override = default; + + const char* GetName() const override { return "ObstacleCollisionResponseSystem"; } + void Update(Registry& registry, float deltaTime) override; + }; + + } +} diff --git a/engine/include/ECS/PlayerCollisionResponseSystem.hpp b/engine/include/ECS/PlayerCollisionResponseSystem.hpp new file mode 100644 index 0000000..0acf686 --- /dev/null +++ b/engine/include/ECS/PlayerCollisionResponseSystem.hpp @@ -0,0 +1,38 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** PlayerCollisionResponseSystem - Handles player collision responses +*/ + +#pragma once + +#include "ISystem.hpp" +#include "Registry.hpp" +#include "Component.hpp" + +namespace RType { + namespace ECS { + + /** + * @brief Handles responses to player collisions + * + * Processes CollisionEvent components on players and: + * - Applies damage from enemy collisions + * - Destroys enemies on collision + * - Handles player-obstacle collisions (optional physics) + * - Applies powerup effects (future enhancement) + * + * This system should run after CollisionDetectionSystem + */ + class PlayerCollisionResponseSystem : public ISystem { + public: + PlayerCollisionResponseSystem() = default; + ~PlayerCollisionResponseSystem() override = default; + + const char* GetName() const override { return "PlayerCollisionResponseSystem"; } + void Update(Registry& registry, float deltaTime) override; + }; + + } // namespace ECS +} // namespace RType diff --git a/engine/include/ECS/PlayerFactory.hpp b/engine/include/ECS/PlayerFactory.hpp new file mode 100644 index 0000000..6ada9f0 --- /dev/null +++ b/engine/include/ECS/PlayerFactory.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "Registry.hpp" +#include "Component.hpp" +#include "Renderer/IRenderer.hpp" +#include "Math/Types.hpp" +#include + +namespace RType { + + namespace ECS { + + class PlayerFactory { + public: + static Entity CreatePlayer(Registry& registry, uint8_t playerNumber, uint64_t playerHash, + float startX, float startY, Renderer::IRenderer* renderer); + private: + static Math::Color GetPlayerColor(uint8_t playerNumber); + static std::string GetPlayerSpritePath(uint8_t playerNumber); + }; + + } + +} diff --git a/engine/include/ECS/PlayerSystem.hpp b/engine/include/ECS/PlayerSystem.hpp new file mode 100644 index 0000000..d7e3d36 --- /dev/null +++ b/engine/include/ECS/PlayerSystem.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "ISystem.hpp" +#include "Renderer/IRenderer.hpp" + +namespace RType { + + namespace ECS { + + class PlayerSystem : public ISystem { + public: + explicit PlayerSystem(Renderer::IRenderer* renderer = nullptr); + ~PlayerSystem() override = default; + + void Update(Registry& registry, float deltaTime) override; + const char* GetName() const override { return "PlayerSystem"; } + + static Entity CreatePlayer(Registry& registry, uint8_t playerNumber, uint64_t playerHash, + float startX, float startY, Renderer::IRenderer* renderer); + static void ClampPlayerToScreen(Registry& registry, Entity player, float screenWidth = 1280.0f, + float screenHeight = 720.0f); + private: + Renderer::IRenderer* m_renderer; + }; + + } + +} diff --git a/engine/include/ECS/PowerUpCollisionSystem.hpp b/engine/include/ECS/PowerUpCollisionSystem.hpp new file mode 100644 index 0000000..f6dbc1e --- /dev/null +++ b/engine/include/ECS/PowerUpCollisionSystem.hpp @@ -0,0 +1,34 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** PowerUpCollisionSystem +*/ + +#pragma once + +#include "ISystem.hpp" +#include "CollisionDetectionSystem.hpp" +#include "Renderer/IRenderer.hpp" +#include "../Audio/IAudio.hpp" + +namespace RType { + namespace ECS { + + class PowerUpCollisionSystem : public ISystem { + public: + explicit PowerUpCollisionSystem(Renderer::IRenderer* renderer = nullptr); + ~PowerUpCollisionSystem() override = default; + + void Update(Registry& registry, float deltaTime) override; + const char* GetName() const override { return "PowerUpCollisionSystem"; } + + void SetPowerUpSound(Audio::SoundId id) { m_powerUpSound = id; } + + private: + Renderer::IRenderer* m_renderer; + Audio::SoundId m_powerUpSound = Audio::INVALID_SOUND_ID; + }; + + } +} diff --git a/engine/include/ECS/PowerUpFactory.hpp b/engine/include/ECS/PowerUpFactory.hpp new file mode 100644 index 0000000..4bced5f --- /dev/null +++ b/engine/include/ECS/PowerUpFactory.hpp @@ -0,0 +1,59 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** PowerUpFactory +*/ + +#pragma once + +#include "Registry.hpp" +#include "Component.hpp" +#include "Renderer/IRenderer.hpp" +#include "Math/Types.hpp" +#include + +namespace RType { + namespace ECS { + class EffectFactory; + } +} + +namespace RType { + namespace ECS { + + class PowerUpFactory { + public: + // Create a powerup entity in the world + static Entity CreatePowerUp( + Registry& registry, + PowerUpType type, + float startX, + float startY, + Renderer::IRenderer* renderer, + const EffectFactory* effectFactory = nullptr + ); + + // Apply powerup effect to player + static void ApplyPowerUpToPlayer( + Registry& registry, + Entity player, + PowerUpType type, + Renderer::IRenderer* renderer + ); + + static Entity CreateForcePod( + Registry& registry, + Entity owner, + Renderer::IRenderer* renderer + ); + + // Get powerup properties + static Math::Color GetPowerUpColor(PowerUpType type); + static const char* GetPowerUpSpritePath(PowerUpType type); + static const char* GetPowerUpName(PowerUpType type); + static float GetPowerUpScale(PowerUpType type); + }; + + } +} diff --git a/engine/include/ECS/PowerUpSpawnSystem.hpp b/engine/include/ECS/PowerUpSpawnSystem.hpp new file mode 100644 index 0000000..2abeb35 --- /dev/null +++ b/engine/include/ECS/PowerUpSpawnSystem.hpp @@ -0,0 +1,48 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** PowerUpSpawnSystem +*/ + +#pragma once + +#include "ISystem.hpp" +#include "Renderer/IRenderer.hpp" +#include + +namespace RType { + namespace ECS { + + class EffectFactory; + + class PowerUpSpawnSystem : public ISystem { + public: + explicit PowerUpSpawnSystem( + Renderer::IRenderer* renderer = nullptr, + float screenWidth = 1280.0f, + float screenHeight = 720.0f + ); + ~PowerUpSpawnSystem() override = default; + + void Update(Registry& registry, float deltaTime) override; + const char* GetName() const override { return "PowerUpSpawnSystem"; } + + void SpawnRandomPowerUp(Registry& registry); + static void DestroyPowerUpsOffScreen(Registry& registry); + + void SetSpawnInterval(float interval) { m_spawnInterval = interval; } + void SetEffectFactory(const EffectFactory* effectFactory) { m_effectFactory = effectFactory; } + + private: + Renderer::IRenderer* m_renderer; + float m_screenWidth; + float m_screenHeight; + float m_spawnTimer = 0.0f; + float m_spawnInterval = 5.0f; + const EffectFactory* m_effectFactory = nullptr; + std::mt19937 m_rng; + }; + + } +} diff --git a/engine/include/ECS/Registry.hpp b/engine/include/ECS/Registry.hpp new file mode 100644 index 0000000..3f0e4e6 --- /dev/null +++ b/engine/include/ECS/Registry.hpp @@ -0,0 +1,232 @@ +#pragma once + +#include "Entity.hpp" +#include "Component.hpp" +#include "SparseArray.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace RType { + + namespace ECS { + + class IComponentPool { + public: + virtual ~IComponentPool() = default; + virtual bool Has(Entity entity) const = 0; + virtual void Remove(Entity entity) = 0; + }; + + template + class ComponentPool : public IComponentPool { + public: + T& Add(Entity entity, T&& component = T{}); + T& Get(Entity entity); + const T& Get(Entity entity) const; + bool Has(Entity entity) const override; + void Remove(Entity entity) override; + std::vector GetEntities() const; + private: + SparseArray m_components; + }; + + class Registry { + public: + Registry(); + ~Registry() = default; + + Entity CreateEntity(); + void DestroyEntity(Entity entity); + bool IsEntityAlive(Entity entity) const; + + template + T& AddComponent(Entity entity, T&& component = T{}); + + template + T& GetComponent(Entity entity); + + template + const T& GetComponent(Entity entity) const; + + template + bool HasComponent(Entity entity) const; + + template + void RemoveComponent(Entity entity); + + template + std::vector GetEntitiesWithComponent() const; + size_t GetEntityCount() const { return m_entityCount; } + private: + template + ComponentPool* GetOrCreatePool(); + template + ComponentPool* GetPool(); + template + const ComponentPool* GetPool() const; + + Entity m_nextEntityID; + size_t m_entityCount; + std::unordered_set m_aliveEntities; + std::unordered_map> m_componentPools; + std::vector m_freeEntityIds; + }; + + template + T& ComponentPool::Add(Entity entity, T&& component) { + return m_components.insert_at(entity, std::move(component)).value(); + } + + template + T& ComponentPool::Get(Entity entity) { + if (entity >= m_components.size() || !m_components[entity].has_value()) { + std::ostringstream oss; + oss << "Component '" << typeid(T).name() << "' not found for entity " << entity; + throw std::runtime_error(oss.str()); + } + return m_components[entity].value(); + } + + template + const T& ComponentPool::Get(Entity entity) const { + if (entity >= m_components.size() || !m_components[entity].has_value()) { + std::ostringstream oss; + oss << "Component '" << typeid(T).name() << "' not found for entity " << entity; + throw std::runtime_error(oss.str()); + } + return m_components[entity].value(); + } + + template + bool ComponentPool::Has(Entity entity) const { + return entity < m_components.size() && m_components[entity].has_value(); + } + + template + void ComponentPool::Remove(Entity entity) { + if (entity >= m_components.size() || !m_components[entity].has_value()) { + std::ostringstream oss; + oss << "Component '" << typeid(T).name() << "' not found for entity " << entity; + throw std::runtime_error(oss.str()); + } + m_components.erase(entity); + } + + template + std::vector ComponentPool::GetEntities() const { + std::vector entities; + for (size_t i = 0; i < m_components.size(); ++i) { + if (m_components[i].has_value()) { + entities.push_back(static_cast(i)); + } + } + return entities; + } + + template + T& Registry::AddComponent(Entity entity, T&& component) { + if (entity == NULL_ENTITY) { + throw std::invalid_argument("Cannot add component to NULL_ENTITY"); + } + if (!IsEntityAlive(entity)) { + std::ostringstream oss; + oss << "Cannot add component '" << typeid(T).name() << "' to non-existent entity " + << entity; + throw std::runtime_error(oss.str()); + } + ComponentPool* pool = GetOrCreatePool(); + return pool->Add(entity, std::forward(component)); + } + + template + T& Registry::GetComponent(Entity entity) { + ComponentPool* pool = GetPool(); + if (!pool) { + std::ostringstream oss; + oss << "Component type '" << typeid(T).name() << "' not registered"; + throw std::runtime_error(oss.str()); + } + return pool->Get(entity); + } + + template + const T& Registry::GetComponent(Entity entity) const { + const ComponentPool* pool = GetPool(); + if (!pool) { + std::ostringstream oss; + oss << "Component type '" << typeid(T).name() << "' not registered"; + throw std::runtime_error(oss.str()); + } + return pool->Get(entity); + } + + template + bool Registry::HasComponent(Entity entity) const { + const ComponentPool* pool = GetPool(); + if (!pool) { + return false; + } + return pool->Has(entity); + } + + template + void Registry::RemoveComponent(Entity entity) { + ComponentPool* pool = GetPool(); + if (pool) { + pool->Remove(entity); + } + } + + template + std::vector Registry::GetEntitiesWithComponent() const { + const ComponentPool* pool = GetPool(); + if (!pool) { + return std::vector(); + } + return pool->GetEntities(); + } + + template + ComponentPool* Registry::GetOrCreatePool() { + ComponentID typeID = std::type_index(typeid(T)); + auto it = m_componentPools.find(typeID); + if (it == m_componentPools.end()) { + auto pool = std::make_unique>(); + ComponentPool* poolPtr = pool.get(); + m_componentPools[typeID] = std::move(pool); + return poolPtr; + } + return static_cast*>(it->second.get()); + } + + template + ComponentPool* Registry::GetPool() { + ComponentID typeID = std::type_index(typeid(T)); + auto it = m_componentPools.find(typeID); + if (it == m_componentPools.end()) { + return nullptr; + } + return static_cast*>(it->second.get()); + } + + template + const ComponentPool* Registry::GetPool() const { + ComponentID typeID = std::type_index(typeid(T)); + auto it = m_componentPools.find(typeID); + if (it == m_componentPools.end()) { + return nullptr; + } + return static_cast*>(it->second.get()); + } + + + } + +} \ No newline at end of file diff --git a/engine/include/ECS/RenderingSystem.hpp b/engine/include/ECS/RenderingSystem.hpp new file mode 100644 index 0000000..bc01ad1 --- /dev/null +++ b/engine/include/ECS/RenderingSystem.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "ISystem.hpp" +#include "Renderer/IRenderer.hpp" + +namespace RType { + + namespace ECS { + + class RenderingSystem : public ISystem { + public: + explicit RenderingSystem(Renderer::IRenderer* renderer); + ~RenderingSystem() override = default; + + void Update(Registry& registry, float deltaTime) override; + const char* GetName() const override { return "RenderingSystem"; } + private: + Renderer::IRenderer* m_renderer; + }; + + } + +} diff --git a/engine/include/ECS/ScoreSystem.hpp b/engine/include/ECS/ScoreSystem.hpp new file mode 100644 index 0000000..80e1214 --- /dev/null +++ b/engine/include/ECS/ScoreSystem.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "ISystem.hpp" + +namespace RType { + namespace ECS { + + class ScoreSystem : public ISystem { + public: + ScoreSystem() = default; + ~ScoreSystem() override = default; + + const char* GetName() const override { return "ScoreSystem"; } + + void Update(Registry& registry, float deltaTime) override; + }; + } +} \ No newline at end of file diff --git a/engine/include/ECS/ScrollingSystem.hpp b/engine/include/ECS/ScrollingSystem.hpp new file mode 100644 index 0000000..6037b0c --- /dev/null +++ b/engine/include/ECS/ScrollingSystem.hpp @@ -0,0 +1,20 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** ScrollingSystem +*/ + +#pragma once +#include "ISystem.hpp" + +namespace RType { + namespace ECS { + class ScrollingSystem : public ISystem { + public: + ScrollingSystem() = default; + const char* GetName() const override { return "ScrollingSystem"; } + void Update(Registry& registry, float deltaTime) override; + }; + } +} diff --git a/engine/include/ECS/ShieldSystem.hpp b/engine/include/ECS/ShieldSystem.hpp new file mode 100644 index 0000000..f4a77d8 --- /dev/null +++ b/engine/include/ECS/ShieldSystem.hpp @@ -0,0 +1,25 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** ShieldSystem +*/ + +#pragma once + +#include "ISystem.hpp" + +namespace RType { + namespace ECS { + + class ShieldSystem : public ISystem { + public: + ShieldSystem() = default; + ~ShieldSystem() override = default; + + void Update(Registry& registry, float deltaTime) override; + const char* GetName() const override { return "ShieldSystem"; } + }; + + } +} diff --git a/engine/include/ECS/ShootingSystem.hpp b/engine/include/ECS/ShootingSystem.hpp new file mode 100644 index 0000000..fa309b1 --- /dev/null +++ b/engine/include/ECS/ShootingSystem.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "ISystem.hpp" +#include "../Renderer/IRenderer.hpp" +#include "../Audio/IAudio.hpp" + +namespace RType { + namespace ECS { + + class EffectFactory; + + class ShootingSystem : public ISystem { + public: + ShootingSystem(Renderer::SpriteId bulletSprite); + ~ShootingSystem() override = default; + + const char* GetName() const override { return "ShootingSystem"; } + + void Update(Registry& registry, float deltaTime) override; + + void SetShootSound(Audio::SoundId soundId) { m_shootSound = soundId; } + void SetEffectFactory(EffectFactory* effectFactory) { m_effectFactory = effectFactory; } + private: + void CreateSpreadShot(Registry& registry, Entity shooter, const Position& pos, int damage); + void CreateLaserShot(Registry& registry, Entity shooter, const Position& pos, int damage); + + Renderer::SpriteId m_bulletSprite; + Audio::SoundId m_shootSound = Audio::INVALID_SOUND_ID; + EffectFactory* m_effectFactory = nullptr; + }; + } +} \ No newline at end of file diff --git a/engine/include/ECS/SparseArray.hpp b/engine/include/ECS/SparseArray.hpp new file mode 100644 index 0000000..8520f52 --- /dev/null +++ b/engine/include/ECS/SparseArray.hpp @@ -0,0 +1,184 @@ +/** + * @file SparseArray.hpp + * @brief Implémentation d'un sparse array pour le système ECS + */ + +#pragma once + +#include +#include +#include +#include + +/** + * @class SparseArray + * @brief Container sparse array pour stocker des composants ECS + * + * Un sparse array est un tableau creux où l'index correspond directement à l'ID d'une entité. + * Les composants sont stockés dans un std::vector>, permettant + * d'avoir des "trous" (indices sans composant). + * + * @tparam Component Le type de composant à stocker + * + * @note Cette implémentation est optimisée pour les accès directs par index (O(1)) + * et les itérations séquentielles (cache-friendly). + * + * @example + * @code + * SparseArray positions; + * positions.insert_at(5, Position{10, 20}); // Entity 5 + * positions.insert_at(7, Position{30, 40}); // Entity 7 + * + * // Accès direct + * if (positions[5].has_value()) { + * Position& pos = positions[5].value(); + * } + * @endcode + */ +template +class SparseArray { + using value_type = std::optional; + + using reference_type = value_type&; + using const_reference_type = value_type const&; + + using sparse_array_t = std::vector; + + using size_type = typename sparse_array_t::size_type; + using iterator = typename sparse_array_t::iterator; + using const_iterator = typename sparse_array_t::const_iterator; +public: + SparseArray() = default; + + SparseArray(SparseArray const& other) : _data(other._data) { + } + + SparseArray(SparseArray&& other) noexcept : _data(std::move(other._data)) { + } + + ~SparseArray() = default; + + SparseArray& operator=(SparseArray const& other) { + if (this != &other) { + _data = other._data; + } + return *this; + } + + SparseArray& operator=(SparseArray&& other) noexcept { + if (this != &other) { + _data = std::move(other._data); + } + return *this; + } + + iterator begin() { + return _data.begin(); + } + + const_iterator begin() const { + return _data.begin(); + } + + const_iterator cbegin() const { + return _data.cbegin(); + } + + iterator end() { + return _data.end(); + } + + const_iterator end() const { + return _data.end(); + } + + const_iterator cend() const { + return _data.cend(); + } + + reference_type operator[](size_t idx) { + if (idx >= _data.size()) { + ensure_capacity(idx + 1); + } + return _data[idx]; + } + + const_reference_type operator[](size_t idx) const { + if (idx >= _data.size()) { + throw std::out_of_range("SparseArray::operator[] const: index out of range"); + } + return _data[idx]; + } + + size_type size() const { + return _data.size(); + } + + reference_type insert_at(size_type pos, Component const& component) { + if (pos >= _data.size()) { + ensure_capacity(pos + 1); + } + _data[pos] = component; + return _data[pos]; + } + + reference_type insert_at(size_type pos, Component&& component) { + if (pos >= _data.size()) { + ensure_capacity(pos + 1); + } + _data[pos] = std::move(component); + return _data[pos]; + } + + template + reference_type emplace_at(size_type pos, Params&&... params) { + if (pos >= _data.size()) { + ensure_capacity(pos + 1); + } + _data[pos].emplace(std::forward(params)...); + return _data[pos]; + } + + void erase(size_type pos) { + if (pos >= _data.size()) { + return; + } + _data[pos].reset(); + } + + size_type capacity() const { + return _data.capacity(); + } + + bool empty() const { + for (const auto& opt : _data) { + if (opt.has_value()) { + return false; + } + } + return true; + } + + void clear() { + _data.clear(); + } + + void reserve(size_type new_capacity) { + _data.reserve(new_capacity); + } +private: + void ensure_capacity(size_type required_size) { + if (required_size <= _data.size()) { + return; + } + + size_type new_capacity = required_size + (required_size / 2); + if (new_capacity < 8) { + new_capacity = 8; + } + _data.reserve(new_capacity); + _data.resize(required_size); + } + + sparse_array_t _data; +}; \ No newline at end of file diff --git a/engine/include/ECS/TextRenderingSystem.hpp b/engine/include/ECS/TextRenderingSystem.hpp new file mode 100644 index 0000000..2f31b89 --- /dev/null +++ b/engine/include/ECS/TextRenderingSystem.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "ISystem.hpp" +#include "Renderer/IRenderer.hpp" + +namespace RType { + namespace ECS { + + class TextRenderingSystem : public ISystem { + public: + explicit TextRenderingSystem(Renderer::IRenderer* renderer); + ~TextRenderingSystem() override = default; + + void Update(Registry& registry, float deltaTime) override; + const char* GetName() const override { return "TextRenderingSystem"; } + private: + Renderer::IRenderer* m_renderer; + }; + + } +} diff --git a/engine/include/ECS/ThirdBulletSystem.hpp b/engine/include/ECS/ThirdBulletSystem.hpp new file mode 100644 index 0000000..10d8f6a --- /dev/null +++ b/engine/include/ECS/ThirdBulletSystem.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "ISystem.hpp" +#include "Registry.hpp" + +namespace RType { + namespace ECS { + + class ThirdBulletSystem : public ISystem { + public: + struct Direction { + float vx; + float vy; + }; + + ThirdBulletSystem() = default; + ~ThirdBulletSystem() override = default; + + const char* GetName() const override { return "ThirdBulletSystem"; } + + void Update(Registry& registry, float deltaTime) override; + + private: + void SpawnSmallProjectile(Registry& registry, float x, float y); + }; + + } +} diff --git a/engine/include/Math/Types.hpp b/engine/include/Math/Types.hpp new file mode 100644 index 0000000..e1f145c --- /dev/null +++ b/engine/include/Math/Types.hpp @@ -0,0 +1,30 @@ +#pragma once + +namespace Math { + + struct Vector2 { + float x = 0.0f; + float y = 0.0f; + + Vector2() = default; + Vector2(float x, float y) + : x(x), y(y) {} + }; + + struct Color { + float r = 1.0f; + float g = 1.0f; + float b = 1.0f; + float a = 1.0f; + + Color() = default; + Color(float r, float g, float b, float a = 1.0f) + : r(r), g(g), b(b), a(a) {} + }; + + struct Rectangle { + Vector2 position{0.0f, 0.0f}; + Vector2 size{0.0f, 0.0f}; + }; + +} diff --git a/engine/include/Physics/IPhysics.hpp b/engine/include/Physics/IPhysics.hpp new file mode 100644 index 0000000..e4ca536 --- /dev/null +++ b/engine/include/Physics/IPhysics.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include "Core/Module.hpp" +#include "Core/Logger.hpp" +#include "Core/Engine.hpp" + +namespace Physics { + + struct Vector2 { + float x = 0.0f; + float y = 0.0f; + + Vector2() = default; + Vector2(float x, float y) + : x(x), y(y) {} + }; + + struct BodyDef { + Vector2 position{0.0f, 0.0f}; + Vector2 velocity{0.0f, 0.0f}; + float mass = 1.0f; + bool isStatic = false; + }; + + class Body { + public: + virtual ~Body() = default; + + virtual Vector2 GetPosition() const = 0; + virtual void SetPosition(const Vector2& position) = 0; + virtual Vector2 GetVelocity() const = 0; + virtual void SetVelocity(const Vector2& velocity) = 0; + }; + + class IPhysics : public RType::Core::IModule { + public: + virtual ~IPhysics() = default; + + virtual const char* GetName() const override = 0; + virtual RType::Core::ModulePriority GetPriority() const override = 0; + virtual bool Initialize(RType::Core::Engine* engine) override = 0; + virtual void Shutdown() override = 0; + virtual void Update(float deltaTime) override = 0; + + virtual std::shared_ptr CreateBody(const BodyDef& def) = 0; + virtual void DestroyBody(Body* body) = 0; + virtual void SetGravity(const Vector2& gravity) = 0; + }; + +} diff --git a/engine/include/Renderer/IRenderer.hpp b/engine/include/Renderer/IRenderer.hpp new file mode 100644 index 0000000..6012dfb --- /dev/null +++ b/engine/include/Renderer/IRenderer.hpp @@ -0,0 +1,178 @@ +#pragma once + +#define RTYPE_INCLUDE_WINDOWS_H +#include "Core/Platform.hpp" +#include +#include +#include "Core/Module.hpp" +#include "Math/Types.hpp" + +namespace Renderer { + + using TextureId = std::uint32_t; + using SpriteId = std::uint32_t; + using FontId = std::uint32_t; + + constexpr TextureId INVALID_TEXTURE_ID = 0; + constexpr SpriteId INVALID_SPRITE_ID = 0; + constexpr FontId INVALID_FONT_ID = 0; + + using Math::Color; + using Math::Rectangle; + using Math::Vector2; + + struct WindowConfig { + std::string title{"R-Type"}; + std::uint32_t width = 1280; + std::uint32_t height = 720; + bool fullscreen = false; + bool resizable = false; + std::uint32_t targetFramerate = 60; + }; + + struct TextureConfig { + bool smooth = true; + bool repeated = false; + bool generateMipmaps = false; + }; + + struct Transform2D { + Vector2 position{0.0f, 0.0f}; + Vector2 scale{1.0f, 1.0f}; + float rotation = 0.0f; + Vector2 origin{0.0f, 0.0f}; + }; + + struct TextParams { + Vector2 position{0.0f, 0.0f}; + Color color{}; + float rotation = 0.0f; + float scale = 1.0f; + float letterSpacing = 0.0f; + float lineSpacing = 0.0f; + bool centered = false; + }; + + struct Camera2D { + Vector2 center{0.0f, 0.0f}; + Vector2 size{1280.0f, 720.0f}; + }; + + struct RenderStats { + std::uint32_t drawCalls = 0; + std::uint32_t textureSwitches = 0; + }; + + enum class Key { + Unknown = -1, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R, + S, + T, + U, + V, + W, + X, + Y, + Z, + Num0, + Num1, + Num2, + Num3, + Num4, + Num5, + Num6, + Num7, + Num8, + Num9, + Escape, + Space, + Enter, + Backspace, + Tab, + Left, + Right, + Up, + Down, + LShift, + RShift, + LControl, + RControl, + LAlt, + RAlt + }; + + class IRenderer : public RType::Core::IModule { + public: + ~IRenderer() override = default; + + const char* GetName() const override = 0; + RType::Core::ModulePriority GetPriority() const override = 0; + bool Initialize(RType::Core::Engine* engine) override = 0; + void Shutdown() override = 0; + void Update(float deltaTime) override = 0; + + virtual bool CreateWindow(const WindowConfig& config) = 0; + virtual void Destroy() = 0; + virtual bool IsWindowOpen() const = 0; + virtual void Resize(std::uint32_t width, std::uint32_t height) = 0; + virtual void SetWindowTitle(const std::string& title) = 0; + + virtual void BeginFrame() = 0; + virtual void EndFrame() = 0; + virtual void Clear(const Color& color) = 0; + + virtual TextureId LoadTexture(const std::string& path, + const TextureConfig& config = TextureConfig{}) = 0; + virtual void UnloadTexture(TextureId textureId) = 0; + virtual Vector2 GetTextureSize(TextureId textureId) const = 0; + + virtual SpriteId CreateSprite(TextureId textureId, const Rectangle& region) = 0; + virtual void DestroySprite(SpriteId spriteId) = 0; + + // Update the texture region of an existing sprite (for animations) + virtual void SetSpriteRegion(SpriteId spriteId, const Rectangle& region) = 0; + + virtual void DrawSprite(SpriteId spriteId, const Transform2D& transform, + const Color& tint = Color{}) = 0; + virtual void DrawRectangle(const Rectangle& rectangle, const Color& color) = 0; + + virtual FontId LoadFont(const std::string& path, std::uint32_t characterSize) = 0; + virtual void UnloadFont(FontId fontId) = 0; + virtual void DrawText(FontId fontId, const std::string& text, const TextParams& params) = 0; + + virtual void SetCamera(const Camera2D& camera) = 0; + virtual void ResetCamera() = 0; + + virtual RenderStats GetRenderStats() const = 0; + + virtual float GetDeltaTime() = 0; + + virtual bool IsKeyPressed(Key key) const = 0; + + enum class MouseButton { + Left, + Right, + Middle + }; + virtual bool IsMouseButtonPressed(MouseButton button) const = 0; + virtual Vector2 GetMousePosition() const = 0; + }; + +} diff --git a/engine/include/Renderer/SFMLRenderer.hpp b/engine/include/Renderer/SFMLRenderer.hpp new file mode 100644 index 0000000..3d8edd1 --- /dev/null +++ b/engine/include/Renderer/SFMLRenderer.hpp @@ -0,0 +1,105 @@ +#pragma once + +#include "IRenderer.hpp" +#include +#include +#include + +namespace Renderer { + + class SFMLRenderer : public IRenderer { + public: + SFMLRenderer(); + ~SFMLRenderer() override; + + const char* GetName() const override; + RType::Core::ModulePriority GetPriority() const override; + bool Initialize(RType::Core::Engine* engine) override; + void Shutdown() override; + void Update(float deltaTime) override; + bool ShouldUpdateInRenderThread() const override; + + bool CreateWindow(const WindowConfig& config) override; + void Destroy() override; + bool IsWindowOpen() const override; + void Resize(std::uint32_t width, std::uint32_t height) override; + void SetWindowTitle(const std::string& title) override; + + void BeginFrame() override; + void EndFrame() override; + void Clear(const Color& color) override; + + TextureId LoadTexture(const std::string& path, + const TextureConfig& config = TextureConfig{}) override; + void UnloadTexture(TextureId textureId) override; + Vector2 GetTextureSize(TextureId textureId) const override; + + SpriteId CreateSprite(TextureId textureId, const Rectangle& region) override; + void DestroySprite(SpriteId spriteId) override; + void SetSpriteRegion(SpriteId spriteId, const Rectangle& region) override; + + void DrawSprite(SpriteId spriteId, const Transform2D& transform, + const Color& tint = Color{}) override; + void DrawRectangle(const Rectangle& rectangle, const Color& color) override; + + FontId LoadFont(const std::string& path, std::uint32_t characterSize) override; + void UnloadFont(FontId fontId) override; + void DrawText(FontId fontId, const std::string& text, + const TextParams& params) override; + + void SetCamera(const Camera2D& camera) override; + void ResetCamera() override; + + RenderStats GetRenderStats() const override; + + float GetDeltaTime() override; + + bool IsKeyPressed(Key key) const override; + bool IsMouseButtonPressed(MouseButton button) const override; + Vector2 GetMousePosition() const override; + + const sf::RenderWindow* GetWindow() const { return m_window.get(); } + void ProcessEvents(); + private: + static sf::Color ToSFMLColor(const Color& color); + static sf::Vector2f ToSFMLVector(const Vector2& vec); + static sf::IntRect ToSFMLRect(const Rectangle& rect); + static sf::Keyboard::Key ToSFMLKey(Key key); + + std::unique_ptr m_window; + WindowConfig m_windowConfig; + + struct TextureData { + std::shared_ptr texture; + TextureConfig config; + }; + std::unordered_map m_textures; + TextureId m_nextTextureId = 1; + + struct SpriteData { + sf::Sprite sprite; + TextureId textureId; + }; + std::unordered_map m_sprites; + SpriteId m_nextSpriteId = 1; + + struct FontData { + std::shared_ptr font; + std::uint32_t characterSize; + }; + std::unordered_map m_fonts; + FontId m_nextFontId = 1; + + sf::View m_defaultView; + sf::View m_currentView; + bool m_usingCustomCamera = false; + + RType::Core::Engine* m_engine = nullptr; + + RenderStats m_stats; + + sf::Clock m_clock; + float m_lastDeltaTime = 0.0f; + }; + +} diff --git a/engine/src/Animation/AnimationModule.cpp b/engine/src/Animation/AnimationModule.cpp new file mode 100644 index 0000000..da57b1b --- /dev/null +++ b/engine/src/Animation/AnimationModule.cpp @@ -0,0 +1,254 @@ +#include "Animation/AnimationModule.hpp" +#include +#include +#include +#include + +namespace Animation { + + AnimationModule::AnimationModule() = default; + + AnimationModule::~AnimationModule() { + Shutdown(); + } + + bool AnimationModule::Initialize(RType::Core::Engine* engine) { + m_engine = engine; + return true; + } + + void AnimationModule::Shutdown() { + UnloadAll(); + m_engine = nullptr; + } + + void AnimationModule::Update(float /* deltaTime */) { + } + + AnimationClipId AnimationModule::CreateClip(const AnimationClipConfig& config) { + if (config.name.empty() || config.frames.empty()) { + return INVALID_CLIP_ID; + } + + AnimationClipId id = m_nextClipId++; + m_clips[id] = config; + m_clipNameToId[config.name] = id; + + return id; + } + + AnimationClipId AnimationModule::CreateClipFromGrid(const std::string& name, + const std::string& texturePath, + const GridLayout& layout, + bool looping) { + if (name.empty() || layout.columns == 0 || layout.rows == 0) { + return INVALID_CLIP_ID; + } + + AnimationClipConfig config; + config.name = name; + config.texturePath = texturePath; + config.looping = looping; + + std::uint32_t totalFrames = layout.columns * layout.rows; + std::uint32_t frameCount = (layout.frameCount > 0) ? + std::min(layout.frameCount, totalFrames - layout.startFrame) : + totalFrames - layout.startFrame; + + config.frames.reserve(frameCount); + + for (std::uint32_t i = 0; i < frameCount; ++i) { + std::uint32_t frameIndex = layout.startFrame + i; + std::uint32_t col = frameIndex % layout.columns; + std::uint32_t row = frameIndex / layout.columns; + + FrameDef frame; + frame.region.position.x = static_cast(col) * layout.frameWidth; + frame.region.position.y = static_cast(row) * layout.frameHeight; + frame.region.size.x = layout.frameWidth; + frame.region.size.y = layout.frameHeight; + frame.duration = layout.defaultDuration; + + config.frames.push_back(frame); + } + + return CreateClip(config); + } + + AnimationClipId AnimationModule::LoadClipFromJson(const std::string& path) { + std::ifstream file(path); + if (!file.is_open()) { + return INVALID_CLIP_ID; + } + + try { + nlohmann::json json; + file >> json; + + AnimationClipConfig config; + config.name = json.value("name", ""); + config.texturePath = json.value("texture", ""); + config.looping = json.value("looping", false); + config.playbackSpeed = json.value("playbackSpeed", 1.0f); + + if (json.contains("gridLayout")) { + const auto& grid = json["gridLayout"]; + GridLayout layout; + layout.columns = grid.value("columns", 1u); + layout.rows = grid.value("rows", 1u); + layout.startFrame = grid.value("startFrame", 0u); + layout.frameCount = grid.value("frameCount", 0u); + layout.frameWidth = grid.value("frameWidth", 0.0f); + layout.frameHeight = grid.value("frameHeight", 0.0f); + layout.defaultDuration = grid.value("defaultDuration", 0.1f); + + if (layout.frameWidth <= 0.0f && json.contains("frameWidth")) { + layout.frameWidth = json["frameWidth"].get(); + } + if (layout.frameHeight <= 0.0f && json.contains("frameHeight")) { + layout.frameHeight = json["frameHeight"].get(); + } + + return CreateClipFromGrid(config.name, config.texturePath, layout, config.looping); + } + + if (json.contains("frames")) { + for (const auto& frameJson : json["frames"]) { + FrameDef frame; + frame.region.position.x = frameJson.value("x", 0.0f); + frame.region.position.y = frameJson.value("y", 0.0f); + frame.region.size.x = frameJson.value("width", 0.0f); + frame.region.size.y = frameJson.value("height", 0.0f); + frame.duration = frameJson.value("duration", 0.1f); + frame.eventName = frameJson.value("event", ""); + config.frames.push_back(frame); + } + } + + return CreateClip(config); + + } catch (const std::exception&) { + return INVALID_CLIP_ID; + } + } + + void AnimationModule::DestroyClip(AnimationClipId clipId) { + auto it = m_clips.find(clipId); + if (it != m_clips.end()) { + m_clipNameToId.erase(it->second.name); + m_clips.erase(it); + } + } + + const AnimationClipConfig* AnimationModule::GetClipConfig(AnimationClipId clipId) const { + auto it = m_clips.find(clipId); + return (it != m_clips.end()) ? &it->second : nullptr; + } + + float AnimationModule::GetClipDuration(AnimationClipId clipId) const { + const auto* config = GetClipConfig(clipId); + if (!config) { + return 0.0f; + } + return CalculateClipDuration(*config); + } + + std::size_t AnimationModule::GetClipFrameCount(AnimationClipId clipId) const { + const auto* config = GetClipConfig(clipId); + return config ? config->frames.size() : 0; + } + + bool AnimationModule::IsClipValid(AnimationClipId clipId) const { + return m_clips.find(clipId) != m_clips.end(); + } + + FrameDef AnimationModule::GetFrameAtTime(AnimationClipId clipId, + float time, + bool looping) const { + const auto* config = GetClipConfig(clipId); + if (!config || config->frames.empty()) { + return FrameDef{}; + } + + std::size_t frameIndex = GetFrameIndexAtTime(clipId, time, looping); + if (frameIndex < config->frames.size()) { + return config->frames[frameIndex]; + } + + return config->frames.back(); + } + + std::size_t AnimationModule::GetFrameIndexAtTime(AnimationClipId clipId, + float time, + bool looping) const { + const auto* config = GetClipConfig(clipId); + if (!config || config->frames.empty()) { + return 0; + } + + float duration = CalculateClipDuration(*config); + if (duration <= 0.0f) { + return 0; + } + + float normalizedTime = time; + if (looping) { + normalizedTime = std::fmod(time, duration); + if (normalizedTime < 0.0f) { + normalizedTime += duration; + } + } else { + normalizedTime = std::clamp(time, 0.0f, duration); + } + + float accumulated = 0.0f; + for (std::size_t i = 0; i < config->frames.size(); ++i) { + accumulated += config->frames[i].duration; + if (normalizedTime < accumulated) { + return i; + } + } + + return config->frames.size() - 1; + } + + void AnimationModule::LoadAnimationsFromManifest(const std::string& manifestPath) { + std::ifstream file(manifestPath); + if (!file.is_open()) { + return; + } + + try { + nlohmann::json json; + file >> json; + + if (json.contains("clips")) { + for (const auto& clipPath : json["clips"]) { + LoadClipFromJson(clipPath.get()); + } + } + + } catch (const std::exception&) { + } + } + + void AnimationModule::UnloadAll() { + m_clips.clear(); + m_clipNameToId.clear(); + m_nextClipId = 1; + } + + AnimationClipId AnimationModule::GetClipByName(const std::string& name) const { + auto it = m_clipNameToId.find(name); + return (it != m_clipNameToId.end()) ? it->second : INVALID_CLIP_ID; + } + + float AnimationModule::CalculateClipDuration(const AnimationClipConfig& config) const { + float duration = 0.0f; + for (const auto& frame : config.frames) { + duration += frame.duration; + } + return duration; + } + +} diff --git a/engine/src/Animation/CMakeLists.txt b/engine/src/Animation/CMakeLists.txt new file mode 100644 index 0000000..f174d06 --- /dev/null +++ b/engine/src/Animation/CMakeLists.txt @@ -0,0 +1,31 @@ +set(ANIMATION_SOURCES + AnimationModule.cpp +) + +set(ANIMATION_HEADERS + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/Animation/AnimationTypes.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/Animation/IAnimation.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/Animation/AnimationModule.hpp +) + +add_library(rtype_animation STATIC + ${ANIMATION_SOURCES} + ${ANIMATION_HEADERS} +) + +target_include_directories(rtype_animation PUBLIC + $ + $ +) + +target_link_libraries(rtype_animation + PUBLIC + rtype_core + nlohmann_json::nlohmann_json +) + +target_compile_features(rtype_animation PUBLIC cxx_std_17) + +set_target_properties(rtype_animation PROPERTIES + POSITION_INDEPENDENT_CODE ON +) diff --git a/engine/src/Audio/CMakeLists.txt b/engine/src/Audio/CMakeLists.txt new file mode 100644 index 0000000..d45f60a --- /dev/null +++ b/engine/src/Audio/CMakeLists.txt @@ -0,0 +1,89 @@ +# SFML Audio backend + +set(SFML_AUDIO_FOUND FALSE) + +if(DEFINED CMAKE_TOOLCHAIN_FILE AND CMAKE_TOOLCHAIN_FILE MATCHES "vcpkg") + message(STATUS "Searching for SFML Audio via vcpkg...") + find_package(SFML 2.6 COMPONENTS audio system QUIET) + if(SFML_FOUND) + set(SFML_AUDIO_FOUND TRUE) + message(STATUS "Found SFML ${SFML_VERSION} (audio) via vcpkg") + endif() +endif() + +if(NOT SFML_AUDIO_FOUND) + message(STATUS "Searching for system-installed SFML Audio 2.6+...") + find_package(SFML 2.6 COMPONENTS audio system QUIET) + if(SFML_FOUND) + set(SFML_AUDIO_FOUND TRUE) + message(STATUS "Found system-installed SFML ${SFML_VERSION} (audio)") + endif() +endif() + +if(NOT SFML_AUDIO_FOUND) + message(STATUS "SFML Audio not found, fetching SFML 2.6.2 from source...") + include(FetchContent) + + set(OLD_BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS}) + + FetchContent_Declare( + SFML + GIT_REPOSITORY https://github.com/SFML/SFML.git + GIT_TAG 2.6.2 + GIT_SHALLOW TRUE + ) + + set(SFML_BUILD_AUDIO ON CACHE BOOL "" FORCE) + set(SFML_BUILD_NETWORK OFF CACHE BOOL "" FORCE) + set(BUILD_SHARED_LIBS ON CACHE BOOL "" FORCE) + + FetchContent_MakeAvailable(SFML) + + set(BUILD_SHARED_LIBS ${OLD_BUILD_SHARED_LIBS} CACHE BOOL "" FORCE) + set(SFML_AUDIO_FOUND TRUE) + message(STATUS "SFML 2.6.2 fetched and configured (audio)") +endif() + +if(SFML_AUDIO_FOUND) + if(TARGET SFML::Audio) + set(SFML_AUDIO_LIBS SFML::Audio SFML::System) + message(STATUS "Using SFML Audio targets with SFML:: namespace") + elseif(TARGET sfml-audio) + set(SFML_AUDIO_LIBS sfml-audio sfml-system) + message(STATUS "Using SFML Audio targets with sfml- prefix") + else() + message(FATAL_ERROR "SFML Audio found but targets not available (expected SFML::Audio or sfml-audio).") + endif() +endif() + +add_library(rtype_sfml_audio STATIC + SFMLAudio.cpp +) + +target_include_directories(rtype_sfml_audio PUBLIC + $ + $ +) + +find_package(fmt CONFIG REQUIRED) + +target_link_libraries(rtype_sfml_audio + PUBLIC + rtype_core + ${SFML_AUDIO_LIBS} + fmt::fmt +) + +# Apple platforms need explicit framework linking for OpenAL +if(APPLE) + target_link_libraries(rtype_sfml_audio + PUBLIC + "-framework AudioToolbox" + "-framework AudioUnit" + "-framework CoreAudio" + "-framework ApplicationServices" + ) +endif() + +target_compile_features(rtype_sfml_audio PUBLIC cxx_std_17) + diff --git a/engine/src/Audio/SFMLAudio.cpp b/engine/src/Audio/SFMLAudio.cpp new file mode 100644 index 0000000..ef4c23c --- /dev/null +++ b/engine/src/Audio/SFMLAudio.cpp @@ -0,0 +1,219 @@ +#include "Audio/SFMLAudio.hpp" +#include "Core/Logger.hpp" + +#include + +namespace Audio { + + bool SFMLAudio::ConfigureDevice(const AudioConfig& config) { + SetMasterVolume(config.masterVolume); + return true; + } + + void SFMLAudio::Shutdown() { + StopAll(); + m_activeSounds.clear(); + m_soundBuffers.clear(); + m_music.clear(); + } + + void SFMLAudio::Update(float /*deltaTime*/) { + CleanupStoppedSounds(); + } + + Audio::SoundId SFMLAudio::LoadSound(const std::string& path) { + sf::SoundBuffer buffer; + if (!buffer.loadFromFile(path)) { + RType::Core::Logger::Warning("[SFMLAudio] Failed to load sound '{}'", path); + return INVALID_SOUND_ID; + } + + SoundId id = m_nextSoundId++; + m_soundBuffers.emplace(id, std::move(buffer)); + RType::Core::Logger::Info("[SFMLAudio] Loaded sound '{}' (id={})", path, id); + return id; + } + + void SFMLAudio::UnloadSound(SoundId soundId) { + StopSound(soundId); + m_soundBuffers.erase(soundId); + } + + Audio::MusicId SFMLAudio::LoadMusic(const std::string& path) { + auto music = std::make_unique(); + if (!music->openFromFile(path)) { + RType::Core::Logger::Warning("[SFMLAudio] Failed to load music '{}'", path); + return INVALID_MUSIC_ID; + } + + MusicId id = m_nextMusicId++; + m_music.emplace(id, std::move(music)); + return id; + } + + void SFMLAudio::UnloadMusic(MusicId musicId) { + StopMusic(musicId); + m_music.erase(musicId); + m_musicOriginalVolumes.erase(musicId); + } + + void SFMLAudio::PlaySound(SoundId soundId, const PlaybackOptions& options) { + auto it = m_soundBuffers.find(soundId); + if (it == m_soundBuffers.end()) { + RType::Core::Logger::Warning("[SFMLAudio] PlaySound unknown id={}", soundId); + return; + } + + sf::Sound s; + s.setBuffer(it->second); + float finalVolume = std::clamp(options.volume * m_masterVolume, 0.0f, 1.0f) * 100.0f; + s.setVolume(finalVolume); + s.setPitch(std::max(0.01f, options.pitch)); + s.setLoop(options.loop); + + + s.setRelativeToListener(true); + s.setPosition(0.0f, 0.0f, 0.0f); + s.setMinDistance(1.0f); + s.setAttenuation(0.0f); // No attenuation + + + + s.play(); + m_activeSounds.push_back(std::move(s)); + m_soundOriginalVolumes[&it->second] = options.volume; + RType::Core::Logger::Debug("[SFMLAudio] PlaySound id={} vol={} pitch={} pan={} loop={}", + soundId, options.volume, options.pitch, options.pan, options.loop); + } + + void SFMLAudio::StopSound(SoundId soundId) { + auto it = m_soundBuffers.find(soundId); + if (it == m_soundBuffers.end()) { + return; + } + const sf::SoundBuffer* buffer = &it->second; + + for (auto& s : m_activeSounds) { + if (s.getBuffer() == buffer) { + s.stop(); + } + } + CleanupStoppedSounds(); + } + + void SFMLAudio::PlayMusic(MusicId musicId, const PlaybackOptions& options) { + auto it = m_music.find(musicId); + if (it == m_music.end() || !it->second) { + return; + } + auto& m = *it->second; + float finalVolume = std::clamp(options.volume * m_masterVolume, 0.0f, 1.0f) * 100.0f; + m.setVolume(finalVolume); + m.setPitch(std::max(0.01f, options.pitch)); + m.setLoop(options.loop); + m.play(); + m_musicOriginalVolumes[musicId] = options.volume; + } + + void SFMLAudio::StopMusic(MusicId musicId) { + auto it = m_music.find(musicId); + if (it == m_music.end() || !it->second) { + return; + } + it->second->stop(); + m_musicOriginalVolumes.erase(musicId); + } + + void SFMLAudio::PauseAll() { + for (auto& s : m_activeSounds) { + if (s.getStatus() == sf::Sound::Playing) { + s.pause(); + } + } + for (auto& [id, m] : m_music) { + (void)id; + if (m && m->getStatus() == sf::Music::Playing) { + m->pause(); + } + } + } + + void SFMLAudio::ResumeAll() { + for (auto& s : m_activeSounds) { + if (s.getStatus() == sf::Sound::Paused) { + s.play(); + } + } + for (auto& [id, m] : m_music) { + (void)id; + if (m && m->getStatus() == sf::Music::Paused) { + m->play(); + } + } + } + + void SFMLAudio::StopAll() { + for (auto& s : m_activeSounds) { + s.stop(); + } + m_soundOriginalVolumes.clear(); + for (auto& [id, m] : m_music) { + (void)id; + if (m) { + m->stop(); + } + } + m_musicOriginalVolumes.clear(); + CleanupStoppedSounds(); + } + + void SFMLAudio::SetMasterVolume(float volume) { + m_masterVolume = std::clamp(volume, 0.0f, 1.0f); + UpdateAllVolumes(); + } + + void SFMLAudio::UpdateAllVolumes() { + for (auto& s : m_activeSounds) { + if (s.getStatus() == sf::Sound::Playing || s.getStatus() == sf::Sound::Paused) { + const sf::SoundBuffer* buffer = s.getBuffer(); + auto volIt = m_soundOriginalVolumes.find(buffer); + if (volIt != m_soundOriginalVolumes.end()) { + float finalVolume = std::clamp(volIt->second * m_masterVolume, 0.0f, 1.0f) * 100.0f; + s.setVolume(finalVolume); + } + } + } + + for (auto& [id, m] : m_music) { + if (m && (m->getStatus() == sf::Music::Playing || m->getStatus() == sf::Music::Paused)) { + auto volIt = m_musicOriginalVolumes.find(id); + if (volIt != m_musicOriginalVolumes.end()) { + float finalVolume = std::clamp(volIt->second * m_masterVolume, 0.0f, 1.0f) * 100.0f; + m->setVolume(finalVolume); + } + } + } + } + + float SFMLAudio::GetMasterVolume() const { + return m_masterVolume; + } + + void SFMLAudio::SetListener(const ListenerProperties& listener) { + sf::Listener::setPosition(listener.position.x, listener.position.y, 0.0f); + sf::Listener::setDirection(listener.forward.x, listener.forward.y, 0.0f); + } + + void SFMLAudio::CleanupStoppedSounds() { + m_activeSounds.remove_if([this](const sf::Sound& s) { + if (s.getStatus() == sf::Sound::Stopped) { + const sf::SoundBuffer* buffer = s.getBuffer(); + m_soundOriginalVolumes.erase(buffer); + return true; + } + return false; + }); + } + +} + diff --git a/engine/src/Core/CMakeLists.txt b/engine/src/Core/CMakeLists.txt new file mode 100644 index 0000000..da61b84 --- /dev/null +++ b/engine/src/Core/CMakeLists.txt @@ -0,0 +1,44 @@ +set(CORE_SOURCES + Engine.cpp + ModuleLoader.cpp + ColorFilter.cpp + InputMapping.cpp +) + +set(CORE_HEADERS + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/Core/Engine.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/Core/Module.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/Core/ModuleLoader.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/Core/Logger.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/Core/ColorFilter.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/Core/InputMapping.hpp +) + +add_library(rtype_core STATIC + ${CORE_SOURCES} + ${CORE_HEADERS} +) + +target_include_directories(rtype_core PUBLIC + $ + $ +) + +target_link_libraries(rtype_core PUBLIC + rtype_ecs + ${CMAKE_DL_LIBS} +) + +target_compile_features(rtype_core PUBLIC cxx_std_17) + +set_target_properties(rtype_core PROPERTIES + POSITION_INDEPENDENT_CODE ON +) + +if(WIN32) + target_compile_definitions(rtype_core PUBLIC + NOMINMAX + WIN32_LEAN_AND_MEAN + _CRT_SECURE_NO_WARNINGS + ) +endif() diff --git a/engine/src/Core/ColorFilter.cpp b/engine/src/Core/ColorFilter.cpp new file mode 100644 index 0000000..3301ee8 --- /dev/null +++ b/engine/src/Core/ColorFilter.cpp @@ -0,0 +1,39 @@ +#include "Core/ColorFilter.hpp" +#include + +namespace RType { + namespace Core { + + bool ColorFilter::s_colourBlindMode = false; + + bool ColorFilter::IsColourBlindModeEnabled() { + return s_colourBlindMode; + } + + void ColorFilter::SetColourBlindMode(bool enabled) { + s_colourBlindMode = enabled; + } + + Math::Color ColorFilter::ApplyColourBlindFilter(const Math::Color& color) { + if (!s_colourBlindMode) { + return color; + } + + float r = color.r; + float g = color.g; + float b = color.b; + + float newR = 0.567f * r + 0.433f * g + 0.0f * b; + float newG = 0.558f * r + 0.442f * g + 0.0f * b; + float newB = 0.0f * r + 0.242f * g + 0.758f * b; + + newR = std::clamp(newR, 0.0f, 1.0f); + newG = std::clamp(newG, 0.0f, 1.0f); + newB = std::clamp(newB, 0.0f, 1.0f); + + return Math::Color(newR, newG, newB, color.a); + } + + } +} + diff --git a/engine/src/Core/Engine.cpp b/engine/src/Core/Engine.cpp new file mode 100644 index 0000000..9c91b1d --- /dev/null +++ b/engine/src/Core/Engine.cpp @@ -0,0 +1,168 @@ +#include "../../include/Core/Engine.hpp" +#include + +namespace RType { + + namespace Core { + + Engine::Engine(const EngineConfig& config) + : m_config(config) { + Logger::Info("Creating R-Type Engine"); + } + + Engine::~Engine() { + if (m_initialized) { + Shutdown(); + } + } + + bool Engine::Initialize() { + if (m_initialized) { + Logger::Warning("Engine already initialized"); + return true; + } + + Logger::Info("Initializing R-Type Engine..."); + + SortModulesByPriority(); + + if (!InitializeModules()) { + Logger::Error("Failed to initialize modules"); + return false; + } + + InitializeSystems(); + + m_initialized = true; + Logger::Info("R-Type Engine initialized successfully"); + return true; + } + + void Engine::Shutdown() { + if (!m_initialized) { + return; + } + + Logger::Info("Shutting down R-Type Engine..."); + + ShutdownSystems(); + ShutdownModules(); + m_moduleLoader.UnloadAllPlugins(); + + m_initialized = false; + Logger::Info("R-Type Engine shutdown complete"); + } + + IModule* Engine::LoadPlugin(const std::string& pluginPath) { + IModule* module = m_moduleLoader.LoadPlugin(pluginPath); + + if (module && m_initialized) { + Logger::Info("Initializing late-loaded plugin '{}'", module->GetName()); + if (!module->Initialize(this)) { + Logger::Error("Failed to initialize plugin '{}'", module->GetName()); + m_moduleLoader.UnloadPlugin(module->GetName()); + return nullptr; + } + SortModulesByPriority(); + } + + return module; + } + + bool Engine::UnloadPlugin(const std::string& pluginName) { + IModule* module = m_moduleLoader.GetPlugin(pluginName); + + if (module && m_initialized) { + Logger::Info("Shutting down plugin '{}'", pluginName); + module->Shutdown(); + } + + bool result = m_moduleLoader.UnloadPlugin(pluginName); + + if (result) { + SortModulesByPriority(); + } + + return result; + } + + IModule* Engine::GetModuleByName(const std::string& name) { + for (const auto& [_, module] : m_builtinModules) { + if (std::string(module->GetName()) == name) { + return module.get(); + } + } + + return m_moduleLoader.GetPlugin(name); + } + + std::vector Engine::GetAllModules() const { return m_sortedModules; } + + void Engine::SortModulesByPriority() { + m_sortedModules.clear(); + + for (const auto& [_, module] : m_builtinModules) { + m_sortedModules.push_back(module.get()); + } + + for (IModule* plugin : m_moduleLoader.GetAllPlugins()) { + m_sortedModules.push_back(plugin); + } + + std::sort(m_sortedModules.begin(), m_sortedModules.end(), [](IModule* a, IModule* b) { + return static_cast(a->GetPriority()) < static_cast(b->GetPriority()); + }); + } + + bool Engine::InitializeModules() { + Logger::Info("Initializing {} modules...", m_sortedModules.size()); + + for (IModule* module : m_sortedModules) { + Logger::Info("Initializing module '{}'", module->GetName()); + + if (!module->Initialize(this)) { + Logger::Error("Failed to initialize module '{}'", module->GetName()); + return false; + } + } + + return true; + } + + void Engine::ShutdownModules() { + Logger::Info("Shutting down {} modules...", m_sortedModules.size()); + + for (auto it = m_sortedModules.rbegin(); it != m_sortedModules.rend(); ++it) { + IModule* module = *it; + Logger::Info("Shutting down module '{}'", module->GetName()); + module->Shutdown(); + } + } + + void Engine::UpdateSystems(float deltaTime) { + for (auto& system : m_systems) { + system->Update(m_registry, deltaTime); + } + } + + void Engine::InitializeSystems() { + Logger::Info("Initializing {} systems...", m_systems.size()); + + for (auto& system : m_systems) { + Logger::Info("Initializing system '{}'", system->GetName()); + system->Initialize(m_registry); + } + } + + void Engine::ShutdownSystems() { + Logger::Info("Shutting down {} systems...", m_systems.size()); + + for (auto it = m_systems.rbegin(); it != m_systems.rend(); ++it) { + Logger::Info("Shutting down system '{}'", (*it)->GetName()); + (*it)->Shutdown(); + } + } + + } + +} diff --git a/engine/src/Core/InputMapping.cpp b/engine/src/Core/InputMapping.cpp new file mode 100644 index 0000000..7f6c70e --- /dev/null +++ b/engine/src/Core/InputMapping.cpp @@ -0,0 +1,38 @@ +#include "../include/Core/InputMapping.hpp" + +namespace RType { + namespace Core { + + std::unordered_map InputMapping::s_mappings = { + {"MOVE_UP", Renderer::Key::Up}, + {"MOVE_DOWN", Renderer::Key::Down}, + {"MOVE_LEFT", Renderer::Key::Left}, + {"MOVE_RIGHT", Renderer::Key::Right}, + {"SHOOT", Renderer::Key::Space} + }; + + Renderer::Key InputMapping::GetKey(const std::string& action) { + auto it = s_mappings.find(action); + if (it != s_mappings.end()) { + return it->second; + } + return Renderer::Key::Unknown; + } + + void InputMapping::SetKey(const std::string& action, Renderer::Key key) { + s_mappings[action] = key; + } + + void InputMapping::LoadDefaults() { + s_mappings = { + {"MOVE_UP", Renderer::Key::Up}, + {"MOVE_DOWN", Renderer::Key::Down}, + {"MOVE_LEFT", Renderer::Key::Left}, + {"MOVE_RIGHT", Renderer::Key::Right}, + {"SHOOT", Renderer::Key::Space} + }; + } + + } +} + diff --git a/engine/src/Core/ModuleLoader.cpp b/engine/src/Core/ModuleLoader.cpp new file mode 100644 index 0000000..131fab2 --- /dev/null +++ b/engine/src/Core/ModuleLoader.cpp @@ -0,0 +1,200 @@ +#include "../../include/Core/ModuleLoader.hpp" +#include +#include + +namespace RType { + + namespace Core { + + ModuleLoader::~ModuleLoader() { UnloadAllPlugins(); } + + IModule* ModuleLoader::LoadPlugin(const std::string& pluginPath) { + // Check if file exists + if (!std::filesystem::exists(pluginPath)) { + Logger::Error("Plugin file not found: {}", pluginPath); + return nullptr; + } + + // Extract plugin name from path + std::string pluginName = ExtractPluginName(pluginPath); + + // Check if already loaded + if (IsPluginLoaded(pluginName)) { + Logger::Warning("Plugin '{}' is already loaded", pluginName); + return m_loadedPlugins[pluginName].module; + } + + // Load the shared library + LibraryHandle handle = LoadLibraryFromPath(pluginPath); + if (!handle) { + Logger::Error("Failed to load library '{}': {}", pluginPath, GetLastErrorMessage()); + return nullptr; + } + + // Get the CreateModule function + auto createFunc = + reinterpret_cast(GetFunction(handle, "CreateModule")); + if (!createFunc) { + Logger::Error("Failed to find CreateModule function in '{}': {}", pluginPath, + GetLastErrorMessage()); + FreeLibraryHandle(handle); + return nullptr; + } + + // Get the DestroyModule function + auto destroyFunc = + reinterpret_cast(GetFunction(handle, "DestroyModule")); + if (!destroyFunc) { + Logger::Error("Failed to find DestroyModule function in '{}': {}", pluginPath, + GetLastErrorMessage()); + FreeLibraryHandle(handle); + return nullptr; + } + + // Create the module instance + IModule* module = createFunc(); + if (!module) { + Logger::Error("CreateModule returned nullptr for '{}'", pluginPath); + FreeLibraryHandle(handle); + return nullptr; + } + + // Store plugin info + PluginInfo info; + info.name = module->GetName(); + info.path = pluginPath; + info.handle = handle; + info.module = module; + info.destroyFunc = destroyFunc; + + m_loadedPlugins[info.name] = info; + + Logger::Info("Successfully loaded plugin '{}' from '{}'", info.name, pluginPath); + return module; + } + + bool ModuleLoader::UnloadPlugin(const std::string& pluginName) { + auto it = m_loadedPlugins.find(pluginName); + if (it == m_loadedPlugins.end()) { + Logger::Warning("Plugin '{}' not found for unloading", pluginName); + return false; + } + + PluginInfo& info = it->second; + + // Destroy the module + if (info.destroyFunc && info.module) { + info.destroyFunc(info.module); + info.module = nullptr; + } + + // Unload the library + if (info.handle) { + FreeLibraryHandle(info.handle); + info.handle = nullptr; + } + + // Remove from map + m_loadedPlugins.erase(it); + + Logger::Info("Successfully unloaded plugin '{}'", pluginName); + return true; + } + + void ModuleLoader::UnloadAllPlugins() { + // Get all plugin names first (to avoid modifying map while iterating) + std::vector pluginNames; + pluginNames.reserve(m_loadedPlugins.size()); + for (const auto& [name, _] : m_loadedPlugins) { + pluginNames.push_back(name); + } + + // Unload in reverse order (LIFO) + for (auto it = pluginNames.rbegin(); it != pluginNames.rend(); ++it) { + UnloadPlugin(*it); + } + } + + IModule* ModuleLoader::GetPlugin(const std::string& pluginName) { + auto it = m_loadedPlugins.find(pluginName); + if (it == m_loadedPlugins.end()) { + return nullptr; + } + return it->second.module; + } + + std::vector ModuleLoader::GetAllPlugins() const { + std::vector plugins; + plugins.reserve(m_loadedPlugins.size()); + for (const auto& [_, info] : m_loadedPlugins) { + plugins.push_back(info.module); + } + return plugins; + } + + bool ModuleLoader::IsPluginLoaded(const std::string& pluginName) const { + return m_loadedPlugins.find(pluginName) != m_loadedPlugins.end(); + } + + LibraryHandle ModuleLoader::LoadLibraryFromPath(const std::string& path) { +#ifdef _WIN32 + return ::LoadLibraryA(path.c_str()); +#else + return dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); +#endif + } + + void ModuleLoader::FreeLibraryHandle(LibraryHandle handle) { + if (!handle) + return; +#ifdef _WIN32 + ::FreeLibrary(handle); +#else + dlclose(handle); +#endif + } + + void* ModuleLoader::GetFunction(LibraryHandle handle, const std::string& name) { +#ifdef _WIN32 + return reinterpret_cast(::GetProcAddress(handle, name.c_str())); +#else + return dlsym(handle, name.c_str()); +#endif + } + + std::string ModuleLoader::GetLastErrorMessage() { +#ifdef _WIN32 + DWORD error = GetLastError(); + if (error == 0) { + return "No error"; + } + LPSTR messageBuffer = nullptr; + size_t size = + FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPSTR)&messageBuffer, 0, NULL); + std::string message(messageBuffer, size); + LocalFree(messageBuffer); + return message; +#else + const char* error = dlerror(); + return error ? error : "No error"; +#endif + } + + std::string ModuleLoader::ExtractPluginName(const std::string& path) { + std::filesystem::path p(path); + std::string filename = p.stem().string(); + + // Remove common prefixes like "lib" + if (filename.substr(0, 3) == "lib") { + filename = filename.substr(3); + } + + return filename; + } + + } // namespace Core + +} // namespace RType diff --git a/engine/src/ECS/AnimationSystem.cpp b/engine/src/ECS/AnimationSystem.cpp new file mode 100644 index 0000000..7c7dfa8 --- /dev/null +++ b/engine/src/ECS/AnimationSystem.cpp @@ -0,0 +1,195 @@ +#include "ECS/AnimationSystem.hpp" +#include "ECS/Component.hpp" +#include "ECS/Components/TextLabel.hpp" +#include "ECS/Registry.hpp" +#include +#include + +namespace RType { +namespace ECS { + + AnimationSystem::AnimationSystem(Animation::IAnimation* animation) + : m_animation(animation) {} + + void AnimationSystem::Update(Registry& registry, float deltaTime) { + if (!m_animation) { + return; + } + + m_entitiesToDestroy.clear(); + + UpdateSpriteAnimations(registry, deltaTime); + UpdateVisualEffects(registry, deltaTime); + UpdateFloatingTexts(registry, deltaTime); + UpdatePowerUpGlow(registry, deltaTime); + CleanupCompletedAnimations(registry); + } + + void AnimationSystem::UpdateSpriteAnimations(Registry& registry, float deltaTime) { + auto entities = registry.GetEntitiesWithComponent(); + + for (Entity entity : entities) { + if (!registry.IsEntityAlive(entity)) { + continue; + } + + auto& anim = registry.GetComponent(entity); + if (!anim.playing || anim.clipId == Animation::INVALID_CLIP_ID) { + continue; + } + + float clipDuration = m_animation->GetClipDuration(anim.clipId); + if (clipDuration <= 0.0f) { + continue; + } + + bool regionUninitialized = (anim.currentRegion.size.x <= 0.0f || anim.currentRegion.size.y <= 0.0f); + if (regionUninitialized && anim.currentTime == 0.0f) { + auto firstFrame = m_animation->GetFrameAtTime(anim.clipId, 0.0f, anim.looping); + std::size_t firstFrameIndex = m_animation->GetFrameIndexAtTime(anim.clipId, 0.0f, anim.looping); + anim.currentFrameIndex = firstFrameIndex; + anim.currentRegion = firstFrame.region; + if (registry.HasComponent(entity)) { + registry.GetComponent(entity).needsUpdate = true; + } + } + + anim.currentTime += deltaTime * anim.playbackSpeed; + if (anim.currentTime >= clipDuration) { + if (anim.looping) { + anim.currentTime = std::fmod(anim.currentTime, clipDuration); + } else { + anim.currentTime = clipDuration; + anim.playing = false; + + if (anim.destroyOnComplete) { + m_entitiesToDestroy.push_back(entity); + continue; + } + } + } + + auto frame = m_animation->GetFrameAtTime(anim.clipId, anim.currentTime, anim.looping); + std::size_t newFrameIndex = m_animation->GetFrameIndexAtTime( + anim.clipId, anim.currentTime, anim.looping); + + bool frameChanged = (newFrameIndex != anim.currentFrameIndex); + if (frameChanged || frame.region.size.x > 0.0f) { + anim.currentRegion = frame.region; + anim.currentFrameIndex = newFrameIndex; + + if (!frame.eventName.empty() && registry.HasComponent(entity)) { + auto& events = registry.GetComponent(entity); + events.PushEvent(frame.eventName.c_str()); + } + + if (registry.HasComponent(entity)) { + registry.GetComponent(entity).needsUpdate = true; + } + } + } + } + + void AnimationSystem::UpdateVisualEffects(Registry& registry, float deltaTime) { + auto entities = registry.GetEntitiesWithComponent(); + + for (Entity entity : entities) { + if (!registry.IsEntityAlive(entity)) { + continue; + } + + auto& effect = registry.GetComponent(entity); + effect.lifetime += deltaTime; + + if (effect.owner != NULL_ENTITY && registry.IsEntityAlive(effect.owner) && + registry.HasComponent(effect.owner) && + registry.HasComponent(entity)) { + const auto& ownerPos = registry.GetComponent(effect.owner); + auto& effectPos = registry.GetComponent(entity); + + float offsetX = effect.offsetX; + float offsetY = effect.offsetY; + + effectPos.x = ownerPos.x + offsetX; + effectPos.y = ownerPos.y + offsetY; + } + + if (effect.lifetime >= effect.maxLifetime) { + m_entitiesToDestroy.push_back(entity); + } + } + } + + void AnimationSystem::UpdateFloatingTexts(Registry& registry, float deltaTime) { + auto entities = registry.GetEntitiesWithComponent(); + + for (Entity entity : entities) { + if (!registry.IsEntityAlive(entity)) { + continue; + } + + auto& text = registry.GetComponent(entity); + text.lifetime += deltaTime; + + if (registry.HasComponent(entity)) { + auto& pos = registry.GetComponent(entity); + pos.y += text.velocityY * deltaTime; + } + if (text.lifetime >= text.fadeStartTime && text.maxLifetime > text.fadeStartTime) { + float fadeProgress = (text.lifetime - text.fadeStartTime) / + (text.maxLifetime - text.fadeStartTime); + fadeProgress = std::clamp(fadeProgress, 0.0f, 1.0f); + text.color.a = 1.0f - fadeProgress; + + if (registry.HasComponent(entity)) { + auto& label = registry.GetComponent(entity); + label.color.a = text.color.a; + } + } + + if (text.lifetime >= text.maxLifetime) { + m_entitiesToDestroy.push_back(entity); + } + } + } + + void AnimationSystem::UpdatePowerUpGlow(Registry& registry, float deltaTime) { + auto entities = registry.GetEntitiesWithComponent(); + + for (Entity entity : entities) { + if (!registry.IsEntityAlive(entity)) { + continue; + } + + auto& glow = registry.GetComponent(entity); + glow.time += deltaTime * glow.pulseSpeed; + + float pulse = (std::sin(glow.time) + 1.0f) * 0.5f; + float alpha = glow.minAlpha + (glow.maxAlpha - glow.minAlpha) * pulse; + float scaleMultiplier = 1.0f + glow.scalePulse * (pulse * 2.0f - 1.0f); + float scale = glow.baseScale * scaleMultiplier; + + if (registry.HasComponent(entity)) { + auto& drawable = registry.GetComponent(entity); + drawable.tint.a = alpha; + drawable.scale.x = scale; + drawable.scale.y = scale; + } + } + } + + void AnimationSystem::CleanupCompletedAnimations(Registry& registry) { + std::sort(m_entitiesToDestroy.begin(), m_entitiesToDestroy.end()); + m_entitiesToDestroy.erase( + std::unique(m_entitiesToDestroy.begin(), m_entitiesToDestroy.end()), + m_entitiesToDestroy.end()); + + for (Entity entity : m_entitiesToDestroy) { + if (registry.IsEntityAlive(entity)) { + registry.DestroyEntity(entity); + } + } + } + +} +} diff --git a/engine/src/ECS/AudioSystem.cpp b/engine/src/ECS/AudioSystem.cpp new file mode 100644 index 0000000..8ca9a0a --- /dev/null +++ b/engine/src/ECS/AudioSystem.cpp @@ -0,0 +1,66 @@ +#include "../../include/ECS/AudioSystem.hpp" +#include "../../include/ECS/Component.hpp" + +namespace RType { + namespace ECS { + + void AudioSystem::Update(Registry& registry, float) { + if (!m_audio) { + return; + } + + auto musicEntities = registry.GetEntitiesWithComponent(); + for (auto entity : musicEntities) { + if (!registry.IsEntityAlive(entity) || + !registry.HasComponent(entity)) { + continue; + } + + auto& music = registry.GetComponent(entity); + if (music.musicId == Audio::INVALID_MUSIC_ID) { + registry.RemoveComponent(entity); + continue; + } + + if (music.stop) { + m_audio->StopMusic(music.musicId); + } else if (music.play) { + Audio::PlaybackOptions opts; + opts.volume = music.volume; + opts.pitch = music.pitch; + opts.loop = music.loop; + m_audio->PlayMusic(music.musicId, opts); + } + + registry.RemoveComponent(entity); + } + + auto entities = registry.GetEntitiesWithComponent(); + + for (auto entity : entities) { + if (!registry.IsEntityAlive(entity) || + !registry.HasComponent(entity)) { + continue; + } + + auto& sound = registry.GetComponent(entity); + if (sound.soundId == Audio::INVALID_SOUND_ID) { + registry.RemoveComponent(entity); + continue; + } + + Audio::PlaybackOptions opts; + opts.volume = sound.volume; + opts.pitch = sound.pitch; + opts.pan = sound.pan; + opts.loop = sound.loop; + + + m_audio->PlaySound(sound.soundId, opts); + registry.RemoveComponent(entity); + } + } + + } +} + diff --git a/engine/src/ECS/BlackOrbSystem.cpp b/engine/src/ECS/BlackOrbSystem.cpp new file mode 100644 index 0000000..3bff0a2 --- /dev/null +++ b/engine/src/ECS/BlackOrbSystem.cpp @@ -0,0 +1,110 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** BlackOrbSystem implementation +*/ + +#include "ECS/BlackOrbSystem.hpp" +#include "ECS/Component.hpp" +#include "Core/Logger.hpp" +#include + +namespace RType { + namespace ECS { + + void BlackOrbSystem::Update(Registry& registry, float deltaTime) { + auto orbs = registry.GetEntitiesWithComponent(); + + for (auto orb : orbs) { + if (!registry.IsEntityAlive(orb)) { + continue; + } + + auto& blackOrb = registry.GetComponent(orb); + if (!blackOrb.isActive) { + continue; + } + + const auto& orbPos = registry.GetComponent(orb); + + auto bullets = registry.GetEntitiesWithComponent(); + for (auto bullet : bullets) { + if (!registry.IsEntityAlive(bullet)) { + continue; + } + + if (registry.HasComponent(bullet) || registry.HasComponent(bullet)) { + continue; + } + + const auto& bulletPos = registry.GetComponent(bullet); + float dx = orbPos.x - bulletPos.x; + float dy = orbPos.y - bulletPos.y; + float distance = std::sqrt(dx * dx + dy * dy); + + // Destroy bullet if within absorption radius + if (distance < blackOrb.absorptionRadius) { + registry.DestroyEntity(bullet); + continue; + } + + // Apply attraction force if within attraction radius + if (distance < blackOrb.attractionRadius) { + if (registry.HasComponent(bullet)) { + auto& bulletVel = registry.GetComponent(bullet); + + float dirX = dx / distance; + float dirY = dy / distance; + + float attractionStrength = blackOrb.attractionForce * (1.0f - distance / blackOrb.attractionRadius); + bulletVel.dx += dirX * attractionStrength * deltaTime; + bulletVel.dy += dirY * attractionStrength * deltaTime; + } + } + } + + if (registry.HasComponent(orb)) { + auto& proxDamage = registry.GetComponent(orb); + proxDamage.timeSinceDamage += deltaTime; + + if (proxDamage.timeSinceDamage >= proxDamage.tickRate) { + proxDamage.timeSinceDamage = 0.0f; + + auto players = registry.GetEntitiesWithComponent(); + for (auto player : players) { + if (!registry.IsEntityAlive(player)) { + continue; + } + + const auto& playerPos = registry.GetComponent(player); + float dx = playerPos.x - orbPos.x; + float dy = playerPos.y - orbPos.y; + float distance = std::sqrt(dx * dx + dy * dy); + + if (distance < proxDamage.damageRadius) { + if (registry.HasComponent(player)) { + continue; + } + + // Apply proximity damage + if (registry.HasComponent(player)) { + auto& health = registry.GetComponent(player); + health.current -= static_cast(proxDamage.damageAmount); + + if (health.current < 0) { + health.current = 0; + } + + Core::Logger::Debug("[BlackOrbSystem] Proximity damage {} to player", + proxDamage.damageAmount); + } + } + } + } + } + } + } + + } +} diff --git a/engine/src/ECS/BossAttackSystem.cpp b/engine/src/ECS/BossAttackSystem.cpp new file mode 100644 index 0000000..7615dc3 --- /dev/null +++ b/engine/src/ECS/BossAttackSystem.cpp @@ -0,0 +1,398 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** BossAttackSystem - Handles boss attack patterns +*/ + +#include "ECS/BossAttackSystem.hpp" +#include "ECS/Component.hpp" +#include "Core/Logger.hpp" +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +namespace RType { + namespace ECS { + + void BossAttackSystem::Update(Registry& registry, float deltaTime) { + auto bosses = registry.GetEntitiesWithComponent(); + + for (auto bossEntity : bosses) { + if (!registry.IsEntityAlive(bossEntity)) { + continue; + } + + if (registry.HasComponent(bossEntity)) { + continue; + } + + if (registry.HasComponent(bossEntity)) { + continue; + } + + if (!registry.HasComponent(bossEntity) || + !registry.HasComponent(bossEntity) || + !registry.HasComponent(bossEntity)) { + continue; + } + + auto& attack = registry.GetComponent(bossEntity); + const auto& pos = registry.GetComponent(bossEntity); + const auto& boss = registry.GetComponent(bossEntity); + + attack.timeSinceLastAttack += deltaTime; + + if (attack.timeSinceLastAttack >= attack.attackCooldown) { + attack.timeSinceLastAttack = 0.0f; + + if (boss.bossId == 1) { + switch (attack.currentPattern) { + case BossAttackPattern::FAN_SPRAY: + CreateContinuousFire(registry, bossEntity, pos.x, pos.y); + attack.currentPattern = BossAttackPattern::SPIRAL_WAVE; + break; + case BossAttackPattern::SPIRAL_WAVE: + CreateMine(registry, bossEntity, pos.x, pos.y); + attack.currentPattern = BossAttackPattern::FAN_SPRAY; + break; + default: + CreateContinuousFire(registry, bossEntity, pos.x, pos.y); + attack.currentPattern = BossAttackPattern::SPIRAL_WAVE; + break; + } + } else if (boss.bossId == 2) { + switch (attack.currentPattern) { + case BossAttackPattern::ANIMATED_ORB: + CreateAnimatedOrb(registry, bossEntity, pos.x, pos.y); + attack.currentPattern = BossAttackPattern::SPIRAL_WAVE; // Switch to second attack + break; + case BossAttackPattern::SPIRAL_WAVE: + CreateSecondAttackSpray(registry, bossEntity, pos.x, pos.y); + attack.currentPattern = BossAttackPattern::ANIMATED_ORB; // Switch back to first attack + break; + default: + CreateAnimatedOrb(registry, bossEntity, pos.x, pos.y); + attack.currentPattern = BossAttackPattern::SPIRAL_WAVE; + break; + } + } else if (boss.bossId == 3) { + switch (attack.currentPattern) { + case BossAttackPattern::FAN_SPRAY: + CreateFanSpray(registry, bossEntity, pos.x, pos.y); + attack.currentPattern = BossAttackPattern::THIRD_BULLET; + break; + case BossAttackPattern::THIRD_BULLET: + CreateThirdBullet(registry, bossEntity, pos.x, pos.y); + attack.currentPattern = BossAttackPattern::BLACK_ORB; + break; + case BossAttackPattern::BLACK_ORB: + CreateBlackOrb(registry, bossEntity, pos.x, pos.y); + attack.currentPattern = BossAttackPattern::FAN_SPRAY; + break; + default: + break; + } + } + } + } + } + + void BossAttackSystem::CreateFanSpray(Registry& registry, Entity bossEntity, float bossX, float bossY) { + const int bulletCount = 10; + const float spreadAngle = M_PI / 3; + const float bulletSpeed = 300.0f; + + const float baseAngle = M_PI; + const float startAngle = baseAngle - spreadAngle / 2.0f; + + const float shootX = bossX + 64.0f; + const float shootY = bossY + 188.0f; + + Core::Logger::Info("[BossAttackSystem] Boss firing fan spray with {} bullets", bulletCount); + + for (int i = 0; i < bulletCount; i++) { + float angle = startAngle + (spreadAngle * i / (bulletCount - 1)); + CreateBossBullet(registry, shootX, shootY, angle, bulletSpeed); + } + } + + void BossAttackSystem::CreateBossBullet(Registry& registry, float x, float y, float angle, float speed) { + Entity bullet = registry.CreateEntity(); + + // CRITICAL FIX: Clean up obstacle components from entity ID reuse + if (registry.HasComponent(bullet)) { + std::cerr << "[BOSS CLEANUP] Removing Obstacle from bullet entity " << bullet << std::endl; + registry.RemoveComponent(bullet); + } + if (registry.HasComponent(bullet)) { + std::cerr << "[BOSS CLEANUP] Removing ObstacleMetadata from bullet entity " << bullet << std::endl; + registry.RemoveComponent(bullet); + } + + registry.AddComponent(bullet, Position{x, y}); + + float vx = std::cos(angle) * speed; + float vy = std::sin(angle) * speed; + registry.AddComponent(bullet, Velocity{vx, vy}); + + registry.AddComponent(bullet, Bullet{false}); + + registry.AddComponent(bullet, BossBullet{}); + + registry.AddComponent(bullet, Damage{5}); + + registry.AddComponent(bullet, CircleCollider{10.0f}); + + registry.AddComponent(bullet, + CollisionLayer(CollisionLayers::ENEMY_BULLET, + CollisionLayers::PLAYER)); + + Core::Logger::Debug("[BossAttackSystem] Created bullet at ({}, {}) angle={} speed={}", + x, y, angle, speed); + } + + void BossAttackSystem::CreateBlackOrb(Registry& registry, Entity bossEntity, float bossX, float bossY) { + Entity orb = registry.CreateEntity(); + + // CRITICAL FIX: Clean up obstacle components from entity ID reuse + if (registry.HasComponent(orb)) { + std::cerr << "[BOSS CLEANUP] Removing Obstacle from orb entity " << orb << std::endl; + registry.RemoveComponent(orb); + } + if (registry.HasComponent(orb)) { + std::cerr << "[BOSS CLEANUP] Removing ObstacleMetadata from orb entity " << orb << std::endl; + registry.RemoveComponent(orb); + } + + const float spawnX = bossX + 70.0f; + const float spawnY = bossY + 160.0f; + + registry.AddComponent(orb, Position{spawnX, spawnY}); + + + float vx, vy; + int trajectory = rand() % 3; + const float speed = 70.0f; + + switch(trajectory) { + case 0: + vx = -speed; + vy = 0.0f; + break; + case 1: + vx = -speed * 0.657f; + vy = speed * 0.657f; + break; + case 2: + vx = -speed; + vy = 30.0f; + break; + default: + vx = -speed * 0.707f; + vy = speed * 0.707f; + break; + } + + registry.AddComponent(orb, Velocity{vx, vy}); + + registry.AddComponent(orb, Bullet{bossEntity}); + + registry.AddComponent(orb, BlackOrb{900.0f, 110.0f, 1900.0f}); + registry.AddComponent(orb, ProximityDamage{150.0f, 2.0f, 0.2f}); + + Core::Logger::Info("[BossAttackSystem] Created Black Orb at ({}, {}) trajectory={}", + spawnX, spawnY, trajectory); + } + + void BossAttackSystem::CreateThirdBullet(Registry& registry, Entity bossEntity, float bossX, float bossY) { + Entity thirdBullet = registry.CreateEntity(); + + // CRITICAL FIX: Clean up obstacle components from entity ID reuse + if (registry.HasComponent(thirdBullet)) { + std::cerr << "[BOSS CLEANUP] Removing Obstacle from third bullet entity " << thirdBullet << std::endl; + registry.RemoveComponent(thirdBullet); + } + if (registry.HasComponent(thirdBullet)) { + std::cerr << "[BOSS CLEANUP] Removing ObstacleMetadata from third bullet entity " << thirdBullet << std::endl; + registry.RemoveComponent(thirdBullet); + } + + const float spawnX = bossX + 350.0f; + const float spawnY = bossY + -30.0f; + + registry.AddComponent(thirdBullet, Position{spawnX, spawnY}); + + const float speed = 150.0f; + registry.AddComponent(thirdBullet, Velocity{-speed, 0.0f}); + + registry.AddComponent(thirdBullet, Bullet{bossEntity}); + + registry.AddComponent(thirdBullet, ThirdBullet{0.4f, 50}); + + registry.AddComponent(thirdBullet, CircleCollider{30.0f}); + registry.AddComponent(thirdBullet, + CollisionLayer(CollisionLayers::OBSTACLE, CollisionLayers::PLAYER)); + registry.AddComponent(thirdBullet, Damage{50}); + + registry.AddComponent(thirdBullet, BossBullet{}); + + Core::Logger::Info("[BossAttackSystem] Created Third Bullet at ({}, {})", spawnX, spawnY); + } + + void BossAttackSystem::CreateAnimatedOrb(Registry& registry, Entity bossEntity, float bossX, float bossY) { + const int orbCount = 4; + const float orbSpacing = 100.0f; + const float baseSpeed = 350.0f; + + const float spawnX = bossX + 50.0f; + const float startY = bossY + 50.0f; + + for (int i = 0; i < orbCount; i++) { + Entity orb = registry.CreateEntity(); + + if (registry.HasComponent(orb)) { + registry.RemoveComponent(orb); + } + if (registry.HasComponent(orb)) { + registry.RemoveComponent(orb); + } + + float orbY = startY + (i * orbSpacing); + registry.AddComponent(orb, Position{spawnX, orbY}); + + float speedVariation = baseSpeed + (i * 10.0f); + float angle = -M_PI + (i * 0.1f); + float vx = std::cos(angle) * speedVariation; + float vy = std::sin(angle) * speedVariation * 0.3f; + + registry.AddComponent(orb, Velocity{vx, vy}); + + registry.AddComponent(orb, Bullet{bossEntity}); + registry.AddComponent(orb, BossBullet{}); + registry.AddComponent(orb, WaveAttack{}); + + registry.AddComponent(orb, CircleCollider{15.0f}); + registry.AddComponent(orb, + CollisionLayer(CollisionLayers::ENEMY_BULLET, CollisionLayers::PLAYER)); + registry.AddComponent(orb, Damage{20}); + } + + Core::Logger::Info("[BossAttackSystem] Boss 2 created wave attack burst with {} orbs", orbCount); + } + + void BossAttackSystem::CreateSecondAttackSpray(Registry& registry, Entity bossEntity, float bossX, float bossY) { + const int orbCount = 32; + const float baseSpeed = 300.0f; + + const float spawnX = bossX + 114.0f; + const float spawnY = bossY + 94.0f; + + const float fullCircle = 2.0f * M_PI; + const float angleStep = fullCircle / orbCount; + + for (int i = 0; i < orbCount; i++) { + Entity orb = registry.CreateEntity(); + + if (registry.HasComponent(orb)) { + registry.RemoveComponent(orb); + } + if (registry.HasComponent(orb)) { + registry.RemoveComponent(orb); + } + + registry.AddComponent(orb, Position{spawnX, spawnY}); + + float angle = i * angleStep; + + float speedVariation = baseSpeed + (std::sin(i * 0.5f) * 50.0f); + + float vx = std::cos(angle) * speedVariation; + float vy = std::sin(angle) * speedVariation; + + registry.AddComponent(orb, Velocity{vx, vy}); + + registry.AddComponent(orb, Bullet{bossEntity}); + registry.AddComponent(orb, BossBullet{}); + registry.AddComponent(orb, SecondAttack{}); + + registry.AddComponent(orb, CircleCollider{20.0f}); + registry.AddComponent(orb, + CollisionLayer(CollisionLayers::ENEMY_BULLET, CollisionLayers::PLAYER)); + registry.AddComponent(orb, Damage{10}); + } + + Core::Logger::Info("[BossAttackSystem] Boss 2 created massive 360° spray with {} orbs", orbCount); + } + + void BossAttackSystem::CreateContinuousFire(Registry& registry, Entity bossEntity, float bossX, float bossY) { + const float baseSpeed = 350.0f; + + const float spawnX = bossX + 20.0f; + const float spawnY = bossY + 100.0f; + + Entity bullet = registry.CreateEntity(); + + if (registry.HasComponent(bullet)) { + registry.RemoveComponent(bullet); + } + if (registry.HasComponent(bullet)) { + registry.RemoveComponent(bullet); + } + + registry.AddComponent(bullet, Position{spawnX, spawnY}); + + float vx = -baseSpeed; + float vy = 0.0f; + registry.AddComponent(bullet, Velocity{vx, vy}); + + registry.AddComponent(bullet, Bullet{bossEntity}); + registry.AddComponent(bullet, BossBullet{}); + registry.AddComponent(bullet, FireBullet{}); + + registry.AddComponent(bullet, CircleCollider{12.0f}); + registry.AddComponent(bullet, + CollisionLayer(CollisionLayers::ENEMY_BULLET, CollisionLayers::PLAYER)); + registry.AddComponent(bullet, Damage{12}); + + Core::Logger::Debug("[BossAttackSystem] Boss 3 fired single bullet"); + } + + void BossAttackSystem::CreateMine(Registry& registry, Entity bossEntity, float bossX, float bossY) { + const float minX = bossX + 200.0f; + const float maxX = bossX + 600.0f; + float spawnX = minX + (static_cast(rand()) / RAND_MAX) * (maxX - minX); + + const float minY = 100.0f; + const float maxY = 650.0f; + float spawnY = minY + (static_cast(rand()) / RAND_MAX) * (maxY - minY); + + Entity mine = registry.CreateEntity(); + + if (registry.HasComponent(mine)) { + registry.RemoveComponent(mine); + } + if (registry.HasComponent(mine)) { + registry.RemoveComponent(mine); + } + + registry.AddComponent(mine, Position{spawnX, spawnY}); + + const float scrollSpeed = -100.0f; + registry.AddComponent(mine, Velocity{scrollSpeed, 0.0f}); + + registry.AddComponent(mine, Mine{50.0f, 100.0f, 20.0f}); + + registry.AddComponent(mine, CircleCollider{15.0f}); + registry.AddComponent(mine, + CollisionLayer(CollisionLayers::OBSTACLE, CollisionLayers::PLAYER)); + registry.AddComponent(mine, Damage{35}); + + Core::Logger::Debug("[BossAttackSystem] Boss 3 deployed mine at ({}, {})", spawnX, spawnY); + } + + } +} diff --git a/engine/src/ECS/BossSystem.cpp b/engine/src/ECS/BossSystem.cpp new file mode 100644 index 0000000..16e0826 --- /dev/null +++ b/engine/src/ECS/BossSystem.cpp @@ -0,0 +1,104 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** BossSystem - Handles boss behavior +*/ + +#include "ECS/BossSystem.hpp" +#include "ECS/Component.hpp" +#include "Core/Logger.hpp" +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +namespace RType { + namespace ECS { + + + void StopScrollingAndFixPosition(Registry& registry, Entity entity, float fixedX) { + if (!registry.HasComponent(entity)) { + return; + } + + // Remove scrollable component to stop movement + registry.RemoveComponent(entity); + + // Fix position + if (registry.HasComponent(entity)) { + auto& pos = registry.GetComponent(entity); + pos.x = fixedX; + Core::Logger::Info("[BossSystem] Entity {} stopped scrolling, fixed at x={}", + static_cast(entity), fixedX); + } + } + + bool HasEnteredScreen(Registry& registry, Entity entity, float screenWidth) { + if (!registry.HasComponent(entity)) { + return false; + } + + const auto& pos = registry.GetComponent(entity); + return pos.x < screenWidth; + } + + void BossSystem::Update(Registry& registry, float deltaTime) { + // Process all boss entities + auto bosses = registry.GetEntitiesWithComponent(); + + for (auto bossEntity : bosses) { + if (!registry.IsEntityAlive(bossEntity)) { + continue; + } + + // If boss has entered screen and is still scrolling, stop it + if (registry.HasComponent(bossEntity) && + HasEnteredScreen(registry, bossEntity, 1920.0f)) { + StopScrollingAndFixPosition(registry, bossEntity, 900.0f); + } + + if (registry.HasComponent(bossEntity) && + !registry.HasComponent(bossEntity) && + registry.HasComponent(bossEntity)) { + + auto& movement = registry.GetComponent(bossEntity); + auto& pos = registry.GetComponent(bossEntity); + + movement.timer += deltaTime; + + const float pi = 3.14159265358979323846f; + + // Vertical movement + float timeY = movement.timer * movement.frequencyY * 2.0f * pi; + float newY = movement.centerY + movement.amplitudeY * std::sin(static_cast(timeY)); + + // Horizontal oscillation + float timeX = movement.timer * movement.frequencyX * 2.0f * pi; + float newX = movement.centerX + movement.amplitudeX * std::sin(static_cast(timeX * 2.0)); + + const float minY = 100.0f; + const float maxY = 620.0f; + const float minX = 800.0f; + const float maxX = 1000.0f; + + pos.y = std::max(minY, std::min(maxY, newY)); + pos.x = std::max(minX, std::min(maxX, newX)); + } + + if (registry.HasComponent(bossEntity)) { + auto& flash = registry.GetComponent(bossEntity); + if (flash.isActive) { + flash.timeRemaining -= deltaTime; + if (flash.timeRemaining <= 0.0f) { + flash.isActive = false; + flash.timeRemaining = 0.0f; + } + } + } + } + } + + } +} diff --git a/engine/src/ECS/BulletCollisionResponseSystem.cpp b/engine/src/ECS/BulletCollisionResponseSystem.cpp new file mode 100644 index 0000000..f56c3e1 --- /dev/null +++ b/engine/src/ECS/BulletCollisionResponseSystem.cpp @@ -0,0 +1,189 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** BulletCollisionResponseSystem implementation +*/ + +#include "ECS/BulletCollisionResponseSystem.hpp" +#include "ECS/EffectFactory.hpp" +#include "Core/Logger.hpp" +#include +#include + +namespace RType { + namespace ECS { + + void BulletCollisionResponseSystem::Update(Registry& registry, float deltaTime) { + auto bullets = registry.GetEntitiesWithComponent(); + + static std::unordered_map lastBeamDamageTime; + static float totalTime = 0.0f; + totalTime += deltaTime; + + auto it = lastBeamDamageTime.begin(); + while (it != lastBeamDamageTime.end()) { + uint64_t key = it->first; + Entity bulletEntity = static_cast(key & 0xFFFFFFFF); + Entity otherEntity = static_cast((key >> 32) & 0xFFFFFFFF); + + if (!registry.IsEntityAlive(bulletEntity) || !registry.IsEntityAlive(otherEntity)) { + it = lastBeamDamageTime.erase(it); + } else { + ++it; + } + } + + for (auto bullet : bullets) { + if (!registry.IsEntityAlive(bullet)) { + continue; + } + + if (!registry.HasComponent(bullet)) { + continue; + } + + auto& event = registry.GetComponent(bullet); + Entity other = event.other; + + if (!registry.IsEntityAlive(other)) { + continue; + } + + bool isBeam = false; + if (registry.HasComponent(bullet)) { + const auto& collider = registry.GetComponent(bullet); + isBeam = (collider.width > 500.0f); + } + + bool hitEnemy = registry.HasComponent(other); + bool hitBoss = registry.HasComponent(other); + bool hitPlayer = registry.HasComponent(other); + bool hitObstacle = registry.HasComponent(other); + + bool isEnemyBullet = false; + if (registry.HasComponent(bullet)) { + const auto& bulletLayer = registry.GetComponent(bullet); + isEnemyBullet = (bulletLayer.layer == CollisionLayers::ENEMY_BULLET); + } + + if (isEnemyBullet && (hitEnemy || hitBoss)) { + continue; + } + + bool shouldApplyDamage = true; + constexpr float BEAM_DAMAGE_TICK_INTERVAL = 0.1f; + + if (isBeam && (hitEnemy || hitBoss)) { + uint64_t damageKey = (static_cast(other) << 32) | static_cast(bullet); + + auto damageIt = lastBeamDamageTime.find(damageKey); + if (damageIt != lastBeamDamageTime.end()) { + float timeSinceLastDamage = totalTime - damageIt->second; + if (timeSinceLastDamage < BEAM_DAMAGE_TICK_INTERVAL) { + shouldApplyDamage = false; + } + } + + if (shouldApplyDamage) { + lastBeamDamageTime[damageKey] = totalTime; + } + } + + // Handle enemy damage + if (hitEnemy && registry.HasComponent(other) && shouldApplyDamage) { + auto& health = registry.GetComponent(other); + const auto& damage = registry.GetComponent(bullet); + + int actualDamage = damage.amount; + if (isBeam) { + actualDamage = damage.amount / 10; + } + + health.current -= actualDamage; + + if (m_effectFactory && registry.HasComponent(bullet) && !isBeam) { + const auto& bulletPos = registry.GetComponent(bullet); + m_effectFactory->CreateHitEffect(registry, bulletPos.x, bulletPos.y); + } + + if (health.current <= 0) { + const auto& enemyComp = registry.GetComponent(other); + const auto& bulletComp = registry.GetComponent(bullet); + registry.AddComponent(other, + EnemyKilled(enemyComp.id, bulletComp.owner)); + if (isBeam) { + uint64_t damageKey = (static_cast(other) << 32) | static_cast(bullet); + lastBeamDamageTime.erase(damageKey); + } + } + } + + // Handle boss damage + if (hitBoss && registry.HasComponent(other) && shouldApplyDamage) { + auto& health = registry.GetComponent(other); + const auto& damage = registry.GetComponent(bullet); + + int actualDamage = damage.amount; + if (isBeam) { + actualDamage = damage.amount / 10; + } + + health.current -= actualDamage; + + if (registry.HasComponent(other)) { + auto& flash = registry.GetComponent(other); + flash.Trigger(); + } + + const auto& bulletComp = registry.GetComponent(bullet); + if (bulletComp.owner != NULL_ENTITY && registry.IsEntityAlive(bulletComp.owner)) { + if (registry.HasComponent(bulletComp.owner)) { + auto& score = registry.GetComponent(bulletComp.owner); + score.points += actualDamage * 5; + } + } + + if (health.current <= 0) { + health.current = 0; + } + } + + if (hitPlayer && registry.HasComponent(other)) { + if (registry.HasComponent(other)) { + continue; + } else { + auto& health = registry.GetComponent(other); + const auto& damage = registry.GetComponent(bullet); + health.current -= damage.amount; + + if (health.current < 0) { + health.current = 0; + } + } + } + + bool shouldDestroy = false; + if (hitObstacle) { + const auto& obstacle = registry.GetComponent(other); + if (obstacle.blocking) { + shouldDestroy = true; + + if (m_effectFactory && registry.HasComponent(bullet) && !isBeam) { + const auto& bulletPos = registry.GetComponent(bullet); + m_effectFactory->CreateHitEffect(registry, bulletPos.x, bulletPos.y); + } + } + } + + if (isBeam) { + } else { + if (hitEnemy || hitBoss || hitPlayer || shouldDestroy) { + registry.DestroyEntity(bullet); + } + } + } + } + + } +} diff --git a/engine/src/ECS/CMakeLists.txt b/engine/src/ECS/CMakeLists.txt new file mode 100644 index 0000000..5aca64c --- /dev/null +++ b/engine/src/ECS/CMakeLists.txt @@ -0,0 +1,129 @@ +# ECS CMakeLists.txt +# +# This builds two libraries: +# 1. rtype_ecs_core - Generic ECS components and systems (reusable by any game) +# 2. rtype_ecs - Full ECS including R-Type specific code (for backwards compatibility) + +# ============================================================================= +# Generic ECS Core Library (game-agnostic) +# ============================================================================= +set(ECS_CORE_SOURCES + Registry.cpp + MovementSystem.cpp + RenderingSystem.cpp + TextRenderingSystem.cpp + CollisionDetectionSystem.cpp + ScrollingSystem.cpp + AnimationSystem.cpp + AudioSystem.cpp + InputSystem.cpp + MenuSystem.cpp + HealthSystem.cpp + ScoreSystem.cpp +) + +set(ECS_CORE_HEADERS + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/Entity.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/Registry.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/ISystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/SparseArray.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/Component.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/Components/TextLabel.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/Components/Clickable.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/MovementSystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/RenderingSystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/TextRenderingSystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/CollisionDetectionSystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/ScrollingSystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/AnimationSystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/AudioSystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/InputSystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/MenuSystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/HealthSystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/ScoreSystem.hpp +) + +add_library(rtype_ecs_core STATIC + ${ECS_CORE_SOURCES} + ${ECS_CORE_HEADERS} +) + +target_include_directories(rtype_ecs_core PUBLIC + $ + $ +) + +target_link_libraries(rtype_ecs_core PUBLIC nlohmann_json::nlohmann_json rtype_animation) + +set_target_properties(rtype_ecs_core PROPERTIES + POSITION_INDEPENDENT_CODE ON +) + +# ============================================================================= +# R-Type Specific ECS Extensions (for R-Type game only) +# ============================================================================= +set(RTYPE_ECS_SOURCES + PlayerSystem.cpp + PlayerFactory.cpp + BulletCollisionResponseSystem.cpp + PlayerCollisionResponseSystem.cpp + ObstacleCollisionResponseSystem.cpp + EnemySystem.cpp + EnemyFactory.cpp + ShootingSystem.cpp + LevelLoader.cpp + PowerUpFactory.cpp + PowerUpSpawnSystem.cpp + PowerUpCollisionSystem.cpp + ForcePodSystem.cpp + ShieldSystem.cpp + BossSystem.cpp + BossAttackSystem.cpp + MineSystem.cpp + BlackOrbSystem.cpp + ThirdBulletSystem.cpp + EffectFactory.cpp +) + +set(RTYPE_ECS_HEADERS + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/PlayerSystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/PlayerFactory.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/BulletCollisionResponseSystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/PlayerCollisionResponseSystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/ObstacleCollisionResponseSystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/EnemySystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/EnemyFactory.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/ShootingSystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/LevelLoader.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/PowerUpFactory.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/PowerUpSpawnSystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/PowerUpCollisionSystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/ForcePodSystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/ShieldSystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/BossSystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/BossAttackSystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/MineSystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/BlackOrbSystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/ThirdBulletSystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/EffectFactory.hpp +) + +# Full rtype_ecs library (includes both core and R-Type specific) +# This maintains backwards compatibility with existing code +add_library(rtype_ecs STATIC + ${ECS_CORE_SOURCES} + ${RTYPE_ECS_SOURCES} + ${ECS_CORE_HEADERS} + ${RTYPE_ECS_HEADERS} +) + +target_include_directories(rtype_ecs PUBLIC + $ + $ +) + +target_link_libraries(rtype_ecs PUBLIC nlohmann_json::nlohmann_json rtype_animation) + +set_target_properties(rtype_ecs PROPERTIES + POSITION_INDEPENDENT_CODE ON +) diff --git a/engine/src/ECS/CollisionDetectionSystem.cpp b/engine/src/ECS/CollisionDetectionSystem.cpp new file mode 100644 index 0000000..17fd2e5 --- /dev/null +++ b/engine/src/ECS/CollisionDetectionSystem.cpp @@ -0,0 +1,155 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** CollisionDetectionSystem implementation +*/ + +#include "ECS/CollisionDetectionSystem.hpp" +#include +#include + +namespace RType { + namespace ECS { + + void CollisionDetectionSystem::Update(Registry& registry, float deltaTime) { + (void)deltaTime; + ClearCollisionEvents(registry); + + auto entities = GetCollidableEntities(registry); + + for (size_t i = 0; i < entities.size(); ++i) { + for (size_t j = i + 1; j < entities.size(); ++j) { + Entity a = entities[i]; + Entity b = entities[j]; + + if (!ShouldCollide(registry, a, b)) { + continue; + } + + if (CheckCollision(registry, a, b)) { + registry.AddComponent(a, CollisionEvent(b)); + registry.AddComponent(b, CollisionEvent(a)); + } + } + } + } + + void CollisionDetectionSystem::ClearCollisionEvents(Registry& registry) { + auto entitiesWithEvents = registry.GetEntitiesWithComponent(); + for (auto entity : entitiesWithEvents) { + if (registry.IsEntityAlive(entity)) { + registry.RemoveComponent(entity); + } + } + } + + std::vector CollisionDetectionSystem::GetCollidableEntities(Registry& registry) { + std::vector collidableEntities; + + auto allEntities = registry.GetEntitiesWithComponent(); + + for (auto entity : allEntities) { + bool hasCircleCollider = registry.HasComponent(entity); + bool hasBoxCollider = registry.HasComponent(entity); + + if (hasCircleCollider || hasBoxCollider) { + collidableEntities.push_back(entity); + } + } + + return collidableEntities; + } + + bool CollisionDetectionSystem::ShouldCollide(Registry& registry, Entity a, Entity b) { + if (!registry.HasComponent(a) || !registry.HasComponent(b)) { + return true; + } + const auto& layerA = registry.GetComponent(a); + const auto& layerB = registry.GetComponent(b); + + bool aCanCollideWithB = (layerA.mask & layerB.layer) != 0; + bool bCanCollideWithA = (layerB.mask & layerA.layer) != 0; + + return aCanCollideWithB && bCanCollideWithA; + } + + bool CollisionDetectionSystem::CheckCollision(Registry& registry, Entity a, Entity b) { + if (!registry.HasComponent(a) || !registry.HasComponent(b)) { + return false; + } + + const auto& posA = registry.GetComponent(a); + const auto& posB = registry.GetComponent(b); + + bool aHasCircle = registry.HasComponent(a); + bool bHasCircle = registry.HasComponent(b); + bool aHasBox = registry.HasComponent(a); + bool bHasBox = registry.HasComponent(b); + + if (aHasCircle && bHasCircle) { + const auto& circleA = registry.GetComponent(a); + const auto& circleB = registry.GetComponent(b); + return CheckCircleCircle(posA.x, posA.y, circleA.radius, + posB.x, posB.y, circleB.radius); + } + + if (aHasBox && bHasBox) { + const auto& boxA = registry.GetComponent(a); + const auto& boxB = registry.GetComponent(b); + return CheckAABB(posA.x, posA.y, boxA.width, boxA.height, + posB.x, posB.y, boxB.width, boxB.height); + } + + if (aHasCircle && bHasBox) { + const auto& circle = registry.GetComponent(a); + const auto& box = registry.GetComponent(b); + return CheckCircleAABB(posA.x, posA.y, circle.radius, + posB.x, posB.y, box.width, box.height); + } + + if (aHasBox && bHasCircle) { + const auto& box = registry.GetComponent(a); + const auto& circle = registry.GetComponent(b); + return CheckCircleAABB(posB.x, posB.y, circle.radius, + posA.x, posA.y, box.width, box.height); + } + + return false; + } + + bool CollisionDetectionSystem::CheckCircleCircle(float x1, float y1, float r1, + float x2, float y2, float r2) { + float dx = x2 - x1; + float dy = y2 - y1; + float radiusSum = r1 + r2; + float distanceSquared = dx * dx + dy * dy; + float radiusSumSquared = radiusSum * radiusSum; + + return distanceSquared <= radiusSumSquared; + } + + bool CollisionDetectionSystem::CheckAABB(float x1, float y1, float w1, float h1, + float x2, float y2, float w2, float h2) { + bool separated = + (x1 + w1 <= x2) || + (x1 >= x2 + w2) || + (y1 + h1 <= y2) || + (y1 >= y2 + h2); + + return !separated; + } + + bool CollisionDetectionSystem::CheckCircleAABB(float cx, float cy, float radius, + float bx, float by, float bw, float bh) { + float closestX = std::max(bx, std::min(cx, bx + bw)); + float closestY = std::max(by, std::min(cy, by + bh)); + float dx = cx - closestX; + float dy = cy - closestY; + float distanceSquared = dx * dx + dy * dy; + + return distanceSquared <= (radius * radius); + } + + } +} diff --git a/engine/src/ECS/EffectFactory.cpp b/engine/src/ECS/EffectFactory.cpp new file mode 100644 index 0000000..159af8f --- /dev/null +++ b/engine/src/ECS/EffectFactory.cpp @@ -0,0 +1,410 @@ +#include "ECS/EffectFactory.hpp" +#include "ECS/Component.hpp" +#include "ECS/Components/TextLabel.hpp" +#include "ECS/Registry.hpp" +#include "Core/Logger.hpp" +#include +#include + +namespace RType { +namespace ECS { + + EffectFactory::EffectFactory(const EffectConfig& config) + : m_config(config) {} + + Entity EffectFactory::CreateBaseEffect(Registry& registry, + float x, float y, + Animation::EffectType type, + float duration, + Entity owner, + float offsetX, + float offsetY) { + Entity entity = registry.CreateEntity(); + + registry.AddComponent(entity, Position(x, y)); + registry.AddComponent(entity, VisualEffect(type, duration, owner, offsetX, offsetY)); + + return entity; + } + + Entity EffectFactory::CreateExplosionSmall(Registry& registry, float x, float y) { + return CreateExplosion(registry, x, y, m_config.explosionSmall, 2.0f, 100); + } + + Entity EffectFactory::CreateExplosionLarge(Registry& registry, float x, float y) { + return CreateExplosion(registry, x, y, m_config.explosionLarge, 3.0f, 100); + } + + Entity EffectFactory::CreateExplosion(Registry& registry, + float x, float y, + Animation::AnimationClipId clipId, + float scale, + int layer) { + Entity entity = CreateBaseEffect(registry, x, y, + Animation::EffectType::EXPLOSION_SMALL, 1.0f); + + if (clipId != Animation::INVALID_CLIP_ID) { + auto& anim = registry.AddComponent(entity, + SpriteAnimation(clipId, false, 1.5f)); + anim.destroyOnComplete = true; + + if (clipId == m_config.explosionSmall && + m_config.explosionFirstFrameRegion.size.x > 0.0f && + m_config.explosionFirstFrameRegion.size.y > 0.0f) { + anim.currentRegion = m_config.explosionFirstFrameRegion; + anim.currentFrameIndex = 0; + } + + auto& animatedSprite = registry.AddComponent(entity); + animatedSprite.needsUpdate = true; + } + + auto& drawable = registry.AddComponent(entity, Drawable()); + drawable.layer = layer; + drawable.scale = Math::Vector2(scale, scale); + if (m_config.effectsSprite != Renderer::INVALID_SPRITE_ID) { + drawable.spriteId = m_config.effectsSprite; + } + + return entity; + } + + Entity EffectFactory::CreateBulletImpact(Registry& registry, float x, float y) { + Entity entity = CreateBaseEffect(registry, x, y, + Animation::EffectType::BULLET_IMPACT, 0.3f); + + if (m_config.bulletImpact != Animation::INVALID_CLIP_ID) { + auto& anim = registry.AddComponent(entity, + SpriteAnimation(m_config.bulletImpact, false, 2.0f)); + anim.destroyOnComplete = true; + registry.AddComponent(entity); + } + + auto& drawable = registry.AddComponent(entity, Drawable()); + drawable.layer = 99; + drawable.scale = Math::Vector2(0.5f, 0.5f); + if (m_config.effectsSprite != Renderer::INVALID_SPRITE_ID) { + drawable.spriteId = m_config.effectsSprite; + } + + return entity; + } + + Entity EffectFactory::CreateDamageNumber(Registry& registry, + float x, float y, + int damage, + const Math::Color& color) { + char buffer[32]; + std::snprintf(buffer, sizeof(buffer), "-%d", damage); + return CreateFloatingText(registry, x, y - 20.0f, buffer, color, 1.5f); + } + + Entity EffectFactory::CreateScorePopup(Registry& registry, + float x, float y, + int score, + const Math::Color& color) { + char buffer[32]; + std::snprintf(buffer, sizeof(buffer), "+%d", score); + return CreateFloatingText(registry, x, y - 30.0f, buffer, color, 2.0f); + } + + Entity EffectFactory::CreateFloatingText(Registry& registry, + float x, float y, + const char* text, + const Math::Color& color, + float duration) { + Entity entity = registry.CreateEntity(); + + registry.AddComponent(entity, Position(x, y)); + registry.AddComponent(entity, FloatingText(text, duration, color)); + + auto& label = registry.AddComponent(entity); + label.text = text ? text : ""; + label.fontId = m_config.damageFont; + label.color = color; + label.centered = true; + + return entity; + } + + Entity EffectFactory::CreatePowerUpEffect(Registry& registry, float x, float y) { + Entity entity = CreateBaseEffect(registry, x, y, + Animation::EffectType::POWER_UP_COLLECT, 0.5f); + + if (m_config.powerUpCollect != Animation::INVALID_CLIP_ID) { + auto& anim = registry.AddComponent(entity, + SpriteAnimation(m_config.powerUpCollect, false, 1.5f)); + anim.destroyOnComplete = true; + registry.AddComponent(entity); + } + + auto& drawable = registry.AddComponent(entity, Drawable()); + drawable.layer = 101; + if (m_config.effectsSprite != Renderer::INVALID_SPRITE_ID) { + drawable.spriteId = m_config.effectsSprite; + } + + return entity; + } + + Entity EffectFactory::CreateBossTransitionEffect(Registry& registry, float x, float y) { + Entity entity = CreateBaseEffect(registry, x, y, + Animation::EffectType::BOSS_PHASE_TRANSITION, 2.0f); + + if (m_config.bossPhaseTransition != Animation::INVALID_CLIP_ID) { + auto& anim = registry.AddComponent(entity, + SpriteAnimation(m_config.bossPhaseTransition, false, 1.0f)); + anim.destroyOnComplete = true; + registry.AddComponent(entity); + } + + auto& drawable = registry.AddComponent(entity, Drawable()); + drawable.layer = 102; + if (m_config.effectsSprite != Renderer::INVALID_SPRITE_ID) { + drawable.spriteId = m_config.effectsSprite; + } + + return entity; + } + + Entity EffectFactory::CreateSpawnEffect(Registry& registry, float x, float y) { + Entity entity = CreateBaseEffect(registry, x, y, + Animation::EffectType::SPAWN_EFFECT, 0.5f); + + if (m_config.spawnEffect != Animation::INVALID_CLIP_ID) { + auto& anim = registry.AddComponent(entity, + SpriteAnimation(m_config.spawnEffect, false, 1.0f)); + anim.destroyOnComplete = true; + registry.AddComponent(entity); + } + + auto& drawable = registry.AddComponent(entity, Drawable()); + drawable.layer = 98; + if (m_config.effectsSprite != Renderer::INVALID_SPRITE_ID) { + drawable.spriteId = m_config.effectsSprite; + } + + return entity; + } + + Entity EffectFactory::CreateDeathEffect(Registry& registry, float x, float y) { + Entity entity = CreateBaseEffect(registry, x, y, + Animation::EffectType::DEATH_EFFECT, 0.8f); + + if (m_config.deathEffect != Animation::INVALID_CLIP_ID) { + auto& anim = registry.AddComponent(entity, + SpriteAnimation(m_config.deathEffect, false, 1.0f)); + anim.destroyOnComplete = true; + registry.AddComponent(entity); + } + + auto& drawable = registry.AddComponent(entity, Drawable()); + drawable.layer = 100; + if (m_config.effectsSprite != Renderer::INVALID_SPRITE_ID) { + drawable.spriteId = m_config.effectsSprite; + } + + return entity; + } + + void EffectFactory::CreateHitMarker(Registry& registry, Entity target, int damage) { + if (!registry.IsEntityAlive(target)) { + return; + } + + if (registry.HasComponent(target)) { + registry.GetComponent(target).Trigger(); + } + + if (registry.HasComponent(target)) { + const auto& pos = registry.GetComponent(target); + CreateDamageNumber(registry, pos.x, pos.y, damage); + } + } + + void EffectFactory::CreateEnemyDeathEffect(Registry& registry, + float x, float y, + int scoreValue) { + CreateExplosionSmall(registry, x, y); + + if (scoreValue > 0) { + CreateScorePopup(registry, x, y, scoreValue); + } + } + + Entity EffectFactory::CreateShootingEffect(Registry& registry, float x, float y, Entity owner) { + if (m_config.shootingAnimation == Animation::INVALID_CLIP_ID) { + return registry.CreateEntity(); + } + + constexpr float SHOOTING_EFFECT_OFFSET_X = 27.0f; + constexpr float SHOOTING_EFFECT_OFFSET_Y = -10.0f; + + float offsetX = SHOOTING_EFFECT_OFFSET_X; + float offsetY = SHOOTING_EFFECT_OFFSET_Y; + + if (owner != NULL_ENTITY && registry.IsEntityAlive(owner) && + registry.HasComponent(owner)) { + const auto& shooterComp = registry.GetComponent(owner); + offsetX = shooterComp.offsetX + SHOOTING_EFFECT_OFFSET_X; + offsetY = shooterComp.offsetY + SHOOTING_EFFECT_OFFSET_Y; + } + + Entity entity = CreateBaseEffect(registry, x + offsetX, y + offsetY, + Animation::EffectType::CUSTOM, 0.4f, owner, offsetX, offsetY); + + Renderer::SpriteId spriteId = Renderer::INVALID_SPRITE_ID; + if (m_config.shootingSprite != Renderer::INVALID_SPRITE_ID) { + spriteId = m_config.shootingSprite; + } else if (m_config.effectsSprite != Renderer::INVALID_SPRITE_ID) { + spriteId = m_config.effectsSprite; + } + + if (spriteId == Renderer::INVALID_SPRITE_ID) { + return entity; + } + + auto& drawable = registry.AddComponent(entity, Drawable()); + drawable.layer = 11; + drawable.scale = Math::Vector2(2.0f, 2.0f); + drawable.origin = Math::Vector2(12.5f, 12.5f); + drawable.spriteId = spriteId; + + auto& anim = registry.AddComponent(entity, + SpriteAnimation(m_config.shootingAnimation, false, 1.5f)); + anim.destroyOnComplete = true; + + if (m_config.shootingFirstFrameRegion.size.x > 0.0f && + m_config.shootingFirstFrameRegion.size.y > 0.0f) { + anim.currentRegion = m_config.shootingFirstFrameRegion; + anim.currentFrameIndex = 0; + } + + auto& animatedSprite = registry.AddComponent(entity); + animatedSprite.needsUpdate = true; + + return entity; + } + + Entity EffectFactory::CreateBeam(Registry& registry, float x, float y, Entity owner, float chargeTime, float screenWidth, float beamHeight) { + if (m_config.beamAnimation == Animation::INVALID_CLIP_ID) { + return registry.CreateEntity(); + } + + Entity entity = registry.CreateEntity(); + + registry.AddComponent(entity, Position(x, y)); + registry.AddComponent(entity, Velocity(0.0f, 0.0f)); + + // Calculate offsets from owner's shooter component + float offsetX = 0.0f; + float offsetY = 0.0f; + if (owner != NULL_ENTITY && registry.IsEntityAlive(owner) && + registry.HasComponent(owner)) { + const auto& shooterComp = registry.GetComponent(owner); + offsetX = shooterComp.offsetX; + offsetY = shooterComp.offsetY; + } + + // Add VisualEffect component to make the beam follow the player + if (owner != NULL_ENTITY) { + registry.AddComponent(entity, + VisualEffect(Animation::EffectType::CUSTOM, 10.0f, owner, offsetX, offsetY)); + } + + float frameWidth = 200.0f; + float frameHeight = beamHeight; + if (m_config.beamFirstFrameRegion.size.x > 0.0f && m_config.beamFirstFrameRegion.size.y > 0.0f) { + frameWidth = m_config.beamFirstFrameRegion.size.x; + frameHeight = m_config.beamFirstFrameRegion.size.y; + } + + float beamWidth = screenWidth - x; + if (beamWidth < frameWidth) { + beamWidth = frameWidth; + } + + float scaleX = beamWidth / frameWidth; + float scaleY = beamHeight / frameHeight; + + if (m_config.beamSprite != Renderer::INVALID_SPRITE_ID) { + auto& drawable = registry.AddComponent(entity, Drawable(m_config.beamSprite, 12)); + drawable.scale = Math::Vector2(scaleX, scaleY); + drawable.origin = Math::Vector2(0.0f, frameHeight * 0.5f); + } + + if (m_config.beamAnimation != Animation::INVALID_CLIP_ID) { + auto& anim = registry.AddComponent(entity, + SpriteAnimation(m_config.beamAnimation, true, 1.0f)); + anim.looping = true; + + if (m_config.beamFirstFrameRegion.size.x > 0.0f && + m_config.beamFirstFrameRegion.size.y > 0.0f) { + anim.currentRegion = m_config.beamFirstFrameRegion; + anim.currentFrameIndex = 0; + } + + auto& animatedSprite = registry.AddComponent(entity); + animatedSprite.needsUpdate = true; + } + + int beamDamage = static_cast(50 + (chargeTime / 2.0f) * 100); + registry.AddComponent(entity, Damage(beamDamage)); + registry.AddComponent(entity, Bullet(owner)); + registry.AddComponent(entity, BoxCollider(beamWidth, frameHeight * scaleY)); + registry.AddComponent(entity, + CollisionLayer(CollisionLayers::PLAYER_BULLET, + CollisionLayers::ENEMY | CollisionLayers::OBSTACLE)); + + return entity; + } + + Entity EffectFactory::CreateHitEffect(Registry& registry, float x, float y) { + if (m_config.hitAnimation == Animation::INVALID_CLIP_ID) { + return registry.CreateEntity(); + } + + Entity entity = CreateBaseEffect(registry, x, y, + Animation::EffectType::BULLET_IMPACT, 0.5f); + + Renderer::SpriteId spriteId = Renderer::INVALID_SPRITE_ID; + if (m_config.hitSprite != Renderer::INVALID_SPRITE_ID) { + spriteId = m_config.hitSprite; + } else if (m_config.effectsSprite != Renderer::INVALID_SPRITE_ID) { + spriteId = m_config.effectsSprite; + } + + if (spriteId == Renderer::INVALID_SPRITE_ID) { + return entity; + } + + auto& drawable = registry.AddComponent(entity, Drawable()); + drawable.layer = 99; + drawable.scale = Math::Vector2(2.0f, 2.0f); + if (m_config.hitFirstFrameRegion.size.x > 0.0f && m_config.hitFirstFrameRegion.size.y > 0.0f) { + drawable.origin = Math::Vector2(m_config.hitFirstFrameRegion.size.x * 0.5f, m_config.hitFirstFrameRegion.size.y * 0.5f); + } else { + drawable.origin = Math::Vector2(0.0f, 0.0f); + } + drawable.spriteId = spriteId; + + if (m_config.hitAnimation != Animation::INVALID_CLIP_ID) { + auto& anim = registry.AddComponent(entity, + SpriteAnimation(m_config.hitAnimation, false, 1.0f)); + anim.destroyOnComplete = true; + + if (m_config.hitFirstFrameRegion.size.x > 0.0f && + m_config.hitFirstFrameRegion.size.y > 0.0f) { + anim.currentRegion = m_config.hitFirstFrameRegion; + anim.currentFrameIndex = 0; + } + + auto& animatedSprite = registry.AddComponent(entity); + animatedSprite.needsUpdate = true; + } + + return entity; + } + +} +} diff --git a/engine/src/ECS/EnemyFactory.cpp b/engine/src/ECS/EnemyFactory.cpp new file mode 100644 index 0000000..05acd74 --- /dev/null +++ b/engine/src/ECS/EnemyFactory.cpp @@ -0,0 +1,111 @@ +#include "ECS/EnemyFactory.hpp" +#include "Core/Logger.hpp" +#include +#include +#include + +namespace RType { + + namespace ECS { + + namespace { + + std::atomic s_enemyIdCounter{1}; + + struct EnemyData { + Math::Color color; + const char* spritePath; + float speed; + int health; + int damage; + uint32_t score; + }; + + const std::array ENEMY_DATA_TABLE = {{// BASIC + {Math::Color(1.0f, 1.0f, 1.0f, 1.0f), "../assets/spaceships/nave2.png", 100.0f, 100, 10, 500}, + // FAST + {Math::Color(1.0f, 0.3f, 0.3f, 1.0f), "../assets/spaceships/nave2_red.png", 200.0f, 50, 5, 500}, + // TANK + {Math::Color(0.3f, 0.3f, 1.0f, 1.0f), "../assets/spaceships/nave2_blue.png", 50.0f, 200, 20, 500}, + // BOSS + {Math::Color(1.0f, 0.0f, 1.0f, 1.0f), "../assets/spaceships/nave2.png", 75.0f, 1000, 50, 500}, + // FORMATION + {Math::Color(0.5f, 0.5f, 0.5f, 1.0f), "../assets/spaceships/nave2.png", 100.0f, 100, 10, 500}}}; + + const EnemyData& GetEnemyData(EnemyType type) { + size_t index = static_cast(type); + if (index >= ENEMY_DATA_TABLE.size()) { + return ENEMY_DATA_TABLE[0]; + } + return ENEMY_DATA_TABLE[index]; + } + } + + Entity EnemyFactory::CreateEnemy(Registry& registry, EnemyType type, float startX, float startY, + Renderer::IRenderer* renderer) { + Entity enemy = registry.CreateEntity(); + + registry.AddComponent(enemy, Position(startX, startY)); + + const EnemyData& data = GetEnemyData(type); + registry.AddComponent(enemy, Velocity(-data.speed, 0.0f)); + + uint32_t uniqueId = s_enemyIdCounter.fetch_add(1); + registry.AddComponent(enemy, Enemy(type, uniqueId)); + + registry.AddComponent(enemy, Health(data.health, data.health)); + registry.AddComponent(enemy, Damage(data.damage)); + registry.AddComponent(enemy, ScoreValue(data.score)); + registry.AddComponent(enemy, BoxCollider(50.0f, 50.0f)); + + registry.AddComponent(enemy, CircleCollider(25.0f)); + registry.AddComponent(enemy, + CollisionLayer(CollisionLayers::ENEMY, + CollisionLayers::PLAYER | CollisionLayers::PLAYER_BULLET | CollisionLayers::OBSTACLE)); + + if (renderer) { + std::string spritePath(data.spritePath); + Renderer::TextureId textureId = renderer->LoadTexture(spritePath); + + if (textureId == Renderer::INVALID_TEXTURE_ID) { + Core::Logger::Warning("Failed to load enemy texture: {}, using default", spritePath); + textureId = renderer->LoadTexture("../assets/spaceships/nave2.png"); + } + + if (textureId != Renderer::INVALID_TEXTURE_ID) { + Renderer::SpriteId spriteId = + renderer->CreateSprite(textureId, Renderer::Rectangle{{0.0f, 0.0f}, {256.0f, 256.0f}}); + + auto& drawable = registry.AddComponent(enemy, Drawable(spriteId, 1)); + drawable.tint = data.color; + drawable.scale = Math::Vector2(0.5f, 0.5f); + drawable.origin = Math::Vector2(128.0f, 128.0f); + } else { + Core::Logger::Error("Failed to load any enemy texture for type {}", static_cast(type)); + } + } + + Core::Logger::Info("Created enemy type {} at position ({}, {})", static_cast(type), startX, startY); + + return enemy; + } + + float EnemyFactory::GetEnemySpeed(EnemyType type) { + return GetEnemyData(type).speed; + } + + int EnemyFactory::GetEnemyHealth(EnemyType type) { + return GetEnemyData(type).health; + } + + int EnemyFactory::GetEnemyDamage(EnemyType type) { + return GetEnemyData(type).damage; + } + + uint32_t EnemyFactory::GetEnemyScore(EnemyType type) { + return GetEnemyData(type).score; + } + + } + +} diff --git a/engine/src/ECS/EnemySystem.cpp b/engine/src/ECS/EnemySystem.cpp new file mode 100644 index 0000000..5eed3cc --- /dev/null +++ b/engine/src/ECS/EnemySystem.cpp @@ -0,0 +1,119 @@ +#include "ECS/EnemySystem.hpp" +#include "ECS/EnemyFactory.hpp" +#include "ECS/Component.hpp" +#include "Core/Logger.hpp" +#include +#include +#include + +namespace RType { + + namespace ECS { + + constexpr float ENEMY_SPAWN_OFFSET_X = 50.0f; + constexpr float ENEMY_SPAWN_MARGIN_Y = 50.0f; + constexpr float ENEMY_DESTROY_OFFSET_X = -100.0f; + constexpr float ENEMY_FAST_SIN_AMPLITUDE = 50.0f; + constexpr float ENEMY_FAST_SIN_FREQUENCY = 0.01f; + constexpr float ENEMY_SPAWN_MIN_Y = 50.0f; + + EnemySystem::EnemySystem(Renderer::IRenderer* renderer, float screenWidth, float screenHeight) + : m_renderer(renderer), m_screenWidth(screenWidth), m_screenHeight(screenHeight), + m_rng(std::random_device{}()) {} + + void EnemySystem::Update(Registry& registry, float deltaTime) { + m_spawnTimer += deltaTime; + + if (m_spawnTimer >= m_spawnInterval) { + SpawnRandomEnemy(registry); + m_spawnTimer = 0.0f; + } + + auto enemies = registry.GetEntitiesWithComponent(); + + for (Entity enemy : enemies) { + if (!registry.HasComponent(enemy)) { + continue; + } + + ApplyMovementPattern(registry, enemy, deltaTime); + } + + DestroyEnemiesOffScreen(registry, m_screenWidth); + } + + void EnemySystem::SpawnRandomEnemy(Registry& registry) { + std::uniform_real_distribution yDist(ENEMY_SPAWN_MIN_Y, m_screenHeight - ENEMY_SPAWN_MARGIN_Y); + std::uniform_int_distribution typeDist(0, 2); + + float spawnX = m_screenWidth + ENEMY_SPAWN_OFFSET_X; + float spawnY = yDist(m_rng); + + EnemyType type = static_cast(typeDist(m_rng)); + + EnemyFactory::CreateEnemy(registry, type, spawnX, spawnY, m_renderer); + } + + void EnemySystem::DestroyEnemiesOffScreen(Registry& registry, float /* screenWidth */) { + auto enemies = registry.GetEntitiesWithComponent(); + + for (Entity enemy : enemies) { + if (!registry.HasComponent(enemy)) { + continue; + } + + const auto& pos = registry.GetComponent(enemy); + + if (pos.x < ENEMY_DESTROY_OFFSET_X) { + registry.DestroyEntity(enemy); + } + } + } + + void EnemySystem::ApplyMovementPattern(Registry& registry, Entity enemy, float deltaTime) { + if (!registry.HasComponent(enemy) || !registry.HasComponent(enemy)) { + return; + } + + const auto& enemyComp = registry.GetComponent(enemy); + auto& velocity = registry.GetComponent(enemy); + + switch (enemyComp.type) { + case EnemyType::BASIC: + velocity.dx = -EnemyFactory::GetEnemySpeed(EnemyType::BASIC); + velocity.dy = 0.0f; + break; + + case EnemyType::FAST: { + if (registry.HasComponent(enemy)) { + const auto& pos = registry.GetComponent(enemy); + float speed = EnemyFactory::GetEnemySpeed(EnemyType::FAST); + velocity.dx = -speed; + velocity.dy = std::sin(pos.x * ENEMY_FAST_SIN_FREQUENCY) * ENEMY_FAST_SIN_AMPLITUDE; + } + break; + } + + case EnemyType::TANK: + velocity.dx = -EnemyFactory::GetEnemySpeed(EnemyType::TANK); + velocity.dy = 0.0f; + break; + + case EnemyType::BOSS: + velocity.dx = -EnemyFactory::GetEnemySpeed(EnemyType::BOSS); + velocity.dy = 0.0f; + break; + + case EnemyType::FORMATION: + velocity.dx = -EnemyFactory::GetEnemySpeed(EnemyType::FORMATION); + velocity.dy = 0.0f; + break; + + default: + break; + } + } + + } + +} diff --git a/engine/src/ECS/ForcePodSystem.cpp b/engine/src/ECS/ForcePodSystem.cpp new file mode 100644 index 0000000..e730551 --- /dev/null +++ b/engine/src/ECS/ForcePodSystem.cpp @@ -0,0 +1,62 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** ForcePodSystem +*/ + +#include "ECS/ForcePodSystem.hpp" +#include "ECS/Component.hpp" + +namespace RType { + namespace ECS { + + void ForcePodSystem::Update(Registry& registry, float deltaTime) { + (void)deltaTime; + + auto forcePods = registry.GetEntitiesWithComponent(); + + for (Entity pod : forcePods) { + if (!registry.IsEntityAlive(pod)) { + continue; + } + + auto& forcePodComp = registry.GetComponent(pod); + + // Check if owner still exists + if (!registry.IsEntityAlive(forcePodComp.owner)) { + // Owner died, destroy force pod + registry.DestroyEntity(pod); + continue; + } + + // Update force pod position to follow owner + if (registry.HasComponent(forcePodComp.owner) && + registry.HasComponent(pod)) { + + const auto& ownerPos = registry.GetComponent(forcePodComp.owner); + auto& podPos = registry.GetComponent(pod); + + // Smooth follow (lerp for smooth movement) + float targetX = ownerPos.x + forcePodComp.offsetX; + float targetY = ownerPos.y + forcePodComp.offsetY; + + float lerpFactor = 0.15f; // Adjust for smoothness + podPos.x += (targetX - podPos.x) * lerpFactor; + podPos.y += (targetY - podPos.y) * lerpFactor; + } + + // Sync shoot command with owner + if (registry.HasComponent(forcePodComp.owner) && + registry.HasComponent(pod)) { + + const auto& ownerShoot = registry.GetComponent(forcePodComp.owner); + auto& podShoot = registry.GetComponent(pod); + + podShoot.wantsToShoot = ownerShoot.wantsToShoot; + } + } + } + + } +} diff --git a/engine/src/ECS/HealthSystem.cpp b/engine/src/ECS/HealthSystem.cpp new file mode 100644 index 0000000..54d2637 --- /dev/null +++ b/engine/src/ECS/HealthSystem.cpp @@ -0,0 +1,45 @@ +#include "ECS/HealthSystem.hpp" +#include "ECS/Component.hpp" +#include "Core/Logger.hpp" + +namespace RType { + + namespace ECS { + + void HealthSystem::Update(Registry& registry, float deltaTime) { + (void)deltaTime; + + checkAndDestroyDeadEntities(registry); + } + + void HealthSystem::checkAndDestroyDeadEntities(Registry& registry) { + auto entitiesWithHealth = registry.GetEntitiesWithComponent(); + + for (Entity entity : entitiesWithHealth) { + if (!registry.IsEntityAlive(entity)) { + continue; + } + + if (!registry.HasComponent(entity)) { + continue; + } + + const auto& health = registry.GetComponent(entity); + + if (health.current <= 0) { + if (registry.HasComponent(entity)) { + if (!registry.HasComponent(entity)) { + registry.AddComponent(entity, BossKilled{entity, 1}); + Core::Logger::Info("[HealthSystem] Boss defeated! Marked for level transition"); + } + continue; + } + + registry.DestroyEntity(entity); + } + } + } + + } + +} diff --git a/engine/src/ECS/InputSystem.cpp b/engine/src/ECS/InputSystem.cpp new file mode 100644 index 0000000..1e172c7 --- /dev/null +++ b/engine/src/ECS/InputSystem.cpp @@ -0,0 +1,61 @@ +#include "../include/ECS/InputSystem.hpp" +#include "../include/ECS/Registry.hpp" +#include "../include/ECS/Component.hpp" +#include "../include/Core/InputMapping.hpp" + +namespace RType { + namespace ECS { + + InputSystem::InputSystem(Renderer::IRenderer* renderer) + : m_renderer(renderer) {} + + void InputSystem::Update(Registry& registry, float deltaTime) { + auto entities = registry.GetEntitiesWithComponent(); + + Renderer::Key moveUpKey = Core::InputMapping::GetKey("MOVE_UP"); + Renderer::Key moveDownKey = Core::InputMapping::GetKey("MOVE_DOWN"); + Renderer::Key moveLeftKey = Core::InputMapping::GetKey("MOVE_LEFT"); + Renderer::Key moveRightKey = Core::InputMapping::GetKey("MOVE_RIGHT"); + Renderer::Key shootKey = Core::InputMapping::GetKey("SHOOT"); + + if (moveUpKey == Renderer::Key::Unknown) moveUpKey = Renderer::Key::Up; + if (moveDownKey == Renderer::Key::Unknown) moveDownKey = Renderer::Key::Down; + if (moveLeftKey == Renderer::Key::Unknown) moveLeftKey = Renderer::Key::Left; + if (moveRightKey == Renderer::Key::Unknown) moveRightKey = Renderer::Key::Right; + if (shootKey == Renderer::Key::Unknown) shootKey = Renderer::Key::E; + + for (Entity entity : entities) { + if (!registry.HasComponent(entity)) { + continue; + } + + auto& controllable = registry.GetComponent(entity); + auto& vel = registry.GetComponent(entity); + + vel.dx = 0.0f; + vel.dy = 0.0f; + + if (m_renderer->IsKeyPressed(moveUpKey)) { + vel.dy = -controllable.speed; + } + + if (m_renderer->IsKeyPressed(moveDownKey)) { + vel.dy = controllable.speed; + } + + if (m_renderer->IsKeyPressed(moveLeftKey)) { + vel.dx = -controllable.speed; + } + + if (m_renderer->IsKeyPressed(moveRightKey)) { + vel.dx = controllable.speed; + } + + if (registry.HasComponent(entity)) { + auto& shootCmd = registry.GetComponent(entity); + shootCmd.wantsToShoot = m_renderer->IsKeyPressed(shootKey); + } + } + } + } +} diff --git a/engine/src/ECS/LevelLoader.cpp b/engine/src/ECS/LevelLoader.cpp new file mode 100644 index 0000000..e8fbc57 --- /dev/null +++ b/engine/src/ECS/LevelLoader.cpp @@ -0,0 +1,578 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** LevelLoader - JSON-based level loading implementation +*/ + +#include "ECS/LevelLoader.hpp" +#include "ECS/EnemyFactory.hpp" +#include "Core/Logger.hpp" +#include +#include +#include + +using json = nlohmann::json; + +namespace RType { + + namespace ECS { + + LevelData LevelLoader::LoadFromFile(const std::string& path) { + std::string sourcePath = "../" + path; + std::ifstream file(sourcePath); + + if (!file.is_open()) { + file.open(path); + if (!file.is_open()) { + throw std::runtime_error("Failed to open level file: " + path + " (tried: " + sourcePath + " and " + path + ")"); + } + } + + std::string content((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + return LoadFromString(content); + } + + LevelData LevelLoader::LoadFromString(const std::string& jsonString) { + LevelData level; + + try { + json j = json::parse(jsonString); + + if (j.contains("name")) { + level.name = j["name"].get(); + } + + if (j.contains("assets")) { + const auto& assets = j["assets"]; + + if (assets.contains("textures")) { + for (auto& [key, value] : assets["textures"].items()) { + level.textures[key] = value.get(); + } + } + + if (assets.contains("fonts")) { + for (auto& [key, value] : assets["fonts"].items()) { + FontDef font; + if (value.is_object()) { + font.path = value.value("path", ""); + font.size = value.value("size", 16u); + } else { + font.path = value.get(); + } + level.fonts[key] = font; + } + } + } + + if (j.contains("background")) { + const auto& bg = j["background"]; + level.background.texture = bg.value("texture", ""); + level.background.scrollSpeed = bg.value("scrollSpeed", -150.0f); + level.background.copies = bg.value("copies", 3); + level.background.layer = bg.value("layer", -100); + } + + if (j.contains("obstacles")) { + for (const auto& obs : j["obstacles"]) { + ObstacleDef obstacle; + obstacle.texture = obs.value("texture", ""); + + if (obs.contains("position")) { + obstacle.x = obs["position"].value("x", 0.0f); + obstacle.y = obs["position"].value("y", 0.0f); + } else { + obstacle.x = obs.value("x", 0.0f); + obstacle.y = obs.value("y", 0.0f); + } + + obstacle.scaleWidth = obs.value("scaleWidth", 1200.0f); + obstacle.scaleHeight = obs.value("scaleHeight", 720.0f); + obstacle.scrollSpeed = obs.value("scrollSpeed", -150.0f); + obstacle.layer = obs.value("layer", 1); + + if (obs.contains("colliders")) { + for (const auto& col : obs["colliders"]) { + ColliderDef collider; + collider.x = col.value("x", 0.0f); + collider.y = col.value("y", 0.0f); + collider.width = col.value("width", 0.0f); + collider.height = col.value("height", 0.0f); + obstacle.colliders.push_back(collider); + } + } + + level.obstacles.push_back(obstacle); + } + } + + if (j.contains("enemies")) { + for (const auto& en : j["enemies"]) { + EnemyDef enemy; + enemy.type = en.value("type", "BASIC"); + + if (en.contains("position")) { + enemy.x = en["position"].value("x", 0.0f); + enemy.y = en["position"].value("y", 0.0f); + } else { + enemy.x = en.value("x", 0.0f); + enemy.y = en.value("y", 0.0f); + } + + level.enemies.push_back(enemy); + } + } + + if (j.contains("playerSpawns")) { + for (const auto& spawn : j["playerSpawns"]) { + PlayerSpawnDef ps; + ps.x = spawn.value("x", 100.0f); + ps.y = spawn.value("y", 360.0f); + level.playerSpawns.push_back(ps); + } + } + + if (level.playerSpawns.empty()) { + level.playerSpawns.push_back({100.0f, 200.0f}); + level.playerSpawns.push_back({100.0f, 360.0f}); + level.playerSpawns.push_back({100.0f, 520.0f}); + level.playerSpawns.push_back({100.0f, 680.0f}); + } + + if (j.contains("boss")) { + const auto& bossJson = j["boss"]; + BossDef boss; + boss.texture = bossJson.value("texture", ""); + + if (bossJson.contains("position")) { + boss.x = bossJson["position"].value("x", 0.0f); + boss.y = bossJson["position"].value("y", 0.0f); + } else { + boss.x = bossJson.value("x", 0.0f); + boss.y = bossJson.value("y", 0.0f); + } + + boss.width = bossJson.value("width", 200.0f); + boss.height = bossJson.value("height", 200.0f); + boss.health = bossJson.value("health", 1000); + boss.scrollSpeed = bossJson.value("scrollSpeed", -300.0f); + boss.attackPattern = bossJson.value("attackPattern", 1); + boss.bossId = bossJson.value("bossId", static_cast(1)); + + level.boss = boss; + } + + Core::Logger::Info("Loaded level '{}' with {} obstacles, {} enemies, {} player spawns", + level.name.empty() ? "unnamed" : level.name, + level.obstacles.size(), + level.enemies.size(), + level.playerSpawns.size()); + + } catch (const json::parse_error& e) { + throw std::runtime_error("JSON parse error: " + std::string(e.what())); + } catch (const json::type_error& e) { + throw std::runtime_error("JSON type error: " + std::string(e.what())); + } + + return level; + } + + LoadedAssets LevelLoader::LoadAssets( + const LevelData& level, + Renderer::IRenderer* renderer) { + LoadedAssets assets; + + if (!renderer) { + return assets; + } + + for (const auto& [key, path] : level.textures) { + Renderer::TextureId texId = renderer->LoadTexture("../" + path); + if (texId == Renderer::INVALID_TEXTURE_ID) { + texId = renderer->LoadTexture(path); + } + + if (texId != Renderer::INVALID_TEXTURE_ID) { + assets.textures[key] = texId; + assets.sprites[key] = renderer->CreateSprite(texId, {}); + Core::Logger::Debug("Loaded texture '{}' from '{}'", key, path); + } else { + Core::Logger::Warning("Failed to load texture '{}' from '{}'", key, path); + } + } + + for (const auto& [key, fontDef] : level.fonts) { + Renderer::FontId fontId = renderer->LoadFont("../" + fontDef.path, fontDef.size); + if (fontId == Renderer::INVALID_FONT_ID) { + fontId = renderer->LoadFont(fontDef.path, fontDef.size); + } + + if (fontId != Renderer::INVALID_FONT_ID) { + assets.fonts[key] = fontId; + Core::Logger::Debug("Loaded font '{}' from '{}'", key, fontDef.path); + } else { + Core::Logger::Warning("Failed to load font '{}' from '{}'", key, fontDef.path); + } + } + + return assets; + } + + CreatedEntities LevelLoader::CreateEntities( + Registry& registry, + const LevelData& level, + const LoadedAssets& assets, + Renderer::IRenderer* renderer) { + CreatedEntities entities; + uint32_t obstacleIdCounter = 1; + + CreateBackgrounds(registry, level.background, assets, renderer, entities); + CreateObstacles(registry, level.obstacles, assets, renderer, entities, obstacleIdCounter); + CreateEnemies(registry, level.enemies, assets, renderer, entities); + + Core::Logger::Info("Created {} backgrounds, {} obstacle visuals, {} obstacle colliders, {} enemy entities", + entities.backgrounds.size(), + entities.obstacleVisuals.size(), + entities.obstacleColliders.size(), + entities.enemies.size()); + + return entities; + } + + CreatedEntities LevelLoader::CreateServerEntities( + Registry& registry, + const LevelData& level) { + CreatedEntities entities; + uint32_t obstacleIdCounter = 1; + + CreateServerObstacles(registry, level.obstacles, entities, obstacleIdCounter); + CreateServerEnemies(registry, level.enemies, entities); + CreateServerBoss(registry, level.boss, entities); + + Core::Logger::Info("Created server entities: {} obstacle visuals, {} obstacle colliders, {} enemies, boss: {}", + entities.obstacleVisuals.size(), + entities.obstacleColliders.size(), + entities.enemies.size(), + entities.boss != NULL_ENTITY ? "yes" : "no"); + + return entities; + } + + const std::vector& LevelLoader::GetPlayerSpawns(const LevelData& level) { + return level.playerSpawns; + } + + EnemyType LevelLoader::ParseEnemyType(const std::string& typeStr) { + if (typeStr == "BASIC") + return EnemyType::BASIC; + if (typeStr == "FAST") + return EnemyType::FAST; + if (typeStr == "TANK") + return EnemyType::TANK; + if (typeStr == "BOSS") + return EnemyType::BOSS; + if (typeStr == "FORMATION") + return EnemyType::FORMATION; + + Core::Logger::Warning("Unknown enemy type '{}', defaulting to BASIC", typeStr); + return EnemyType::BASIC; + } + + void LevelLoader::CreateBackgrounds( + Registry& registry, + const BackgroundDef& background, + const LoadedAssets& assets, + Renderer::IRenderer* renderer, + CreatedEntities& entities) { + if (background.texture.empty()) { + return; + } + + auto spriteIt = assets.sprites.find(background.texture); + auto texIt = assets.textures.find(background.texture); + + if (spriteIt == assets.sprites.end() || texIt == assets.textures.end()) { + Core::Logger::Warning("Background texture '{}' not found in loaded assets", background.texture); + return; + } + + Renderer::SpriteId bgSprite = spriteIt->second; + Renderer::TextureId bgTexture = texIt->second; + + Math::Vector2 bgSize = renderer->GetTextureSize(bgTexture); + float scaleX = 1280.0f / bgSize.x; + float scaleY = 720.0f / bgSize.y; + + for (int i = 0; i < background.copies; i++) { + Entity bgEntity = registry.CreateEntity(); + + registry.AddComponent(bgEntity, Position{i * 1280.0f, 0.0f}); + + auto& drawable = registry.AddComponent(bgEntity, Drawable(bgSprite, background.layer)); + drawable.scale = {scaleX, scaleY}; + + registry.AddComponent(bgEntity, Scrollable(background.scrollSpeed)); + + entities.backgrounds.push_back(bgEntity); + } + } + + void LevelLoader::CreateObstacles( + Registry& registry, + const std::vector& obstacles, + const LoadedAssets& assets, + Renderer::IRenderer* renderer, + CreatedEntities& entities, + uint32_t& obstacleIdCounter) { + for (const auto& obs : obstacles) { + auto spriteIt = assets.sprites.find(obs.texture); + auto texIt = assets.textures.find(obs.texture); + + bool hasTexture = (spriteIt != assets.sprites.end() && texIt != assets.textures.end()); + + Entity obsEntity = NULL_ENTITY; + if (hasTexture) { + obsEntity = registry.CreateEntity(); + + registry.AddComponent(obsEntity, Position{obs.x, obs.y}); + + Math::Vector2 obsSize = renderer->GetTextureSize(texIt->second); + auto& drawable = registry.AddComponent(obsEntity, Drawable(spriteIt->second, obs.layer)); + drawable.scale = {obs.scaleWidth / obsSize.x, obs.scaleHeight / obsSize.y}; + drawable.origin = {0.0f, 0.0f}; + + registry.AddComponent(obsEntity, Scrollable(obs.scrollSpeed)); + registry.AddComponent(obsEntity, ObstacleVisual{}); + entities.obstacleVisuals.push_back(obsEntity); + } else { + Core::Logger::Warning("Obstacle texture '{}' not found in loaded assets", obs.texture); + } + + for (const auto& col : obs.colliders) { + Entity colliderEntity = registry.CreateEntity(); + // Store collider position as offset from visual entity (not absolute) + registry.AddComponent(colliderEntity, Position{col.x - obs.x, col.y - obs.y}); + registry.AddComponent(colliderEntity, BoxCollider{col.width, col.height}); + registry.AddComponent(colliderEntity, Scrollable(obs.scrollSpeed)); + registry.AddComponent(colliderEntity, Obstacle(true)); + registry.AddComponent(colliderEntity, + ObstacleMetadata(obstacleIdCounter++, obsEntity, col.x - obs.x, col.y - obs.y)); + registry.AddComponent(colliderEntity, + CollisionLayer(CollisionLayers::OBSTACLE, CollisionLayers::ALL)); + + entities.obstacleColliders.push_back(colliderEntity); + } + } + } + + void LevelLoader::CreateEnemies( + Registry& registry, + const std::vector& enemies, + const LoadedAssets& assets, + Renderer::IRenderer* renderer, + CreatedEntities& entities) { + for (const auto& en : enemies) { + EnemyType type = ParseEnemyType(en.type); + Entity enemy = EnemyFactory::CreateEnemy(registry, type, en.x, en.y, renderer); + entities.enemies.push_back(enemy); + } + } + + void LevelLoader::CreateServerObstacles( + Registry& registry, + const std::vector& obstacles, + CreatedEntities& entities, + uint32_t& obstacleIdCounter) { + for (const auto& obs : obstacles) { + Entity obsEntity = registry.CreateEntity(); + + registry.AddComponent(obsEntity, Position{obs.x, obs.y}); + registry.AddComponent(obsEntity, Scrollable(obs.scrollSpeed)); + registry.AddComponent(obsEntity, ObstacleVisual{}); + entities.obstacleVisuals.push_back(obsEntity); + + for (const auto& col : obs.colliders) { + Entity colliderEntity = registry.CreateEntity(); + // Store collider position as offset from visual entity (not absolute) + registry.AddComponent(colliderEntity, Position{col.x - obs.x, col.y - obs.y}); + registry.AddComponent(colliderEntity, BoxCollider{col.width, col.height}); + registry.AddComponent(colliderEntity, Scrollable(obs.scrollSpeed)); + registry.AddComponent(colliderEntity, Obstacle(true)); + registry.AddComponent(colliderEntity, + ObstacleMetadata(obstacleIdCounter++, obsEntity, col.x - obs.x, col.y - obs.y)); + registry.AddComponent(colliderEntity, + CollisionLayer(CollisionLayers::OBSTACLE, CollisionLayers::ALL)); + + entities.obstacleColliders.push_back(colliderEntity); + } + } + } + + void LevelLoader::CreateServerEnemies( + Registry& registry, + const std::vector& enemies, + CreatedEntities& entities) { + for (const auto& en : enemies) { + EnemyType type = ParseEnemyType(en.type); + Entity enemy = EnemyFactory::CreateEnemy(registry, type, en.x, en.y, nullptr); + entities.enemies.push_back(enemy); + } + } + + void LevelLoader::CreateServerBoss( + Registry& registry, + const std::optional& bossOpt, + CreatedEntities& entities) { + if (!bossOpt.has_value()) { + return; + } + + const BossDef& boss = bossOpt.value(); + + Entity bossEntity = registry.CreateEntity(); + + registry.AddComponent(bossEntity, Boss{boss.bossId}); + + registry.AddComponent(bossEntity, Position{boss.x, boss.y}); + + registry.AddComponent(bossEntity, Velocity{0.0f, 0.0f}); + + registry.AddComponent(bossEntity, Health{boss.health}); + + registry.AddComponent(bossEntity, BoxCollider{boss.width, boss.height}); + + registry.AddComponent(bossEntity, Scrollable{boss.scrollSpeed}); + + auto& bossAttack = registry.AddComponent(bossEntity, BossAttack{0.40f}); + if (boss.bossId == 2) { + bossAttack = registry.AddComponent(bossEntity, BossAttack{1.3f}); + bossAttack.currentPattern = BossAttackPattern::ANIMATED_ORB; + + registry.AddComponent(bossEntity, + BossMovementPattern{150.0f, 60.0f, 0.3f, 0.2f, boss.y, boss.x}); + } else if (boss.bossId == 3) { + bossAttack = registry.AddComponent(bossEntity, BossAttack{3.0f}); + bossAttack.currentPattern = static_cast(boss.attackPattern); + } else { + bossAttack.currentPattern = BossAttackPattern::FAN_SPRAY; + + registry.AddComponent(bossEntity, + BossMovementPattern{180.0f, 120.0f, 0.4f, 0.4f, boss.y, boss.x}); + } + + registry.AddComponent(bossEntity, DamageFlash{0.1f}); + + registry.AddComponent(bossEntity, + CollisionLayer(CollisionLayers::ENEMY, CollisionLayers::PLAYER | CollisionLayers::PLAYER_BULLET)); + + entities.boss = bossEntity; + + Core::Logger::Info("Created boss entity at position ({}, {}) with {} health", + boss.x, boss.y, boss.health); + } + + std::string LevelLoader::SerializeToString(const LevelData& level) { + json j; + + j["name"] = level.name; + + if (!level.textures.empty() || !level.fonts.empty()) { + j["assets"] = json::object(); + + if (!level.textures.empty()) { + j["assets"]["textures"] = json::object(); + for (const auto& [key, path] : level.textures) { + j["assets"]["textures"][key] = path; + } + } + + if (!level.fonts.empty()) { + j["assets"]["fonts"] = json::object(); + for (const auto& [key, font] : level.fonts) { + j["assets"]["fonts"][key] = { + {"path", font.path}, + {"size", font.size} + }; + } + } + } + + j["background"] = { + {"texture", level.background.texture}, + {"scrollSpeed", level.background.scrollSpeed}, + {"copies", level.background.copies}, + {"layer", level.background.layer} + }; + + j["obstacles"] = json::array(); + for (const auto& obs : level.obstacles) { + json obsJson = { + {"texture", obs.texture}, + {"position", {{"x", obs.x}, {"y", obs.y}}}, + {"scaleWidth", obs.scaleWidth}, + {"scaleHeight", obs.scaleHeight}, + {"scrollSpeed", obs.scrollSpeed}, + {"layer", obs.layer} + }; + + if (!obs.colliders.empty()) { + obsJson["colliders"] = json::array(); + for (const auto& col : obs.colliders) { + obsJson["colliders"].push_back({ + {"x", col.x}, + {"y", col.y}, + {"width", col.width}, + {"height", col.height} + }); + } + } + + j["obstacles"].push_back(obsJson); + } + + j["enemies"] = json::array(); + for (const auto& enemy : level.enemies) { + j["enemies"].push_back({ + {"type", enemy.type}, + {"position", {{"x", enemy.x}, {"y", enemy.y}}} + }); + } + + j["playerSpawns"] = json::array(); + for (const auto& spawn : level.playerSpawns) { + j["playerSpawns"].push_back({ + {"x", spawn.x}, + {"y", spawn.y} + }); + } + + return j.dump(4); + } + + void LevelLoader::SaveToFile(const LevelData& level, const std::string& path) { + try { + std::string jsonString = SerializeToString(level); + + std::ofstream file(path); + if (!file.is_open()) { + throw std::runtime_error("Failed to open file for writing: " + path); + } + + file << jsonString; + file.close(); + + Core::Logger::Info("Successfully saved level '{}' to {}", + level.name.empty() ? "unnamed" : level.name, + path); + + } catch (const std::exception& e) { + Core::Logger::Error("Failed to save level: {}", e.what()); + throw; + } + } + + } + +} diff --git a/engine/src/ECS/MenuSystem.cpp b/engine/src/ECS/MenuSystem.cpp new file mode 100644 index 0000000..9b3d9d1 --- /dev/null +++ b/engine/src/ECS/MenuSystem.cpp @@ -0,0 +1,73 @@ +#include "ECS/MenuSystem.hpp" +#include "ECS/Component.hpp" +#include "ECS/Components/Clickable.hpp" +#include "ECS/Components/TextLabel.hpp" + +namespace RType { + namespace ECS { + + MenuSystem::MenuSystem(Renderer::IRenderer* renderer) + : m_renderer(renderer) {} + + void MenuSystem::Update(Registry& registry, float deltaTime) { + if (!m_renderer) + return; + + Renderer::Vector2 mousePos = m_renderer->GetMousePosition(); + bool isMouseDown = m_renderer->IsMouseButtonPressed(Renderer::IRenderer::MouseButton::Left); + + auto entities = registry.GetEntitiesWithComponent(); + + for (Entity entity : entities) { + if (!registry.HasComponent(entity)) + continue; + + const auto& pos = registry.GetComponent(entity); + auto& clickable = registry.GetComponent(entity); + + Drawable* drawable = nullptr; + if (registry.HasComponent(entity)) { + drawable = ®istry.GetComponent(entity); + } + + TextLabel* textLabel = nullptr; + if (registry.HasComponent(entity)) { + textLabel = ®istry.GetComponent(entity); + } + + bool hovered = (mousePos.x >= pos.x && mousePos.x <= pos.x + clickable.width && + mousePos.y >= pos.y && mousePos.y <= pos.y + clickable.height); + + if (hovered) { + if (isMouseDown) { + clickable.state = ButtonState::Active; + if (drawable) + drawable->tint = {0.5f, 0.5f, 0.5f, 1.0f}; + if (textLabel) + textLabel->color = {0.5f, 0.5f, 0.5f, 1.0f}; + } else { + if (m_wasMouseDown && clickable.state == ButtonState::Active) { + if (m_callback) { + m_callback(clickable.actionId); + } + } + clickable.state = ButtonState::Hover; + if (drawable) + drawable->tint = {0.8f, 0.8f, 0.8f, 1.0f}; + if (textLabel) + textLabel->color = {0.8f, 0.8f, 0.8f, 1.0f}; + } + } else { + clickable.state = ButtonState::Idle; + if (drawable) + drawable->tint = {1.0f, 1.0f, 1.0f, 1.0f}; + if (textLabel) + textLabel->color = {1.0f, 1.0f, 1.0f, 1.0f}; + } + } + + m_wasMouseDown = isMouseDown; + } + + } +} diff --git a/engine/src/ECS/MineSystem.cpp b/engine/src/ECS/MineSystem.cpp new file mode 100644 index 0000000..ec0cd4c --- /dev/null +++ b/engine/src/ECS/MineSystem.cpp @@ -0,0 +1,101 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** MineSystem - Handles mine proximity detection and explosions +*/ + +#include "ECS/MineSystem.hpp" +#include "ECS/Component.hpp" +#include "Core/Logger.hpp" +#include + +namespace RType { + namespace ECS { + + float MineSystem::Distance(float x1, float y1, float x2, float y2) { + float dx = x2 - x1; + float dy = y2 - y1; + return std::sqrt(dx * dx + dy * dy); + } + + void MineSystem::Update(Registry& registry, float deltaTime) { + auto mines = registry.GetEntitiesWithComponent(); + + for (auto mineEntity : mines) { + if (!registry.IsEntityAlive(mineEntity)) { + continue; + } + + auto& mine = registry.GetComponent(mineEntity); + const auto& minePos = registry.GetComponent(mineEntity); + + mine.timer += deltaTime; + + if (mine.timer >= mine.lifeTime) { + Core::Logger::Debug("[MineSystem] Mine {} expired", static_cast(mineEntity)); + registry.DestroyEntity(mineEntity); + continue; + } + + if (mine.isExploding) { + mine.explosionTimer += deltaTime; + if (mine.explosionTimer >= 0.6f) { + Core::Logger::Debug("[MineSystem] Mine {} explosion finished", static_cast(mineEntity)); + registry.DestroyEntity(mineEntity); + } + continue; + } + + auto players = registry.GetEntitiesWithComponent(); + for (auto playerEntity : players) { + if (!registry.IsEntityAlive(playerEntity)) { + continue; + } + + if (!registry.HasComponent(playerEntity)) { + continue; + } + + const auto& playerPos = registry.GetComponent(playerEntity); + float distance = Distance(minePos.x, minePos.y, playerPos.x, playerPos.y); + + if (distance <= mine.proximityRadius) { + mine.isExploding = true; + mine.explosionTimer = 0.0f; + + Core::Logger::Info("[MineSystem] Mine {} triggered by player at distance {}", + static_cast(mineEntity), distance); + + for (auto targetPlayer : players) { + if (!registry.IsEntityAlive(targetPlayer)) { + continue; + } + + if (!registry.HasComponent(targetPlayer)) { + continue; + } + + const auto& targetPos = registry.GetComponent(targetPlayer); + float explosionDist = Distance(minePos.x, minePos.y, targetPos.x, targetPos.y); + + if (explosionDist <= mine.explosionRadius) { + if (registry.HasComponent(targetPlayer)) { + auto& health = registry.GetComponent(targetPlayer); + int damage = registry.HasComponent(mineEntity) ? + registry.GetComponent(mineEntity).amount : 30; + health.current -= damage; + + Core::Logger::Info("[MineSystem] Player {} took {} damage from mine explosion", + static_cast(targetPlayer), damage); + } + } + } + break; + } + } + } + } + + } +} diff --git a/engine/src/ECS/MovementSystem.cpp b/engine/src/ECS/MovementSystem.cpp new file mode 100644 index 0000000..11d5bd9 --- /dev/null +++ b/engine/src/ECS/MovementSystem.cpp @@ -0,0 +1,38 @@ +#include "ECS/MovementSystem.hpp" +#include "ECS/Component.hpp" +#include + +namespace RType { + + namespace ECS { + + void MovementSystem::Update(Registry& registry, float deltaTime) { + auto entities = registry.GetEntitiesWithComponent(); + + for (Entity entity : entities) { + if (!registry.HasComponent(entity)) { + continue; + } + + // CRITICAL FIX: Skip obstacles - they should NEVER move! + if (registry.HasComponent(entity)) { + static int obstacleVelocityLog = 0; + if (obstacleVelocityLog < 10) { + std::cerr << "[MOVEMENT BUG] Obstacle entity " << entity + << " has Velocity component! Removing it." << std::endl; + obstacleVelocityLog++; + } + registry.RemoveComponent(entity); + continue; + } + + auto& position = registry.GetComponent(entity); + const auto& velocity = registry.GetComponent(entity); + + position.x += velocity.dx * deltaTime; + position.y += velocity.dy * deltaTime; + } + } + } + +} diff --git a/engine/src/ECS/ObstacleCollisionResponseSystem.cpp b/engine/src/ECS/ObstacleCollisionResponseSystem.cpp new file mode 100644 index 0000000..be288e0 --- /dev/null +++ b/engine/src/ECS/ObstacleCollisionResponseSystem.cpp @@ -0,0 +1,149 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** ObstacleCollisionResponseSystem implementation +*/ + +#include "ECS/ObstacleCollisionResponseSystem.hpp" +#include +#include + +namespace RType { + namespace ECS { + + void ObstacleCollisionResponseSystem::Update(Registry& registry, float deltaTime) { + auto obstacles = registry.GetEntitiesWithComponent(); + + for (auto obstacle : obstacles) { + if (!registry.IsEntityAlive(obstacle)) { + continue; + } + + if (!registry.HasComponent(obstacle)) { + continue; + } + + const auto& obstacleComp = registry.GetComponent(obstacle); + if (!obstacleComp.blocking) { + continue; + } + + auto& event = registry.GetComponent(obstacle); + Entity other = event.other; + + if (!registry.IsEntityAlive(other)) { + continue; + } + + // Handle enemy-obstacle collisions (enemies take damage and are blocked) + if (registry.HasComponent(other)) { + // Apply damage to enemy + if (registry.HasComponent(other)) { + auto& enemyHealth = registry.GetComponent(other); + // Obstacles deal damage to enemies + constexpr int OBSTACLE_DAMAGE = 50; + enemyHealth.current -= OBSTACLE_DAMAGE; + + if (enemyHealth.current <= 0) { + enemyHealth.current = 0; + // Enemy will be destroyed by HealthSystem + } + } + + if (registry.HasComponent(other) && + registry.HasComponent(obstacle)) { + + bool resolved = false; + const bool hasEnemyBox = registry.HasComponent(other); + const bool hasObstacleBox = registry.HasComponent(obstacle); + + if (hasEnemyBox && hasObstacleBox) { + auto& enemyPosRef = registry.GetComponent(other); + const auto& obstaclePos = registry.GetComponent(obstacle); + const auto& enemyBox = registry.GetComponent(other); + const auto& obstacleBox = registry.GetComponent(obstacle); + + float enemyLeft = enemyPosRef.x; + float enemyRight = enemyPosRef.x + enemyBox.width; + float enemyTop = enemyPosRef.y; + float enemyBottom = enemyPosRef.y + enemyBox.height; + + float obstacleLeft = obstaclePos.x; + float obstacleRight = obstaclePos.x + obstacleBox.width; + float obstacleTop = obstaclePos.y; + float obstacleBottom = obstaclePos.y + obstacleBox.height; + + float penRight = obstacleRight - enemyLeft; + float penLeft = enemyRight - obstacleLeft; + float penBottom = obstacleBottom - enemyTop; + float penTop = enemyBottom - obstacleTop; + + if (penLeft > 0.0f && penRight > 0.0f && + penTop > 0.0f && penBottom > 0.0f) { + const float separationBias = 0.5f; + + if (std::min(penLeft, penRight) < + std::min(penTop, penBottom)) { + if (penLeft < penRight) { + enemyPosRef.x -= penLeft + separationBias; + } else { + enemyPosRef.x += penRight + separationBias; + } + if (registry.HasComponent(other)) { + auto& enemyVel = registry.GetComponent(other); + enemyVel.dx = 0.0f; + } + } else { + if (penTop < penBottom) { + enemyPosRef.y -= penTop + separationBias; + } else { + enemyPosRef.y += penBottom + separationBias; + } + if (registry.HasComponent(other)) { + auto& enemyVel = registry.GetComponent(other); + enemyVel.dy = 0.0f; + } + } + resolved = true; + } + } + + if (!resolved) { + if (registry.HasComponent(other)) { + auto& enemyVel = registry.GetComponent(other); + enemyVel.dx = 0.0f; + enemyVel.dy = 0.0f; + } + + const auto& enemyPos = registry.GetComponent(other); + const auto& obstaclePos = registry.GetComponent(obstacle); + float dx = enemyPos.x - obstaclePos.x; + float dy = enemyPos.y - obstaclePos.y; + float distance = std::sqrt(dx * dx + dy * dy); + + if (distance > 0.0f) { + dx /= distance; + dy /= distance; + + float pushDistance = 5.0f; + if (registry.HasComponent(obstacle)) { + const auto& box = registry.GetComponent(obstacle); + pushDistance = std::max(box.width, box.height) * 0.5f + 10.0f; + } else if (registry.HasComponent(obstacle)) { + const auto& circle = registry.GetComponent(obstacle); + pushDistance = circle.radius + 10.0f; + } + + auto& enemyPosRef = registry.GetComponent(other); + enemyPosRef.x = obstaclePos.x + dx * pushDistance; + enemyPosRef.y = obstaclePos.y + dy * pushDistance; + } + } + } + } + } + } + + } +} diff --git a/engine/src/ECS/PlayerCollisionResponseSystem.cpp b/engine/src/ECS/PlayerCollisionResponseSystem.cpp new file mode 100644 index 0000000..2e75ab2 --- /dev/null +++ b/engine/src/ECS/PlayerCollisionResponseSystem.cpp @@ -0,0 +1,324 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** PlayerCollisionResponseSystem implementation +*/ + +#include "ECS/PlayerCollisionResponseSystem.hpp" +#include +#include +#include + +namespace RType { + namespace ECS { + + void PlayerCollisionResponseSystem::Update(Registry& registry, float deltaTime) { + auto players = registry.GetEntitiesWithComponent(); + + for (auto player : players) { + if (!registry.IsEntityAlive(player)) { + continue; + } + + if (registry.HasComponent(player)) { + auto& invincibility = registry.GetComponent(player); + invincibility.remainingTime -= deltaTime; + if (invincibility.remainingTime <= 0.0f) { + invincibility.remainingTime = 0.0f; + } + } + + if (!registry.HasComponent(player)) { + continue; + } + + auto& event = registry.GetComponent(player); + Entity other = event.other; + + if (!registry.IsEntityAlive(other)) { + continue; + } + + bool hitEnemy = registry.HasComponent(other); + bool hitObstacle = registry.HasComponent(other); + bool hitBoss = registry.HasComponent(other); + + if (hitEnemy) { + if (registry.HasComponent(other) && registry.HasComponent(player)) { + if (!registry.HasComponent(player)) { + const auto& damageComp = registry.GetComponent(other); + auto& playerHealth = registry.GetComponent(player); + + playerHealth.current -= damageComp.amount; + if (playerHealth.current < 0) { + playerHealth.current = 0; + } + } + } + + registry.DestroyEntity(other); + } + + if (hitObstacle) { + const auto& obstacle = registry.GetComponent(other); + + if (obstacle.blocking) { + static int serverCollisionLog = 0; + if (serverCollisionLog < 5) { + if (registry.HasComponent(other) && registry.HasComponent(other)) { + const auto& obstPos = registry.GetComponent(other); + const auto& obstBox = registry.GetComponent(other); + std::cout << "[SERVER PLAYER-OBSTACLE COLLISION] Player entity=" << player + << " vs Obstacle entity=" << other + << " at (" << obstPos.x << "," << obstPos.y << ")" + << " size=(" << obstBox.width << "," << obstBox.height << ")" << std::endl; + serverCollisionLog++; + } + } + bool hasShield = registry.HasComponent(player); + + bool isInvincible = false; + if (registry.HasComponent(player)) { + auto& invincibility = registry.GetComponent(player); + if (invincibility.remainingTime > 0.0f) { + isInvincible = true; + } + } + + if (!hasShield && !isInvincible && registry.HasComponent(player)) { + auto& playerHealth = registry.GetComponent(player); + constexpr int OBSTACLE_DAMAGE = 10; + playerHealth.current -= OBSTACLE_DAMAGE; + + if (playerHealth.current < 0) { + playerHealth.current = 0; + } + + constexpr float INVINCIBILITY_DURATION = 1.0f; + if (registry.HasComponent(player)) { + auto& invincibility = registry.GetComponent(player); + invincibility.remainingTime = INVINCIBILITY_DURATION; + } else { + registry.AddComponent(player, Invincibility(INVINCIBILITY_DURATION)); + } + } + + if (registry.HasComponent(player) && + registry.HasComponent(other)) { + + bool resolved = false; + const bool hasPlayerBox = registry.HasComponent(player); + const bool hasObstacleBox = registry.HasComponent(other); + + if (hasPlayerBox && hasObstacleBox) { + auto& playerPosRef = registry.GetComponent(player); + const auto& obstaclePos = registry.GetComponent(other); + const auto& playerBox = registry.GetComponent(player); + const auto& obstacleBox = registry.GetComponent(other); + + float playerLeft = playerPosRef.x; + float playerRight = playerPosRef.x + playerBox.width; + float playerTop = playerPosRef.y; + float playerBottom = playerPosRef.y + playerBox.height; + + float obstacleLeft = obstaclePos.x; + float obstacleRight = obstaclePos.x + obstacleBox.width; + float obstacleTop = obstaclePos.y; + float obstacleBottom = obstaclePos.y + obstacleBox.height; + + float penetrationRight = obstacleRight - playerLeft; + float penetrationLeft = playerRight - obstacleLeft; + float penetrationBottom = obstacleBottom - playerTop; + float penetrationTop = playerBottom - obstacleTop; + + if (penetrationLeft > 0.0f && penetrationRight > 0.0f && + penetrationTop > 0.0f && penetrationBottom > 0.0f) { + const float separationBias = 0.5f; + + if (std::min(penetrationLeft, penetrationRight) < + std::min(penetrationTop, penetrationBottom)) { + if (penetrationLeft < penetrationRight) { + playerPosRef.x -= penetrationLeft + separationBias; + } else { + playerPosRef.x += penetrationRight + separationBias; + } + if (registry.HasComponent(player)) { + auto& playerVel = registry.GetComponent(player); + playerVel.dx = 0.0f; + } + } else { + if (penetrationTop < penetrationBottom) { + playerPosRef.y -= penetrationTop + separationBias; + } else { + playerPosRef.y += penetrationBottom + separationBias; + } + if (registry.HasComponent(player)) { + auto& playerVel = registry.GetComponent(player); + playerVel.dy = 0.0f; + } + } + resolved = true; + } + } + + if (!resolved) { + // Fallback to radial push when AABB data is missing + if (registry.HasComponent(player)) { + auto& playerVel = registry.GetComponent(player); + playerVel.dx = 0.0f; + playerVel.dy = 0.0f; + } + + const auto& playerPos = registry.GetComponent(player); + const auto& obstaclePos = registry.GetComponent(other); + + float dx = playerPos.x - obstaclePos.x; + float dy = playerPos.y - obstaclePos.y; + float distance = std::sqrt(dx * dx + dy * dy); + + if (distance > 0.0f) { + dx /= distance; + dy /= distance; + + float pushDistance = 5.0f; + if (registry.HasComponent(other)) { + const auto& box = registry.GetComponent(other); + pushDistance = std::max(box.width, box.height) * 0.5f + 10.0f; + } else if (registry.HasComponent(other)) { + const auto& circle = registry.GetComponent(other); + pushDistance = circle.radius + 10.0f; + } + + auto& playerPosRef = registry.GetComponent(player); + playerPosRef.x = obstaclePos.x + dx * pushDistance; + playerPosRef.y = obstaclePos.y + dy * pushDistance; + } + } + } + } + } + + if (hitBoss) { + bool hasShield = registry.HasComponent(player); + + bool isInvincible = false; + if (registry.HasComponent(player)) { + auto& invincibility = registry.GetComponent(player); + if (invincibility.remainingTime > 0.0f) { + isInvincible = true; + } + } + + if (!hasShield && !isInvincible && registry.HasComponent(player)) { + auto& playerHealth = registry.GetComponent(player); + constexpr int BOSS_COLLISION_DAMAGE = 10; + playerHealth.current -= BOSS_COLLISION_DAMAGE; + + if (playerHealth.current < 0) { + playerHealth.current = 0; + } + + constexpr float INVINCIBILITY_DURATION = 1.0f; + if (registry.HasComponent(player)) { + auto& invincibility = registry.GetComponent(player); + invincibility.remainingTime = INVINCIBILITY_DURATION; + } else { + registry.AddComponent(player, Invincibility(INVINCIBILITY_DURATION)); + } + } + + if (registry.HasComponent(player) && + registry.HasComponent(other)) { + + bool resolved = false; + const bool hasPlayerBox = registry.HasComponent(player); + const bool hasBossBox = registry.HasComponent(other); + + if (hasPlayerBox && hasBossBox) { + auto& playerPosRef = registry.GetComponent(player); + const auto& bossPos = registry.GetComponent(other); + const auto& playerBox = registry.GetComponent(player); + const auto& bossBox = registry.GetComponent(other); + + float playerLeft = playerPosRef.x; + float playerRight = playerPosRef.x + playerBox.width; + float playerTop = playerPosRef.y; + float playerBottom = playerPosRef.y + playerBox.height; + + float bossLeft = bossPos.x; + float bossRight = bossPos.x + bossBox.width; + float bossTop = bossPos.y; + float bossBottom = bossPos.y + bossBox.height; + + float penetrationRight = bossRight - playerLeft; + float penetrationLeft = playerRight - bossLeft; + float penetrationBottom = bossBottom - playerTop; + float penetrationTop = playerBottom - bossTop; + + if (penetrationLeft > 0.0f && penetrationRight > 0.0f && + penetrationTop > 0.0f && penetrationBottom > 0.0f) { + const float separationBias = 2.0f; + + if (std::min(penetrationLeft, penetrationRight) < + std::min(penetrationTop, penetrationBottom)) { + if (penetrationLeft < penetrationRight) { + playerPosRef.x -= penetrationLeft + separationBias; + } else { + playerPosRef.x += penetrationRight + separationBias; + } + if (registry.HasComponent(player)) { + auto& playerVel = registry.GetComponent(player); + playerVel.dx = 0.0f; + } + } else { + if (penetrationTop < penetrationBottom) { + playerPosRef.y -= penetrationTop + separationBias; + } else { + playerPosRef.y += penetrationBottom + separationBias; + } + if (registry.HasComponent(player)) { + auto& playerVel = registry.GetComponent(player); + playerVel.dy = 0.0f; + } + } + resolved = true; + } + } + + if (!resolved) { + if (registry.HasComponent(player)) { + auto& playerVel = registry.GetComponent(player); + playerVel.dx = 0.0f; + playerVel.dy = 0.0f; + } + + const auto& playerPos = registry.GetComponent(player); + const auto& bossPos = registry.GetComponent(other); + + float dx = playerPos.x - bossPos.x; + float dy = playerPos.y - bossPos.y; + float distance = std::sqrt(dx * dx + dy * dy); + + if (distance > 0.0f) { + dx /= distance; + dy /= distance; + + float pushDistance = 50.0f; + if (registry.HasComponent(other)) { + const auto& box = registry.GetComponent(other); + pushDistance = std::max(box.width, box.height) * 0.5f + 20.0f; + } + + auto& playerPosRef = registry.GetComponent(player); + playerPosRef.x = bossPos.x + dx * pushDistance; + playerPosRef.y = bossPos.y + dy * pushDistance; + } + } + } + } + } + } + + } +} diff --git a/engine/src/ECS/PlayerFactory.cpp b/engine/src/ECS/PlayerFactory.cpp new file mode 100644 index 0000000..113a091 --- /dev/null +++ b/engine/src/ECS/PlayerFactory.cpp @@ -0,0 +1,95 @@ +#include "ECS/PlayerFactory.hpp" +#include "Core/Logger.hpp" +#include +#include +#include + +namespace RType { + + namespace ECS { + + Entity PlayerFactory::CreatePlayer(Registry& registry, uint8_t playerNumber, uint64_t playerHash, + float startX, float startY, Renderer::IRenderer* renderer) { + Entity player = registry.CreateEntity(); + + float yPos = startY + (playerNumber - 1) * 150.0f; + + registry.AddComponent(player, Position(startX, yPos)); + + registry.AddComponent(player, Velocity(0.0f, 0.0f)); + + registry.AddComponent(player, Player(playerNumber, playerHash, false)); + + registry.AddComponent(player, Controllable(200.0f)); + + registry.AddComponent(player, Health(300, 300)); + registry.AddComponent(player, ScoreValue(0)); + registry.AddComponent(player, ScoreTimer(0.0f)); + registry.AddComponent(player, Shooter(0.2f, 50.0f, 0.0f)); + registry.AddComponent(player, ShootCommand()); + registry.AddComponent(player, BoxCollider(25.0f, 25.0f)); + registry.AddComponent(player, CircleCollider(12.5f)); + registry.AddComponent(player, + CollisionLayer(CollisionLayers::PLAYER, + CollisionLayers::ENEMY | CollisionLayers::ENEMY_BULLET | CollisionLayers::OBSTACLE)); + + if (renderer) { + std::string spritePath = GetPlayerSpritePath(playerNumber); + Renderer::TextureId textureId = renderer->LoadTexture(spritePath); + + if (textureId == Renderer::INVALID_TEXTURE_ID) { + Core::Logger::Warning("Failed to load player texture: {}, using default", spritePath); + textureId = renderer->LoadTexture("../assets/spaceships/nave2.png"); + } + + if (textureId != Renderer::INVALID_TEXTURE_ID) { + Renderer::SpriteId spriteId = renderer->CreateSprite( + textureId, Renderer::Rectangle{{0.0f, 0.0f}, {256.0f, 256.0f}}); + + auto& drawable = registry.AddComponent(player, Drawable(spriteId, 2)); + drawable.tint = GetPlayerColor(playerNumber); + drawable.scale = Math::Vector2(0.5f, 0.5f); + drawable.origin = Math::Vector2(128.0f, 128.0f); + } else { + Core::Logger::Error("Failed to load any player texture for player {}", playerNumber); + } + } + + Core::Logger::Info("Created player #{} at position ({}, {})", playerNumber, startX, yPos); + + return player; + } + + Math::Color PlayerFactory::GetPlayerColor(uint8_t playerNumber) { + switch (playerNumber) { + case 1: + return Math::Color(0.2f, 0.6f, 1.0f, 1.0f); + case 2: + return Math::Color(1.0f, 0.2f, 0.2f, 1.0f); + case 3: + return Math::Color(0.2f, 1.0f, 0.2f, 1.0f); + case 4: + return Math::Color(1.0f, 0.8f, 0.2f, 1.0f); + default: + return Math::Color(1.0f, 1.0f, 1.0f, 1.0f); + } + } + + std::string PlayerFactory::GetPlayerSpritePath(uint8_t playerNumber) { + switch (playerNumber) { + case 1: + return "../assets/spaceships/player_blue.png"; + case 2: + return "../assets/spaceships/player_red.png"; + case 3: + return "../assets/spaceships/player_green.png"; + case 4: + return "../assets/spaceships/nave2.png"; + default: + return "../assets/spaceships/nave2.png"; + } + } + + } + +} diff --git a/engine/src/ECS/PlayerSystem.cpp b/engine/src/ECS/PlayerSystem.cpp new file mode 100644 index 0000000..2e078b2 --- /dev/null +++ b/engine/src/ECS/PlayerSystem.cpp @@ -0,0 +1,56 @@ +#include "ECS/PlayerSystem.hpp" +#include "ECS/Component.hpp" +#include "ECS/PlayerFactory.hpp" +#include + +namespace RType { + + namespace ECS { + + PlayerSystem::PlayerSystem(Renderer::IRenderer* renderer) + : m_renderer(renderer) {} + + void PlayerSystem::Update(Registry& registry, float /* deltaTime */) { + auto players = registry.GetEntitiesWithComponent(); + + for (Entity player : players) { + if (!registry.HasComponent(player)) { + continue; + } + + ClampPlayerToScreen(registry, player, 1280.0f, 720.0f); + } + } + + Entity PlayerSystem::CreatePlayer(Registry& registry, uint8_t playerNumber, uint64_t playerHash, + float startX, float startY, Renderer::IRenderer* renderer) { + return PlayerFactory::CreatePlayer(registry, playerNumber, playerHash, startX, startY, renderer); + } + + void PlayerSystem::ClampPlayerToScreen(Registry& registry, Entity player, float screenWidth, + float screenHeight) { + if (!registry.HasComponent(player)) { + return; + } + + auto& pos = registry.GetComponent(player); + + float minX = 0.0f; + float maxX = screenWidth; + float minY = 0.0f; + float maxY = screenHeight; + + if (registry.HasComponent(player)) { + const auto& collider = registry.GetComponent(player); + maxX -= collider.width; + maxY -= collider.height; + } + + pos.x = std::clamp(pos.x, minX, maxX); + pos.y = std::clamp(pos.y, minY, maxY); + } + + + } + +} diff --git a/engine/src/ECS/PowerUpCollisionSystem.cpp b/engine/src/ECS/PowerUpCollisionSystem.cpp new file mode 100644 index 0000000..1dcdbad --- /dev/null +++ b/engine/src/ECS/PowerUpCollisionSystem.cpp @@ -0,0 +1,76 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** PowerUpCollisionSystem +*/ + +#include "ECS/PowerUpCollisionSystem.hpp" +#include "ECS/PowerUpFactory.hpp" +#include "ECS/CollisionDetectionSystem.hpp" +#include "ECS/Component.hpp" +#include "Core/Logger.hpp" + +namespace RType { + namespace ECS { + + PowerUpCollisionSystem::PowerUpCollisionSystem(Renderer::IRenderer* renderer) + : m_renderer(renderer) {} + + void PowerUpCollisionSystem::Update(Registry& registry, float deltaTime) { + (void)deltaTime; + + auto powerups = registry.GetEntitiesWithComponent(); + auto players = registry.GetEntitiesWithComponent(); + + if (powerups.empty() || players.empty()) { + return; + } + + std::vector powerupsToDestroy; + + for (Entity powerup : powerups) { + if (!registry.IsEntityAlive(powerup)) { + continue; + } + + for (Entity player : players) { + if (!registry.IsEntityAlive(player)) { + continue; + } + + if (CollisionDetectionSystem::CheckCollision(registry, powerup, player)) { + const auto& powerupComp = registry.GetComponent(powerup); + + PowerUpFactory::ApplyPowerUpToPlayer( + registry, + player, + powerupComp.type, + m_renderer + ); + + Core::Logger::Info( + "[PowerUpCollisionSystem] Player collected powerup: {}", + PowerUpFactory::GetPowerUpName(powerupComp.type) + ); + + if (m_powerUpSound != Audio::INVALID_SOUND_ID) { + auto sfx = registry.CreateEntity(); + auto& se = registry.AddComponent(sfx, SoundEffect(m_powerUpSound, 1.0f)); + se.pitch = 1.0f; + } + + powerupsToDestroy.push_back(powerup); + break; + } + } + } + + // Destroy collected powerups + for (Entity powerup : powerupsToDestroy) { + registry.DestroyEntity(powerup); + } + } + + } +} diff --git a/engine/src/ECS/PowerUpFactory.cpp b/engine/src/ECS/PowerUpFactory.cpp new file mode 100644 index 0000000..bf97a9c --- /dev/null +++ b/engine/src/ECS/PowerUpFactory.cpp @@ -0,0 +1,265 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** PowerUpFactory +*/ + +#include "ECS/PowerUpFactory.hpp" +#include "ECS/EffectFactory.hpp" +#include "Animation/AnimationTypes.hpp" +#include "Core/Logger.hpp" +#include +#include +#include + +namespace RType { + namespace ECS { + + namespace { + std::atomic s_powerupIdCounter{1}; + + struct PowerUpData { + Math::Color color; + const char* spritePath; + const char* name; + }; + + const std::array POWERUP_DATA_TABLE = {{ + // FIRE_RATE_BOOST - Red + {Math::Color(1.0f, 0.2f, 0.2f, 1.0f), + "../assets/powerups/spread.png", "Fire Rate Boost"}, + // SPREAD_SHOT - Yellow + {Math::Color(1.0f, 1.0f, 0.2f, 1.0f), + "../assets/powerups/spread.png", "Spread Shot"}, + // LASER_BEAM - Cyan + {Math::Color(0.2f, 1.0f, 1.0f, 1.0f), + "../assets/powerups/laser.png", "Laser Beam"}, + // FORCE_POD - Orange (iconic R-Type color) + {Math::Color(1.0f, 0.6f, 0.0f, 1.0f), + "../assets/powerups/force_pod.png", "Force Pod"}, + // SPEED_BOOST - Green + {Math::Color(0.2f, 1.0f, 0.2f, 1.0f), + "../assets/powerups/speed.png", "Speed Boost"}, + // SHIELD - Blue + {Math::Color(0.4f, 0.4f, 1.0f, 1.0f), + "../assets/powerups/shield.png", "Shield"} + }}; + } + + Entity PowerUpFactory::CreatePowerUp( + Registry& registry, + PowerUpType type, + float startX, float startY, + Renderer::IRenderer* renderer, + const EffectFactory* effectFactory + ) { + Entity powerup = registry.CreateEntity(); + + registry.AddComponent(powerup, Position(startX, startY)); + registry.AddComponent(powerup, Velocity(-50.0f, 0.0f)); + + uint32_t uniqueId = s_powerupIdCounter.fetch_add(1); + registry.AddComponent(powerup, PowerUp(type, uniqueId)); + + registry.AddComponent(powerup, BoxCollider(32.0f, 32.0f)); + registry.AddComponent(powerup, + CollisionLayer(CollisionLayers::POWERUP, + CollisionLayers::PLAYER)); + + if (renderer) { + const PowerUpData& data = POWERUP_DATA_TABLE[static_cast(type)]; + + Renderer::TextureId textureId = renderer->LoadTexture(data.spritePath); + if (textureId == Renderer::INVALID_TEXTURE_ID) { + textureId = renderer->LoadTexture("../assets/powerups/laser.png"); + } + + if (textureId != Renderer::INVALID_TEXTURE_ID) { + Renderer::SpriteId spriteId = renderer->CreateSprite( + textureId, + Renderer::Rectangle{{0.0f, 0.0f}, {64.0f, 64.0f}} + ); + + auto& drawable = registry.AddComponent(powerup, Drawable(spriteId, 5)); + drawable.tint = data.color; + float scale = GetPowerUpScale(type); + drawable.scale = Math::Vector2(scale, scale); + + if (type == PowerUpType::FORCE_POD && effectFactory) { + const auto& config = effectFactory->GetConfig(); + if (config.forcePodAnimation != Animation::INVALID_CLIP_ID) { + if (config.forcePodSprite != Renderer::INVALID_SPRITE_ID) { + drawable.spriteId = config.forcePodSprite; + } + + auto& anim = registry.AddComponent(powerup, + SpriteAnimation(config.forcePodAnimation, true, 1.0f)); + anim.looping = true; + + if (config.forcePodFirstFrameRegion.size.x > 0.0f && config.forcePodFirstFrameRegion.size.y > 0.0f) { + anim.currentRegion = config.forcePodFirstFrameRegion; + anim.currentFrameIndex = 0; + } + + auto& animatedSprite = registry.AddComponent(powerup); + animatedSprite.needsUpdate = true; + } else { + auto& glow = registry.AddComponent(powerup); + glow.baseScale = scale; + } + } else { + auto& glow = registry.AddComponent(powerup); + glow.baseScale = scale; + } + } + } + + return powerup; + } + + void PowerUpFactory::ApplyPowerUpToPlayer( + Registry& registry, + Entity player, + PowerUpType type, + Renderer::IRenderer* renderer + ) { + if (!registry.HasComponent(player)) { + registry.AddComponent(player, ActivePowerUps()); + } + + auto& activePowerUps = registry.GetComponent(player); + + switch (type) { + case PowerUpType::FIRE_RATE_BOOST: { + if (!activePowerUps.hasFireRateBoost) { + activePowerUps.hasFireRateBoost = true; + + if (registry.HasComponent(player)) { + auto& shooter = registry.GetComponent(player); + shooter.fireRate *= 0.5f; + } + } + break; + } + + case PowerUpType::SPREAD_SHOT: { + if (!activePowerUps.hasSpreadShot) { + activePowerUps.hasSpreadShot = true; + activePowerUps.hasLaserBeam = false; + + if (registry.HasComponent(player)) { + registry.RemoveComponent(player); + } + registry.AddComponent( + player, + WeaponSlot(WeaponType::SPREAD, 0.3f, 20) + ); + } + break; + } + + case PowerUpType::LASER_BEAM: { + if (!activePowerUps.hasLaserBeam) { + activePowerUps.hasLaserBeam = true; + activePowerUps.hasSpreadShot = false; + + if (registry.HasComponent(player)) { + registry.RemoveComponent(player); + } + registry.AddComponent( + player, + WeaponSlot(WeaponType::LASER, 0.15f, 40) + ); + } + break; + } + + case PowerUpType::FORCE_POD: { + CreateForcePod(registry, player, renderer); + break; + } + + case PowerUpType::SPEED_BOOST: { + activePowerUps.speedMultiplier += 0.3f; + + if (registry.HasComponent(player)) { + auto& controllable = registry.GetComponent(player); + controllable.speed = 200.0f * activePowerUps.speedMultiplier; + } + break; + } + + case PowerUpType::SHIELD: { + if (!activePowerUps.hasShield) { + activePowerUps.hasShield = true; + constexpr float SHIELD_DURATION_SECONDS = 5.0f; + registry.AddComponent(player, Shield(SHIELD_DURATION_SECONDS)); + } + break; + } + } + } + + Entity PowerUpFactory::CreateForcePod( + Registry& registry, + Entity owner, + Renderer::IRenderer* renderer + ) { + Entity forcePod = registry.CreateEntity(); + + if (registry.HasComponent(owner)) { + const auto& ownerPos = registry.GetComponent(owner); + registry.AddComponent(forcePod, Position(ownerPos.x - 60.0f, ownerPos.y)); + } else { + registry.AddComponent(forcePod, Position(0.0f, 0.0f)); + } + + registry.AddComponent(forcePod, ForcePod(owner, -60.0f, 0.0f)); + registry.AddComponent(forcePod, BoxCollider(40.0f, 40.0f)); + + registry.AddComponent(forcePod, Shooter(0.25f, 50.0f, 20.0f)); + registry.AddComponent(forcePod, ShootCommand()); + + if (renderer) { + Renderer::TextureId textureId = renderer->LoadTexture("../assets/powerups/force_pod.png"); + if (textureId == Renderer::INVALID_TEXTURE_ID) { + textureId = renderer->LoadTexture("../assets/spaceships/player_blue.png"); + } + + if (textureId != Renderer::INVALID_TEXTURE_ID) { + Renderer::SpriteId spriteId = renderer->CreateSprite( + textureId, + Renderer::Rectangle{{0.0f, 0.0f}, {96.0f, 96.0f}} + ); + + auto& drawable = registry.AddComponent(forcePod, Drawable(spriteId, 9)); + drawable.tint = Math::Color(1.0f, 0.6f, 0.0f, 1.0f); + float scale = GetPowerUpScale(PowerUpType::FORCE_POD); + drawable.scale = Math::Vector2(scale, scale); + } + } + + return forcePod; + } + + Math::Color PowerUpFactory::GetPowerUpColor(PowerUpType type) { + return POWERUP_DATA_TABLE[static_cast(type)].color; + } + + const char* PowerUpFactory::GetPowerUpSpritePath(PowerUpType type) { + return POWERUP_DATA_TABLE[static_cast(type)].spritePath; + } + + const char* PowerUpFactory::GetPowerUpName(PowerUpType type) { + return POWERUP_DATA_TABLE[static_cast(type)].name; + } + + float PowerUpFactory::GetPowerUpScale(PowerUpType type) { + if (type == PowerUpType::FORCE_POD) { + return 0.33f * 5.0f; // 5x scale for force pod (upscaled more) + } + return 2.5f; + } + } +} diff --git a/engine/src/ECS/PowerUpSpawnSystem.cpp b/engine/src/ECS/PowerUpSpawnSystem.cpp new file mode 100644 index 0000000..a4016d7 --- /dev/null +++ b/engine/src/ECS/PowerUpSpawnSystem.cpp @@ -0,0 +1,83 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** PowerUpSpawnSystem +*/ + +#include "ECS/PowerUpSpawnSystem.hpp" +#include "ECS/PowerUpFactory.hpp" +#include "ECS/EffectFactory.hpp" +#include "ECS/Component.hpp" +#include "Core/Logger.hpp" +#include + +namespace RType { + namespace ECS { + + PowerUpSpawnSystem::PowerUpSpawnSystem( + Renderer::IRenderer* renderer, + float screenWidth, + float screenHeight + ) + : m_renderer(renderer), + m_screenWidth(screenWidth), + m_screenHeight(screenHeight), + m_rng(std::chrono::steady_clock::now().time_since_epoch().count()) + { + } + + void PowerUpSpawnSystem::Update(Registry& registry, float deltaTime) { + m_spawnTimer += deltaTime; + + if (m_spawnTimer >= m_spawnInterval) { + SpawnRandomPowerUp(registry); + m_spawnTimer = 0.0f; + } + + DestroyPowerUpsOffScreen(registry); + } + + void PowerUpSpawnSystem::SpawnRandomPowerUp(Registry& registry) { + // Random powerup type (0-5) + std::uniform_int_distribution typeDist(0, 5); + PowerUpType type = static_cast(typeDist(m_rng)); + + // Random Y position (50 to screenHeight - 50) + std::uniform_real_distribution yDist(50.0f, m_screenHeight - 50.0f); + float spawnY = yDist(m_rng); + + float spawnX = m_screenWidth + 50.0f; + + PowerUpFactory::CreatePowerUp( + registry, + type, + spawnX, + spawnY, + m_renderer, + m_effectFactory + ); + + Core::Logger::Info("[PowerUpSpawnSystem] Spawned {} powerup at ({}, {})", + PowerUpFactory::GetPowerUpName(type), spawnX, spawnY); + } + + void PowerUpSpawnSystem::DestroyPowerUpsOffScreen(Registry& registry) { + auto powerups = registry.GetEntitiesWithComponent(); + + for (Entity powerup : powerups) { + if (!registry.IsEntityAlive(powerup)) { + continue; + } + + if (registry.HasComponent(powerup)) { + const auto& pos = registry.GetComponent(powerup); + if (pos.x < -100.0f) { + registry.DestroyEntity(powerup); + } + } + } + } + + } +} diff --git a/engine/src/ECS/Registry.cpp b/engine/src/ECS/Registry.cpp new file mode 100644 index 0000000..96525e9 --- /dev/null +++ b/engine/src/ECS/Registry.cpp @@ -0,0 +1,80 @@ +#include "ECS/Registry.hpp" +#include + +namespace RType { + + namespace ECS { + + Registry::Registry() + : m_nextEntityID(1), m_entityCount(0) {} + + Entity Registry::CreateEntity() { + Entity newEntity; + + if (!m_freeEntityIds.empty()) { + newEntity = m_freeEntityIds.back(); + m_freeEntityIds.pop_back(); + +#ifdef DEBUG + // CRITICAL: Verify entity has no components before reuse + // If this fires, DestroyEntity didn't clean up properly + for (const auto& [componentID, pool] : m_componentPools) { + if (pool->Has(newEntity)) { + std::cerr << "[CRITICAL] Entity " << newEntity + << " being reused but still has components!" << std::endl; + pool->Remove(newEntity); // Emergency cleanup + } + } +#endif + } else { + newEntity = m_nextEntityID++; + } + + m_aliveEntities.insert(newEntity); + m_entityCount++; + return newEntity; + } + + void Registry::DestroyEntity(Entity entity) { + if (entity == NULL_ENTITY) { + return; + } + if (!IsEntityAlive(entity)) { + return; + } + + // Remove all components from this entity + for (auto& [componentID, pool] : m_componentPools) { + if (pool->Has(entity)) { + pool->Remove(entity); + } + } + +#ifdef DEBUG + // Debug validation: Verify all components were actually removed + // This helps catch component persistence bugs that cause entity type confusion + size_t remainingComponents = 0; + for (const auto& [componentID, pool] : m_componentPools) { + if (pool->Has(entity)) { + remainingComponents++; + } + } + if (remainingComponents > 0) { + std::cerr << "[DEBUG] WARNING: Entity " << entity + << " still has " << remainingComponents + << " component(s) after DestroyEntity cleanup!" << std::endl; + } +#endif + + m_aliveEntities.erase(entity); + m_entityCount--; + m_freeEntityIds.push_back(entity); + } + + bool Registry::IsEntityAlive(Entity entity) const { + return m_aliveEntities.find(entity) != m_aliveEntities.end(); + } + + } + +} diff --git a/engine/src/ECS/RenderingSystem.cpp b/engine/src/ECS/RenderingSystem.cpp new file mode 100644 index 0000000..2e62614 --- /dev/null +++ b/engine/src/ECS/RenderingSystem.cpp @@ -0,0 +1,73 @@ +#include "ECS/RenderingSystem.hpp" +#include "ECS/Component.hpp" +#include "Core/ColorFilter.hpp" +#include + +namespace RType { + + namespace ECS { + + RenderingSystem::RenderingSystem(Renderer::IRenderer* renderer) + : m_renderer(renderer) {} + + void RenderingSystem::Update(Registry& registry, float deltaTime) { + if (!m_renderer) { + return; + } + + auto entities = registry.GetEntitiesWithComponent(); + + std::vector> renderableEntities; + for (Entity entity : entities) { + if (!registry.IsEntityAlive(entity)) { + continue; + } + if (registry.HasComponent(entity)) { + const auto& drawable = registry.GetComponent(entity); + renderableEntities.emplace_back(entity, drawable.layer); + } + } + + std::sort(renderableEntities.begin(), renderableEntities.end(), + [](const std::pair& a, const std::pair& b) { + return a.second < b.second; + }); + + for (const auto& pair : renderableEntities) { + Entity entity = pair.first; + const auto& position = registry.GetComponent(entity); + const auto& drawable = registry.GetComponent(entity); + + if (drawable.spriteId == Renderer::INVALID_SPRITE_ID) { + continue; + } + + if (registry.HasComponent(entity) && registry.HasComponent(entity)) { + auto& animatedSprite = registry.GetComponent(entity); + const auto& anim = registry.GetComponent(entity); + + if (anim.currentRegion.size.x > 0 && anim.currentRegion.size.y > 0) { + if (animatedSprite.needsUpdate) { + m_renderer->SetSpriteRegion(drawable.spriteId, anim.currentRegion); + animatedSprite.needsUpdate = false; + } + } + } + + Renderer::Transform2D transform; + transform.position = Renderer::Vector2(position.x, position.y); + transform.scale = drawable.scale; + transform.rotation = drawable.rotation; + transform.origin = drawable.origin; + + Math::Color finalColor = drawable.tint; + if (RType::Core::ColorFilter::IsColourBlindModeEnabled()) { + finalColor = RType::Core::ColorFilter::ApplyColourBlindFilter(drawable.tint); + } + + m_renderer->DrawSprite(drawable.spriteId, transform, finalColor); + } + } + } + +} diff --git a/engine/src/ECS/ScoreSystem.cpp b/engine/src/ECS/ScoreSystem.cpp new file mode 100644 index 0000000..303ac93 --- /dev/null +++ b/engine/src/ECS/ScoreSystem.cpp @@ -0,0 +1,61 @@ +#include "../../include/ECS/ScoreSystem.hpp" +#include "../../include/ECS/Component.hpp" + + +namespace RType { + namespace ECS { + + void ScoreSystem::Update(Registry& registry, float deltaTime) { + auto enemiesKilled = registry.GetEntitiesWithComponent(); + + if (enemiesKilled.empty()) { + } + + for (auto enemyKilled : enemiesKilled) { + if (!registry.IsEntityAlive(enemyKilled) || !registry.HasComponent(enemyKilled) || !registry.HasComponent(enemyKilled)) { + continue; + } + const auto& enemyKilledComp = registry.GetComponent(enemyKilled); + Entity killer = enemyKilledComp.killedBy; + const auto& ennemyScoreValue = registry.GetComponent(enemyKilled); + + if (killer == NULL_ENTITY || !registry.IsEntityAlive(killer) || !registry.HasComponent(killer)) { + registry.RemoveComponent(enemyKilled); + continue; + } + + auto& killerScoreComp = registry.GetComponent(killer); + killerScoreComp.points += ennemyScoreValue.points; + + registry.RemoveComponent(enemyKilled); + } + + // +10 toutes les 10 secondes + constexpr float SCORE_INTERVAL_SECONDS = 10.0f; + constexpr uint32_t SCORE_PER_INTERVAL = 10; + + auto timedEntities = registry.GetEntitiesWithComponent(); + for (auto entity : timedEntities) { + if (!registry.IsEntityAlive(entity) || + !registry.HasComponent(entity) || + !registry.HasComponent(entity) || + !registry.HasComponent(entity)) { + continue; + } + + auto& timer = registry.GetComponent(entity); + timer.elapsed += deltaTime; + + if (timer.elapsed < SCORE_INTERVAL_SECONDS) { + continue; + } + + uint32_t intervals = static_cast(timer.elapsed / SCORE_INTERVAL_SECONDS); + timer.elapsed -= static_cast(intervals) * SCORE_INTERVAL_SECONDS; + + auto& score = registry.GetComponent(entity); + score.points += intervals * SCORE_PER_INTERVAL; + } + } + } +} \ No newline at end of file diff --git a/engine/src/ECS/ScrollingSystem.cpp b/engine/src/ECS/ScrollingSystem.cpp new file mode 100644 index 0000000..608437b --- /dev/null +++ b/engine/src/ECS/ScrollingSystem.cpp @@ -0,0 +1,115 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** ScrollingSystem +*/ + +#include "../../include/ECS/ScrollingSystem.hpp" +#include "../../include/ECS/Registry.hpp" +#include "../../include/ECS/Component.hpp" +#include + +namespace RType { + namespace ECS { + + void ScrollingSystem::Update(Registry& registry, float deltaTime) { + auto scrollables = registry.GetEntitiesWithComponent(); + + static bool loggedScrollingSystem = false; + if (!loggedScrollingSystem) { + std::cout << "[ScrollingSystem] Two-pass update running" << std::endl; + loggedScrollingSystem = true; + } + + // First pass: scroll visual entities (obstacles, backgrounds, etc.) + for (auto entity : scrollables) { + if (!registry.IsEntityAlive(entity)) { + continue; + } + + // Skip obstacle colliders in first pass (they'll be synced in second pass) + bool isObstacleCollider = registry.HasComponent(entity) && + !registry.HasComponent(entity); + if (isObstacleCollider) { + continue; + } + + const auto& scrollable = registry.GetComponent(entity); + + if (!registry.HasComponent(entity)) { + continue; + } + + auto& pos = registry.GetComponent(entity); + pos.x = pos.x + scrollable.speed * deltaTime; + + float destroyThreshold = -1500.0f; + if (pos.x < destroyThreshold) { + registry.DestroyEntity(entity); + } + } + + // Second pass: synchronize obstacle colliders to their visual entities + static int serverSyncDebug = 0; + for (auto entity : scrollables) { + if (!registry.IsEntityAlive(entity)) { + continue; + } + + bool isObstacleCollider = registry.HasComponent(entity) && + !registry.HasComponent(entity); + + if (!isObstacleCollider) { + continue; + } + + if (!registry.HasComponent(entity) || + !registry.HasComponent(entity)) { + continue; + } + + const auto& metadata = registry.GetComponent(entity); + auto& colliderPos = registry.GetComponent(entity); + + if (metadata.visualEntity != NULL_ENTITY && + registry.IsEntityAlive(metadata.visualEntity) && + registry.HasComponent(metadata.visualEntity) && + registry.HasComponent(metadata.visualEntity)) { + + const auto& visualPos = registry.GetComponent(metadata.visualEntity); + + // Debug first few server syncs + if (serverSyncDebug < 3) { + std::cout << "[ScrollingSystem SYNC] Collider " << entity + << ": visual=(" << visualPos.x << "," << visualPos.y << ")" + << " offset=(" << metadata.offsetX << "," << metadata.offsetY << ")" + << " -> collider=(" << visualPos.x + metadata.offsetX << "," + << visualPos.y + metadata.offsetY << ")" << std::endl; + serverSyncDebug++; + } + + // Collider position = visual position + stored offset + colliderPos.x = visualPos.x + metadata.offsetX; + colliderPos.y = visualPos.y + metadata.offsetY; + } + else if (metadata.visualEntity != NULL_ENTITY && + registry.IsEntityAlive(metadata.visualEntity) && + !registry.HasComponent(metadata.visualEntity)) { + registry.DestroyEntity(entity); + continue; + } + else if (registry.HasComponent(entity)) { + const auto& scrollable = registry.GetComponent(entity); + colliderPos.x = colliderPos.x + scrollable.speed * deltaTime; + } + + // Destroy threshold for obstacle colliders + float destroyThreshold = -2000.0f; + if (colliderPos.x < destroyThreshold) { + registry.DestroyEntity(entity); + } + } + } + } +} diff --git a/engine/src/ECS/ShieldSystem.cpp b/engine/src/ECS/ShieldSystem.cpp new file mode 100644 index 0000000..1ba20f4 --- /dev/null +++ b/engine/src/ECS/ShieldSystem.cpp @@ -0,0 +1,75 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** ShieldSystem +*/ + +#include "ECS/ShieldSystem.hpp" +#include "ECS/Component.hpp" + +namespace RType { + namespace ECS { + + void ShieldSystem::Update(Registry& registry, float deltaTime) { + auto shields = registry.GetEntitiesWithComponent(); + + std::vector expiredShields; + + for (Entity entity : shields) { + if (!registry.IsEntityAlive(entity)) { + continue; + } + + auto& shield = registry.GetComponent(entity); + + if (shield.duration > 0.0f) { + shield.timeRemaining -= deltaTime; + + if (shield.timeRemaining <= 0.0f) { + expiredShields.push_back(entity); + } + } + + if (registry.HasComponent(entity)) { + auto& drawable = registry.GetComponent(entity); + drawable.tint = Math::Color(0.7f, 0.7f, 1.0f, 1.0f); + } + } + + for (Entity entity : expiredShields) { + registry.RemoveComponent(entity); + + if (registry.HasComponent(entity)) { + auto& active = registry.GetComponent(entity); + active.hasShield = false; + } + + if (registry.HasComponent(entity)) { + auto& drawable = registry.GetComponent(entity); + drawable.tint = Math::Color(1.0f, 1.0f, 1.0f, 1.0f); + } + } + + auto players = registry.GetEntitiesWithComponent(); + for (Entity entity : players) { + if (!registry.IsEntityAlive(entity)) { + continue; + } + + if (!registry.HasComponent(entity) && registry.HasComponent(entity)) { + auto& drawable = registry.GetComponent(entity); + if (registry.HasComponent(entity)) { + const auto& active = registry.GetComponent(entity); + if (!active.hasShield && drawable.tint.r < 0.8f && drawable.tint.b > 0.8f) { + drawable.tint = Math::Color(1.0f, 1.0f, 1.0f, 1.0f); + } + } else if (drawable.tint.r < 0.8f && drawable.tint.b > 0.8f) { + drawable.tint = Math::Color(1.0f, 1.0f, 1.0f, 1.0f); + } + } + } + } + + } +} diff --git a/engine/src/ECS/ShootingSystem.cpp b/engine/src/ECS/ShootingSystem.cpp new file mode 100644 index 0000000..f33f12c --- /dev/null +++ b/engine/src/ECS/ShootingSystem.cpp @@ -0,0 +1,276 @@ +#include "../../include/ECS/ScoreSystem.hpp" +#include "../../include/ECS/Component.hpp" +#include "../../include/ECS/ShootingSystem.hpp" +#include "../../include/ECS/RenderingSystem.hpp" +#include "../../include/ECS/EffectFactory.hpp" +#include +#include + +namespace RType { + namespace ECS { + + ShootingSystem::ShootingSystem(Renderer::SpriteId bulletSprite) + : m_bulletSprite(bulletSprite) {} + + void ShootingSystem::Update(Registry& registry, float deltaTime) { + + auto bullets = registry.GetEntitiesWithComponent(); + std::vector bulletsToDestroy; + + for (auto bulletEntity : bullets) { + if (registry.IsEntityAlive(bulletEntity) && registry.HasComponent(bulletEntity)) { + auto& pos = registry.GetComponent(bulletEntity); + if (pos.x > 1380.0f || pos.x < -100.0f || pos.y > 820.0f || pos.y < -100.0f) { + bulletsToDestroy.push_back(bulletEntity); + } + } + } + + for (auto entity : bulletsToDestroy) { + registry.DestroyEntity(entity); + } + + auto shooters = registry.GetEntitiesWithComponent(); + struct BulletSpawn { + float x, y; + Entity shooter; + }; + std::vector spawns; + + for (auto shooterEntity : shooters) { + if (!registry.IsEntityAlive(shooterEntity) || !registry.HasComponent(shooterEntity)) { + continue; + } + + // Skip entities with WeaponSlot - they use special weapons instead of base Shooter + if (registry.HasComponent(shooterEntity)) { + continue; + } + + // CRITICAL FIX: Prevent obstacles from shooting due to entity ID reuse + // Actively clean up contaminated components + if (registry.HasComponent(shooterEntity) || + registry.HasComponent(shooterEntity)) { + std::cerr << "[SHOOTING] BLOCKED obstacle entity " << shooterEntity + << " from shooting (has Shooter=" << registry.HasComponent(shooterEntity) + << " ShootCommand=" << registry.HasComponent(shooterEntity) << ")" << std::endl; + if (registry.HasComponent(shooterEntity)) { + registry.RemoveComponent(shooterEntity); + } + if (registry.HasComponent(shooterEntity)) { + registry.RemoveComponent(shooterEntity); + } + continue; + } + + auto& shooterComp = registry.GetComponent(shooterEntity); + + shooterComp.cooldown = shooterComp.cooldown - deltaTime; + + if (shooterComp.cooldown < 0.0f) { + shooterComp.cooldown = 0.0f; + } + + if (registry.HasComponent(shooterEntity)) { + auto& shootCmd = registry.GetComponent(shooterEntity); + + if (shootCmd.wantsToShoot && shooterComp.cooldown <= 0.0f) { + if (registry.HasComponent(shooterEntity)) { + const auto& positionComp = registry.GetComponent(shooterEntity); + + spawns.push_back({positionComp.x + shooterComp.offsetX, + positionComp.y + shooterComp.offsetY, + shooterEntity}); + + if (m_effectFactory) { + m_effectFactory->CreateShootingEffect(registry, + positionComp.x + shooterComp.offsetX, + positionComp.y + shooterComp.offsetY, + shooterEntity); + } + + shooterComp.cooldown = shooterComp.fireRate; + shootCmd.wantsToShoot = false; + } + } + } + } + + for (const auto& spawn : spawns) { + auto bulletEntity = registry.CreateEntity(); + + // CRITICAL FIX: Clean up obstacle components from entity ID reuse + if (registry.HasComponent(bulletEntity)) { + std::cerr << "[SHOOTING CLEANUP] Removing Obstacle from bullet entity " << bulletEntity << std::endl; + registry.RemoveComponent(bulletEntity); + } + if (registry.HasComponent(bulletEntity)) { + std::cerr << "[SHOOTING CLEANUP] Removing ObstacleMetadata from bullet entity " << bulletEntity << std::endl; + registry.RemoveComponent(bulletEntity); + } + + registry.AddComponent(bulletEntity, Position(spawn.x, spawn.y)); + registry.AddComponent(bulletEntity, Velocity(600.0f, 0.0f)); + registry.AddComponent(bulletEntity, Bullet(spawn.shooter)); + + if (m_bulletSprite != 0) { + auto& d = registry.AddComponent(bulletEntity, Drawable(m_bulletSprite, 2)); + d.scale = {0.1f, 0.1f}; + d.origin = Math::Vector2(128.0f, 128.0f); + } + + registry.AddComponent(bulletEntity, Damage(25)); + registry.AddComponent(bulletEntity, BoxCollider(10.0f, 5.0f)); + + registry.AddComponent(bulletEntity, CircleCollider(5.0f)); + registry.AddComponent(bulletEntity, + CollisionLayer(CollisionLayers::PLAYER_BULLET, + CollisionLayers::ENEMY | CollisionLayers::OBSTACLE)); + + if (m_shootSound != Audio::INVALID_SOUND_ID) { + auto sfx = registry.CreateEntity(); + auto& se = registry.AddComponent(sfx, SoundEffect(m_shootSound, 1.0f)); + se.pitch = 1.0f; + } + } + + auto weaponSlots = registry.GetEntitiesWithComponent(); + + for (auto entity : weaponSlots) { + if (!registry.IsEntityAlive(entity)) continue; + + // CRITICAL FIX: Prevent obstacles from shooting with weapon slots + if (registry.HasComponent(entity) || + registry.HasComponent(entity)) { + std::cerr << "[SHOOTING] BLOCKED obstacle entity " << entity + << " from using weapon slot (has WeaponSlot=" << registry.HasComponent(entity) + << " ShootCommand=" << registry.HasComponent(entity) << ")" << std::endl; + if (registry.HasComponent(entity)) { + registry.RemoveComponent(entity); + } + if (registry.HasComponent(entity)) { + registry.RemoveComponent(entity); + } + continue; + } + + auto& weapon = registry.GetComponent(entity); + if (!weapon.enabled) continue; + + weapon.cooldown -= deltaTime; + if (weapon.cooldown < 0.0f) weapon.cooldown = 0.0f; + + if (registry.HasComponent(entity)) { + auto& shootCmd = registry.GetComponent(entity); + + if (shootCmd.wantsToShoot && weapon.cooldown <= 0.0f) { + if (registry.HasComponent(entity)) { + const auto& pos = registry.GetComponent(entity); + + switch (weapon.type) { + case WeaponType::SPREAD: + CreateSpreadShot(registry, entity, pos, weapon.damage); + break; + case WeaponType::LASER: + CreateLaserShot(registry, entity, pos, weapon.damage); + break; + default: + break; + } + + weapon.cooldown = weapon.fireRate; + shootCmd.wantsToShoot = false; + } + } + } + } + } + + void ShootingSystem::CreateSpreadShot(Registry& registry, Entity shooter, const Position& pos, int damage) { + float offsetX = 50.0f; + float offsetY = 20.0f; + + if (registry.HasComponent(shooter)) { + const auto& shooterComp = registry.GetComponent(shooter); + offsetX = shooterComp.offsetX; + offsetY = shooterComp.offsetY; + } + + if (m_effectFactory) { + m_effectFactory->CreateShootingEffect(registry, pos.x + offsetX, pos.y + offsetY, shooter); + } + + float angles[] = {-15.0f, 0.0f, 15.0f}; // degrees + + for (float angle : angles) { + float radians = angle * 3.14159f / 180.0f; + float vx = 600.0f * std::cos(radians); + float vy = 600.0f * std::sin(radians); + + auto bullet = registry.CreateEntity(); + + // CRITICAL FIX: Clean up obstacle components from entity ID reuse + if (registry.HasComponent(bullet)) { + registry.RemoveComponent(bullet); + } + if (registry.HasComponent(bullet)) { + registry.RemoveComponent(bullet); + } + + registry.AddComponent(bullet, Position(pos.x + offsetX, pos.y + offsetY)); + registry.AddComponent(bullet, Velocity(vx, vy)); + registry.AddComponent(bullet, Bullet(shooter)); + registry.AddComponent(bullet, Damage(damage)); + registry.AddComponent(bullet, BoxCollider(8.0f, 4.0f)); + registry.AddComponent(bullet, + CollisionLayer(CollisionLayers::PLAYER_BULLET, + CollisionLayers::ENEMY | CollisionLayers::OBSTACLE)); + + if (m_bulletSprite != 0) { + auto& d = registry.AddComponent(bullet, Drawable(m_bulletSprite, 2)); + d.scale = {0.08f, 0.08f}; + d.tint = Math::Color(1.0f, 1.0f, 0.0f, 1.0f); // Yellow for spread + } + } + } + + void ShootingSystem::CreateLaserShot(Registry& registry, Entity shooter, const Position& pos, int damage) { + float offsetX = 50.0f; + float offsetY = 20.0f; + + if (registry.HasComponent(shooter)) { + const auto& shooterComp = registry.GetComponent(shooter); + offsetX = shooterComp.offsetX; + offsetY = shooterComp.offsetY; + } + + if (m_effectFactory) { + m_effectFactory->CreateShootingEffect(registry, pos.x + offsetX, pos.y + offsetY, shooter); + } + + auto bullet = registry.CreateEntity(); + + if (registry.HasComponent(bullet)) { + registry.RemoveComponent(bullet); + } + if (registry.HasComponent(bullet)) { + registry.RemoveComponent(bullet); + } + + registry.AddComponent(bullet, Position(pos.x + offsetX, pos.y + offsetY)); + registry.AddComponent(bullet, Velocity(800.0f, 0.0f)); // Faster + registry.AddComponent(bullet, Bullet(shooter)); + registry.AddComponent(bullet, Damage(damage)); + registry.AddComponent(bullet, BoxCollider(30.0f, 3.0f)); // Longer + registry.AddComponent(bullet, + CollisionLayer(CollisionLayers::PLAYER_BULLET, + CollisionLayers::ENEMY | CollisionLayers::OBSTACLE)); + + if (m_bulletSprite != 0) { + auto& d = registry.AddComponent(bullet, Drawable(m_bulletSprite, 2)); + d.scale = {0.3f, 0.05f}; + d.tint = Math::Color(0.0f, 1.0f, 1.0f, 1.0f); + } + } + } +} diff --git a/engine/src/ECS/TextRenderingSystem.cpp b/engine/src/ECS/TextRenderingSystem.cpp new file mode 100644 index 0000000..d421c70 --- /dev/null +++ b/engine/src/ECS/TextRenderingSystem.cpp @@ -0,0 +1,43 @@ +#include "ECS/TextRenderingSystem.hpp" +#include "ECS/Component.hpp" +#include "ECS/Components/TextLabel.hpp" +#include "Core/ColorFilter.hpp" + +namespace RType { + namespace ECS { + + TextRenderingSystem::TextRenderingSystem(Renderer::IRenderer* renderer) + : m_renderer(renderer) {} + + void TextRenderingSystem::Update(Registry& registry, float deltaTime) { + if (!m_renderer) + return; + + auto entities = registry.GetEntitiesWithComponent(); + + for (Entity entity : entities) { + if (!registry.IsEntityAlive(entity)) { + continue; + } + if (!registry.HasComponent(entity)) + continue; + + const auto& pos = registry.GetComponent(entity); + const auto& label = registry.GetComponent(entity); + + Renderer::TextParams params; + params.position = {pos.x + label.offsetX, pos.y + label.offsetY}; + Math::Color finalColor = label.color; + if (RType::Core::ColorFilter::IsColourBlindModeEnabled()) { + finalColor = RType::Core::ColorFilter::ApplyColourBlindFilter(label.color); + } + params.color = finalColor; + params.scale = 1.0f; + params.rotation = 0.0f; + params.centered = label.centered; + + m_renderer->DrawText(label.fontId, label.text, params); + } + } + } +} diff --git a/engine/src/ECS/ThirdBulletSystem.cpp b/engine/src/ECS/ThirdBulletSystem.cpp new file mode 100644 index 0000000..7ea3e87 --- /dev/null +++ b/engine/src/ECS/ThirdBulletSystem.cpp @@ -0,0 +1,73 @@ +#include "ECS/ThirdBulletSystem.hpp" +#include "ECS/Component.hpp" +#include "Core/Logger.hpp" +#include +#include + +namespace RType { + namespace ECS { + + void ThirdBulletSystem::Update(Registry& registry, float deltaTime) { + auto thirdBullets = registry.GetEntitiesWithComponent(); + + for (auto bulletEntity : thirdBullets) { + if (!registry.IsEntityAlive(bulletEntity)) { + continue; + } + + auto& thirdBullet = registry.GetComponent(bulletEntity); + if (!thirdBullet.isActive) { + continue; + } + + const auto& pos = registry.GetComponent(bulletEntity); + thirdBullet.timeSinceSpawn += deltaTime; + + if (thirdBullet.timeSinceSpawn >= thirdBullet.spawnInterval) { + thirdBullet.timeSinceSpawn = 0.0f; + SpawnSmallProjectile(registry, pos.x, pos.y); + } + } + } + + void ThirdBulletSystem::SpawnSmallProjectile(Registry& registry, float x, float y) { + const float speed = 300.0f; + + Direction directions[3] = { + {0.0f, -speed}, + {0.0f, speed}, + {speed, 0.0f} + }; + + for (int i = 0; i < 3; i++) { + Entity smallBullet = registry.CreateEntity(); + + // CRITICAL FIX: Clean up obstacle components from entity ID reuse + if (registry.HasComponent(smallBullet)) { + std::cerr << "[THIRDBULLET CLEANUP] Removing Obstacle from bullet entity " << smallBullet << std::endl; + registry.RemoveComponent(smallBullet); + } + if (registry.HasComponent(smallBullet)) { + std::cerr << "[THIRDBULLET CLEANUP] Removing ObstacleMetadata from bullet entity " << smallBullet << std::endl; + registry.RemoveComponent(smallBullet); + } + + registry.AddComponent(smallBullet, Position{x, y}); + + registry.AddComponent(smallBullet, Velocity{directions[i].vx, directions[i].vy}); + + registry.AddComponent(smallBullet, BossBullet{}); + registry.AddComponent(smallBullet, Bullet{}); + + registry.AddComponent(smallBullet, CircleCollider{10.0f}); + registry.AddComponent(smallBullet, + CollisionLayer(CollisionLayers::OBSTACLE, CollisionLayers::PLAYER)); + + registry.AddComponent(smallBullet, Damage{10}); + } + + Core::Logger::Debug("[ThirdBulletSystem] Spawned 3 cross projectiles at ({}, {})", x, y); + } + + } +} diff --git a/engine/src/Renderer/CMakeLists.txt b/engine/src/Renderer/CMakeLists.txt new file mode 100644 index 0000000..ee397d2 --- /dev/null +++ b/engine/src/Renderer/CMakeLists.txt @@ -0,0 +1,114 @@ +# Three-tier SFML 2.x dependency resolution: +# 1. vcpkg (if CMAKE_TOOLCHAIN_FILE is set) +# 2. System-installed SFML 2.x (via find_package) +# 3. FetchContent (download and build SFML 2.6.2 from source) + +set(SFML_FOUND FALSE) + +# Tier 1: Try vcpkg first (already configured by root CMakeLists.txt) +if(DEFINED CMAKE_TOOLCHAIN_FILE AND CMAKE_TOOLCHAIN_FILE MATCHES "vcpkg") + message(STATUS "Searching for SFML 2.6+ via vcpkg...") + find_package(SFML 2.6 COMPONENTS graphics window system QUIET) + if(SFML_FOUND) + message(STATUS "Found SFML ${SFML_VERSION} via vcpkg") + endif() +endif() + +# Tier 2: Try system-installed SFML 2.6+ +if(NOT SFML_FOUND) + message(STATUS "Searching for system-installed SFML 2.6+...") + find_package(SFML 2.6 COMPONENTS graphics window system QUIET) + if(SFML_FOUND) + message(STATUS "Found system-installed SFML ${SFML_VERSION}") + endif() +endif() + +# Tier 3: Fetch SFML 2.6.2 from source if not found +if(NOT SFML_FOUND) + message(STATUS "SFML 2.x not found, fetching SFML 2.6.2 from source...") + + include(FetchContent) + + # Save current BUILD_SHARED_LIBS to avoid polluting other dependencies + set(OLD_BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS}) + + FetchContent_Declare( + SFML + GIT_REPOSITORY https://github.com/SFML/SFML.git + GIT_TAG 2.6.2 + GIT_SHALLOW TRUE + ) + + # Don't build audio and network modules (we only need graphics/window/system) + set(SFML_BUILD_AUDIO OFF CACHE BOOL "" FORCE) + set(SFML_BUILD_NETWORK OFF CACHE BOOL "" FORCE) + set(BUILD_SHARED_LIBS ON CACHE BOOL "" FORCE) + + FetchContent_MakeAvailable(SFML) + + # Restore original BUILD_SHARED_LIBS setting + set(BUILD_SHARED_LIBS ${OLD_BUILD_SHARED_LIBS} CACHE BOOL "" FORCE) + + set(SFML_LIBRARIES sfml-graphics sfml-window sfml-system) + set(SFML_FOUND TRUE) + message(STATUS "SFML 2.6.2 fetched and configured") +endif() + +# Determine library names based on target availability +if(SFML_FOUND) + if(TARGET SFML::Graphics) + # vcpkg uses SFML:: namespace + set(SFML_LIBRARIES SFML::Graphics SFML::Window SFML::System) + message(STATUS "Using SFML 2.x with SFML:: namespace") + elseif(TARGET sfml-graphics) + # FetchContent or manual install uses sfml- prefix + set(SFML_LIBRARIES sfml-graphics sfml-window sfml-system) + message(STATUS "Using SFML 2.x with sfml- prefix") + else() + message(FATAL_ERROR "SFML found but could not determine target names. " + "Expected SFML::Graphics or sfml-graphics targets. " + "Please ensure SFML 2.6+ is properly installed.") + endif() +else() + message(FATAL_ERROR "SFML 2.6+ could not be found via vcpkg, system packages, or FetchContent. " + "Please check your CMake configuration.") +endif() + +# Create SFML Renderer library +add_library(rtype_sfml_renderer SHARED + SFMLRenderer.cpp +) + +target_include_directories(rtype_sfml_renderer PUBLIC + $ + $ +) + +target_link_libraries(rtype_sfml_renderer + PUBLIC + rtype_core + rtype_ecs + PRIVATE + ${SFML_LIBRARIES} +) + +target_compile_features(rtype_sfml_renderer PUBLIC cxx_std_17) + +if(WIN32) + target_compile_definitions(rtype_sfml_renderer PRIVATE + NOMINMAX + WIN32_LEAN_AND_MEAN + _CRT_SECURE_NO_WARNINGS + ) +endif() + +set_target_properties(rtype_sfml_renderer PROPERTIES + PREFIX "" + OUTPUT_NAME "SFMLRenderer" + POSITION_INDEPENDENT_CODE ON +) + +install(TARGETS rtype_sfml_renderer + LIBRARY DESTINATION lib/rtype/modules + RUNTIME DESTINATION bin/rtype/modules +) diff --git a/engine/src/Renderer/SFMLRenderer.cpp b/engine/src/Renderer/SFMLRenderer.cpp new file mode 100644 index 0000000..c7b7fe9 --- /dev/null +++ b/engine/src/Renderer/SFMLRenderer.cpp @@ -0,0 +1,541 @@ +#include "Renderer/SFMLRenderer.hpp" +#include "Core/Engine.hpp" +#include "Core/Logger.hpp" +#include +#include + +namespace Renderer { + + SFMLRenderer::SFMLRenderer() { + RType::Core::Logger::Debug("SFMLRenderer constructor called"); + } + + SFMLRenderer::~SFMLRenderer() { + RType::Core::Logger::Debug("SFMLRenderer destructor called"); + Shutdown(); + } + + const char* SFMLRenderer::GetName() const { + return "SFMLRenderer"; + } + + RType::Core::ModulePriority SFMLRenderer::GetPriority() const { + return RType::Core::ModulePriority::High; + } + + bool SFMLRenderer::Initialize(RType::Core::Engine* engine) { + RType::Core::Logger::Info("Initializing SFMLRenderer module (SFML 2.6)"); + m_engine = engine; + m_clock.restart(); + return true; + } + + void SFMLRenderer::Shutdown() { + RType::Core::Logger::Info("Shutting down SFMLRenderer module"); + + m_sprites.clear(); + m_textures.clear(); + m_fonts.clear(); + + Destroy(); + m_engine = nullptr; + } + + void SFMLRenderer::Update(float /* deltaTime */) { + if (!m_window || !m_window->isOpen()) { + return; + } + + ProcessEvents(); + + m_stats.drawCalls = 0; + m_stats.textureSwitches = 0; + } + + bool SFMLRenderer::ShouldUpdateInRenderThread() const { + return true; + } + + bool SFMLRenderer::CreateWindow(const WindowConfig& config) { + RType::Core::Logger::Info("Creating window: {}x{} - {}", config.width, config.height, config.title); + + m_windowConfig = config; + + sf::Uint32 style = sf::Style::Default; + if (config.fullscreen) { + style = sf::Style::Fullscreen; + } else if (!config.resizable) { + style = sf::Style::Titlebar | sf::Style::Close; + } + + m_window = std::make_unique( + sf::VideoMode(config.width, config.height), + config.title, + style); + + if (!m_window) { + RType::Core::Logger::Error("Failed to create SFML window"); + return false; + } + + if (config.targetFramerate > 0) { + m_window->setFramerateLimit(config.targetFramerate); + } + + m_defaultView.setSize(1280.0f, 720.0f); + m_defaultView.setCenter(640.0f, 360.0f); + m_currentView = m_defaultView; + m_window->setView(m_currentView); + + RType::Core::Logger::Info("Window created successfully"); + return true; + } + + void SFMLRenderer::Destroy() { + if (m_window) { + RType::Core::Logger::Info("Destroying window"); + m_window->close(); + m_window.reset(); + } + } + + bool SFMLRenderer::IsWindowOpen() const { + return m_window && m_window->isOpen(); + } + + void SFMLRenderer::Resize(std::uint32_t width, std::uint32_t height) { + if (!m_window) { + RType::Core::Logger::Warning("Cannot resize: window not created"); + return; + } + + if (width == 0 || height == 0) { + RType::Core::Logger::Warning("Invalid window dimensions: {}x{}", width, height); + return; + } + + m_windowConfig.width = width; + m_windowConfig.height = height; + + const float targetWidth = 1280.0f; + const float targetHeight = 720.0f; + + m_defaultView.setSize(targetWidth, targetHeight); + m_defaultView.setCenter(targetWidth / 2.0f, targetHeight / 2.0f); + m_defaultView.setViewport(sf::FloatRect(0.0f, 0.0f, 1.0f, 1.0f)); + + if (!m_usingCustomCamera) { + m_currentView = m_defaultView; + m_window->setView(m_currentView); + } + + RType::Core::Logger::Info("Window resized to {}x{}", width, height); + } + + void SFMLRenderer::SetWindowTitle(const std::string& title) { + if (!m_window) { + RType::Core::Logger::Warning("Cannot set title: window not created"); + return; + } + + m_windowConfig.title = title; + m_window->setTitle(title); + } + + void SFMLRenderer::BeginFrame() { + if (!m_window) { + return; + } + + m_stats.drawCalls = 0; + m_stats.textureSwitches = 0; + } + + void SFMLRenderer::EndFrame() { + if (!m_window) { + return; + } + + m_window->display(); + } + + void SFMLRenderer::Clear(const Color& color) { + if (!m_window) { + return; + } + + m_window->clear(ToSFMLColor(color)); + } + + TextureId SFMLRenderer::LoadTexture(const std::string& path, const TextureConfig& config) { + auto texture = std::make_shared(); + + if (!texture->loadFromFile(path)) { + RType::Core::Logger::Error("Failed to load texture: {}", path); + return 0; + } + texture->setSmooth(config.smooth); + texture->setRepeated(config.repeated); + + TextureId id = m_nextTextureId++; + m_textures[id] = {texture, config}; + + RType::Core::Logger::Debug("Loaded texture: {} (ID: {})", path, id); + return id; + } + + void SFMLRenderer::UnloadTexture(TextureId textureId) { + auto it = m_textures.find(textureId); + if (it == m_textures.end()) { + RType::Core::Logger::Warning("Attempted to unload non-existent texture ID: {}", textureId); + return; + } + + for (auto spriteIt = m_sprites.begin(); spriteIt != m_sprites.end();) { + if (spriteIt->second.textureId == textureId) { + spriteIt = m_sprites.erase(spriteIt); + } else { + ++spriteIt; + } + } + + m_textures.erase(it); + RType::Core::Logger::Debug("Unloaded texture ID: {}", textureId); + } + + Vector2 SFMLRenderer::GetTextureSize(TextureId textureId) const { + auto it = m_textures.find(textureId); + if (it == m_textures.end()) { + return {0.0f, 0.0f}; + } + sf::Vector2u size = it->second.texture->getSize(); + return {static_cast(size.x), static_cast(size.y)}; + } + + SpriteId SFMLRenderer::CreateSprite(TextureId textureId, const Rectangle& region) { + auto it = m_textures.find(textureId); + if (it == m_textures.end()) { + RType::Core::Logger::Error("Cannot create sprite: texture ID {} not found", textureId); + return 0; + } + + SpriteId id = m_nextSpriteId++; + + SpriteData spriteData; + spriteData.sprite.setTexture(*it->second.texture); + spriteData.textureId = textureId; + + if (region.size.x > 0 && region.size.y > 0) { + spriteData.sprite.setTextureRect(sf::IntRect( + static_cast(region.position.x), + static_cast(region.position.y), + static_cast(region.size.x), + static_cast(region.size.y))); + } + + m_sprites[id] = spriteData; + + RType::Core::Logger::Debug("Created sprite ID: {} from texture ID: {}", id, textureId); + return id; + } + + void SFMLRenderer::DestroySprite(SpriteId spriteId) { + auto it = m_sprites.find(spriteId); + if (it == m_sprites.end()) { + RType::Core::Logger::Warning("Attempted to destroy non-existent sprite ID: {}", spriteId); + return; + } + + m_sprites.erase(it); + RType::Core::Logger::Debug("Destroyed sprite ID: {}", spriteId); + } + + void SFMLRenderer::SetSpriteRegion(SpriteId spriteId, const Rectangle& region) { + auto it = m_sprites.find(spriteId); + if (it == m_sprites.end()) { + RType::Core::Logger::Warning("Attempted to set region for non-existent sprite ID: {}", spriteId); + return; + } + + it->second.sprite.setTextureRect(ToSFMLRect(region)); + } + + void SFMLRenderer::DrawSprite(SpriteId spriteId, const Transform2D& transform, const Color& tint) { + if (!m_window) { + return; + } + + auto it = m_sprites.find(spriteId); + if (it == m_sprites.end()) { + RType::Core::Logger::Warning("Cannot draw sprite: ID {} not found", spriteId); + return; + } + + sf::Sprite& sprite = it->second.sprite; + + sprite.setPosition(ToSFMLVector(transform.position)); + sprite.setScale(ToSFMLVector(transform.scale)); + sprite.setRotation(transform.rotation); + sprite.setOrigin(ToSFMLVector(transform.origin)); + + if (tint.r > 0 || tint.g > 0 || tint.b > 0 || tint.a > 0) { + sprite.setColor(ToSFMLColor(tint)); + } else { + sprite.setColor(sf::Color::White); + } + + m_window->draw(sprite); + m_stats.drawCalls++; + } + + void SFMLRenderer::DrawRectangle(const Rectangle& rectangle, const Color& color) { + if (!m_window) { + return; + } + + sf::RectangleShape rect(ToSFMLVector(rectangle.size)); + rect.setPosition(ToSFMLVector(rectangle.position)); + rect.setFillColor(ToSFMLColor(color)); + + m_window->draw(rect); + m_stats.drawCalls++; + } + + FontId SFMLRenderer::LoadFont(const std::string& path, std::uint32_t characterSize) { + auto font = std::make_shared(); + + if (!font->loadFromFile(path)) { + RType::Core::Logger::Error("Failed to load font: {}", path); + return 0; + } + + FontId id = m_nextFontId++; + m_fonts[id] = {font, characterSize}; + + RType::Core::Logger::Debug("Loaded font: {} (ID: {}, size: {})", path, id, characterSize); + return id; + } + + void SFMLRenderer::UnloadFont(FontId fontId) { + auto it = m_fonts.find(fontId); + if (it == m_fonts.end()) { + RType::Core::Logger::Warning("Attempted to unload non-existent font ID: {}", fontId); + return; + } + + m_fonts.erase(it); + RType::Core::Logger::Debug("Unloaded font ID: {}", fontId); + } + + void SFMLRenderer::DrawText(FontId fontId, const std::string& text, const TextParams& params) { + if (!m_window) { + return; + } + + auto it = m_fonts.find(fontId); + if (it == m_fonts.end()) { + RType::Core::Logger::Warning("Cannot draw text: font ID {} not found", fontId); + return; + } + + sf::Text sfText; + sfText.setFont(*it->second.font); + sfText.setString(text); + sfText.setCharacterSize(it->second.characterSize); + sfText.setRotation(params.rotation); + sfText.setFillColor(ToSFMLColor(params.color)); + sfText.setScale(sf::Vector2f(params.scale, params.scale)); + + if (params.centered) { + sf::FloatRect bounds = sfText.getLocalBounds(); + sfText.setOrigin(bounds.left + bounds.width / 2.0f, bounds.top + bounds.height / 2.0f); + } + + sfText.setPosition(ToSFMLVector(params.position)); + + m_window->draw(sfText); + m_stats.drawCalls++; + } + + void SFMLRenderer::SetCamera(const Camera2D& camera) { + if (!m_window) { + return; + } + + m_currentView.setCenter(ToSFMLVector(camera.center)); + m_currentView.setSize(camera.size.x, camera.size.y); + m_window->setView(m_currentView); + m_usingCustomCamera = true; + + RType::Core::Logger::Debug("Camera set to center: ({}, {}), size: ({}, {})", + camera.center.x, camera.center.y, + camera.size.x, camera.size.y); + } + + void SFMLRenderer::ResetCamera() { + if (!m_window) { + return; + } + + m_currentView = m_defaultView; + m_window->setView(m_currentView); + m_usingCustomCamera = false; + + RType::Core::Logger::Debug("Camera reset to default view"); + } + + RenderStats SFMLRenderer::GetRenderStats() const { + return m_stats; + } + + float SFMLRenderer::GetDeltaTime() { + m_lastDeltaTime = m_clock.restart().asSeconds(); + return m_lastDeltaTime; + } + + void SFMLRenderer::ProcessEvents() { + if (!m_window) { + return; + } + + sf::Event event; + while (m_window->pollEvent(event)) { + if (event.type == sf::Event::Closed) { + m_window->close(); + } else if (event.type == sf::Event::Resized) { + Resize(event.size.width, event.size.height); + } + } + } + + sf::Color SFMLRenderer::ToSFMLColor(const Color& color) { + return sf::Color( + static_cast(color.r * 255.0f), + static_cast(color.g * 255.0f), + static_cast(color.b * 255.0f), + static_cast(color.a * 255.0f)); + } + + sf::Vector2f SFMLRenderer::ToSFMLVector(const Vector2& vec) { + return sf::Vector2f(vec.x, vec.y); + } + + sf::IntRect SFMLRenderer::ToSFMLRect(const Rectangle& rect) { + return sf::IntRect( + static_cast(rect.position.x), + static_cast(rect.position.y), + static_cast(rect.size.x), + static_cast(rect.size.y)); + } + + sf::Keyboard::Key SFMLRenderer::ToSFMLKey(Key key) { + static const std::unordered_map keyMapping = { + {Key::A, sf::Keyboard::A}, + {Key::B, sf::Keyboard::B}, + {Key::C, sf::Keyboard::C}, + {Key::D, sf::Keyboard::D}, + {Key::E, sf::Keyboard::E}, + {Key::F, sf::Keyboard::F}, + {Key::G, sf::Keyboard::G}, + {Key::H, sf::Keyboard::H}, + {Key::I, sf::Keyboard::I}, + {Key::J, sf::Keyboard::J}, + {Key::K, sf::Keyboard::K}, + {Key::L, sf::Keyboard::L}, + {Key::M, sf::Keyboard::M}, + {Key::N, sf::Keyboard::N}, + {Key::O, sf::Keyboard::O}, + {Key::P, sf::Keyboard::P}, + {Key::Q, sf::Keyboard::Q}, + {Key::R, sf::Keyboard::R}, + {Key::S, sf::Keyboard::S}, + {Key::T, sf::Keyboard::T}, + {Key::U, sf::Keyboard::U}, + {Key::V, sf::Keyboard::V}, + {Key::W, sf::Keyboard::W}, + {Key::X, sf::Keyboard::X}, + {Key::Y, sf::Keyboard::Y}, + {Key::Z, sf::Keyboard::Z}, + {Key::Num0, sf::Keyboard::Num0}, + {Key::Num1, sf::Keyboard::Num1}, + {Key::Num2, sf::Keyboard::Num2}, + {Key::Num3, sf::Keyboard::Num3}, + {Key::Num4, sf::Keyboard::Num4}, + {Key::Num5, sf::Keyboard::Num5}, + {Key::Num6, sf::Keyboard::Num6}, + {Key::Num7, sf::Keyboard::Num7}, + {Key::Num8, sf::Keyboard::Num8}, + {Key::Num9, sf::Keyboard::Num9}, + {Key::Escape, sf::Keyboard::Escape}, + {Key::Space, sf::Keyboard::Space}, + {Key::Enter, sf::Keyboard::Return}, + {Key::Backspace, sf::Keyboard::BackSpace}, + {Key::Tab, sf::Keyboard::Tab}, + {Key::Left, sf::Keyboard::Left}, + {Key::Right, sf::Keyboard::Right}, + {Key::Up, sf::Keyboard::Up}, + {Key::Down, sf::Keyboard::Down}, + {Key::LShift, sf::Keyboard::LShift}, + {Key::RShift, sf::Keyboard::RShift}, + {Key::LControl, sf::Keyboard::LControl}, + {Key::RControl, sf::Keyboard::RControl}, + {Key::LAlt, sf::Keyboard::LAlt}, + {Key::RAlt, sf::Keyboard::RAlt}}; + + auto it = keyMapping.find(key); + return (it != keyMapping.end()) ? it->second : sf::Keyboard::Unknown; + } + + bool SFMLRenderer::IsKeyPressed(Key key) const { + return sf::Keyboard::isKeyPressed(ToSFMLKey(key)); + } + + bool SFMLRenderer::IsMouseButtonPressed(MouseButton button) const { + sf::Mouse::Button sfBtn; + switch (button) { + case MouseButton::Left: + sfBtn = sf::Mouse::Left; + break; + case MouseButton::Right: + sfBtn = sf::Mouse::Right; + break; + case MouseButton::Middle: + sfBtn = sf::Mouse::Middle; + break; + default: + return false; + } + return sf::Mouse::isButtonPressed(sfBtn); + } + + Vector2 SFMLRenderer::GetMousePosition() const { + if (!m_window) + return {0, 0}; + sf::Vector2i pos = sf::Mouse::getPosition(*m_window); + sf::Vector2f worldPos = m_window->mapPixelToCoords(pos, m_currentView); + return {worldPos.x, worldPos.y}; + } + +} + +extern "C" { +#ifdef _WIN32 +__declspec(dllexport) +#else +__attribute__((visibility("default"))) +#endif +RType::Core::IModule* +CreateModule() { + return new Renderer::SFMLRenderer(); +} + +#ifdef _WIN32 +__declspec(dllexport) +#else +__attribute__((visibility("default"))) +#endif +void +DestroyModule(RType::Core::IModule* module) { + delete module; +} +}; diff --git a/engine/src/main.cpp b/engine/src/main.cpp new file mode 100644 index 0000000..151bf6a --- /dev/null +++ b/engine/src/main.cpp @@ -0,0 +1,27 @@ +#include +#include "../include/Core/Engine.hpp" + +using namespace RType; + +int main() { + Core::Logger::Info("R-Type Engine starting..."); + + Core::EngineConfig config; + config.pluginPath = "./plugins"; + + auto engine = std::make_unique(config); + + if (!engine->Initialize()) { + Core::Logger::Error("Failed to initialize engine"); + return 1; + } + Core::Logger::Info("Engine initialized with {} modules", engine->GetAllModules().size()); + + auto& registry = engine->GetRegistry(); + Core::Logger::Info("Registry has {} entities", registry.GetEntityCount()); + + engine->Shutdown(); + + Core::Logger::Info("Engine stopped"); + return 0; +} diff --git a/games/breakout/README.md b/games/breakout/README.md new file mode 100644 index 0000000..41a0b80 --- /dev/null +++ b/games/breakout/README.md @@ -0,0 +1,33 @@ +# Breakout + +Breakout game demonstrating the reusability of the R-Type game engine. + +## Objective + +Destroy all bricks by bouncing the ball with the paddle. + +## Controls + +- **Left Arrow** : Move the paddle to the left +- **Right Arrow** : Move the paddle to the right +- **C** : Toggle colour-blind mode (ON/OFF) +- **Escape** : Quit the game + +## Game Mechanics + +- **Ball** : Bounces off walls (left, right, top) and the paddle +- **Paddle** : The bounce angle depends on where the ball hits the paddle +- **Bricks** : A brick is destroyed in one hit +- **Acceleration** : The ball gradually accelerates over time +- **Ball Loss** : If the ball goes out at the bottom, it reappears at the center with a random downward direction +- **Score** : Increases with each brick destroyed +- **Victory** : When all bricks are destroyed, a "WIN" message appears + +## Compilation + +```bash +cd build +make breakout +./breakout +``` + diff --git a/games/breakout/include/BallAccelerationSystem.hpp b/games/breakout/include/BallAccelerationSystem.hpp new file mode 100644 index 0000000..c71bc04 --- /dev/null +++ b/games/breakout/include/BallAccelerationSystem.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "ECS/ISystem.hpp" + +namespace Breakout { + namespace ECS { + + class BallAccelerationSystem : public RType::ECS::ISystem { + public: + BallAccelerationSystem() = default; + ~BallAccelerationSystem() override = default; + + const char* GetName() const override { return "BallAccelerationSystem"; } + void Update(RType::ECS::Registry& registry, float deltaTime) override; + + private: + float m_elapsedTime = 0.0f; + static constexpr float ACCELERATION_RATE = 2.5f; + static constexpr float MAX_SPEED_MULTIPLIER = 2.5f; + }; + + } +} diff --git a/games/breakout/include/BreakoutPhysicsSystem.hpp b/games/breakout/include/BreakoutPhysicsSystem.hpp new file mode 100644 index 0000000..932e035 --- /dev/null +++ b/games/breakout/include/BreakoutPhysicsSystem.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "ECS/ISystem.hpp" +#include "ECS/Registry.hpp" +#include "ECS/Component.hpp" + +namespace Breakout { + namespace ECS { + + class BreakoutPhysicsSystem : public RType::ECS::ISystem { + public: + BreakoutPhysicsSystem() = default; + ~BreakoutPhysicsSystem() override = default; + + const char* GetName() const override { return "BreakoutPhysicsSystem"; } + void Update(RType::ECS::Registry& registry, float deltaTime) override; + + private: + void HandleBallPaddleCollision(RType::ECS::Registry& registry, RType::ECS::Entity ball, RType::ECS::Entity paddle); + void HandleBallBrickCollision(RType::ECS::Registry& registry, RType::ECS::Entity ball, RType::ECS::Entity brick); + void HandleBallWallCollision(RType::ECS::Registry& registry, RType::ECS::Entity ball, float screenWidth, float screenHeight); + }; + + } +} + diff --git a/games/breakout/include/BreakoutRenderSystem.hpp b/games/breakout/include/BreakoutRenderSystem.hpp new file mode 100644 index 0000000..68e518a --- /dev/null +++ b/games/breakout/include/BreakoutRenderSystem.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "ECS/ISystem.hpp" +#include "Renderer/IRenderer.hpp" + +namespace Breakout { + namespace ECS { + + class BreakoutRenderSystem : public RType::ECS::ISystem { + public: + explicit BreakoutRenderSystem(Renderer::IRenderer* renderer); + ~BreakoutRenderSystem() override = default; + + const char* GetName() const override { return "BreakoutRenderSystem"; } + void Update(RType::ECS::Registry& registry, float deltaTime) override; + + private: + Renderer::IRenderer* m_renderer; + }; + + } +} diff --git a/games/breakout/src/BallAccelerationSystem.cpp b/games/breakout/src/BallAccelerationSystem.cpp new file mode 100644 index 0000000..8d2b8e0 --- /dev/null +++ b/games/breakout/src/BallAccelerationSystem.cpp @@ -0,0 +1,45 @@ +#include "BallAccelerationSystem.hpp" +#include "ECS/Registry.hpp" +#include "ECS/Component.hpp" +#include + +namespace Breakout { + namespace ECS { + + void BallAccelerationSystem::Update(RType::ECS::Registry& registry, float deltaTime) { + m_elapsedTime += deltaTime; + + RType::ECS::Entity ballEntity = RType::ECS::NULL_ENTITY; + auto entities = registry.GetEntitiesWithComponent(); + for (auto entity : entities) { + if (registry.HasComponent(entity)) { + ballEntity = entity; + break; + } + } + + if (ballEntity == RType::ECS::NULL_ENTITY || !registry.HasComponent(ballEntity)) { + return; + } + + auto& ballVel = registry.GetComponent(ballEntity); + + float speedMultiplier = 1.0f + (m_elapsedTime * ACCELERATION_RATE / 50.0f); + if (speedMultiplier > MAX_SPEED_MULTIPLIER) { + speedMultiplier = MAX_SPEED_MULTIPLIER; + } + + float currentSpeed = std::sqrt(ballVel.dx * ballVel.dx + ballVel.dy * ballVel.dy); + float targetSpeed = 250.0f * speedMultiplier; + + if (currentSpeed < targetSpeed && currentSpeed > 0.0f) { + float newSpeed = currentSpeed + (targetSpeed - currentSpeed) * deltaTime; + float dirX = ballVel.dx / currentSpeed; + float dirY = ballVel.dy / currentSpeed; + ballVel.dx = dirX * newSpeed; + ballVel.dy = dirY * newSpeed; + } + } + + } +} diff --git a/games/breakout/src/BreakoutPhysicsSystem.cpp b/games/breakout/src/BreakoutPhysicsSystem.cpp new file mode 100644 index 0000000..4bd6125 --- /dev/null +++ b/games/breakout/src/BreakoutPhysicsSystem.cpp @@ -0,0 +1,245 @@ +#include "BreakoutPhysicsSystem.hpp" +#include "ECS/Registry.hpp" +#include "ECS/Component.hpp" +#include +#include + +namespace Breakout { + namespace ECS { + + void BreakoutPhysicsSystem::Update(RType::ECS::Registry& registry, float deltaTime) { + constexpr float SCREEN_WIDTH = 800.0f; + constexpr float SCREEN_HEIGHT = 900.0f; + + RType::ECS::Entity ballEntity = RType::ECS::NULL_ENTITY; + auto entities = registry.GetEntitiesWithComponent(); + for (auto entity : entities) { + if (registry.HasComponent(entity)) { + ballEntity = entity; + break; + } + } + + if (ballEntity == RType::ECS::NULL_ENTITY) { + return; + } + + if (!registry.HasComponent(ballEntity) || + !registry.HasComponent(ballEntity)) { + return; + } + + const auto& ballPos = registry.GetComponent(ballEntity); + const auto& ballCollider = registry.GetComponent(ballEntity); + float ballRadius = ballCollider.radius; + + RType::ECS::Entity paddleEntity = RType::ECS::NULL_ENTITY; + auto controllableEntities = registry.GetEntitiesWithComponent(); + for (auto entity : controllableEntities) { + if (registry.HasComponent(entity)) { + paddleEntity = entity; + break; + } + } + + if (paddleEntity != RType::ECS::NULL_ENTITY && + registry.HasComponent(paddleEntity) && + registry.HasComponent(paddleEntity)) { + const auto& paddlePos = registry.GetComponent(paddleEntity); + const auto& paddleCollider = registry.GetComponent(paddleEntity); + + float paddleLeft = paddlePos.x - paddleCollider.width / 2.0f; + float paddleRight = paddlePos.x + paddleCollider.width / 2.0f; + float paddleTop = paddlePos.y - paddleCollider.height / 2.0f; + float paddleBottom = paddlePos.y + paddleCollider.height / 2.0f; + + float closestX = std::max(paddleLeft, std::min(ballPos.x, paddleRight)); + float closestY = std::max(paddleTop, std::min(ballPos.y, paddleBottom)); + + float dx = ballPos.x - closestX; + float dy = ballPos.y - closestY; + float distanceSquared = dx * dx + dy * dy; + + if (distanceSquared < (ballRadius * ballRadius)) { + HandleBallPaddleCollision(registry, ballEntity, paddleEntity); + } + } + + auto brickEntities = registry.GetEntitiesWithComponent(); + bool ballCollided = false; + for (auto brick : brickEntities) { + if (ballCollided) { + break; + } + if (registry.HasComponent(brick)) { + continue; + } + if (!registry.HasComponent(brick) || + !registry.HasComponent(brick) || + !registry.IsEntityAlive(brick)) { + continue; + } + + const auto& brickPos = registry.GetComponent(brick); + const auto& brickCollider = registry.GetComponent(brick); + + float brickLeft = brickPos.x - brickCollider.width / 2.0f; + float brickRight = brickPos.x + brickCollider.width / 2.0f; + float brickTop = brickPos.y - brickCollider.height / 2.0f; + float brickBottom = brickPos.y + brickCollider.height / 2.0f; + + float closestX = std::max(brickLeft, std::min(ballPos.x, brickRight)); + float closestY = std::max(brickTop, std::min(ballPos.y, brickBottom)); + + float dx = ballPos.x - closestX; + float dy = ballPos.y - closestY; + float distanceSquared = dx * dx + dy * dy; + + if (distanceSquared < (ballRadius * ballRadius)) { + HandleBallBrickCollision(registry, ballEntity, brick); + ballCollided = true; + } + } + + HandleBallWallCollision(registry, ballEntity, SCREEN_WIDTH, SCREEN_HEIGHT); + } + + void BreakoutPhysicsSystem::HandleBallPaddleCollision(RType::ECS::Registry& registry, RType::ECS::Entity ball, RType::ECS::Entity paddle) { + if (!registry.HasComponent(ball) || !registry.HasComponent(paddle) || + !registry.HasComponent(ball) || !registry.HasComponent(paddle) || + !registry.HasComponent(ball)) { + return; + } + + const auto& ballPos = registry.GetComponent(ball); + const auto& paddlePos = registry.GetComponent(paddle); + const auto& paddleCollider = registry.GetComponent(paddle); + const auto& ballCollider = registry.GetComponent(ball); + auto& ballVel = registry.GetComponent(ball); + auto& ballPosRef = registry.GetComponent(ball); + + float ballRadius = ballCollider.radius; + float paddleLeft = paddlePos.x - paddleCollider.width / 2.0f; + float paddleRight = paddlePos.x + paddleCollider.width / 2.0f; + float paddleTop = paddlePos.y - paddleCollider.height / 2.0f; + float paddleBottom = paddlePos.y + paddleCollider.height / 2.0f; + + float relativeX = (ballPos.x - paddlePos.x) / (paddleCollider.width / 2.0f); + relativeX = std::max(-1.0f, std::min(1.0f, relativeX)); + + float angle = relativeX * 60.0f * (3.14159f / 180.0f); + float speed = std::sqrt(ballVel.dx * ballVel.dx + ballVel.dy * ballVel.dy); + if (speed < 250.0f) { + speed = 300.0f; + } + + ballVel.dx = speed * std::sin(angle); + ballVel.dy = -std::abs(speed * std::cos(angle)); + + if (ballPos.y > paddlePos.y) { + ballPosRef.y = paddleBottom + ballRadius + 0.1f; + } else { + ballPosRef.y = paddleTop - ballRadius - 0.1f; + } + + if (ballPos.x < paddleLeft) { + ballPosRef.x = paddleLeft - ballRadius - 0.1f; + } else if (ballPos.x > paddleRight) { + ballPosRef.x = paddleRight + ballRadius + 0.1f; + } + } + + void BreakoutPhysicsSystem::HandleBallBrickCollision(RType::ECS::Registry& registry, RType::ECS::Entity ball, RType::ECS::Entity brick) { + if (!registry.HasComponent(ball) || !registry.HasComponent(brick) || + !registry.HasComponent(ball) || !registry.HasComponent(brick) || + !registry.HasComponent(brick) || !registry.HasComponent(ball)) { + return; + } + + const auto& ballPos = registry.GetComponent(ball); + const auto& brickPos = registry.GetComponent(brick); + const auto& brickCollider = registry.GetComponent(brick); + const auto& ballCollider = registry.GetComponent(ball); + auto& ballVel = registry.GetComponent(ball); + auto& ballPosRef = registry.GetComponent(ball); + auto& brickHealth = registry.GetComponent(brick); + + float brickLeft = brickPos.x - brickCollider.width / 2.0f; + float brickRight = brickPos.x + brickCollider.width / 2.0f; + float brickTop = brickPos.y - brickCollider.height / 2.0f; + float brickBottom = brickPos.y + brickCollider.height / 2.0f; + + float ballRadius = ballCollider.radius; + float overlapLeft = (ballPos.x + ballRadius) - brickLeft; + float overlapRight = brickRight - (ballPos.x - ballRadius); + float overlapTop = (ballPos.y + ballRadius) - brickTop; + float overlapBottom = brickBottom - (ballPos.y - ballRadius); + + float minOverlap = std::min({overlapLeft, overlapRight, overlapTop, overlapBottom}); + + if (minOverlap == overlapLeft || minOverlap == overlapRight) { + ballVel.dx = -ballVel.dx; + if (minOverlap == overlapLeft) { + ballPosRef.x = brickLeft - ballRadius - 0.1f; + } else { + ballPosRef.x = brickRight + ballRadius + 0.1f; + } + } else { + ballVel.dy = std::abs(ballVel.dy); + if (minOverlap == overlapTop) { + ballPosRef.y = brickTop - ballRadius - 0.1f; + } else { + ballPosRef.y = brickBottom + ballRadius + 0.1f; + } + } + + brickHealth.current--; + if (brickHealth.current <= 0) { + registry.DestroyEntity(brick); + } + } + + void BreakoutPhysicsSystem::HandleBallWallCollision(RType::ECS::Registry& registry, RType::ECS::Entity ball, float screenWidth, float screenHeight) { + if (ball == RType::ECS::NULL_ENTITY || !registry.IsEntityAlive(ball)) { + return; + } + + if (!registry.HasComponent(ball) || !registry.HasComponent(ball) || + !registry.HasComponent(ball)) { + return; + } + + auto& ballPos = registry.GetComponent(ball); + auto& ballVel = registry.GetComponent(ball); + float radius = registry.GetComponent(ball).radius; + + if (ballPos.x - radius <= 0.0f) { + ballPos.x = radius; + ballVel.dx = std::abs(ballVel.dx); + } else if (ballPos.x + radius >= screenWidth) { + ballPos.x = screenWidth - radius; + ballVel.dx = -std::abs(ballVel.dx); + } + + if (ballPos.y - radius <= 0.0f) { + ballPos.y = radius; + ballVel.dy = std::abs(ballVel.dy); + } + + if (ballPos.y + radius >= screenHeight) { + ballPos.x = screenWidth / 2.0f; + ballPos.y = screenHeight / 2.0f; + + static std::random_device rd; + static std::mt19937 gen(rd()); + std::uniform_real_distribution angleDist(-45.0f, 45.0f); + + float angle = angleDist(gen) * (3.14159f / 180.0f); + ballVel.dx = 250.0f * std::sin(angle); + ballVel.dy = 250.0f * std::abs(std::cos(angle)); + } + } + + } +} + diff --git a/games/breakout/src/BreakoutRenderSystem.cpp b/games/breakout/src/BreakoutRenderSystem.cpp new file mode 100644 index 0000000..333a32f --- /dev/null +++ b/games/breakout/src/BreakoutRenderSystem.cpp @@ -0,0 +1,72 @@ +#include "BreakoutRenderSystem.hpp" +#include "ECS/Registry.hpp" +#include "ECS/Component.hpp" +#include "Core/ColorFilter.hpp" + +namespace Breakout { + namespace ECS { + + BreakoutRenderSystem::BreakoutRenderSystem(Renderer::IRenderer* renderer) + : m_renderer(renderer) {} + + void BreakoutRenderSystem::Update(RType::ECS::Registry& registry, float deltaTime) { + if (!m_renderer) { + return; + } + + auto boxEntities = registry.GetEntitiesWithComponent(); + for (auto entity : boxEntities) { + if (!registry.IsEntityAlive(entity) || !registry.HasComponent(entity)) { + continue; + } + + const auto& pos = registry.GetComponent(entity); + const auto& collider = registry.GetComponent(entity); + + if (registry.HasComponent(entity)) { + const auto& drawable = registry.GetComponent(entity); + if (drawable.spriteId != Renderer::INVALID_SPRITE_ID) { + continue; + } + + Renderer::Rectangle rect; + rect.position = Renderer::Vector2(pos.x - collider.width / 2.0f, pos.y - collider.height / 2.0f); + rect.size = Renderer::Vector2(collider.width, collider.height); + Math::Color finalColor = drawable.tint; + if (RType::Core::ColorFilter::IsColourBlindModeEnabled()) { + finalColor = RType::Core::ColorFilter::ApplyColourBlindFilter(drawable.tint); + } + m_renderer->DrawRectangle(rect, finalColor); + } + } + + auto circleEntities = registry.GetEntitiesWithComponent(); + for (auto entity : circleEntities) { + if (!registry.IsEntityAlive(entity) || !registry.HasComponent(entity)) { + continue; + } + + const auto& pos = registry.GetComponent(entity); + const auto& collider = registry.GetComponent(entity); + + if (registry.HasComponent(entity)) { + const auto& drawable = registry.GetComponent(entity); + if (drawable.spriteId != Renderer::INVALID_SPRITE_ID) { + continue; + } + + float radius = collider.radius; + Renderer::Rectangle rect; + rect.position = Renderer::Vector2(pos.x - radius, pos.y - radius); + rect.size = Renderer::Vector2(radius * 2.0f, radius * 2.0f); + Math::Color finalColor = drawable.tint; + if (RType::Core::ColorFilter::IsColourBlindModeEnabled()) { + finalColor = RType::Core::ColorFilter::ApplyColourBlindFilter(drawable.tint); + } + m_renderer->DrawRectangle(rect, finalColor); + } + } + } + + } +} diff --git a/games/breakout/src/main.cpp b/games/breakout/src/main.cpp new file mode 100644 index 0000000..5c8ee70 --- /dev/null +++ b/games/breakout/src/main.cpp @@ -0,0 +1,301 @@ +#include "Core/Engine.hpp" +#include "Core/ColorFilter.hpp" +#include "ECS/Registry.hpp" +#include "ECS/Component.hpp" +#include "ECS/MovementSystem.hpp" +#include "ECS/RenderingSystem.hpp" +#include "ECS/InputSystem.hpp" +#include "ECS/TextRenderingSystem.hpp" +#include "ECS/Components/TextLabel.hpp" +#include "Renderer/SFMLRenderer.hpp" +#include "BreakoutPhysicsSystem.hpp" +#include "BreakoutRenderSystem.hpp" +#include "BallAccelerationSystem.hpp" +#include +#include +#include +#include +#include + +using namespace RType; +using namespace RType::ECS; +using namespace Breakout::ECS; + +const Math::Color BRICK_COLORS[] = { + {1.0f, 0.2f, 0.2f, 1.0f}, + {1.0f, 0.5f, 0.2f, 1.0f}, + {1.0f, 1.0f, 0.2f, 1.0f}, + {0.2f, 1.0f, 0.2f, 1.0f}, + {0.2f, 0.5f, 1.0f, 1.0f}, + {0.5f, 0.2f, 1.0f, 1.0f}, + {0.2f, 1.0f, 1.0f, 1.0f} +}; + +Entity CreatePaddle(Registry& registry, float x, float y) { + Entity paddle = registry.CreateEntity(); + registry.AddComponent(paddle, Position{x, y}); + registry.AddComponent(paddle, Velocity{0.0f, 0.0f}); + registry.AddComponent(paddle, Controllable{480.0f}); + registry.AddComponent(paddle, BoxCollider{100.0f, 20.0f}); + + Drawable drawable(Renderer::INVALID_SPRITE_ID, 10); + drawable.tint = {0.8f, 0.8f, 1.0f, 1.0f}; + registry.AddComponent(paddle, std::move(drawable)); + return paddle; +} + +Entity CreateBall(Registry& registry, float x, float y) { + Entity ball = registry.CreateEntity(); + registry.AddComponent(ball, Position{x, y}); + + static std::random_device rd; + static std::mt19937 gen(rd()); + std::uniform_real_distribution angleDist(-45.0f, 45.0f); + + float angle = angleDist(gen) * (3.14159f / 180.0f); + float velX = 250.0f * std::sin(angle); + float velY = 250.0f * std::abs(std::cos(angle)); + + registry.AddComponent(ball, Velocity{velX, velY}); + registry.AddComponent(ball, CircleCollider{10.0f}); + + Drawable drawable(Renderer::INVALID_SPRITE_ID, 12); + drawable.tint = {1.0f, 1.0f, 1.0f, 1.0f}; + registry.AddComponent(ball, std::move(drawable)); + return ball; +} + +Entity CreateBrick(Registry& registry, float x, float y, const Math::Color& color) { + Entity brick = registry.CreateEntity(); + registry.AddComponent(brick, Position{x, y}); + registry.AddComponent(brick, Health{1, 1}); + registry.AddComponent(brick, BoxCollider{80.0f, 30.0f}); + + Drawable drawable(Renderer::INVALID_SPRITE_ID, 5); + drawable.tint = color; + registry.AddComponent(brick, std::move(drawable)); + return brick; +} + +void CreateBrickWall(Registry& registry, float startX, float startY, int rows, int cols) { + const float BRICK_WIDTH = 80.0f; + const float BRICK_HEIGHT = 30.0f; + const float BRICK_SPACING = 5.0f; + + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + float x = startX + col * (BRICK_WIDTH + BRICK_SPACING); + float y = startY + row * (BRICK_HEIGHT + BRICK_SPACING); + CreateBrick(registry, x, y, BRICK_COLORS[row % 7]); + } + } +} + +int main() { + std::cout << "=== Breakout Game - Engine Reusability Demo ===" << std::endl; + + auto renderer = std::make_shared(); + Renderer::WindowConfig config; + config.title = "Breakout"; + config.width = 800; + config.height = 900; + config.resizable = false; + config.fullscreen = false; + config.targetFramerate = 60; + + if (!renderer->CreateWindow(config)) { + std::cerr << "Failed to create window" << std::endl; + return 1; + } + + Renderer::Camera2D camera; + camera.center = Renderer::Vector2(400.0f, 450.0f); + camera.size = Renderer::Vector2(800.0f, 900.0f); + renderer->SetCamera(camera); + + Core::EngineConfig engineConfig; + engineConfig.pluginPath = "./plugins"; + auto engine = std::make_unique(engineConfig); + + if (!engine->Initialize()) { + std::cerr << "Failed to initialize engine" << std::endl; + return 1; + } + + auto& registry = engine->GetRegistry(); + + const float SCREEN_WIDTH = 800.0f; + const float SCREEN_HEIGHT = 900.0f; + const int BRICK_COLS = 9; + const int BRICK_ROWS = 7; + const float BRICK_WIDTH = 80.0f; + const float BRICK_SPACING = 5.0f; + const float WALL_TOTAL_WIDTH = BRICK_COLS * BRICK_WIDTH + (BRICK_COLS - 1) * BRICK_SPACING; + const float WALL_START_X = (SCREEN_WIDTH - WALL_TOTAL_WIDTH) / 2.0f + BRICK_WIDTH / 2.0f; + + Entity paddle = CreatePaddle(registry, SCREEN_WIDTH / 2.0f, SCREEN_HEIGHT - 80.0f); + Entity ball = CreateBall(registry, SCREEN_WIDTH / 2.0f, SCREEN_HEIGHT / 2.0f); + CreateBrickWall(registry, WALL_START_X, 30.0f, BRICK_ROWS, BRICK_COLS); + + Entity scoreEntity = registry.CreateEntity(); + registry.AddComponent(scoreEntity, Position{20.0f, SCREEN_HEIGHT - 40.0f}); + + Renderer::FontId font = renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 16); + if (font == Renderer::INVALID_FONT_ID) { + font = renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 16); + } + + Renderer::FontId winFont = renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 80); + if (winFont == Renderer::INVALID_FONT_ID) { + winFont = renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 80); + } + if (winFont == Renderer::INVALID_FONT_ID) { + winFont = font; + } + + if (font != Renderer::INVALID_FONT_ID) { + TextLabel scoreLabel("SCORE: 0000", font, 16); + scoreLabel.color = {1.0f, 1.0f, 1.0f, 1.0f}; + registry.AddComponent(scoreEntity, std::move(scoreLabel)); + } + + Entity winWEntity = NULL_ENTITY; + Entity winIEntity = NULL_ENTITY; + Entity winNEntity = NULL_ENTITY; + + engine->RegisterSystem(std::make_unique(renderer.get())); + engine->RegisterSystem(std::make_unique()); + engine->RegisterSystem(std::make_unique()); + engine->RegisterSystem(std::make_unique()); + engine->RegisterSystem(std::make_unique(renderer.get())); + engine->RegisterSystem(std::make_unique(renderer.get())); + engine->RegisterSystem(std::make_unique(renderer.get())); + + int score = 0; + bool running = true; + bool cKeyPressed = false; + bool colourBlindMode = false; + + std::ifstream settingsFile("breakout_settings.json"); + if (!settingsFile.is_open()) { + settingsFile.open("../breakout_settings.json"); + } + if (settingsFile.is_open()) { + std::string line; + while (std::getline(settingsFile, line)) { + if (line.find("colourBlindMode=") == 0) { + std::string value = line.substr(17); + colourBlindMode = (value == "true" || value == "1"); + Core::ColorFilter::SetColourBlindMode(colourBlindMode); + break; + } + } + settingsFile.close(); + } + + while (renderer->IsWindowOpen() && running) { + float deltaTime = renderer->GetDeltaTime(); + renderer->Update(deltaTime); + renderer->BeginFrame(); + renderer->Clear({0.0f, 0.0f, 0.0f, 1.0f}); + + if (registry.IsEntityAlive(paddle) && registry.HasComponent(paddle)) { + registry.GetComponent(paddle).dy = 0.0f; + } + + engine->UpdateSystems(deltaTime); + + if (registry.IsEntityAlive(paddle) && registry.HasComponent(paddle) && registry.HasComponent(paddle)) { + auto& paddlePos = registry.GetComponent(paddle); + const auto& collider = registry.GetComponent(paddle); + paddlePos.y = SCREEN_HEIGHT - 80.0f; + float halfWidth = collider.width / 2.0f; + paddlePos.x = std::max(halfWidth, std::min(SCREEN_WIDTH - halfWidth, paddlePos.x)); + } + + auto brickEntities = registry.GetEntitiesWithComponent(); + int remainingBricks = 0; + for (auto entity : brickEntities) { + if (!registry.HasComponent(entity)) { + remainingBricks++; + } + } + + int newScore = (BRICK_ROWS * BRICK_COLS) - remainingBricks; + if (newScore != score) { + score = newScore; + if (registry.HasComponent(scoreEntity)) { + char scoreText[32]; + std::snprintf(scoreText, sizeof(scoreText), "SCORE: %04d", score); + registry.GetComponent(scoreEntity).text = scoreText; + } + } + + static bool winMessageShown = false; + if (remainingBricks == 0 && !winMessageShown) { + std::cout << "You won! All bricks destroyed!" << std::endl; + + if (registry.IsEntityAlive(paddle)) { + registry.DestroyEntity(paddle); + } + if (registry.IsEntityAlive(ball)) { + registry.DestroyEntity(ball); + } + + const float WIN_Y = SCREEN_HEIGHT / 2.0f - 50.0f; + const float LETTER_SPACING = 120.0f; + const float WIN_START_X = SCREEN_WIDTH / 2.0f - LETTER_SPACING; + + winWEntity = registry.CreateEntity(); + registry.AddComponent(winWEntity, Position{WIN_START_X, WIN_Y}); + TextLabel wLabel("W", winFont, 80); + wLabel.color = {1.0f, 0.2f, 0.2f, 1.0f}; + wLabel.centered = true; + registry.AddComponent(winWEntity, std::move(wLabel)); + + winIEntity = registry.CreateEntity(); + registry.AddComponent(winIEntity, Position{WIN_START_X + LETTER_SPACING, WIN_Y}); + TextLabel iLabel("I", winFont, 80); + iLabel.color = {1.0f, 0.5f, 0.2f, 1.0f}; + iLabel.centered = true; + registry.AddComponent(winIEntity, std::move(iLabel)); + + winNEntity = registry.CreateEntity(); + registry.AddComponent(winNEntity, Position{WIN_START_X + LETTER_SPACING * 2, WIN_Y}); + TextLabel nLabel("N", winFont, 80); + nLabel.color = {1.0f, 1.0f, 0.2f, 1.0f}; + nLabel.centered = true; + registry.AddComponent(winNEntity, std::move(nLabel)); + + winMessageShown = true; + } + + renderer->EndFrame(); + + if (renderer->IsKeyPressed(Renderer::Key::Escape)) { + running = false; + } + + if (renderer->IsKeyPressed(Renderer::Key::C) && !cKeyPressed) { + cKeyPressed = true; + colourBlindMode = !colourBlindMode; + Core::ColorFilter::SetColourBlindMode(colourBlindMode); + std::cout << "Colour-blind mode: " << (colourBlindMode ? "ON" : "OFF") << std::endl; + + std::ofstream outFile("breakout_settings.json"); + if (!outFile.is_open()) { + outFile.open("../breakout_settings.json"); + } + if (outFile.is_open()) { + outFile << "colourBlindMode=" << (colourBlindMode ? "true" : "false") << std::endl; + outFile.close(); + } + } else if (!renderer->IsKeyPressed(Renderer::Key::C)) { + cKeyPressed = false; + } + } + + std::cout << "Final Score: " << score << std::endl; + engine->Shutdown(); + return 0; +} diff --git a/games/rtype/client/include/EditorState.hpp b/games/rtype/client/include/EditorState.hpp new file mode 100644 index 0000000..6b5b2a8 --- /dev/null +++ b/games/rtype/client/include/EditorState.hpp @@ -0,0 +1,85 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** EditorState +*/ + +#pragma once + +#include "GameStateMachine.hpp" +#include "editor/EditorTypes.hpp" +#include "editor/EditorUIManager.hpp" +#include "editor/EditorEntityManager.hpp" +#include "editor/EditorAssetLibrary.hpp" +#include "editor/EditorInputHandler.hpp" +#include "editor/EditorPropertyManager.hpp" +#include "ECS/Registry.hpp" +#include "ECS/RenderingSystem.hpp" +#include "ECS/TextRenderingSystem.hpp" +#include "Renderer/IRenderer.hpp" +#include +#include +#include +#include + +namespace RType { + namespace Client { + + class EditorCanvasManager; + class EditorFileManager; + + class EditorState : public IState { + public: + EditorState(GameStateMachine& machine, GameContext& context); + ~EditorState() override; + + void Init() override; + void Cleanup() override; + void HandleInput() override; + void Update(float dt) override; + void Draw() override; + + private: + GameStateMachine& m_machine; + GameContext& m_context; + + RType::ECS::Registry m_registry; + std::shared_ptr m_renderer; + std::unique_ptr m_renderingSystem; + std::unique_ptr m_textSystem; + + std::unique_ptr m_canvasManager; + std::unique_ptr m_assetLibrary; + std::unique_ptr m_uiManager; + std::unique_ptr m_entityManager; + std::unique_ptr m_inputHandler; + std::unique_ptr m_propertyManager; + std::unique_ptr m_fileManager; + + // Fonts + Renderer::FontId m_fontSmall = Renderer::INVALID_FONT_ID; + Renderer::FontId m_fontMedium = Renderer::INVALID_FONT_ID; + + // UI entities for cleanup + std::vector m_entities; + std::vector m_statusBarEntities; + + // Input state + bool m_leftMousePressed = false; + + // Editor state + EditorPaletteSelection m_selection; + Math::Vector2 m_lastMouseWorld{0.0f, 0.0f}; + std::string m_currentLevelPath; + std::string m_levelName = "Custom Level"; + bool m_hasUnsavedChanges = false; + + void handleSelectionAt(const Math::Vector2& mouseWorld); + void updatePropertyPanel(); + void deleteSelectedEntity(); + void saveCurrentLevel(); + }; + + } +} diff --git a/games/rtype/client/include/GameState.hpp b/games/rtype/client/include/GameState.hpp new file mode 100644 index 0000000..b9575cf --- /dev/null +++ b/games/rtype/client/include/GameState.hpp @@ -0,0 +1,392 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** GameState +*/ + +#pragma once + +#include "GameStateMachine.hpp" +#include "ECS/Registry.hpp" +#include "ECS/RenderingSystem.hpp" +#include "ECS/TextRenderingSystem.hpp" +#include "ECS/ScrollingSystem.hpp" +#include "ECS/ShootingSystem.hpp" +#include "ECS/MovementSystem.hpp" +#include "ECS/InputSystem.hpp" +#include "ECS/CollisionDetectionSystem.hpp" +#include "ECS/BulletCollisionResponseSystem.hpp" +#include "ECS/PlayerCollisionResponseSystem.hpp" +#include "ECS/ObstacleCollisionResponseSystem.hpp" +#include "ECS/HealthSystem.hpp" +#include "ECS/ScoreSystem.hpp" +#include "ECS/ShieldSystem.hpp" +#include "ECS/ForcePodSystem.hpp" +#include "ECS/PowerUpSpawnSystem.hpp" +#include "ECS/PowerUpCollisionSystem.hpp" +#include "ECS/AudioSystem.hpp" +#include "ECS/AnimationSystem.hpp" +#include "ECS/EffectFactory.hpp" +#include "ECS/Component.hpp" +#include "ECS/PowerUpFactory.hpp" +#include "ECS/LevelLoader.hpp" +#include "Animation/AnimationModule.hpp" +#include "Renderer/IRenderer.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace RType { + namespace Client { + + // Player HUD data for multiplayer scoreboard + struct PlayerHUDData { + bool active = false; + uint32_t score = 0; + int lives = 3; + int health = 100; + int maxHealth = 100; + bool isDead = false; + RType::ECS::Entity scoreEntity = RType::ECS::NULL_ENTITY; + RType::ECS::Entity playerEntity = RType::ECS::NULL_ENTITY; + RType::ECS::Entity powerupSpreadEntity = RType::ECS::NULL_ENTITY; + RType::ECS::Entity powerupLaserEntity = RType::ECS::NULL_ENTITY; + RType::ECS::Entity powerupSpeedEntity = RType::ECS::NULL_ENTITY; + RType::ECS::Entity powerupShieldEntity = RType::ECS::NULL_ENTITY; + }; + + struct PredictedInput { + uint32_t sequence = 0; + uint8_t inputs = 0; + float predictedX = 0.0f; + float predictedY = 0.0f; + float deltaTime = 0.0f; + }; + + // Entity interpolation state for smooth rendering of remote entities + struct InterpolationState { + float prevX = 0.0f; + float prevY = 0.0f; + float targetX = 0.0f; + float targetY = 0.0f; + float interpTime = 0.0f; + float interpDuration = 1.0f / 60.0f; + }; + + class InGameState : public IState { + public: + InGameState(GameStateMachine& machine, GameContext& context, uint32_t seed, const std::string& levelPath = "assets/levels/level1.json"); + ~InGameState() override = default; + + void Init() override; + void Cleanup() override; + void HandleInput() override; + void Update(float dt) override; + void Draw() override; + private: + // Level loading + void loadLevel(const std::string& levelPath); + void initializeFromLevel(); + + // Player System (Keane) + void initializePlayers(); + + // Ennemy System (Dryss) + void spawnEnemies(); + + void initializeUI(); + void updateHUD(); + void updatePowerUpIcons(); + void updatePowerUpText(RType::ECS::Entity& textEntity, const std::string& text, + bool isActive, float x, float y); + void renderChargeBar(); + void renderHealthBars(); + void renderBossHealthBar(); + void updateBossHealthBar(); + void initializeBossHealthBar(); + void destroyBossHealthBar(); + void renderGameOverOverlay(); + void renderVictoryOverlay(); + void renderLevelTransition(); + void triggerGameOverIfNeeded(); + void triggerVictoryIfNeeded(); + void enterResultsScreen(); + void createBeamEntity(); + void updateBeam(float dt); + + // Level progression + void checkBossDefeated(); + + // ECS systems + void createSystems(); + + // Server state update handler + void OnServerStateUpdate(uint32_t tick, const std::vector& entities, const std::vector& inputAcks); + void ReconcileWithServer(const network::InputAck& ack); + void OnLevelComplete(uint8_t completedLevel, uint8_t nextLevel); + void ApplyPowerUpStateToPlayer(ECS::Entity playerEntity, const network::EntityState& entityState); + + // Component cleanup helper for entity type validation + void CleanupInvalidComponents(ECS::Entity entity, network::EntityType expectedType); + + void cleanupForLevelTransition(); + + struct EnemySpriteConfig { + Renderer::SpriteId sprite = Renderer::INVALID_SPRITE_ID; + Math::Color tint{1.0f, 1.0f, 1.0f, 1.0f}; + float rotation = 0.0f; + }; + + struct EnemyBulletSpriteConfig { + Renderer::SpriteId sprite; + Math::Color tint; + float scale; + }; + EnemySpriteConfig GetEnemySpriteConfig(uint8_t enemyType) const; + EnemyBulletSpriteConfig GetEnemyBulletSpriteConfig(uint8_t enemyType) const; + Renderer::SpriteId GetPowerUpSprite(ECS::PowerUpType type) const; + + std::pair FindPlayerNameAndNumber(uint64_t ownerHash, const std::unordered_set& assignedNumbers) const; + void CreatePlayerNameLabel(RType::ECS::Entity playerEntity, const std::string& playerName, float x, float y); + void UpdatePlayerNameLabelPosition(RType::ECS::Entity playerEntity, float x, float y); + void DestroyPlayerNameLabel(RType::ECS::Entity playerEntity); + + // Level transition + void UpdateLevelTransition(float dt); + void LoadNextLevel(); + private: + GameStateMachine& m_machine; + GameContext& m_context; + uint32_t m_gameSeed; + + RType::ECS::Registry m_registry; + std::shared_ptr m_renderer; + + // Systems + std::unique_ptr m_scrollingSystem; + std::unique_ptr m_renderingSystem; + std::unique_ptr m_textSystem; + std::unique_ptr m_movementSystem; + std::unique_ptr m_inputSystem; + std::unique_ptr m_collisionDetectionSystem; + std::unique_ptr m_bulletResponseSystem; + std::unique_ptr m_playerResponseSystem; + std::unique_ptr m_obstacleResponseSystem; + std::unique_ptr m_healthSystem; + std::unique_ptr m_scoreSystem; + std::unique_ptr m_shootingSystem; + std::unique_ptr m_audioSystem; + std::unique_ptr m_shieldSystem; + std::unique_ptr m_forcePodSystem; + std::unique_ptr m_powerUpSpawnSystem; + std::unique_ptr m_powerUpCollisionSystem; + std::unique_ptr m_animationSystem; + std::unique_ptr m_animationModule; + std::unique_ptr m_effectFactory; + + // Bullet textures and sprites + Renderer::TextureId m_bulletTexture = Renderer::INVALID_TEXTURE_ID; + Renderer::SpriteId m_bulletSprite = Renderer::INVALID_SPRITE_ID; + + Audio::SoundId m_playerShootSound = Audio::INVALID_SOUND_ID; + Audio::MusicId m_shootMusic = Audio::INVALID_MUSIC_ID; + Audio::SoundId m_powerUpSound = Audio::INVALID_SOUND_ID; + Audio::MusicId m_powerUpMusic = Audio::INVALID_MUSIC_ID; + float m_shootSfxCooldown = 0.0f; + Audio::MusicId m_gameMusic = Audio::INVALID_MUSIC_ID; + bool m_gameMusicPlaying = false; + Audio::MusicId m_bossMusic = Audio::INVALID_MUSIC_ID; + bool m_bossMusicPlaying = false; + Audio::MusicId m_gameOverMusic = Audio::INVALID_MUSIC_ID; + bool m_gameOverMusicPlaying = false; + Audio::MusicId m_victoryMusic = Audio::INVALID_MUSIC_ID; + bool m_victoryMusicPlaying = false; + + // Enemy bullet textures and sprites + Renderer::TextureId m_enemyBulletGreenTexture = Renderer::INVALID_TEXTURE_ID; + Renderer::TextureId m_enemyBulletYellowTexture = Renderer::INVALID_TEXTURE_ID; + Renderer::TextureId m_enemyBulletPurpleTexture = Renderer::INVALID_TEXTURE_ID; + Renderer::SpriteId m_enemyBulletGreenSprite = Renderer::INVALID_SPRITE_ID; + Renderer::SpriteId m_enemyBulletYellowSprite = Renderer::INVALID_SPRITE_ID; + Renderer::SpriteId m_enemyBulletPurpleSprite = Renderer::INVALID_SPRITE_ID; + + // Background and obstacles entities + std::vector m_backgroundEntities; + std::vector m_obstacleSpriteEntities; + std::vector m_obstacleColliderEntities; + + bool m_escapeKeyPressed = false; + + // Network synchronization + float m_localScrollOffset = 0.0f; + float m_serverScrollOffset = 0.0f; + + std::chrono::steady_clock::time_point m_lastInputTime; + uint8_t m_currentInputs = 0; + uint8_t m_previousInputs = 0; + + std::deque m_inputHistory; + uint32_t m_inputSequence = 0; + uint32_t m_lastAckedSequence = 0; + float m_predictedX = 0.0f; + float m_predictedY = 0.0f; + static constexpr size_t MAX_INPUT_HISTORY = 120; + static constexpr float PREDICTION_SPEED = 200.0f; + + // Entity interpolation for remote entities + std::unordered_map m_interpolationStates; + + // Player ships tracking (network entities → ECS entities) + std::unordered_map m_networkEntityMap; + std::unordered_map m_bulletFlagsMap; // Track bullet flags to detect type changes + RType::ECS::Entity m_localPlayerEntity = RType::ECS::NULL_ENTITY; // Local player entity mirrored from server + + // Level progression tracking with visual transition + enum class TransitionPhase { + NONE, + FADE_OUT, + LOADING, + FADE_IN + }; + + struct LevelProgressionState { + bool bossDefeated = false; + bool levelComplete = false; + bool allLevelsComplete = false; + float transitionTimer = 0.0f; + float victoryElapsed = 0.0f; + int currentLevelNumber = 1; + int nextLevelNumber = 2; + int totalLevels = 3; + TransitionPhase transitionPhase = TransitionPhase::NONE; + float fadeAlpha = 0.0f; + }; + LevelProgressionState m_levelProgress; + + // Individual player ship sprites + Renderer::TextureId m_playerGreenTexture = Renderer::INVALID_TEXTURE_ID; + Renderer::TextureId m_playerBlueTexture = Renderer::INVALID_TEXTURE_ID; + Renderer::TextureId m_playerRedTexture = Renderer::INVALID_TEXTURE_ID; + Renderer::SpriteId m_playerGreenSprite = Renderer::INVALID_SPRITE_ID; + Renderer::SpriteId m_playerBlueSprite = Renderer::INVALID_SPRITE_ID; + Renderer::SpriteId m_playerRedSprite = Renderer::INVALID_SPRITE_ID; + + // Enemy ship sprites + Renderer::TextureId m_enemyGreenTexture = Renderer::INVALID_TEXTURE_ID; + Renderer::TextureId m_enemyRedTexture = Renderer::INVALID_TEXTURE_ID; + Renderer::TextureId m_enemyBlueTexture = Renderer::INVALID_TEXTURE_ID; + Renderer::SpriteId m_enemyGreenSprite = Renderer::INVALID_SPRITE_ID; + Renderer::SpriteId m_enemyRedSprite = Renderer::INVALID_SPRITE_ID; + Renderer::SpriteId m_enemyBlueSprite = Renderer::INVALID_SPRITE_ID; + + // Power-up sprites + Renderer::SpriteId m_powerupSpreadSprite = Renderer::INVALID_SPRITE_ID; + Renderer::SpriteId m_powerupLaserSprite = Renderer::INVALID_SPRITE_ID; + Renderer::SpriteId m_powerupForcePodSprite = Renderer::INVALID_SPRITE_ID; + Renderer::SpriteId m_powerupSpeedSprite = Renderer::INVALID_SPRITE_ID; + Renderer::SpriteId m_powerupShieldSprite = Renderer::INVALID_SPRITE_ID; + + // Explosion animation + Renderer::TextureId m_explosionTexture = Renderer::INVALID_TEXTURE_ID; + Renderer::SpriteId m_explosionSprite = Renderer::INVALID_SPRITE_ID; + Animation::AnimationClipId m_explosionClipId = Animation::INVALID_CLIP_ID; + Renderer::TextureId m_shootingTexture = Renderer::INVALID_TEXTURE_ID; + Renderer::SpriteId m_shootingSprite = Renderer::INVALID_SPRITE_ID; + Animation::AnimationClipId m_shootingClipId = Animation::INVALID_CLIP_ID; + + Renderer::TextureId m_forcePodTexture = Renderer::INVALID_TEXTURE_ID; + Renderer::SpriteId m_forcePodSprite = Renderer::INVALID_SPRITE_ID; + Animation::AnimationClipId m_forcePodClipId = Animation::INVALID_CLIP_ID; + Renderer::TextureId m_beamTexture = Renderer::INVALID_TEXTURE_ID; + Renderer::SpriteId m_beamSprite = Renderer::INVALID_SPRITE_ID; + Animation::AnimationClipId m_beamClipId = Animation::INVALID_CLIP_ID; + Renderer::TextureId m_hitTexture = Renderer::INVALID_TEXTURE_ID; + Renderer::SpriteId m_hitSprite = Renderer::INVALID_SPRITE_ID; + Animation::AnimationClipId m_hitClipId = Animation::INVALID_CLIP_ID; + Animation::AnimationClipId m_waveAttackClipId = Animation::INVALID_CLIP_ID; + Animation::AnimationClipId m_secondAttackClipId = Animation::INVALID_CLIP_ID; + Animation::AnimationClipId m_fireBulletClipId = Animation::INVALID_CLIP_ID; + Animation::AnimationClipId m_mineClipId = Animation::INVALID_CLIP_ID; + Animation::AnimationClipId m_mineExplosionClipId = Animation::INVALID_CLIP_ID; + + // HUD fonts + Renderer::FontId m_hudFont = Renderer::INVALID_FONT_ID; + Renderer::FontId m_hudFontSmall = Renderer::INVALID_FONT_ID; + Renderer::FontId m_gameOverFontLarge = Renderer::INVALID_FONT_ID; + Renderer::FontId m_gameOverFontMedium = Renderer::INVALID_FONT_ID; + + // HUD entities - local player info (left side) + RType::ECS::Entity m_hudPlayerEntity = RType::ECS::NULL_ENTITY; + RType::ECS::Entity m_hudScoreEntity = RType::ECS::NULL_ENTITY; + RType::ECS::Entity m_hudLivesEntity = RType::ECS::NULL_ENTITY; + + // HUD entities - all players scoreboard (right side) + RType::ECS::Entity m_hudScoreboardTitle = RType::ECS::NULL_ENTITY; + std::array m_playersHUD; + + // Local player state for HUD + uint32_t m_playerScore = 0; + int m_playerLives = 3; + float m_scoreAccumulator = 0.0f; // For time-based score testing + + // Game Over overlay + bool m_isGameOver = false; + float m_gameOverElapsed = 0.0f; + bool m_gameOverEnterPressed = false; + bool m_gameOverEscapePressed = false; + RType::ECS::Entity m_gameOverTitleEntity = RType::ECS::NULL_ENTITY; + RType::ECS::Entity m_gameOverScoreEntity = RType::ECS::NULL_ENTITY; + RType::ECS::Entity m_gameOverHintEntity = RType::ECS::NULL_ENTITY; + + RType::ECS::Entity m_victoryTitleEntity = RType::ECS::NULL_ENTITY; + RType::ECS::Entity m_victoryScoreEntity = RType::ECS::NULL_ENTITY; + RType::ECS::Entity m_victoryHintEntity = RType::ECS::NULL_ENTITY; + + bool m_isCharging = false; + float m_chargeTime = 0.0f; + RType::ECS::Entity m_beamEntity = RType::ECS::NULL_ENTITY; + float m_beamDuration = 0.0f; + static constexpr float MAX_CHARGE_TIME = 2.0f; // 2 seconds for full charge + + bool m_isNetworkSession = false; + std::unordered_map m_obstacleIdToCollider; + + // Player name tracking + std::unordered_map m_playerNameMap; + std::unordered_map m_playerNameLabels; + std::unordered_set m_assignedPlayerNumbers; + + // Level loader data + RType::ECS::LevelData m_levelData; + RType::ECS::LoadedAssets m_levelAssets; + RType::ECS::CreatedEntities m_levelEntities; + std::string m_currentLevelPath = "assets/levels/level1.json"; + + bool m_bossWarningActive = false; + bool m_bossWarningTriggered = false; + float m_bossWarningTimer = 0.0f; + static constexpr float BOSS_WARNING_DURATION = 4.0f; + bool m_bossWarningFlashState = false; + void renderBossWarning(); + + // Boss health bar + struct { + bool active = false; + int currentHealth = 0; + int maxHealth = 1000; + uint32_t bossNetworkId = 0; + RType::ECS::Entity titleEntity = RType::ECS::NULL_ENTITY; + RType::ECS::Entity barBackgroundEntity = RType::ECS::NULL_ENTITY; + RType::ECS::Entity barForegroundEntity = RType::ECS::NULL_ENTITY; + } m_bossHealthBar; + }; + + } +} diff --git a/games/rtype/client/include/GameStateMachine.hpp b/games/rtype/client/include/GameStateMachine.hpp new file mode 100644 index 0000000..8796ac6 --- /dev/null +++ b/games/rtype/client/include/GameStateMachine.hpp @@ -0,0 +1,199 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** GameStateMachine +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include "Renderer/IRenderer.hpp" +#include "ECS/Registry.hpp" +#include "Audio/IAudio.hpp" +#include "../../libs/network/include/GameClient.hpp" +#include "../../libs/network/include/Protocol.hpp" +#include "../../libs/network/include/INetworkModule.hpp" +#include "Core/Logger.hpp" + +namespace RType { + namespace Client { + + enum class GameState { + None, + Menu, + Lobby, + Game, + Exit + }; + + struct GameContext { + std::shared_ptr renderer; + std::shared_ptr registry; + std::shared_ptr networkClient; + std::shared_ptr networkModule; + std::shared_ptr audio; + + std::string playerName; + std::string serverIp; + uint16_t serverPort; + uint64_t playerHash; + uint8_t playerNumber; + std::vector allPlayers; + + uint32_t roomId = 0; + std::string roomName; + }; + + class IState { + public: + virtual ~IState() = default; + + virtual void Init() = 0; + virtual void Cleanup() {} + virtual void HandleInput() = 0; + virtual void Update(float dt) = 0; + virtual void Draw() = 0; + }; + + class GameStateMachine { + public: + GameStateMachine() = default; + ~GameStateMachine() { + while (!m_states.empty()) { + PopStateImmediate(); + } + } + + void PushState(std::unique_ptr state) { + if (m_isDispatching) { + m_pending = PendingOp{OpType::Push, std::move(state)}; + return; + } + PushStateImmediate(std::move(state)); + } + + void PopState() { + if (m_isDispatching) { + m_pending = PendingOp{OpType::Pop, nullptr}; + return; + } + PopStateImmediate(); + } + + void ChangeState(std::unique_ptr state) { + if (m_isDispatching) { + m_pending = PendingOp{OpType::Change, std::move(state)}; + return; + } + ChangeStateImmediate(std::move(state)); + } + + IState* GetCurrentState() { + return m_states.empty() ? nullptr : m_states.top().get(); + } + + void Update(float dt) { + if (GetCurrentState()) { + m_isDispatching = true; + GetCurrentState()->Update(dt); + m_isDispatching = false; + ApplyPending(); + if (m_states.empty()) { + Core::Logger::Warning("[GameStateMachine] State stack is empty after Update!"); + } + } + } + + void Draw() { + if (GetCurrentState()) { + m_isDispatching = true; + GetCurrentState()->Draw(); + m_isDispatching = false; + ApplyPending(); + if (m_states.empty()) { + Core::Logger::Warning("[GameStateMachine] State stack is empty after Draw!"); + } + } + } + + void HandleInput() { + if (GetCurrentState()) { + m_isDispatching = true; + GetCurrentState()->HandleInput(); + m_isDispatching = false; + ApplyPending(); + + if (m_states.empty()) { + Core::Logger::Warning("[GameStateMachine] State stack is empty after HandleInput!"); + } + } + } + + bool IsRunning() const { return !m_states.empty(); } + size_t GetStateCount() const { return m_states.size(); } + private: + enum class OpType { + Push, + Pop, + Change + }; + + struct PendingOp { + OpType type; + std::unique_ptr state; + }; + + void ApplyPending() { + if (!m_pending.has_value()) { + return; + } + PendingOp op = std::move(*m_pending); + m_pending.reset(); + + switch (op.type) { + case OpType::Push: + PushStateImmediate(std::move(op.state)); + break; + case OpType::Pop: + PopStateImmediate(); + break; + case OpType::Change: + ChangeStateImmediate(std::move(op.state)); + break; + } + } + + void PushStateImmediate(std::unique_ptr state) { + m_states.push(std::move(state)); + m_states.top()->Init(); + } + + void PopStateImmediate() { + if (!m_states.empty()) { + m_states.top()->Cleanup(); + m_states.pop(); + } else { + Core::Logger::Warning("[GameStateMachine] Attempted to pop state from empty stack!"); + } + } + + void ChangeStateImmediate(std::unique_ptr state) { + if (!m_states.empty()) { + m_states.top()->Cleanup(); + m_states.pop(); + } + PushStateImmediate(std::move(state)); + } + + std::stack> m_states; + bool m_isDispatching = false; + std::optional m_pending; + }; + + } +} diff --git a/games/rtype/client/include/LobbyState.hpp b/games/rtype/client/include/LobbyState.hpp new file mode 100644 index 0000000..03bf537 --- /dev/null +++ b/games/rtype/client/include/LobbyState.hpp @@ -0,0 +1,93 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** LobbyState +*/ + +#pragma once + +#include "GameStateMachine.hpp" +#include "LobbyClient.hpp" +#include "ECS/Component.hpp" +#include "ECS/Registry.hpp" +#include "ECS/RenderingSystem.hpp" +#include "ECS/TextRenderingSystem.hpp" +#include "ECS/AudioSystem.hpp" +#include "Renderer/IRenderer.hpp" +#include "GameState.hpp" +#include "Audio/IAudio.hpp" + +#include +#include +#include +#include +#include + +namespace RType { + namespace Client { + + class LobbyState : public IState { + public: + LobbyState(GameStateMachine& machine, GameContext& context); + LobbyState(GameStateMachine& machine, GameContext& context, network::NetworkTcpSocket&& socket); + ~LobbyState() override = default; + + void Init() override; + void Cleanup() override; + void HandleInput() override; + void Update(float dt) override; + void Draw() override; + private: + void createUI(); + void updateLobbyState(); + void updateOrCreatePlayerEntity(const network::PlayerInfo& player); + void updatePlayerCardVisuals(uint8_t playerNum, const network::PlayerInfo& player); + void removePlayer(uint8_t playerNum); + Renderer::TextureId getPlayerTexture(uint8_t playerNum); + + GameStateMachine& m_machine; + GameContext& m_context; + + std::unique_ptr m_client; + std::string m_playerName; + + RType::ECS::Registry m_registry; + std::shared_ptr m_renderer; + std::unique_ptr m_renderingSystem; + std::unique_ptr m_textSystem; + std::unique_ptr m_audioSystem; + + Audio::MusicId m_lobbyMusic = Audio::INVALID_MUSIC_ID; + bool m_lobbyMusicPlaying = false; + + Renderer::FontId m_fontLarge = Renderer::INVALID_FONT_ID; + Renderer::FontId m_fontMedium = Renderer::INVALID_FONT_ID; + Renderer::FontId m_fontSmall = Renderer::INVALID_FONT_ID; + + Renderer::TextureId m_playerBlueTexture = Renderer::INVALID_TEXTURE_ID; + Renderer::TextureId m_playerGreenTexture = Renderer::INVALID_TEXTURE_ID; + Renderer::TextureId m_nave2BlueTexture = Renderer::INVALID_TEXTURE_ID; + Renderer::TextureId m_nave2Texture = Renderer::INVALID_TEXTURE_ID; + Renderer::TextureId m_bgTexture = Renderer::INVALID_TEXTURE_ID; + + RType::ECS::Entity m_titleEntity = RType::ECS::NULL_ENTITY; + RType::ECS::Entity m_instructionsEntity = RType::ECS::NULL_ENTITY; + RType::ECS::Entity m_bgEntity = RType::ECS::NULL_ENTITY; + RType::ECS::Entity m_errorEntity = RType::ECS::NULL_ENTITY; + RType::ECS::Entity m_countdownEntity = RType::ECS::NULL_ENTITY; + + std::unordered_map m_playerEntities; + std::unordered_map m_playerSprites; + + std::string m_lastStatus; + std::string m_errorMessage; + float m_errorTimer = 0.0f; + uint8_t m_countdownSeconds = 0; + bool m_rKeyPressed = false; + bool m_escapeKeyPressed = false; + bool m_hasError = false; + }; + + } +} diff --git a/games/rtype/client/include/MenuState.hpp b/games/rtype/client/include/MenuState.hpp new file mode 100644 index 0000000..6af39f7 --- /dev/null +++ b/games/rtype/client/include/MenuState.hpp @@ -0,0 +1,81 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** MenuState +*/ + +#pragma once + +#include "GameStateMachine.hpp" +#include "ECS/Registry.hpp" +#include "ECS/RenderingSystem.hpp" +#include "ECS/TextRenderingSystem.hpp" +#include "ECS/AudioSystem.hpp" +#include "Renderer/IRenderer.hpp" +#include "Audio/IAudio.hpp" +#include +#include + +namespace RType { + namespace Client { + + class MenuState : public IState { + public: + MenuState(GameStateMachine& machine, GameContext& context); + ~MenuState() override = default; + + void Init() override; + void Cleanup() override; + void HandleInput() override; + void Update(float dt) override; + void Draw() override; + private: + void createUI(); + void updateAnimations(float dt); + void updateMenuSelection(); + + enum class MenuItem { + PLAY = 0, + EDITOR = 1, + SETTINGS = 2, + QUIT = 3, + COUNT = 4 + }; + + GameStateMachine& m_machine; + GameContext& m_context; + + RType::ECS::Registry m_registry; + std::shared_ptr m_renderer; + std::unique_ptr m_renderingSystem; + std::unique_ptr m_textSystem; + std::unique_ptr m_audioSystem; + + Audio::MusicId m_menuMusic = Audio::INVALID_MUSIC_ID; + bool m_menuMusicPlaying = false; + Audio::MusicId m_selectMusic = Audio::INVALID_MUSIC_ID; + + Renderer::FontId m_fontLarge = Renderer::INVALID_FONT_ID; + Renderer::FontId m_fontMedium = Renderer::INVALID_FONT_ID; + Renderer::FontId m_fontSmall = Renderer::INVALID_FONT_ID; + Renderer::TextureId m_bgTexture = Renderer::INVALID_TEXTURE_ID; + + std::vector m_entities; + RType::ECS::Entity m_titleEntity = RType::ECS::NULL_ENTITY; + RType::ECS::Entity m_subtitleEntity = RType::ECS::NULL_ENTITY; + std::vector m_menuItems; + + int m_selectedIndex = 0; + bool m_upKeyPressed = false; + bool m_downKeyPressed = false; + bool m_enterKeyPressed = false; + bool m_escKeyPressed = false; + int m_ignoreInputFrames = 1; + + float m_animTime = 0.0f; + float m_titlePulse = 0.0f; + }; + + } +} diff --git a/games/rtype/client/include/ResultsState.hpp b/games/rtype/client/include/ResultsState.hpp new file mode 100644 index 0000000..8df39c0 --- /dev/null +++ b/games/rtype/client/include/ResultsState.hpp @@ -0,0 +1,77 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** ResultsState +*/ + +#pragma once + +#include "GameStateMachine.hpp" +#include "ECS/Registry.hpp" +#include "ECS/RenderingSystem.hpp" +#include "ECS/TextRenderingSystem.hpp" +#include "Renderer/IRenderer.hpp" + +#include +#include +#include +#include + +namespace RType { + namespace Client { + + class ResultsState : public IState { + public: + ResultsState(GameStateMachine& machine, + GameContext& context, + std::vector> scores); + ~ResultsState() override = default; + + void Init() override; + void Cleanup() override; + void HandleInput() override; + void Update(float dt) override; + void Draw() override; + + private: + void createUI(); + + private: + GameStateMachine& m_machine; + GameContext& m_context; + std::vector> m_scores; + + RType::ECS::Registry m_registry; + std::shared_ptr m_renderer; + std::unique_ptr m_renderingSystem; + std::unique_ptr m_textSystem; + + Renderer::FontId m_fontLarge = Renderer::INVALID_FONT_ID; + Renderer::FontId m_fontMedium = Renderer::INVALID_FONT_ID; + Renderer::FontId m_fontSmall = Renderer::INVALID_FONT_ID; + + Renderer::TextureId m_bgTexture = Renderer::INVALID_TEXTURE_ID; + Renderer::SpriteId m_bgSprite = Renderer::INVALID_SPRITE_ID; + Renderer::Vector2 m_bgTextureSize{0.0f, 0.0f}; + + RType::ECS::Entity m_bgEntity = RType::ECS::NULL_ENTITY; + + bool m_drawScorePanel = false; + Renderer::Rectangle m_scorePanelRect{}; + float m_scorePanelHeaderHeight = 44.0f; + float m_scorePanelRowStartY = 0.0f; + float m_scorePanelRowStepY = 36.0f; + size_t m_scorePanelRowCount = 0; + float m_colRankX = 0.0f; + float m_colNameX = 0.0f; + float m_colScoreX = 0.0f; + bool m_escapePressed = false; + + Audio::MusicId m_endMusic = Audio::INVALID_MUSIC_ID; + }; + + } +} + + diff --git a/games/rtype/client/include/RoomListState.hpp b/games/rtype/client/include/RoomListState.hpp new file mode 100644 index 0000000..28926d0 --- /dev/null +++ b/games/rtype/client/include/RoomListState.hpp @@ -0,0 +1,88 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** RoomListState - Room selection screen +*/ + +#pragma once + +#include "GameStateMachine.hpp" +#include "RoomClient.hpp" +#include "ECS/Component.hpp" +#include "ECS/Registry.hpp" +#include "ECS/RenderingSystem.hpp" +#include "ECS/TextRenderingSystem.hpp" +#include "Renderer/IRenderer.hpp" + +#include +#include +#include +#include + +namespace RType { + namespace Client { + + class RoomListState : public IState { + public: + RoomListState(GameStateMachine& machine, GameContext& context); + ~RoomListState() override = default; + + void Init() override; + void Cleanup() override; + void HandleInput() override; + void Update(float dt) override; + void Draw() override; + + private: + void createUI(); + void updateRoomList(); + void createRoomEntities(); + void clearRoomEntities(); + void handleCreateRoom(); + void handleJoinRoom(); + + GameStateMachine& m_machine; + GameContext& m_context; + + std::unique_ptr m_roomClient; + + RType::ECS::Registry m_registry; + std::shared_ptr m_renderer; + std::unique_ptr m_renderingSystem; + std::unique_ptr m_textSystem; + + Renderer::FontId m_fontLarge = Renderer::INVALID_FONT_ID; + Renderer::FontId m_fontMedium = Renderer::INVALID_FONT_ID; + Renderer::FontId m_fontSmall = Renderer::INVALID_FONT_ID; + Renderer::TextureId m_bgTexture = Renderer::INVALID_TEXTURE_ID; + + RType::ECS::Entity m_titleEntity = RType::ECS::NULL_ENTITY; + RType::ECS::Entity m_instructionsEntity = RType::ECS::NULL_ENTITY; + RType::ECS::Entity m_bgEntity = RType::ECS::NULL_ENTITY; + RType::ECS::Entity m_noRoomsEntity = RType::ECS::NULL_ENTITY; + RType::ECS::Entity m_errorEntity = RType::ECS::NULL_ENTITY; + + std::vector m_roomEntities; + std::vector m_rooms; + + int m_selectedIndex = 0; + bool m_upKeyPressed = false; + bool m_downKeyPressed = false; + bool m_enterKeyPressed = false; + bool m_cKeyPressed = false; + bool m_escapeKeyPressed = false; + + bool m_hasError = false; + std::string m_errorMessage; + float m_errorTimer = 0.0f; + + float m_refreshTimer = 0.0f; + static constexpr float REFRESH_INTERVAL = 2.0f; + + bool m_creatingRoom = false; + std::string m_newRoomName; + }; + + } +} diff --git a/games/rtype/client/include/SettingsState.hpp b/games/rtype/client/include/SettingsState.hpp new file mode 100644 index 0000000..7245c4c --- /dev/null +++ b/games/rtype/client/include/SettingsState.hpp @@ -0,0 +1,185 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** SettingsState +*/ + +#pragma once + +#include "GameStateMachine.hpp" +#include "ECS/Registry.hpp" +#include "ECS/RenderingSystem.hpp" +#include "ECS/TextRenderingSystem.hpp" +#include "ECS/AudioSystem.hpp" +#include "Renderer/IRenderer.hpp" +#include "Audio/IAudio.hpp" +#include "Core/ColorFilter.hpp" +#include +#include +#include +#include + +namespace RType { + namespace Client { + + class SettingsState : public IState { + public: + SettingsState(GameStateMachine& machine, GameContext& context); + ~SettingsState() override = default; + + void Init() override; + void Cleanup() override; + void HandleInput() override; + void Update(float dt) override; + void Draw() override; + + static bool IsColourBlindModeEnabled(); + static void SetColourBlindMode(bool enabled); + static void LoadSettingsFromFile(); + static std::uint32_t GetScreenWidth(); + static std::uint32_t GetScreenHeight(); + static bool GetFullscreen(); + static std::uint32_t GetTargetFramerate(); + static float GetMasterVolume(); + static bool IsMuted(); + + private: + void createUI(); + void createScreenUI(); + void createAudioUI(); + void createCommandsUI(); + void updateAnimations(float dt); + void updateMenuSelection(); + void updateScreenMenuSelection(); + void updateAudioMenuSelection(); + void updateCommandsMenuSelection(); + void toggleColourBlindMode(); + void enterSubMenu(bool screen); + void enterCommandsMenu(); + void exitSubMenu(); + void changeResolution(int direction); + void toggleFullscreen(); + void changeFrameRate(int direction); + void changeMasterVolume(int direction); + void toggleMute(); + void startRebind(int actionIndex); + void processRebind(); + void applyScreenSettings(); + void saveSettings(); + void loadSettings(); + void clearMenuUI(); + static std::string keyToString(Renderer::Key key); + static Renderer::Key stringToKey(const std::string& str); + + enum class SettingsItem { + COLOUR_BLIND = 0, + SCREEN = 1, + AUDIO = 2, + COMMANDS = 3, + BACK = 4, + COUNT = 5 + }; + + enum class ScreenItem { + RESOLUTION = 0, + FULLSCREEN = 1, + FRAME_RATE = 2, + BACK = 3, + COUNT = 4 + }; + + enum class AudioItem { + MASTER_VOLUME = 0, + MUTE = 1, + BACK = 2, + COUNT = 3 + }; + + enum class CommandsItem { + MOVE_UP = 0, + MOVE_DOWN = 1, + MOVE_LEFT = 2, + MOVE_RIGHT = 3, + SHOOT = 4, + BACK = 5, + COUNT = 6 + }; + + struct Resolution { + std::uint32_t width; + std::uint32_t height; + std::string name; + }; + + GameStateMachine& m_machine; + GameContext& m_context; + + RType::ECS::Registry m_registry; + std::shared_ptr m_renderer; + std::unique_ptr m_renderingSystem; + std::unique_ptr m_textSystem; + std::unique_ptr m_audioSystem; + + Audio::MusicId m_selectMusic = Audio::INVALID_MUSIC_ID; + + Renderer::FontId m_fontLarge = Renderer::INVALID_FONT_ID; + Renderer::FontId m_fontMedium = Renderer::INVALID_FONT_ID; + Renderer::FontId m_fontSmall = Renderer::INVALID_FONT_ID; + Renderer::TextureId m_bgTexture = Renderer::INVALID_TEXTURE_ID; + + std::vector m_entities; + RType::ECS::Entity m_titleEntity = RType::ECS::NULL_ENTITY; + std::vector m_menuItems; + std::vector m_screenMenuItems; + std::vector m_audioMenuItems; + std::vector m_commandsMenuItems; + + int m_selectedIndex = 0; + int m_screenSelectedIndex = 0; + int m_audioSelectedIndex = 0; + int m_commandsSelectedIndex = 0; + bool m_upKeyPressed = false; + bool m_downKeyPressed = false; + bool m_leftKeyPressed = false; + bool m_rightKeyPressed = false; + bool m_enterKeyPressed = false; + bool m_escKeyPressed = false; + + float m_animTime = 0.0f; + + bool m_colourBlindMode = false; + bool m_inScreenMenu = false; + bool m_inAudioMenu = false; + bool m_inCommandsMenu = false; + bool m_waitingForRebind = false; + int m_rebindingActionIndex = -1; + + std::uint32_t m_screenWidth = 1280; + std::uint32_t m_screenHeight = 720; + bool m_fullscreen = false; + std::uint32_t m_targetFramerate = 60; + int m_resolutionIndex = 0; + int m_framerateIndex = 2; + + static const std::vector RESOLUTIONS; + static const std::vector FRAMERATES; + static const std::vector FRAMERATE_NAMES; + + float m_masterVolume = 1.0f; + float m_volumeBeforeMute = 1.0f; + bool m_muted = false; + + static bool s_colourBlindMode; + static std::uint32_t s_screenWidth; + static std::uint32_t s_screenHeight; + static bool s_fullscreen; + static std::uint32_t s_targetFramerate; + static float s_masterVolume; + static bool s_muted; + static const std::string SETTINGS_FILE_PATH; + }; + + } +} + diff --git a/games/rtype/client/include/editor/EditorAssetLibrary.hpp b/games/rtype/client/include/editor/EditorAssetLibrary.hpp new file mode 100644 index 0000000..8397b97 --- /dev/null +++ b/games/rtype/client/include/editor/EditorAssetLibrary.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include "editor/EditorTypes.hpp" +#include "Renderer/IRenderer.hpp" +#include "Math/Types.hpp" +#include +#include +#include + +namespace RType { + namespace Client { + + struct EditorAssetDefinition { + std::string id; + std::string displayName; + EditorEntityType type = EditorEntityType::OBSTACLE; + std::string texturePath; + Math::Vector2 defaultSize{0.0f, 0.0f}; + float defaultScrollSpeed = -50.0f; + int defaultLayer = 1; + std::string enemyType; + std::string powerUpType; + }; + + struct EditorAssetResource { + EditorAssetDefinition definition; + Renderer::TextureId textureId = Renderer::INVALID_TEXTURE_ID; + Renderer::SpriteId spriteId = Renderer::INVALID_SPRITE_ID; + Math::Vector2 textureSize{128.0f, 128.0f}; + }; + + class EditorAssetLibrary { + public: + explicit EditorAssetLibrary(Renderer::IRenderer* renderer); + + bool Initialize(); + + const EditorAssetResource* GetResource(const std::string& id) const; + const std::vector& GetResources(EditorEntityType type) const; + + private: + Renderer::IRenderer* m_renderer; + std::unordered_map m_resourcesById; + std::unordered_map> m_resourcesByType; + + std::vector buildDefaultDefinitions() const; + bool loadResource(EditorAssetResource& resource); + }; + + } +} + diff --git a/games/rtype/client/include/editor/EditorCanvasManager.hpp b/games/rtype/client/include/editor/EditorCanvasManager.hpp new file mode 100644 index 0000000..59815df --- /dev/null +++ b/games/rtype/client/include/editor/EditorCanvasManager.hpp @@ -0,0 +1,43 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** EditorCanvasManager +*/ + +#pragma once + +#include "EditorTypes.hpp" +#include "Renderer/IRenderer.hpp" +#include "Math/Types.hpp" +#include + +namespace RType { + namespace Client { + + class EditorCanvasManager { + public: + explicit EditorCanvasManager(Renderer::IRenderer* renderer); + ~EditorCanvasManager() = default; + + void HandleCameraInput(); + void ApplyCamera(); + void DrawGrid(); + + Math::Vector2 ScreenToWorld(Math::Vector2 screenPos) const; + Math::Vector2 WorldToScreen(Math::Vector2 worldPos) const; + + const EditorCamera& GetCamera() const { return m_camera; } + const EditorGrid& GetGrid() const { return m_grid; } + + void ToggleGridSnap() { m_grid.snapToGrid = !m_grid.snapToGrid; } + float SnapToGrid(float value) const; + + private: + Renderer::IRenderer* m_renderer; + EditorCamera m_camera; + EditorGrid m_grid; + }; + + } +} diff --git a/games/rtype/client/include/editor/EditorColliderManager.hpp b/games/rtype/client/include/editor/EditorColliderManager.hpp new file mode 100644 index 0000000..11fb700 --- /dev/null +++ b/games/rtype/client/include/editor/EditorColliderManager.hpp @@ -0,0 +1,47 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** EditorColliderManager +*/ + +#pragma once + +#include "editor/EditorTypes.hpp" +#include "ECS/LevelLoader.hpp" +#include "Renderer/IRenderer.hpp" +#include "Math/Types.hpp" +#include + +namespace RType { + namespace Client { + + class EditorColliderManager { + public: + explicit EditorColliderManager(Renderer::IRenderer* renderer); + ~EditorColliderManager() = default; + + void DrawColliders(const EditorEntityData* entity, int selectedIndex = -1) const; + void DrawHandles(const EditorEntityData* entity, int colliderIndex) const; + ColliderHandle GetHandleAt(const EditorEntityData* entity, + const Math::Vector2& worldPos) const; + int AddCollider(EditorEntityData& entity, const Math::Vector2& worldPos); + bool RemoveCollider(EditorEntityData& entity, int colliderIndex); + void ResizeCollider(EditorEntityData& entity, int colliderIndex, + ColliderHandle::Type handleType, + const Math::Vector2& worldPos); + + void SetDragStart(const Math::Vector2& pos) { m_dragStart = pos; } + const Math::Vector2& GetDragStart() const { return m_dragStart; } + + private: + Renderer::IRenderer* m_renderer; + Math::Vector2 m_dragStart{0.0f, 0.0f}; + + void drawCollider(const ECS::ColliderDef& collider, const Math::Color& color) const; + void drawHandle(const Math::Vector2& pos) const; + Math::Rectangle getColliderRect(const ECS::ColliderDef& collider) const; + }; + + } +} diff --git a/games/rtype/client/include/editor/EditorConstants.hpp b/games/rtype/client/include/editor/EditorConstants.hpp new file mode 100644 index 0000000..69036b6 --- /dev/null +++ b/games/rtype/client/include/editor/EditorConstants.hpp @@ -0,0 +1,102 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** EditorConstants +*/ + +#pragma once + +#include "Math/Types.hpp" + +namespace RType { + namespace Client { + namespace EditorConstants { + + namespace UI { + constexpr float PALETTE_PANEL_LEFT = 10.0f; + constexpr float PALETTE_PANEL_WIDTH = 220.0f; + constexpr float PALETTE_BUTTON_HEIGHT = 22.0f; + constexpr float PALETTE_START_Y = 80.0f; + constexpr float TOOLBAR_START_Y = 45.0f; + constexpr float TOOLBAR_BUTTON_HEIGHT = 24.0f; + constexpr float TOOLBAR_BUTTON_GAP = 8.0f; + + constexpr float PROPERTY_PANEL_X = 920.0f; + constexpr float PROPERTY_PANEL_WIDTH = 340.0f; + constexpr float PROPERTY_PANEL_START_Y = 90.0f; + constexpr float PROPERTY_ROW_HEIGHT = 28.0f; + constexpr float PROPERTY_VALUE_OFFSET_X = 100.0f; + constexpr float TOOLBAR_RIGHT_X = PROPERTY_PANEL_X; + constexpr float TOOLBAR_RIGHT_WIDTH = PROPERTY_PANEL_WIDTH; + + constexpr float STATUS_BAR_X = 10.0f; + constexpr float STATUS_BAR_Y = 10.0f; + constexpr float STATUS_BAR_LINE_HEIGHT = 14.0f; + } + + namespace Colors { + inline const Math::Color BACKGROUND{0.1f, 0.1f, 0.15f, 1.0f}; + inline const Math::Color GRID{0.3f, 0.3f, 0.3f, 0.5f}; + inline const Math::Color SELECTION_OUTLINE{1.0f, 1.0f, 0.0f, 1.0f}; + constexpr float PREVIEW_ALPHA = 0.35f; + + inline const Math::Color UI_HEADER{0.4f, 0.86f, 0.9f, 1.0f}; + inline const Math::Color UI_TEXT{0.75f, 0.75f, 0.8f, 1.0f}; + inline const Math::Color UI_HINT{0.5f, 0.86f, 1.0f, 0.7f}; + inline const Math::Color UI_ACTIVE{1.0f, 0.7f, 0.0f, 1.0f}; + inline const Math::Color UI_HOVER{0.9f, 0.9f, 0.95f, 1.0f}; + } + + namespace Camera { + constexpr float PAN_SPEED = 500.0f; + constexpr float ZOOM_SPEED = 0.1f; + constexpr float INITIAL_X = 640.0f; + constexpr float INITIAL_Y = 360.0f; + constexpr float INITIAL_ZOOM = 1.0f; + + constexpr float MIN_X = -1000.0f; + constexpr float MAX_X = 15000.0f; + constexpr float MIN_Y = -500.0f; + constexpr float MAX_Y = 1200.0f; + constexpr float MIN_ZOOM = 0.25f; + constexpr float MAX_ZOOM = 2.0f; + } + + namespace Grid { + constexpr float CELL_SIZE = 50.0f; + constexpr float LINE_THICKNESS = 1.0f; + } + + namespace PropertySteps { + constexpr float POSITION_STEP = 25.0f; + constexpr float SCALE_STEP = 10.0f; + constexpr float LAYER_STEP = 1.0f; + constexpr float SCROLL_SPEED_STEP = 5.0f; + constexpr float MIN_SCALE = 10.0f; + } + + namespace Collider { + constexpr float HANDLE_SIZE = 8.0f; + constexpr float MIN_COLLIDER_SIZE = 10.0f; + constexpr float COLLIDER_LINE_THICKNESS = 2.0f; + constexpr float SNAP_DISTANCE = 5.0f; + + inline const Math::Color COLLIDER_NORMAL{0.0f, 1.0f, 0.0f, 0.6f}; + inline const Math::Color COLLIDER_SELECTED{1.0f, 0.5f, 0.0f, 0.8f}; + inline const Math::Color COLLIDER_HANDLE{1.0f, 1.0f, 0.0f, 1.0f}; + } + + namespace Input { + constexpr size_t MAX_INPUT_BUFFER_SIZE = 8; + constexpr int NUMBER_KEY_COUNT = 10; + } + + namespace Selection { + constexpr float OUTLINE_THICKNESS = 3.0f; + inline const Math::Color OUTLINE_COLOR{1.0f, 0.85f, 0.35f, 1.0f}; + } + + } + } +} diff --git a/games/rtype/client/include/editor/EditorDrawing.hpp b/games/rtype/client/include/editor/EditorDrawing.hpp new file mode 100644 index 0000000..4645baa --- /dev/null +++ b/games/rtype/client/include/editor/EditorDrawing.hpp @@ -0,0 +1,35 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** EditorDrawing +*/ + +#pragma once + +#include "Math/Types.hpp" +#include "Renderer/IRenderer.hpp" +#include "ECS/LevelLoader.hpp" + +namespace RType { + namespace Client { + namespace EditorDrawing { + + void DrawRectangleOutline(Renderer::IRenderer* renderer, + const Math::Rectangle& rect, + const Math::Color& color, + float thickness = 3.0f); + + void DrawCollider(Renderer::IRenderer* renderer, + const ECS::ColliderDef& collider, + const Math::Color& color, + float lineThickness = 2.0f); + + void DrawHandle(Renderer::IRenderer* renderer, + const Math::Vector2& position, + const Math::Color& color, + float size = 8.0f); + + } + } +} diff --git a/games/rtype/client/include/editor/EditorEntityManager.hpp b/games/rtype/client/include/editor/EditorEntityManager.hpp new file mode 100644 index 0000000..f72118f --- /dev/null +++ b/games/rtype/client/include/editor/EditorEntityManager.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include "editor/EditorTypes.hpp" +#include "editor/EditorAssetLibrary.hpp" +#include "ECS/Registry.hpp" +#include "Renderer/IRenderer.hpp" +#include +#include + +namespace RType { + namespace Client { + + class EditorColliderManager; + + class EditorEntityManager { + public: + EditorEntityManager(Renderer::IRenderer* renderer, RType::ECS::Registry& registry, EditorAssetLibrary& assets); + ~EditorEntityManager(); + + EditorEntityData* PlaceEntity(const EditorPaletteSelection& selection, const Math::Vector2& worldPos); + void DrawPlacementPreview(EditorMode mode, + EditorEntityType type, + const std::string& identifier, + const Math::Vector2& worldPos) const; + void DrawSelectionOutline() const; + void DrawColliders(int selectedColliderIndex = -1) const; + void DrawColliderHandles(int colliderIndex) const; + + bool SelectAt(const Math::Vector2& worldPos); + void ClearSelection(); + bool DeleteSelected(); + + ColliderHandle GetColliderHandleAt(const Math::Vector2& worldPos) const; + void AddCollider(const Math::Vector2& worldPos); + bool RemoveCollider(int colliderIndex); + void ResizeCollider(int colliderIndex, ColliderHandle::Type handleType, const Math::Vector2& worldPos); + int GetSelectedColliderIndex() const { return m_selectedColliderIndex; } + void SetSelectedCollider(int index) { m_selectedColliderIndex = index; } + + const std::vector& GetEntities() const { return m_entities; } + EditorEntityData* GetSelectedEntity(); + const EditorEntityData* GetSelectedEntity() const; + + void SyncEntity(EditorEntityData& data); + void RebuildDefaultCollider(EditorEntityData& data); + + private: + Math::Vector2 getDefaultSize(EditorEntityType type) const; + Math::Color getColor(EditorEntityType type) const; + const EditorAssetResource* getResource(const std::string& id) const; + void applyEntityToComponents(EditorEntityData& data); + void destroyEntity(EditorEntityData& data); + void drawOutline(const EditorEntityData& entity) const; + void initializeDefaultCollider(EditorEntityData& data) const; + void createColliderEntities(EditorEntityData& data); + + Renderer::IRenderer* m_renderer; + RType::ECS::Registry& m_registry; + EditorAssetLibrary& m_assets; + std::unique_ptr m_colliderManager; + std::vector m_entities; + int m_selectedIndex = -1; + int m_selectedColliderIndex = -1; + }; + + } +} diff --git a/games/rtype/client/include/editor/EditorFileManager.hpp b/games/rtype/client/include/editor/EditorFileManager.hpp new file mode 100644 index 0000000..e0140fa --- /dev/null +++ b/games/rtype/client/include/editor/EditorFileManager.hpp @@ -0,0 +1,57 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** EditorFileManager - Save/Load level files +*/ + +#pragma once + +#include "editor/EditorTypes.hpp" +#include "editor/EditorAssetLibrary.hpp" +#include "ECS/LevelLoader.hpp" +#include "ECS/Registry.hpp" +#include "Renderer/IRenderer.hpp" +#include +#include + +namespace RType { + namespace Client { + + class EditorFileManager { + public: + explicit EditorFileManager(EditorAssetLibrary& assets); + + bool SaveLevel(const std::string& path, + const std::vector& entities, + const std::string& levelName); + + std::vector LoadLevel(const std::string& path, + ECS::Registry& registry, + Renderer::IRenderer* renderer); + + const std::string& GetLastError() const { return m_lastError; } + + private: + ECS::LevelData GatherLevelData(const std::vector& entities, + const std::string& levelName); + + EditorEntityData ConvertObstacleToEditor(const ECS::ObstacleDef& obs, + ECS::Registry& registry, + Renderer::IRenderer* renderer); + EditorEntityData ConvertEnemyToEditor(const ECS::EnemyDef& enemy, + ECS::Registry& registry, + Renderer::IRenderer* renderer); + EditorEntityData ConvertSpawnToEditor(const ECS::PlayerSpawnDef& spawn, + ECS::Registry& registry, + Renderer::IRenderer* renderer); + EditorEntityData ConvertBackgroundToEditor(const ECS::BackgroundDef& bg, + ECS::Registry& registry, + Renderer::IRenderer* renderer); + + EditorAssetLibrary& m_assets; + std::string m_lastError; + }; + + } +} diff --git a/games/rtype/client/include/editor/EditorGeometry.hpp b/games/rtype/client/include/editor/EditorGeometry.hpp new file mode 100644 index 0000000..21f6d1a --- /dev/null +++ b/games/rtype/client/include/editor/EditorGeometry.hpp @@ -0,0 +1,52 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** EditorGeometry +*/ + +#pragma once + +#include "Math/Types.hpp" + +namespace RType { + namespace Client { + namespace EditorGeometry { + + inline Math::Rectangle BuildRect(float x, float y, float width, float height) { + Math::Rectangle rect; + rect.position = {x, y}; + rect.size = {width, height}; + return rect; + } + + inline bool PointInRect(const Math::Vector2& point, const Math::Rectangle& rect) { + return point.x >= rect.position.x && + point.x <= rect.position.x + rect.size.x && + point.y >= rect.position.y && + point.y <= rect.position.y + rect.size.y; + } + + inline Math::Rectangle GetEntityBounds(float centerX, float centerY, float width, float height) { + return BuildRect(centerX - width / 2.0f, centerY - height / 2.0f, width, height); + } + + inline Math::Rectangle GetHandleBounds(float x, float y, float handleSize) { + return BuildRect(x - handleSize / 2.0f, y - handleSize / 2.0f, handleSize, handleSize); + } + + inline bool PointInEntityBounds(const Math::Vector2& point, + float centerX, float centerY, + float width, float height) { + float left = centerX - width / 2.0f; + float right = centerX + width / 2.0f; + float top = centerY - height / 2.0f; + float bottom = centerY + height / 2.0f; + + return point.x >= left && point.x <= right && + point.y >= top && point.y <= bottom; + } + + } + } +} diff --git a/games/rtype/client/include/editor/EditorInputHandler.hpp b/games/rtype/client/include/editor/EditorInputHandler.hpp new file mode 100644 index 0000000..bd578e5 --- /dev/null +++ b/games/rtype/client/include/editor/EditorInputHandler.hpp @@ -0,0 +1,31 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** EditorInputHandler +*/ + +#pragma once + +#include "Renderer/IRenderer.hpp" +#include +#include + +namespace RType { + namespace Client { + + class EditorInputHandler { + public: + explicit EditorInputHandler(Renderer::IRenderer* renderer); + ~EditorInputHandler() = default; + + void HandleKeyPress(Renderer::Key key, const std::function& action); + void Update(); + + private: + Renderer::IRenderer* m_renderer; + std::unordered_map m_keyStates; + }; + + } +} diff --git a/games/rtype/client/include/editor/EditorPropertyManager.hpp b/games/rtype/client/include/editor/EditorPropertyManager.hpp new file mode 100644 index 0000000..170b8fa --- /dev/null +++ b/games/rtype/client/include/editor/EditorPropertyManager.hpp @@ -0,0 +1,53 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** EditorPropertyManager +*/ + +#pragma once + +#include "editor/EditorTypes.hpp" +#include "Renderer/IRenderer.hpp" +#include +#include + +namespace RType { + namespace Client { + + class EditorPropertyManager { + public: + explicit EditorPropertyManager(Renderer::IRenderer* renderer); + ~EditorPropertyManager() = default; + + bool HandleInput(EditorEntityData* selectedEntity, + std::function)> handleKeyPress); + + EditableProperty GetActiveProperty() const { return m_activeProperty; } + const std::string& GetInputBuffer() const { return m_inputBuffer; } + void ClearInput(); + void SetOnPropertyChanged(std::function callback); + void SetOnEntityDeleted(std::function callback); + void SetOnPropertyCycled(std::function callback); + + private: + Renderer::IRenderer* m_renderer; + + EditableProperty m_activeProperty = EditableProperty::POSITION_X; + std::string m_inputBuffer; + + std::function m_onPropertyChanged; + std::function m_onEntityDeleted; + std::function m_onPropertyCycled; + + void cycleProperty(); + void applyPropertyDelta(EditorEntityData& entity, float delta); + void setPropertyValue(EditorEntityData& entity, float value); + float getPropertyValue(const EditorEntityData& entity) const; + float getPropertyStep() const; + void handleNumberInput(Renderer::Key key); + void handleBackspace(EditorEntityData* entity); + }; + + } +} diff --git a/games/rtype/client/include/editor/EditorTypes.hpp b/games/rtype/client/include/editor/EditorTypes.hpp new file mode 100644 index 0000000..8b18fd4 --- /dev/null +++ b/games/rtype/client/include/editor/EditorTypes.hpp @@ -0,0 +1,116 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** EditorTypes +*/ + +#pragma once + +#include "ECS/Entity.hpp" +#include "ECS/LevelLoader.hpp" +#include "Math/Types.hpp" +#include +#include + +namespace RType { + namespace Client { + + enum class EditorEntityType { + BACKGROUND, + OBSTACLE, + ENEMY, + POWERUP, + PLAYER_SPAWN + }; + + enum class EditorMode { + SELECT, + PLACE_ENEMY, + PLACE_OBSTACLE, + PLACE_POWERUP, + PLACE_PLAYER_SPAWN, + PLACE_BACKGROUND, + EDIT_COLLIDERS + }; + + enum class EditableProperty { + POSITION_X, + POSITION_Y, + SCALE_WIDTH, + SCALE_HEIGHT, + LAYER, + SCROLL_SPEED, + COLLIDER_X, + COLLIDER_Y, + COLLIDER_WIDTH, + COLLIDER_HEIGHT, + COUNT + }; + + enum class ColliderEditMode { + NONE, + ADD, + REMOVE, + RESIZE, + MOVE + }; + + struct ColliderHandle { + int colliderIndex = -1; + enum class Type { NONE, BODY, TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT } type = Type::NONE; + }; + + struct EditorEntityData { + ECS::Entity entity = ECS::NULL_ENTITY; + EditorEntityType type = EditorEntityType::OBSTACLE; + std::vector colliderEntities; + std::string presetId; + + // Common properties + float x = 0.0f; + float y = 0.0f; + float scaleWidth = 100.0f; + float scaleHeight = 100.0f; + int layer = 1; + float scrollSpeed = -50.0f; + + // Type-specific data + std::string textureKey; + std::string enemyType; + std::string powerUpType; + std::vector colliders; + + // Selection state + bool isSelected = false; + }; + + struct EditorCamera { + float x = 640.0f; + float y = 360.0f; + float zoom = 1.0f; + + float minX = -1000.0f; + float maxX = 15000.0f; + float minY = -500.0f; + float maxY = 1200.0f; + + float minZoom = 0.25f; + float maxZoom = 2.0f; + }; + + struct EditorGrid { + bool enabled = true; + float cellSize = 50.0f; + bool snapToGrid = false; + Math::Color gridColor{0.3f, 0.3f, 0.3f, 0.5f}; + }; + + struct EditorPaletteSelection { + EditorMode mode = EditorMode::SELECT; + EditorEntityType entityType = EditorEntityType::OBSTACLE; + std::string subtype; + }; + + } +} diff --git a/games/rtype/client/include/editor/EditorUIManager.hpp b/games/rtype/client/include/editor/EditorUIManager.hpp new file mode 100644 index 0000000..e422f29 --- /dev/null +++ b/games/rtype/client/include/editor/EditorUIManager.hpp @@ -0,0 +1,96 @@ +#pragma once + +#include "editor/EditorTypes.hpp" +#include "editor/EditorAssetLibrary.hpp" +#include "ECS/Component.hpp" +#include "ECS/Registry.hpp" +#include "Renderer/IRenderer.hpp" +#include "Math/Types.hpp" +#include +#include +#include +#include + +namespace RType { + namespace Client { + + class EditorUIManager { + public: + EditorUIManager(Renderer::IRenderer* renderer, + EditorAssetLibrary& assets, + ECS::Registry& registry, + std::vector& trackedEntities, + Renderer::FontId fontSmall, + Renderer::FontId fontMedium); + + void InitializePalette(); + void InitializePropertiesPanel(); + void InitializeColliderPanel(); + void UpdateHover(Math::Vector2 mouseScreen); + bool HandleActionClick(Math::Vector2 mouseScreen); + std::optional HandleClick(Math::Vector2 mouseScreen); + void SetActiveSelection(const EditorPaletteSelection& selection); + const EditorPaletteSelection& GetActiveSelection() const { return m_activeSelection; } + void UpdatePropertyPanel(const EditorEntityData* selected, + EditableProperty activeProperty, + const std::string& inputBuffer); + void UpdateColliderPanel(const EditorEntityData* selected, int selectedColliderIndex); + void SetOnSaveRequested(const std::function& callback); + + private: + struct PaletteEntry { + std::string label; + EditorMode mode = EditorMode::SELECT; + EditorEntityType entityType = EditorEntityType::OBSTACLE; + std::string subtype; + Math::Rectangle bounds; + ECS::Entity textEntity = ECS::NULL_ENTITY; + bool hovered = false; + const EditorAssetResource* resource = nullptr; + }; + + struct PropertyField { + EditableProperty property = EditableProperty::POSITION_X; + ECS::Entity nameEntity = ECS::NULL_ENTITY; + ECS::Entity valueEntity = ECS::NULL_ENTITY; + }; + + struct ActionButton { + std::string label; + Math::Rectangle bounds; + ECS::Entity textEntity = ECS::NULL_ENTITY; + bool hovered = false; + std::function onClick; + }; + + void createCategoryLabel(const std::string& label, float y); + void createPaletteButton(PaletteEntry entry, float y); + void InitializeToolbar(); + void refreshPaletteVisuals(); + void refreshActionButtons(); + + Renderer::IRenderer* m_renderer; + EditorAssetLibrary& m_assets; + ECS::Registry& m_registry; + std::vector& m_trackedEntities; + Renderer::FontId m_fontSmall; + Renderer::FontId m_fontMedium; + + std::vector m_entries; + std::vector m_actionButtons; + EditorPaletteSelection m_activeSelection; + std::vector m_propertyFields; + ECS::Entity m_propertiesHeader = ECS::NULL_ENTITY; + ECS::Entity m_selectedInfoEntity = ECS::NULL_ENTITY; + ECS::Entity m_propertyHintEntity = ECS::NULL_ENTITY; + + ECS::Entity m_colliderPanelHeader = ECS::NULL_ENTITY; + ECS::Entity m_colliderCountEntity = ECS::NULL_ENTITY; + ECS::Entity m_colliderHintEntity = ECS::NULL_ENTITY; + + std::function m_onSaveRequested; + }; + + } +} + diff --git a/games/rtype/client/main.cpp b/games/rtype/client/main.cpp new file mode 100644 index 0000000..85743d6 --- /dev/null +++ b/games/rtype/client/main.cpp @@ -0,0 +1,96 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** main +*/ + +#include "GameStateMachine.hpp" +#include "MenuState.hpp" +#include "SettingsState.hpp" +#include "Renderer/SFMLRenderer.hpp" +#include "AsioNetworkModule.hpp" +#include "Audio/SFMLAudio.hpp" +#include +#include + +int main(int argc, char* argv[]) { + std::string serverIp = "127.0.0.1"; + uint16_t serverPort = 4242; + std::string playerName = "Player1"; + + if (argc > 1) { + serverIp = argv[1]; + } + if (argc > 2) { + serverPort = static_cast(std::stoi(argv[2])); + } + if (argc > 3) { + playerName = argv[3]; + } + + std::cout << "=== R-Type Client ===" << std::endl; + std::cout << "Server IP: " << serverIp << std::endl; + std::cout << "Server Port: " << serverPort << std::endl; + std::cout << "Player Name: " << playerName << std::endl; + std::cout << "=====================" << std::endl; + + RType::Client::SettingsState::LoadSettingsFromFile(); + + auto renderer = std::make_shared(); + auto networkModule = std::make_shared(); + networkModule->Initialize(nullptr); + auto audio = std::make_shared(); + + Renderer::WindowConfig config; + config.title = "R-Type - " + playerName; + config.width = RType::Client::SettingsState::GetScreenWidth(); + config.height = RType::Client::SettingsState::GetScreenHeight(); + config.fullscreen = RType::Client::SettingsState::GetFullscreen(); + config.resizable = !config.fullscreen; + config.targetFramerate = RType::Client::SettingsState::GetTargetFramerate(); + + if (!renderer->CreateWindow(config)) { + std::cerr << "Failed to create window" << std::endl; + return 1; + } + + RType::Client::GameContext context; + context.renderer = renderer; + context.networkModule = networkModule; + context.audio = audio; + context.serverIp = serverIp; + context.serverPort = serverPort; + context.playerName = playerName; + + Audio::AudioConfig audioConfig; + audioConfig.masterVolume = RType::Client::SettingsState::GetMasterVolume(); + audio->ConfigureDevice(audioConfig); + + if (RType::Client::SettingsState::IsMuted()) { + audio->SetMasterVolume(0.0f); + } + + RType::Client::GameStateMachine machine; + + machine.PushState(std::make_unique(machine, context)); + + while (renderer->IsWindowOpen() && machine.IsRunning()) { + float dt = renderer->GetDeltaTime(); + + renderer->Update(dt); + if (audio) { + audio->Update(dt); + } + renderer->BeginFrame(); + + machine.HandleInput(); + machine.Update(dt); + machine.Draw(); + + renderer->EndFrame(); + } + + std::cout << "Exiting R-Type client..." << std::endl; + return 0; +} diff --git a/games/rtype/client/src/EditorState.cpp b/games/rtype/client/src/EditorState.cpp new file mode 100644 index 0000000..8eb5475 --- /dev/null +++ b/games/rtype/client/src/EditorState.cpp @@ -0,0 +1,342 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** EditorState +*/ + +#include "EditorState.hpp" +#include "editor/EditorCanvasManager.hpp" +#include "editor/EditorUIManager.hpp" +#include "editor/EditorEntityManager.hpp" +#include "editor/EditorFileManager.hpp" +#include "editor/EditorConstants.hpp" +#include "ECS/Component.hpp" +#include "ECS/Components/TextLabel.hpp" +#include "Core/Logger.hpp" +#include +#include +#include +#include + +using namespace RType::Client::EditorConstants; + +namespace RType { + namespace Client { + + EditorState::EditorState(GameStateMachine& machine, GameContext& context) + : m_machine(machine) + , m_context(context) + , m_renderer(context.renderer) + { + } + + EditorState::~EditorState() = default; + + void EditorState::Init() { + Core::Logger::Info("[EditorState] Initializing level editor..."); + + m_renderingSystem = std::make_unique(m_renderer.get()); + m_textSystem = std::make_unique(m_renderer.get()); + + m_fontSmall = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 12); + if (m_fontSmall == Renderer::INVALID_FONT_ID) { + m_fontSmall = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 12); + } + + m_fontMedium = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 16); + if (m_fontMedium == Renderer::INVALID_FONT_ID) { + m_fontMedium = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 16); + } + + for (int i = 0; i < 3; ++i) { + ECS::Entity statusEntity = m_registry.CreateEntity(); + m_entities.push_back(statusEntity); + m_statusBarEntities.push_back(statusEntity); + + float yPos = UI::STATUS_BAR_Y + (i * UI::STATUS_BAR_LINE_HEIGHT); + m_registry.AddComponent(statusEntity, ECS::Position{UI::STATUS_BAR_X, yPos}); + + ECS::TextLabel statusLabel; + statusLabel.text = ""; + statusLabel.fontId = m_fontSmall; + statusLabel.characterSize = 10; + statusLabel.color = Colors::UI_TEXT; + m_registry.AddComponent(statusEntity, std::move(statusLabel)); + } + + m_inputHandler = std::make_unique(m_renderer.get()); + m_propertyManager = std::make_unique(m_renderer.get()); + m_canvasManager = std::make_unique(m_renderer.get()); + m_assetLibrary = std::make_unique(m_renderer.get()); + m_assetLibrary->Initialize(); + + m_fileManager = std::make_unique(*m_assetLibrary); + m_entityManager = std::make_unique(m_renderer.get(), m_registry, *m_assetLibrary); + m_uiManager = std::make_unique(m_renderer.get(), *m_assetLibrary, m_registry, m_entities, m_fontSmall, m_fontMedium); + m_uiManager->InitializePalette(); + m_uiManager->SetOnSaveRequested([this]() { + saveCurrentLevel(); + }); + m_selection = m_uiManager->GetActiveSelection(); + + m_propertyManager->SetOnPropertyChanged([this](EditorEntityData& entity) { + m_entityManager->RebuildDefaultCollider(entity); + m_entityManager->SyncEntity(entity); + m_hasUnsavedChanges = true; + updatePropertyPanel(); + }); + + m_propertyManager->SetOnEntityDeleted([this]() { + deleteSelectedEntity(); + }); + + m_propertyManager->SetOnPropertyCycled([this]() { + updatePropertyPanel(); + }); + + updatePropertyPanel(); + + Core::Logger::Info("[EditorState] Level editor initialized"); + } + + void EditorState::Cleanup() { + Core::Logger::Info("[EditorState] Cleaning up level editor..."); + + Renderer::Camera2D defaultCam{{0.0f, 0.0f}, {1280.0f, 720.0f}}; + m_renderer->SetCamera(defaultCam); + m_renderer->ResetCamera(); + + for (ECS::Entity entity : m_entities) { + if (m_registry.IsEntityAlive(entity)) { + m_registry.DestroyEntity(entity); + } + } + m_entities.clear(); + + Core::Logger::Info("[EditorState] Level editor cleaned up"); + } + + void EditorState::HandleInput() { + m_inputHandler->HandleKeyPress(Renderer::Key::Escape, [this]() { + if (m_hasUnsavedChanges) { + Core::Logger::Warning("[EditorState] Exiting with unsaved changes"); + } + Core::Logger::Info("[EditorState] Returning to menu..."); + if (m_machine.IsRunning() && m_machine.GetCurrentState() == this) { + m_machine.PopState(); + } else { + Core::Logger::Error("[EditorState] Cannot pop state - state machine is empty or invalid"); + } + }); + + m_inputHandler->HandleKeyPress(Renderer::Key::S, [this]() { + if (m_renderer->IsKeyPressed(Renderer::Key::LControl) || + m_renderer->IsKeyPressed(Renderer::Key::RControl)) { + saveCurrentLevel(); + } + }); + + m_inputHandler->HandleKeyPress(Renderer::Key::O, [this]() { + if (m_renderer->IsKeyPressed(Renderer::Key::LControl) || + m_renderer->IsKeyPressed(Renderer::Key::RControl)) { + + std::string path = m_currentLevelPath.empty() + ? "assets/levels/level1.json" + : m_currentLevelPath; + + auto loadedEntities = m_fileManager->LoadLevel(path, m_registry, m_renderer.get()); + if (!loadedEntities.empty()) { + Core::Logger::Info("[EditorState] Level loaded from {}", path); + m_currentLevelPath = path; + m_hasUnsavedChanges = false; + } else { + Core::Logger::Error("[EditorState] Failed to load: {}", m_fileManager->GetLastError()); + } + } + }); + + bool blockCameraInput = false; + if (m_propertyManager && m_entityManager && m_entityManager->GetSelectedEntity()) { + auto handleKeyPress = [this](Renderer::Key key, std::function action) { + m_inputHandler->HandleKeyPress(key, action); + }; + blockCameraInput = m_propertyManager->HandleInput(m_entityManager->GetSelectedEntity(), handleKeyPress); + } + + Math::Vector2 mouseScreen = m_renderer->GetMousePosition(); + if (m_uiManager) { + m_uiManager->UpdateHover(mouseScreen); + } + + if (m_canvasManager && !blockCameraInput) { + m_canvasManager->HandleCameraInput(); + } + + bool isLeftPressed = m_renderer->IsMouseButtonPressed(Renderer::IRenderer::MouseButton::Left); + if (isLeftPressed && !m_leftMousePressed) { + bool consumedByUI = false; + + if (m_uiManager) { + if (m_uiManager->HandleActionClick(mouseScreen)) { + consumedByUI = true; + } else { + auto selection = m_uiManager->HandleClick(mouseScreen); + if (selection.has_value()) { + m_selection = selection.value(); + consumedByUI = true; + if (m_propertyManager) { + m_propertyManager->ClearInput(); + } + } + } + } + + if (!consumedByUI && m_canvasManager && m_entityManager) { + Math::Vector2 mouseWorld = m_canvasManager->ScreenToWorld(mouseScreen); + if (m_selection.mode != EditorMode::SELECT) { + if (m_entityManager->PlaceEntity(m_selection, mouseWorld)) { + m_hasUnsavedChanges = true; + if (m_propertyManager) { + m_propertyManager->ClearInput(); + } + + m_selection.mode = EditorMode::SELECT; + m_selection.subtype.clear(); + if (m_uiManager) { + m_uiManager->SetActiveSelection(m_selection); + } + updatePropertyPanel(); + } + } else { + handleSelectionAt(mouseWorld); + } + } + } + m_leftMousePressed = isLeftPressed; + } + + void EditorState::Update(float dt) { + (void)dt; + + if (!m_canvasManager || m_statusBarEntities.size() < 3) { + return; + } + + const auto& camera = m_canvasManager->GetCamera(); + Math::Vector2 mouseScreen = m_renderer->GetMousePosition(); + Math::Vector2 mouseWorld = m_canvasManager->ScreenToWorld(mouseScreen); + m_lastMouseWorld = mouseWorld; + + auto formatLine = [](int index, const auto&... args) { + std::ostringstream oss; + oss << std::fixed << std::setprecision(1); + (oss << ... << args); + return oss.str(); + }; + + if (m_registry.IsEntityAlive(m_statusBarEntities[0])) { + auto& label0 = m_registry.GetComponent(m_statusBarEntities[0]); + label0.text = formatLine(0, "camera: (", camera.x, ", ", camera.y, ")"); + } + + if (m_registry.IsEntityAlive(m_statusBarEntities[1])) { + auto& label1 = m_registry.GetComponent(m_statusBarEntities[1]); + label1.text = formatLine(1, "zoom: ", camera.zoom, "x"); + } + + if (m_registry.IsEntityAlive(m_statusBarEntities[2])) { + auto& label2 = m_registry.GetComponent(m_statusBarEntities[2]); + label2.text = formatLine(2, "mouse: (", mouseWorld.x, ", ", mouseWorld.y, ")"); + } + } + + void EditorState::Draw() { + m_renderer->Clear(Colors::BACKGROUND); + + if (m_canvasManager) { + m_canvasManager->ApplyCamera(); + m_canvasManager->DrawGrid(); + } + + m_renderingSystem->Update(m_registry, 0.0f); + + if (m_entityManager) { + m_entityManager->DrawSelectionOutline(); + + if (m_entityManager->GetSelectedEntity()) { + m_entityManager->DrawColliders(m_entityManager->GetSelectedColliderIndex()); + } + + m_entityManager->DrawPlacementPreview(m_selection.mode, + m_selection.entityType, + m_selection.subtype, + m_lastMouseWorld); + } + + if (m_canvasManager) { + m_renderer->ResetCamera(); + } + + m_textSystem->Update(m_registry, 0.0f); + } + + void EditorState::handleSelectionAt(const Math::Vector2& mouseWorld) { + if (!m_entityManager) { + return; + } + + if (!m_entityManager->SelectAt(mouseWorld)) { + m_entityManager->ClearSelection(); + } + + if (m_propertyManager) { + m_propertyManager->ClearInput(); + } + updatePropertyPanel(); + } + + void EditorState::updatePropertyPanel() { + if (!m_uiManager || !m_entityManager || !m_propertyManager) { + return; + } + const EditorEntityData* selected = m_entityManager->GetSelectedEntity(); + m_uiManager->UpdatePropertyPanel(selected, m_propertyManager->GetActiveProperty(), m_propertyManager->GetInputBuffer()); + m_uiManager->UpdateColliderPanel(selected, m_entityManager->GetSelectedColliderIndex()); + } + + void EditorState::deleteSelectedEntity() { + if (!m_entityManager) { + return; + } + + if (m_entityManager->DeleteSelected()) { + m_hasUnsavedChanges = true; + if (m_propertyManager) { + m_propertyManager->ClearInput(); + } + updatePropertyPanel(); + } + } + + void EditorState::saveCurrentLevel() { + if (!m_fileManager || !m_entityManager) { + return; + } + + std::string path = m_currentLevelPath.empty() + ? "assets/levels/custom_level.json" + : m_currentLevelPath; + + if (m_fileManager->SaveLevel(path, m_entityManager->GetEntities(), m_levelName)) { + Core::Logger::Info("[EditorState] Level saved to {}", path); + m_hasUnsavedChanges = false; + m_currentLevelPath = path; + } else { + Core::Logger::Error("[EditorState] Failed to save: {}", m_fileManager->GetLastError()); + } + } + + + } +} diff --git a/games/rtype/client/src/GameState.cpp b/games/rtype/client/src/GameState.cpp new file mode 100644 index 0000000..9a98acb --- /dev/null +++ b/games/rtype/client/src/GameState.cpp @@ -0,0 +1,14 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** GameState - Main aggregator file +** This file includes all the split GameState implementation files +*/ + +#include "game/GameStateInit.cpp" +#include "game/GameStateUI.cpp" +#include "game/GameStateNetwork.cpp" +#include "game/GameStateUpdate.cpp" +#include "game/GameStateHelpers.cpp" +#include "game/GameStateTransition.cpp" diff --git a/games/rtype/client/src/LobbyState.cpp b/games/rtype/client/src/LobbyState.cpp new file mode 100644 index 0000000..878cfa2 --- /dev/null +++ b/games/rtype/client/src/LobbyState.cpp @@ -0,0 +1,10 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** LobbyState - Main aggregator file +** This file includes all the split LobbyState implementation files +*/ + +#include "lobby/LobbyStateInit.cpp" +#include "lobby/LobbyStateUpdate.cpp" diff --git a/games/rtype/client/src/MenuState.cpp b/games/rtype/client/src/MenuState.cpp new file mode 100644 index 0000000..3e0f61f --- /dev/null +++ b/games/rtype/client/src/MenuState.cpp @@ -0,0 +1,316 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** MenuState +*/ + +#include "MenuState.hpp" +#include "LobbyState.hpp" +#include "EditorState.hpp" +#include "RoomListState.hpp" +#include "SettingsState.hpp" +#include "ECS/Components/TextLabel.hpp" +#include "ECS/Component.hpp" +#include "ECS/AudioSystem.hpp" +#include "Core/Logger.hpp" +#include +#include + +using namespace RType::ECS; + +namespace RType { + namespace Client { + + MenuState::MenuState(GameStateMachine& machine, GameContext& context) : m_machine(machine), m_context(context) { + m_renderer = context.renderer; + m_renderingSystem = std::make_unique(m_renderer.get()); + m_textSystem = std::make_unique(m_renderer.get()); + } + + void MenuState::Init() { + std::cout << "[MenuState] Initializing modern UI..." << std::endl; + m_ignoreInputFrames = 1; + + if (m_context.audio) { + m_audioSystem = std::make_unique(m_context.audio.get()); + m_menuMusic = m_context.audio->LoadMusic("assets/sounds/menu.flac"); + if (m_menuMusic == Audio::INVALID_MUSIC_ID) { + m_menuMusic = m_context.audio->LoadMusic("../assets/sounds/menu.flac"); + } + m_menuMusicPlaying = false; + + m_selectMusic = m_context.audio->LoadMusic("assets/sounds/select.flac"); + if (m_selectMusic == Audio::INVALID_MUSIC_ID) { + m_selectMusic = m_context.audio->LoadMusic("../assets/sounds/select.flac"); + } + } + + m_fontLarge = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 48); + if (m_fontLarge == Renderer::INVALID_FONT_ID) { + m_fontLarge = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 48); + } + + m_fontMedium = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 24); + if (m_fontMedium == Renderer::INVALID_FONT_ID) { + m_fontMedium = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 24); + } + + m_fontSmall = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 16); + if (m_fontSmall == Renderer::INVALID_FONT_ID) { + m_fontSmall = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 16); + } + + m_bgTexture = m_renderer->LoadTexture("assets/backgrounds/1.jpg"); + if (m_bgTexture == Renderer::INVALID_TEXTURE_ID) { + m_bgTexture = m_renderer->LoadTexture("../assets/backgrounds/1.jpg"); + } + + createUI(); + } + + void MenuState::Cleanup() { + std::cout << "[MenuState] Cleaning up..." << std::endl; + + if (m_context.audio && m_menuMusic != Audio::INVALID_MUSIC_ID) { + m_context.audio->StopMusic(m_menuMusic); + m_context.audio->UnloadMusic(m_menuMusic); + m_menuMusic = Audio::INVALID_MUSIC_ID; + m_menuMusicPlaying = false; + } + + if (m_context.audio && m_selectMusic != Audio::INVALID_MUSIC_ID) { + m_context.audio->StopMusic(m_selectMusic); + m_context.audio->UnloadMusic(m_selectMusic); + m_selectMusic = Audio::INVALID_MUSIC_ID; + } + + for (Entity entity : m_entities) { + if (m_registry.IsEntityAlive(entity)) { + m_registry.DestroyEntity(entity); + } + } + m_entities.clear(); + } + + void MenuState::createUI() { + if (m_bgTexture != Renderer::INVALID_TEXTURE_ID) { + Entity bg = m_registry.CreateEntity(); + m_entities.push_back(bg); + m_registry.AddComponent(bg, Position{0.0f, 0.0f}); + + Renderer::SpriteId spriteId = m_renderer->CreateSprite(m_bgTexture, {}); + Drawable drawable(spriteId, -10); + + Renderer::Vector2 texSize = m_renderer->GetTextureSize(m_bgTexture); + if (texSize.x > 0 && texSize.y > 0) { + drawable.scale = {1280.0f / texSize.x, 720.0f / texSize.y}; + } + + m_registry.AddComponent(bg, std::move(drawable)); + } + + if (m_fontLarge == Renderer::INVALID_FONT_ID) + return; + + m_titleEntity = m_registry.CreateEntity(); + m_entities.push_back(m_titleEntity); + m_registry.AddComponent(m_titleEntity, Position{640.0f, 200.0f}); + TextLabel titleLabel("R-TYPE", m_fontLarge, 72); + titleLabel.color = {0.0f, 1.0f, 1.0f, 1.0f}; + titleLabel.centered = true; + m_registry.AddComponent(m_titleEntity, std::move(titleLabel)); + + m_subtitleEntity = m_registry.CreateEntity(); + m_entities.push_back(m_subtitleEntity); + m_registry.AddComponent(m_subtitleEntity, Position{640.0f, 280.0f}); + TextLabel subtitleLabel("EPIC SPACE SHOOTER", m_fontSmall, 16); + subtitleLabel.color = {0.5f, 0.86f, 1.0f, 0.95f}; + subtitleLabel.centered = true; + m_registry.AddComponent(m_subtitleEntity, std::move(subtitleLabel)); + + const char* menuLabels[] = {"PLAY", "LEVEL EDITOR", "SETTINGS", "QUIT"}; + float startY = 360.0f; + float itemSpacing = 60.0f; + + for (int i = 0; i < 4; i++) { + Entity menuItem = m_registry.CreateEntity(); + m_entities.push_back(menuItem); + m_menuItems.push_back(menuItem); + + m_registry.AddComponent(menuItem, Position{640.0f, startY + i * itemSpacing}); + TextLabel label(menuLabels[i], m_fontMedium, 24); + label.color = {0.7f, 0.7f, 0.7f, 1.0f}; + label.centered = true; + m_registry.AddComponent(menuItem, std::move(label)); + } + + Entity controlsText = m_registry.CreateEntity(); + m_entities.push_back(controlsText); + m_registry.AddComponent(controlsText, Position{640.0f, 620.0f}); + TextLabel controlsLabel("USE ARROWS TO NAVIGATE | ENTER TO SELECT", m_fontSmall, 12); + controlsLabel.color = {0.5f, 0.86f, 1.0f, 0.85f}; + controlsLabel.centered = true; + m_registry.AddComponent(controlsText, std::move(controlsLabel)); + + Entity versionText = m_registry.CreateEntity(); + m_entities.push_back(versionText); + m_registry.AddComponent(versionText, Position{20.0f, 680.0f}); + TextLabel versionLabel("v1.0.0 ALPHA", m_fontSmall, 12); + versionLabel.color = {0.42f, 0.18f, 0.48f, 0.8f}; + m_registry.AddComponent(versionText, std::move(versionLabel)); + } + + void MenuState::updateAnimations(float dt) { + m_animTime += dt; + + m_titlePulse = std::sin(m_animTime * 2.0f) * 0.3f + 1.0f; + + if (m_titleEntity != NULL_ENTITY && m_registry.IsEntityAlive(m_titleEntity)) { + auto& titleLabel = m_registry.GetComponent(m_titleEntity); + titleLabel.color.a = 0.7f + (m_titlePulse - 1.0f); + } + } + + void MenuState::updateMenuSelection() { + for (size_t i = 0; i < m_menuItems.size(); i++) { + if (m_registry.IsEntityAlive(m_menuItems[i])) { + auto& label = m_registry.GetComponent(m_menuItems[i]); + + if (static_cast(i) == m_selectedIndex) { + float pulse = std::sin(m_animTime * 4.0f) * 0.3f + 0.7f; + label.color = {1.0f, 0.08f + pulse * 0.5f, 0.58f, 1.0f}; + } else { + label.color = {0.5f, 0.5f, 0.5f, 0.8f}; + } + } + } + } + + void MenuState::HandleInput() { + if (m_ignoreInputFrames == 0 && m_renderer->IsKeyPressed(Renderer::Key::Escape) && !m_escKeyPressed) { + m_ignoreInputFrames = 1; + } + + if (m_ignoreInputFrames > 0) { + m_upKeyPressed = m_renderer->IsKeyPressed(Renderer::Key::Up); + m_downKeyPressed = m_renderer->IsKeyPressed(Renderer::Key::Down); + m_enterKeyPressed = m_renderer->IsKeyPressed(Renderer::Key::Enter); + m_escKeyPressed = m_renderer->IsKeyPressed(Renderer::Key::Escape); + m_ignoreInputFrames--; + return; + } + + auto playSelectSound = [this]() { + if (m_context.audio && m_selectMusic != Audio::INVALID_MUSIC_ID) { + m_context.audio->StopMusic(m_selectMusic); + Audio::PlaybackOptions opts; + opts.volume = 1.0f; + opts.loop = false; + m_context.audio->PlayMusic(m_selectMusic, opts); + } + }; + + if (m_renderer->IsKeyPressed(Renderer::Key::Up) && !m_upKeyPressed) { + m_upKeyPressed = true; + playSelectSound(); + m_selectedIndex--; + if (m_selectedIndex < 0) { + m_selectedIndex = static_cast(MenuItem::COUNT) - 1; + } + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Up)) { + m_upKeyPressed = false; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Down) && !m_downKeyPressed) { + m_downKeyPressed = true; + playSelectSound(); + m_selectedIndex++; + if (m_selectedIndex >= static_cast(MenuItem::COUNT)) { + m_selectedIndex = 0; + } + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Down)) { + m_downKeyPressed = false; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Enter) && !m_enterKeyPressed) { + m_enterKeyPressed = true; + playSelectSound(); + + switch (static_cast(m_selectedIndex)) { + case MenuItem::PLAY: + std::cout << "[MenuState] Starting game... Transitioning to Room Selection" << std::endl; + std::cout << "[MenuState] Connecting to " << m_context.serverIp << ":" << m_context.serverPort << " as '" << m_context.playerName << "'" << std::endl; + if (m_context.audio && m_menuMusic != Audio::INVALID_MUSIC_ID) { + m_context.audio->StopMusic(m_menuMusic); + m_menuMusicPlaying = false; + } + m_machine.PushState(std::make_unique(m_machine, m_context)); + break; + + case MenuItem::EDITOR: + std::cout << "[MenuState] Opening Level Editor..." << std::endl; + if (m_context.audio && m_menuMusic != Audio::INVALID_MUSIC_ID) { + m_context.audio->StopMusic(m_menuMusic); + m_menuMusicPlaying = false; + } + m_machine.PushState(std::make_unique(m_machine, m_context)); + break; + + case MenuItem::SETTINGS: + std::cout << "[MenuState] Opening Settings..." << std::endl; + m_machine.PushState(std::make_unique(m_machine, m_context)); + break; + + case MenuItem::QUIT: + std::cout << "[MenuState] Quitting..." << std::endl; + m_machine.PopState(); + break; + + default: + break; + } + + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Enter)) { + m_enterKeyPressed = false; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Escape) && !m_escKeyPressed) { + m_escKeyPressed = true; + playSelectSound(); + Core::Logger::Info("[MenuState] Escape pressed. Quitting..."); + m_machine.PopState(); + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Escape)) { + m_escKeyPressed = false; + } + } + + void MenuState::Update(float dt) { + updateAnimations(dt); + updateMenuSelection(); + + if (m_audioSystem && m_menuMusic != Audio::INVALID_MUSIC_ID && !m_menuMusicPlaying) { + auto cmd = m_registry.CreateEntity(); + auto& me = m_registry.AddComponent(cmd, MusicEffect(m_menuMusic)); + me.play = true; + me.stop = false; + me.loop = true; + me.volume = 0.35f; + me.pitch = 1.0f; + m_menuMusicPlaying = true; + } + + if (m_audioSystem) { + m_audioSystem->Update(m_registry, dt); + } + } + + void MenuState::Draw() { + m_renderer->Clear({0.05f, 0.05f, 0.1f, 1.0f}); + + m_renderingSystem->Update(m_registry, 0.0f); + m_textSystem->Update(m_registry, 0.0f); + } + + } +} diff --git a/games/rtype/client/src/ResultsState.cpp b/games/rtype/client/src/ResultsState.cpp new file mode 100644 index 0000000..a9887e2 --- /dev/null +++ b/games/rtype/client/src/ResultsState.cpp @@ -0,0 +1,267 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** ResultsState +*/ + +#include "ResultsState.hpp" +#include "RoomListState.hpp" +#include "ECS/Components/TextLabel.hpp" + +#include +#include +#include +#include + +using namespace RType::ECS; + +namespace RType { + namespace Client { + + ResultsState::ResultsState(GameStateMachine& machine, GameContext& context, std::vector> scores) : m_machine(machine), m_context(context), m_scores(std::move(scores)) { + m_renderer = context.renderer; + m_renderingSystem = std::make_unique(m_renderer.get()); + m_textSystem = std::make_unique(m_renderer.get()); + } + + void ResultsState::Init() { + m_fontLarge = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 36); + if (m_fontLarge == Renderer::INVALID_FONT_ID) { + m_fontLarge = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 36); + } + + m_fontMedium = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 18); + if (m_fontMedium == Renderer::INVALID_FONT_ID) { + m_fontMedium = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 18); + } + + m_fontSmall = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 14); + if (m_fontSmall == Renderer::INVALID_FONT_ID) { + m_fontSmall = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 14); + } + + m_bgTexture = m_renderer->LoadTexture("assets/backgrounds/game over/preview.png"); + if (m_bgTexture == Renderer::INVALID_TEXTURE_ID) { + m_bgTexture = m_renderer->LoadTexture("../assets/backgrounds/game over/preview.png"); + } + if (m_bgTexture != Renderer::INVALID_TEXTURE_ID) { + m_bgSprite = m_renderer->CreateSprite(m_bgTexture, {}); + m_bgTextureSize = m_renderer->GetTextureSize(m_bgTexture); + } else { + m_bgTexture = m_renderer->LoadTexture("assets/backgrounds/1.jpg"); + if (m_bgTexture == Renderer::INVALID_TEXTURE_ID) { + m_bgTexture = m_renderer->LoadTexture("../assets/backgrounds/1.jpg"); + } + if (m_bgTexture != Renderer::INVALID_TEXTURE_ID) { + m_bgSprite = m_renderer->CreateSprite(m_bgTexture, {}); + m_bgTextureSize = m_renderer->GetTextureSize(m_bgTexture); + } + } + m_escapePressed = m_renderer->IsKeyPressed(Renderer::Key::Escape); + + if (m_context.audio) { + m_endMusic = m_context.audio->LoadMusic("assets/sounds/end.flac"); + if (m_endMusic == Audio::INVALID_MUSIC_ID) { + m_endMusic = m_context.audio->LoadMusic("../assets/sounds/end.flac"); + } + if (m_endMusic != Audio::INVALID_MUSIC_ID) { + Audio::PlaybackOptions opts; + opts.loop = true; + opts.volume = 0.5f; + m_context.audio->PlayMusic(m_endMusic, opts); + } + } + + std::sort(m_scores.begin(), m_scores.end(), [](const auto& a, const auto& b) { return a.second > b.second; }); + + createUI(); + } + + void ResultsState::Cleanup() { + if (m_context.audio && m_endMusic != Audio::INVALID_MUSIC_ID) { + m_context.audio->StopMusic(m_endMusic); + m_context.audio->UnloadMusic(m_endMusic); + m_endMusic = Audio::INVALID_MUSIC_ID; + } + + auto entities = m_registry.GetEntitiesWithComponent(); + for (auto e : entities) { + if (m_registry.IsEntityAlive(e)) { + m_registry.DestroyEntity(e); + } + } + } + + void ResultsState::HandleInput() { + if (m_renderer->IsKeyPressed(Renderer::Key::Escape) && !m_escapePressed) { + m_escapePressed = true; + if (m_context.audio && m_endMusic != Audio::INVALID_MUSIC_ID) { + m_context.audio->StopMusic(m_endMusic); + } + m_machine.ChangeState(std::make_unique(m_machine, m_context)); + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Escape)) { + m_escapePressed = false; + } + } + + void ResultsState::Update(float dt) { + (void)dt; + } + + void ResultsState::Draw() { + m_renderer->Clear({0.05f, 0.05f, 0.12f, 1.0f}); + m_renderingSystem->Update(m_registry, 0.0f); + + if (m_drawScorePanel) { + Renderer::Rectangle border = m_scorePanelRect; + border.position.x -= 4.0f; + border.position.y -= 4.0f; + border.size.x += 8.0f; + border.size.y += 8.0f; + m_renderer->DrawRectangle(border, Renderer::Color(0.0f, 0.0f, 0.0f, 0.90f)); + m_renderer->DrawRectangle(m_scorePanelRect, Renderer::Color(0.0f, 0.0f, 0.0f, 0.62f)); + Renderer::Rectangle headerRect = m_scorePanelRect; + headerRect.size.y = m_scorePanelHeaderHeight; + m_renderer->DrawRectangle(headerRect, Renderer::Color(0.0f, 0.0f, 0.0f, 0.72f)); + Renderer::Rectangle sep = m_scorePanelRect; + sep.position.y += m_scorePanelHeaderHeight; + sep.size.y = 2.0f; + m_renderer->DrawRectangle(sep, Renderer::Color(0.20f, 0.60f, 1.00f, 0.45f)); + + for (size_t i = 0; i < m_scorePanelRowCount; ++i) { + Renderer::Rectangle rowRect; + rowRect.position = Renderer::Vector2(m_scorePanelRect.position.x + 12.0f, m_scorePanelRowStartY + static_cast(i) * m_scorePanelRowStepY - 16.0f); + rowRect.size = Renderer::Vector2(m_scorePanelRect.size.x - 24.0f, m_scorePanelRowStepY); + float a = (i % 2 == 0) ? 0.10f : 0.06f; + m_renderer->DrawRectangle(rowRect, Renderer::Color(0.02f, 0.08f, 0.18f, a)); + } + + Renderer::Rectangle hintStrip; + hintStrip.position = Renderer::Vector2(0.0f, 630.0f); + hintStrip.size = Renderer::Vector2(1280.0f, 60.0f); + m_renderer->DrawRectangle(hintStrip, Renderer::Color(0.0f, 0.0f, 0.0f, 0.30f)); + } + + m_textSystem->Update(m_registry, 0.0f); + } + + void ResultsState::createUI() { + if (m_bgSprite != Renderer::INVALID_SPRITE_ID) { + m_bgEntity = m_registry.CreateEntity(); + m_registry.AddComponent(m_bgEntity, Position{0.0f, 0.0f}); + + Drawable drawable(m_bgSprite, -10); + + if (m_bgTextureSize.x > 0 && m_bgTextureSize.y > 0) { + drawable.scale = {1280.0f / m_bgTextureSize.x, 720.0f / m_bgTextureSize.y}; + } + + m_registry.AddComponent(m_bgEntity, std::move(drawable)); + } + + if (m_fontLarge == Renderer::INVALID_FONT_ID) { + return; + } + + const float screenW = 1280.0f; + const float panelW = 920.0f; + const float panelH = 430.0f; + const float panelX = (screenW - panelW) * 0.5f; + const float panelY = 185.0f; + + m_drawScorePanel = true; + m_scorePanelRect.position = Renderer::Vector2(panelX, panelY); + m_scorePanelRect.size = Renderer::Vector2(panelW, panelH); + m_scorePanelRowStepY = 36.0f; + m_scorePanelRowStartY = panelY + m_scorePanelHeaderHeight + 46.0f; + + m_colRankX = panelX + 70.0f; + m_colNameX = panelX + 170.0f; + m_colScoreX = panelX + panelW - 130.0f; + + Entity titleEntity = m_registry.CreateEntity(); + m_registry.AddComponent(titleEntity, Position{640.0f, 90.0f}); + TextLabel titleLabel("RESULTS", m_fontLarge, 40); + titleLabel.centered = true; + titleLabel.color = {0.10f, 0.45f, 1.00f, 1.0f}; + m_registry.AddComponent(titleEntity, std::move(titleLabel)); + + Entity subtitleEntity = m_registry.CreateEntity(); + m_registry.AddComponent(subtitleEntity, Position{640.0f, 140.0f}); + TextLabel subtitle("Final scores", m_fontSmall != Renderer::INVALID_FONT_ID ? m_fontSmall : m_fontMedium, 16); + subtitle.centered = true; + subtitle.color = {0.20f, 0.60f, 1.00f, 0.95f}; + m_registry.AddComponent(subtitleEntity, std::move(subtitle)); + + { + Entity headerRank = m_registry.CreateEntity(); + m_registry.AddComponent(headerRank, Position{m_colRankX, panelY + 26.0f}); + TextLabel hRank("RANK", m_fontSmall != Renderer::INVALID_FONT_ID ? m_fontSmall : m_fontMedium, 14); + hRank.centered = true; + hRank.color = {0.75f, 0.88f, 1.00f, 0.95f}; + m_registry.AddComponent(headerRank, std::move(hRank)); + + Entity headerName = m_registry.CreateEntity(); + m_registry.AddComponent(headerName, Position{m_colNameX, panelY + 26.0f}); + TextLabel hName("NAME", m_fontSmall != Renderer::INVALID_FONT_ID ? m_fontSmall : m_fontMedium, 14); + hName.centered = false; + hName.color = {0.75f, 0.88f, 1.00f, 0.95f}; + m_registry.AddComponent(headerName, std::move(hName)); + + Entity headerScore = m_registry.CreateEntity(); + m_registry.AddComponent(headerScore, Position{m_colScoreX, panelY + 26.0f}); + TextLabel hScore("SCORE", m_fontSmall != Renderer::INVALID_FONT_ID ? m_fontSmall : m_fontMedium, 14); + hScore.centered = true; + hScore.color = {0.75f, 0.88f, 1.00f, 0.95f}; + m_registry.AddComponent(headerScore, std::move(hScore)); + } + + size_t maxLines = std::min(m_scores.size(), 8); + m_scorePanelRowCount = maxLines; + + for (size_t i = 0; i < maxLines; i++) { + const auto& [name, score] = m_scores[i]; + + const float y = m_scorePanelRowStartY + static_cast(i) * m_scorePanelRowStepY; + + Entity rankEnt = m_registry.CreateEntity(); + m_registry.AddComponent(rankEnt, Position{m_colRankX, y}); + TextLabel rankLabel(std::to_string(i + 1), m_fontMedium != Renderer::INVALID_FONT_ID ? m_fontMedium : m_fontLarge, 18); + rankLabel.centered = true; + rankLabel.color = {0.75f, 0.88f, 1.00f, 1.0f}; + m_registry.AddComponent(rankEnt, std::move(rankLabel)); + + Entity nameEnt = m_registry.CreateEntity(); + m_registry.AddComponent(nameEnt, Position{m_colNameX, y}); + std::string displayName = name; + if (displayName.size() > 18) { + displayName = displayName.substr(0, 18); + } + TextLabel nameLabel(displayName, m_fontMedium != Renderer::INVALID_FONT_ID ? m_fontMedium : m_fontLarge, 18); + nameLabel.centered = false; + nameLabel.color = {0.90f, 0.95f, 1.00f, 1.0f}; + m_registry.AddComponent(nameEnt, std::move(nameLabel)); + + Entity scoreEnt = m_registry.CreateEntity(); + m_registry.AddComponent(scoreEnt, Position{m_colScoreX, y}); + std::ostringstream ss; + ss << std::setw(8) << std::setfill('0') << score; + TextLabel scoreLabel(ss.str(), m_fontMedium != Renderer::INVALID_FONT_ID ? m_fontMedium : m_fontLarge, 18); + scoreLabel.centered = true; + scoreLabel.color = {0.75f, 0.88f, 1.00f, 1.0f}; + m_registry.AddComponent(scoreEnt, std::move(scoreLabel)); + } + + Entity hint = m_registry.CreateEntity(); + m_registry.AddComponent(hint, Position{640.0f, 650.0f}); + TextLabel hintLabel("Press ESC to return to room selection", m_fontSmall != Renderer::INVALID_FONT_ID ? m_fontSmall : m_fontMedium, 14); + hintLabel.centered = true; + hintLabel.color = {0.20f, 0.60f, 1.00f, 0.95f}; + m_registry.AddComponent(hint, std::move(hintLabel)); + } + + } +} + + diff --git a/games/rtype/client/src/SettingsState.cpp b/games/rtype/client/src/SettingsState.cpp new file mode 100644 index 0000000..f107d81 --- /dev/null +++ b/games/rtype/client/src/SettingsState.cpp @@ -0,0 +1,1365 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** SettingsState +*/ + +#include "SettingsState.hpp" +#include "ECS/Components/TextLabel.hpp" +#include "ECS/Component.hpp" +#include "Core/Logger.hpp" +#include "Core/InputMapping.hpp" +#include +#include +#include +#include + +using namespace RType::ECS; +using json = nlohmann::json; + +namespace { + const std::unordered_map KEY_TO_STRING = { + {Renderer::Key::A, "A"}, {Renderer::Key::B, "B"}, {Renderer::Key::C, "C"}, + {Renderer::Key::D, "D"}, {Renderer::Key::E, "E"}, {Renderer::Key::F, "F"}, + {Renderer::Key::G, "G"}, {Renderer::Key::H, "H"}, {Renderer::Key::I, "I"}, + {Renderer::Key::J, "J"}, {Renderer::Key::K, "K"}, {Renderer::Key::L, "L"}, + {Renderer::Key::M, "M"}, {Renderer::Key::N, "N"}, {Renderer::Key::O, "O"}, + {Renderer::Key::P, "P"}, {Renderer::Key::Q, "Q"}, {Renderer::Key::R, "R"}, + {Renderer::Key::S, "S"}, {Renderer::Key::T, "T"}, {Renderer::Key::U, "U"}, + {Renderer::Key::V, "V"}, {Renderer::Key::W, "W"}, {Renderer::Key::X, "X"}, + {Renderer::Key::Y, "Y"}, {Renderer::Key::Z, "Z"}, + {Renderer::Key::Num0, "0"}, {Renderer::Key::Num1, "1"}, {Renderer::Key::Num2, "2"}, + {Renderer::Key::Num3, "3"}, {Renderer::Key::Num4, "4"}, {Renderer::Key::Num5, "5"}, + {Renderer::Key::Num6, "6"}, {Renderer::Key::Num7, "7"}, {Renderer::Key::Num8, "8"}, + {Renderer::Key::Num9, "9"}, + {Renderer::Key::Space, "SPACE"}, {Renderer::Key::Enter, "ENTER"}, + {Renderer::Key::Escape, "ESC"}, {Renderer::Key::Tab, "TAB"}, + {Renderer::Key::Left, "LEFT"}, {Renderer::Key::Right, "RIGHT"}, + {Renderer::Key::Up, "UP"}, {Renderer::Key::Down, "DOWN"}, + {Renderer::Key::LShift, "LSHIFT"}, {Renderer::Key::RShift, "RSHIFT"}, + {Renderer::Key::LControl, "LCTRL"}, {Renderer::Key::RControl, "RCTRL"}, + {Renderer::Key::LAlt, "LALT"}, {Renderer::Key::RAlt, "RALT"} + }; + + const std::unordered_map STRING_TO_KEY = { + {"A", Renderer::Key::A}, {"B", Renderer::Key::B}, {"C", Renderer::Key::C}, + {"D", Renderer::Key::D}, {"E", Renderer::Key::E}, {"F", Renderer::Key::F}, + {"G", Renderer::Key::G}, {"H", Renderer::Key::H}, {"I", Renderer::Key::I}, + {"J", Renderer::Key::J}, {"K", Renderer::Key::K}, {"L", Renderer::Key::L}, + {"M", Renderer::Key::M}, {"N", Renderer::Key::N}, {"O", Renderer::Key::O}, + {"P", Renderer::Key::P}, {"Q", Renderer::Key::Q}, {"R", Renderer::Key::R}, + {"S", Renderer::Key::S}, {"T", Renderer::Key::T}, {"U", Renderer::Key::U}, + {"V", Renderer::Key::V}, {"W", Renderer::Key::W}, {"X", Renderer::Key::X}, + {"Y", Renderer::Key::Y}, {"Z", Renderer::Key::Z}, + {"0", Renderer::Key::Num0}, {"1", Renderer::Key::Num1}, {"2", Renderer::Key::Num2}, + {"3", Renderer::Key::Num3}, {"4", Renderer::Key::Num4}, {"5", Renderer::Key::Num5}, + {"6", Renderer::Key::Num6}, {"7", Renderer::Key::Num7}, {"8", Renderer::Key::Num8}, + {"9", Renderer::Key::Num9}, + {"SPACE", Renderer::Key::Space}, {"ENTER", Renderer::Key::Enter}, + {"ESC", Renderer::Key::Escape}, {"TAB", Renderer::Key::Tab}, + {"LEFT", Renderer::Key::Left}, {"RIGHT", Renderer::Key::Right}, + {"UP", Renderer::Key::Up}, {"DOWN", Renderer::Key::Down}, + {"LSHIFT", Renderer::Key::LShift}, {"RSHIFT", Renderer::Key::RShift}, + {"LCTRL", Renderer::Key::LControl}, {"RCTRL", Renderer::Key::RControl}, + {"LALT", Renderer::Key::LAlt}, {"RALT", Renderer::Key::RAlt} + }; + + const std::vector REBINDABLE_KEYS = { + Renderer::Key::A, Renderer::Key::B, Renderer::Key::C, Renderer::Key::D, Renderer::Key::E, + Renderer::Key::F, Renderer::Key::G, Renderer::Key::H, Renderer::Key::I, Renderer::Key::J, + Renderer::Key::K, Renderer::Key::L, Renderer::Key::M, Renderer::Key::N, Renderer::Key::O, + Renderer::Key::P, Renderer::Key::Q, Renderer::Key::R, Renderer::Key::S, Renderer::Key::T, + Renderer::Key::U, Renderer::Key::V, Renderer::Key::W, Renderer::Key::X, Renderer::Key::Y, + Renderer::Key::Z, + Renderer::Key::Num0, Renderer::Key::Num1, Renderer::Key::Num2, Renderer::Key::Num3, + Renderer::Key::Num4, Renderer::Key::Num5, Renderer::Key::Num6, Renderer::Key::Num7, + Renderer::Key::Num8, Renderer::Key::Num9, + Renderer::Key::Space, Renderer::Key::Enter, Renderer::Key::Escape, Renderer::Key::Tab, + Renderer::Key::Left, Renderer::Key::Right, Renderer::Key::Up, Renderer::Key::Down, + Renderer::Key::LShift, Renderer::Key::RShift, Renderer::Key::LControl, Renderer::Key::RControl, + Renderer::Key::LAlt, Renderer::Key::RAlt + }; + + const std::vector ACTION_KEYS = {"MOVE_UP", "MOVE_DOWN", "MOVE_LEFT", "MOVE_RIGHT", "SHOOT"}; +} + +namespace RType { + namespace Client { + + const std::string SettingsState::SETTINGS_FILE_PATH = "settings.json"; + bool SettingsState::s_colourBlindMode = false; + std::uint32_t SettingsState::s_screenWidth = 1280; + std::uint32_t SettingsState::s_screenHeight = 720; + bool SettingsState::s_fullscreen = false; + std::uint32_t SettingsState::s_targetFramerate = 60; + float SettingsState::s_masterVolume = 1.0f; + bool SettingsState::s_muted = false; + + const std::vector SettingsState::RESOLUTIONS = { + {1280, 720, "1280x720"}, + {1920, 1080, "1920x1080"}, + {2560, 1440, "2560x1440"}, + {3840, 2160, "3840x2160"} + }; + + const std::vector SettingsState::FRAMERATES = {30, 60, 120, 0}; + const std::vector SettingsState::FRAMERATE_NAMES = {"30", "60", "120", "Unlimited"}; + + SettingsState::SettingsState(GameStateMachine& machine, GameContext& context) + : m_machine(machine), m_context(context) { + m_renderer = context.renderer; + m_renderingSystem = std::make_unique(m_renderer.get()); + m_textSystem = std::make_unique(m_renderer.get()); + } + + void SettingsState::Init() { + std::cout << "[SettingsState] Initializing..." << std::endl; + + if (m_context.audio) { + m_audioSystem = std::make_unique(m_context.audio.get()); + m_selectMusic = m_context.audio->LoadMusic("assets/sounds/select.flac"); + if (m_selectMusic == Audio::INVALID_MUSIC_ID) { + m_selectMusic = m_context.audio->LoadMusic("../assets/sounds/select.flac"); + } + } + + m_fontLarge = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 48); + if (m_fontLarge == Renderer::INVALID_FONT_ID) { + m_fontLarge = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 48); + } + + m_fontMedium = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 24); + if (m_fontMedium == Renderer::INVALID_FONT_ID) { + m_fontMedium = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 24); + } + + m_fontSmall = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 16); + if (m_fontSmall == Renderer::INVALID_FONT_ID) { + m_fontSmall = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 16); + } + + m_bgTexture = m_renderer->LoadTexture("assets/backgrounds/1.jpg"); + if (m_bgTexture == Renderer::INVALID_TEXTURE_ID) { + m_bgTexture = m_renderer->LoadTexture("../assets/backgrounds/1.jpg"); + } + + loadSettings(); + createUI(); + } + + void SettingsState::Cleanup() { + std::cout << "[SettingsState] Cleaning up..." << std::endl; + + if (m_context.audio && m_selectMusic != Audio::INVALID_MUSIC_ID) { + m_context.audio->StopMusic(m_selectMusic); + m_context.audio->UnloadMusic(m_selectMusic); + m_selectMusic = Audio::INVALID_MUSIC_ID; + } + + for (Entity entity : m_entities) { + if (m_registry.IsEntityAlive(entity)) { + m_registry.DestroyEntity(entity); + } + } + m_entities.clear(); + } + + void SettingsState::createUI() { + m_menuItems.clear(); + + if (m_bgTexture != Renderer::INVALID_TEXTURE_ID) { + Entity bg = m_registry.CreateEntity(); + m_entities.push_back(bg); + m_registry.AddComponent(bg, Position{0.0f, 0.0f}); + + Renderer::SpriteId spriteId = m_renderer->CreateSprite(m_bgTexture, {}); + Drawable drawable(spriteId, -10); + + Renderer::Vector2 texSize = m_renderer->GetTextureSize(m_bgTexture); + if (texSize.x > 0 && texSize.y > 0) { + drawable.scale = {1280.0f / texSize.x, 720.0f / texSize.y}; + } + + m_registry.AddComponent(bg, std::move(drawable)); + } + + if (m_fontLarge == Renderer::INVALID_FONT_ID) + return; + + m_titleEntity = m_registry.CreateEntity(); + m_entities.push_back(m_titleEntity); + m_registry.AddComponent(m_titleEntity, Position{640.0f, 150.0f}); + TextLabel titleLabel("SETTINGS", m_fontLarge, 72); + titleLabel.color = {0.0f, 1.0f, 1.0f, 1.0f}; + titleLabel.centered = true; + m_registry.AddComponent(m_titleEntity, std::move(titleLabel)); + + float startY = 250.0f; + float itemSpacing = 70.0f; + + Entity colourBlindItem = m_registry.CreateEntity(); + m_entities.push_back(colourBlindItem); + m_menuItems.push_back(colourBlindItem); + m_registry.AddComponent(colourBlindItem, Position{640.0f, startY}); + std::string colourBlindText = "COLOUR-BLIND MODE: " + std::string(m_colourBlindMode ? "ON" : "OFF"); + TextLabel colourBlindLabel(colourBlindText, m_fontMedium, 24); + colourBlindLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; + colourBlindLabel.centered = true; + m_registry.AddComponent(colourBlindItem, std::move(colourBlindLabel)); + + Entity screenItem = m_registry.CreateEntity(); + m_entities.push_back(screenItem); + m_menuItems.push_back(screenItem); + m_registry.AddComponent(screenItem, Position{640.0f, startY + itemSpacing}); + TextLabel screenLabel("SCREEN", m_fontMedium, 24); + screenLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; + screenLabel.centered = true; + m_registry.AddComponent(screenItem, std::move(screenLabel)); + + Entity audioItem = m_registry.CreateEntity(); + m_entities.push_back(audioItem); + m_menuItems.push_back(audioItem); + m_registry.AddComponent(audioItem, Position{640.0f, startY + itemSpacing * 2}); + TextLabel audioLabel("AUDIO", m_fontMedium, 24); + audioLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; + audioLabel.centered = true; + m_registry.AddComponent(audioItem, std::move(audioLabel)); + + Entity commandsItem = m_registry.CreateEntity(); + m_entities.push_back(commandsItem); + m_menuItems.push_back(commandsItem); + m_registry.AddComponent(commandsItem, Position{640.0f, startY + itemSpacing * 3}); + TextLabel commandsLabel("COMMANDS", m_fontMedium, 24); + commandsLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; + commandsLabel.centered = true; + m_registry.AddComponent(commandsItem, std::move(commandsLabel)); + + Entity backItem = m_registry.CreateEntity(); + m_entities.push_back(backItem); + m_menuItems.push_back(backItem); + m_registry.AddComponent(backItem, Position{640.0f, startY + itemSpacing * 4}); + TextLabel backLabel("BACK", m_fontMedium, 24); + backLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; + backLabel.centered = true; + m_registry.AddComponent(backItem, std::move(backLabel)); + + Entity controlsText = m_registry.CreateEntity(); + m_entities.push_back(controlsText); + m_registry.AddComponent(controlsText, Position{640.0f, 600.0f}); + TextLabel controlsLabel("USE ARROWS TO NAVIGATE | ENTER TO SELECT | ESC TO GO BACK", m_fontSmall, 12); + controlsLabel.color = {0.5f, 0.86f, 1.0f, 0.85f}; + controlsLabel.centered = true; + m_registry.AddComponent(controlsText, std::move(controlsLabel)); + } + + void SettingsState::createScreenUI() { + for (Entity entity : m_screenMenuItems) { + if (m_registry.IsEntityAlive(entity)) { + m_registry.DestroyEntity(entity); + } + } + m_screenMenuItems.clear(); + + if (m_fontLarge == Renderer::INVALID_FONT_ID) + return; + + Entity titleEntity = m_registry.CreateEntity(); + m_entities.push_back(titleEntity); + m_screenMenuItems.push_back(titleEntity); + m_registry.AddComponent(titleEntity, Position{640.0f, 150.0f}); + TextLabel titleLabel("SCREEN SETTINGS", m_fontLarge, 72); + titleLabel.color = {0.0f, 1.0f, 1.0f, 1.0f}; + titleLabel.centered = true; + m_registry.AddComponent(titleEntity, std::move(titleLabel)); + + float startY = 280.0f; + float itemSpacing = 70.0f; + + Entity resolutionItem = m_registry.CreateEntity(); + m_entities.push_back(resolutionItem); + m_screenMenuItems.push_back(resolutionItem); + m_registry.AddComponent(resolutionItem, Position{640.0f, startY}); + std::string resolutionText = "RESOLUTION: " + RESOLUTIONS[m_resolutionIndex].name; + TextLabel resolutionLabel(resolutionText, m_fontMedium, 24); + resolutionLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; + resolutionLabel.centered = true; + m_registry.AddComponent(resolutionItem, std::move(resolutionLabel)); + + Entity fullscreenItem = m_registry.CreateEntity(); + m_entities.push_back(fullscreenItem); + m_screenMenuItems.push_back(fullscreenItem); + m_registry.AddComponent(fullscreenItem, Position{640.0f, startY + itemSpacing}); + std::string fullscreenText = "FULLSCREEN: " + std::string(m_fullscreen ? "ON" : "OFF"); + TextLabel fullscreenLabel(fullscreenText, m_fontMedium, 24); + fullscreenLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; + fullscreenLabel.centered = true; + m_registry.AddComponent(fullscreenItem, std::move(fullscreenLabel)); + + Entity framerateItem = m_registry.CreateEntity(); + m_entities.push_back(framerateItem); + m_screenMenuItems.push_back(framerateItem); + m_registry.AddComponent(framerateItem, Position{640.0f, startY + itemSpacing * 2}); + std::string framerateText = "FRAME RATE: " + FRAMERATE_NAMES[m_framerateIndex]; + TextLabel framerateLabel(framerateText, m_fontMedium, 24); + framerateLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; + framerateLabel.centered = true; + m_registry.AddComponent(framerateItem, std::move(framerateLabel)); + + Entity backItem = m_registry.CreateEntity(); + m_entities.push_back(backItem); + m_screenMenuItems.push_back(backItem); + m_registry.AddComponent(backItem, Position{640.0f, startY + itemSpacing * 3}); + TextLabel backLabel("BACK", m_fontMedium, 24); + backLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; + backLabel.centered = true; + m_registry.AddComponent(backItem, std::move(backLabel)); + + Entity controlsText = m_registry.CreateEntity(); + m_entities.push_back(controlsText); + m_screenMenuItems.push_back(controlsText); + m_registry.AddComponent(controlsText, Position{640.0f, 600.0f}); + TextLabel controlsLabel("UP/DOWN: NAVIGATE | LEFT/RIGHT: CHANGE VALUE | ESC: BACK", m_fontSmall, 12); + controlsLabel.color = {0.5f, 0.86f, 1.0f, 0.85f}; + controlsLabel.centered = true; + m_registry.AddComponent(controlsText, std::move(controlsLabel)); + } + + void SettingsState::createAudioUI() { + for (Entity entity : m_audioMenuItems) { + if (m_registry.IsEntityAlive(entity)) { + m_registry.DestroyEntity(entity); + } + } + m_audioMenuItems.clear(); + + if (m_fontLarge == Renderer::INVALID_FONT_ID) + return; + + Entity titleEntity = m_registry.CreateEntity(); + m_entities.push_back(titleEntity); + m_audioMenuItems.push_back(titleEntity); + m_registry.AddComponent(titleEntity, Position{640.0f, 150.0f}); + TextLabel titleLabel("AUDIO SETTINGS", m_fontLarge, 72); + titleLabel.color = {0.0f, 1.0f, 1.0f, 1.0f}; + titleLabel.centered = true; + m_registry.AddComponent(titleEntity, std::move(titleLabel)); + + float startY = 280.0f; + float itemSpacing = 70.0f; + + Entity volumeItem = m_registry.CreateEntity(); + m_entities.push_back(volumeItem); + m_audioMenuItems.push_back(volumeItem); + m_registry.AddComponent(volumeItem, Position{640.0f, startY}); + int volumePercent = static_cast(std::round(m_masterVolume * 100.0f)); + volumePercent = (volumePercent / 5) * 5; + std::string volumeText = "MASTER VOLUME: " + std::to_string(volumePercent) + "%"; + TextLabel volumeLabel(volumeText, m_fontMedium, 24); + volumeLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; + volumeLabel.centered = true; + m_registry.AddComponent(volumeItem, std::move(volumeLabel)); + + Entity muteItem = m_registry.CreateEntity(); + m_entities.push_back(muteItem); + m_audioMenuItems.push_back(muteItem); + m_registry.AddComponent(muteItem, Position{640.0f, startY + itemSpacing}); + std::string muteText = "MUTE: " + std::string(m_muted ? "ON" : "OFF"); + TextLabel muteLabel(muteText, m_fontMedium, 24); + muteLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; + muteLabel.centered = true; + m_registry.AddComponent(muteItem, std::move(muteLabel)); + + Entity backItem = m_registry.CreateEntity(); + m_entities.push_back(backItem); + m_audioMenuItems.push_back(backItem); + m_registry.AddComponent(backItem, Position{640.0f, startY + itemSpacing * 2}); + TextLabel backLabel("BACK", m_fontMedium, 24); + backLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; + backLabel.centered = true; + m_registry.AddComponent(backItem, std::move(backLabel)); + + Entity controlsText = m_registry.CreateEntity(); + m_entities.push_back(controlsText); + m_audioMenuItems.push_back(controlsText); + m_registry.AddComponent(controlsText, Position{640.0f, 600.0f}); + TextLabel controlsLabel("UP/DOWN: NAVIGATE | LEFT/RIGHT: CHANGE VALUE | ESC: BACK", m_fontSmall, 12); + controlsLabel.color = {0.5f, 0.86f, 1.0f, 0.85f}; + controlsLabel.centered = true; + m_registry.AddComponent(controlsText, std::move(controlsLabel)); + } + + void SettingsState::createCommandsUI() { + for (Entity entity : m_commandsMenuItems) { + if (m_registry.IsEntityAlive(entity)) { + m_registry.DestroyEntity(entity); + } + } + m_commandsMenuItems.clear(); + + if (m_fontLarge == Renderer::INVALID_FONT_ID) + return; + + Entity titleEntity = m_registry.CreateEntity(); + m_entities.push_back(titleEntity); + m_commandsMenuItems.push_back(titleEntity); + m_registry.AddComponent(titleEntity, Position{640.0f, 150.0f}); + TextLabel titleLabel("COMMANDS", m_fontLarge, 72); + titleLabel.color = {0.0f, 1.0f, 1.0f, 1.0f}; + titleLabel.centered = true; + m_registry.AddComponent(titleEntity, std::move(titleLabel)); + + float startY = 260.0f; + float itemSpacing = 55.0f; + + std::vector actionNames = {"MOVE UP", "MOVE DOWN", "MOVE LEFT", "MOVE RIGHT", "SHOOT"}; + + for (size_t i = 0; i < actionNames.size(); i++) { + Entity actionItem = m_registry.CreateEntity(); + m_entities.push_back(actionItem); + m_commandsMenuItems.push_back(actionItem); + m_registry.AddComponent(actionItem, Position{640.0f, startY + itemSpacing * i}); + + Renderer::Key currentKey = Core::InputMapping::GetKey(ACTION_KEYS[i]); + std::string keyName = keyToString(currentKey); + std::string actionText = actionNames[i] + ": " + keyName; + if (m_waitingForRebind && m_rebindingActionIndex == static_cast(i)) { + actionText = actionNames[i] + ": PRESS A KEY..."; + } + + TextLabel actionLabel(actionText, m_fontMedium, 24); + actionLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; + actionLabel.centered = true; + m_registry.AddComponent(actionItem, std::move(actionLabel)); + } + + Entity backItem = m_registry.CreateEntity(); + m_entities.push_back(backItem); + m_commandsMenuItems.push_back(backItem); + m_registry.AddComponent(backItem, Position{640.0f, startY + itemSpacing * 5}); + TextLabel backLabel("BACK", m_fontMedium, 24); + backLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; + backLabel.centered = true; + m_registry.AddComponent(backItem, std::move(backLabel)); + + Entity controlsText = m_registry.CreateEntity(); + m_entities.push_back(controlsText); + m_commandsMenuItems.push_back(controlsText); + m_registry.AddComponent(controlsText, Position{640.0f, 600.0f}); + std::string controlsTextStr = m_waitingForRebind ? + "PRESS ANY KEY TO REBIND | ESC TO CANCEL" : + "UP/DOWN: NAVIGATE | ENTER: REBIND | ESC: BACK"; + TextLabel controlsLabel(controlsTextStr, m_fontSmall, 12); + controlsLabel.color = {0.5f, 0.86f, 1.0f, 0.85f}; + controlsLabel.centered = true; + m_registry.AddComponent(controlsText, std::move(controlsLabel)); + } + + void SettingsState::updateAnimations(float dt) { + m_animTime += dt; + } + + void SettingsState::updateMenuSelection() { + if (m_inScreenMenu) { + updateScreenMenuSelection(); + return; + } + if (m_inAudioMenu) { + updateAudioMenuSelection(); + return; + } + if (m_inCommandsMenu) { + updateCommandsMenuSelection(); + return; + } + + for (size_t i = 0; i < m_menuItems.size(); i++) { + if (m_registry.IsEntityAlive(m_menuItems[i])) { + auto& label = m_registry.GetComponent(m_menuItems[i]); + + if (static_cast(i) == m_selectedIndex) { + float pulse = std::sin(m_animTime * 4.0f) * 0.3f + 0.7f; + label.color = {1.0f, 0.08f + pulse * 0.5f, 0.58f, 1.0f}; + } else { + label.color = {0.7f, 0.7f, 0.7f, 1.0f}; + } + } + } + + if (m_menuItems.size() > 0 && m_registry.IsEntityAlive(m_menuItems[0])) { + auto& label = m_registry.GetComponent(m_menuItems[0]); + std::string colourBlindText = "COLOUR-BLIND MODE: " + std::string(m_colourBlindMode ? "ON" : "OFF"); + label.text = colourBlindText; + } + } + + void SettingsState::updateScreenMenuSelection() { + for (size_t i = 0; i < m_screenMenuItems.size(); i++) { + if (m_registry.IsEntityAlive(m_screenMenuItems[i])) { + auto& label = m_registry.GetComponent(m_screenMenuItems[i]); + + if (i == 0) { + continue; + } + + int itemIndex = static_cast(i) - 1; + if (itemIndex == m_screenSelectedIndex) { + float pulse = std::sin(m_animTime * 4.0f) * 0.3f + 0.7f; + label.color = {1.0f, 0.08f + pulse * 0.5f, 0.58f, 1.0f}; + } else { + label.color = {0.7f, 0.7f, 0.7f, 1.0f}; + } + } + } + + if (m_screenMenuItems.size() > 1 && m_registry.IsEntityAlive(m_screenMenuItems[1])) { + auto& label = m_registry.GetComponent(m_screenMenuItems[1]); + std::string resolutionText = "RESOLUTION: " + RESOLUTIONS[m_resolutionIndex].name; + label.text = resolutionText; + } + + if (m_screenMenuItems.size() > 2 && m_registry.IsEntityAlive(m_screenMenuItems[2])) { + auto& label = m_registry.GetComponent(m_screenMenuItems[2]); + std::string fullscreenText = "FULLSCREEN: " + std::string(m_fullscreen ? "ON" : "OFF"); + label.text = fullscreenText; + } + + if (m_screenMenuItems.size() > 3 && m_registry.IsEntityAlive(m_screenMenuItems[3])) { + auto& label = m_registry.GetComponent(m_screenMenuItems[3]); + std::string framerateText = "FRAME RATE: " + FRAMERATE_NAMES[m_framerateIndex]; + label.text = framerateText; + } + } + + void SettingsState::updateAudioMenuSelection() { + for (size_t i = 0; i < m_audioMenuItems.size(); i++) { + if (m_registry.IsEntityAlive(m_audioMenuItems[i])) { + auto& label = m_registry.GetComponent(m_audioMenuItems[i]); + + if (i == 0) { + continue; + } + + int itemIndex = static_cast(i) - 1; + if (itemIndex == m_audioSelectedIndex) { + float pulse = std::sin(m_animTime * 4.0f) * 0.3f + 0.7f; + label.color = {1.0f, 0.08f + pulse * 0.5f, 0.58f, 1.0f}; + } else { + label.color = {0.7f, 0.7f, 0.7f, 1.0f}; + } + } + } + + if (m_audioMenuItems.size() > 1 && m_registry.IsEntityAlive(m_audioMenuItems[1])) { + auto& label = m_registry.GetComponent(m_audioMenuItems[1]); + int volumePercent = static_cast(std::round(m_masterVolume * 100.0f)); + volumePercent = (volumePercent / 5) * 5; + std::string volumeText = "MASTER VOLUME: " + std::to_string(volumePercent) + "%"; + label.text = volumeText; + } + + if (m_audioMenuItems.size() > 2 && m_registry.IsEntityAlive(m_audioMenuItems[2])) { + auto& label = m_registry.GetComponent(m_audioMenuItems[2]); + std::string muteText = "MUTE: " + std::string(m_muted ? "ON" : "OFF"); + label.text = muteText; + } + } + + void SettingsState::updateCommandsMenuSelection() { + std::vector actionNames = {"MOVE UP", "MOVE DOWN", "MOVE LEFT", "MOVE RIGHT", "SHOOT"}; + + for (size_t i = 0; i < m_commandsMenuItems.size(); i++) { + if (m_registry.IsEntityAlive(m_commandsMenuItems[i])) { + auto& label = m_registry.GetComponent(m_commandsMenuItems[i]); + + if (i == 0) { + continue; + } + + int itemIndex = static_cast(i) - 1; + if (itemIndex == m_commandsSelectedIndex) { + float pulse = std::sin(m_animTime * 4.0f) * 0.3f + 0.7f; + label.color = {1.0f, 0.08f + pulse * 0.5f, 0.58f, 1.0f}; + } else { + label.color = {0.7f, 0.7f, 0.7f, 1.0f}; + } + + if (itemIndex >= 0 && itemIndex < static_cast(actionNames.size())) { + Renderer::Key currentKey = Core::InputMapping::GetKey(ACTION_KEYS[itemIndex]); + std::string keyName = keyToString(currentKey); + std::string actionText = actionNames[itemIndex] + ": " + keyName; + if (m_waitingForRebind && m_rebindingActionIndex == itemIndex) { + actionText = actionNames[itemIndex] + ": PRESS A KEY..."; + } + label.text = actionText; + } else if (itemIndex == static_cast(actionNames.size())) { + label.text = "BACK"; + } + } + } + } + + void SettingsState::HandleInput() { + auto playSelectSound = [this]() { + if (m_context.audio && m_selectMusic != Audio::INVALID_MUSIC_ID) { + m_context.audio->StopMusic(m_selectMusic); + Audio::PlaybackOptions opts; + opts.volume = 1.0f; + opts.loop = false; + m_context.audio->PlayMusic(m_selectMusic, opts); + } + }; + + if (m_inScreenMenu) { + if (m_renderer->IsKeyPressed(Renderer::Key::Up) && !m_upKeyPressed) { + m_upKeyPressed = true; + playSelectSound(); + m_screenSelectedIndex--; + if (m_screenSelectedIndex < 0) { + m_screenSelectedIndex = static_cast(ScreenItem::COUNT) - 1; + } + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Up)) { + m_upKeyPressed = false; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Down) && !m_downKeyPressed) { + m_downKeyPressed = true; + playSelectSound(); + m_screenSelectedIndex++; + if (m_screenSelectedIndex >= static_cast(ScreenItem::COUNT)) { + m_screenSelectedIndex = 0; + } + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Down)) { + m_downKeyPressed = false; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Left) && !m_leftKeyPressed) { + m_leftKeyPressed = true; + playSelectSound(); + switch (static_cast(m_screenSelectedIndex)) { + case ScreenItem::RESOLUTION: + changeResolution(-1); + break; + case ScreenItem::FULLSCREEN: + toggleFullscreen(); + break; + case ScreenItem::FRAME_RATE: + changeFrameRate(-1); + break; + default: + break; + } + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Left)) { + m_leftKeyPressed = false; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Right) && !m_rightKeyPressed) { + m_rightKeyPressed = true; + playSelectSound(); + switch (static_cast(m_screenSelectedIndex)) { + case ScreenItem::RESOLUTION: + changeResolution(1); + break; + case ScreenItem::FULLSCREEN: + toggleFullscreen(); + break; + case ScreenItem::FRAME_RATE: + changeFrameRate(1); + break; + default: + break; + } + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Right)) { + m_rightKeyPressed = false; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Enter) && !m_enterKeyPressed) { + m_enterKeyPressed = true; + playSelectSound(); + if (static_cast(m_screenSelectedIndex) == ScreenItem::BACK) { + exitSubMenu(); + } + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Enter)) { + m_enterKeyPressed = false; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Escape) && !m_escKeyPressed) { + m_escKeyPressed = true; + playSelectSound(); + exitSubMenu(); + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Escape)) { + m_escKeyPressed = false; + } + return; + } + + if (m_inAudioMenu) { + if (m_renderer->IsKeyPressed(Renderer::Key::Up) && !m_upKeyPressed) { + m_upKeyPressed = true; + playSelectSound(); + m_audioSelectedIndex--; + if (m_audioSelectedIndex < 0) { + m_audioSelectedIndex = static_cast(AudioItem::COUNT) - 1; + } + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Up)) { + m_upKeyPressed = false; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Down) && !m_downKeyPressed) { + m_downKeyPressed = true; + playSelectSound(); + m_audioSelectedIndex++; + if (m_audioSelectedIndex >= static_cast(AudioItem::COUNT)) { + m_audioSelectedIndex = 0; + } + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Down)) { + m_downKeyPressed = false; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Left) && !m_leftKeyPressed) { + m_leftKeyPressed = true; + playSelectSound(); + switch (static_cast(m_audioSelectedIndex)) { + case AudioItem::MASTER_VOLUME: + changeMasterVolume(-1); + break; + case AudioItem::MUTE: + toggleMute(); + break; + default: + break; + } + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Left)) { + m_leftKeyPressed = false; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Right) && !m_rightKeyPressed) { + m_rightKeyPressed = true; + playSelectSound(); + switch (static_cast(m_audioSelectedIndex)) { + case AudioItem::MASTER_VOLUME: + changeMasterVolume(1); + break; + case AudioItem::MUTE: + toggleMute(); + break; + default: + break; + } + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Right)) { + m_rightKeyPressed = false; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Enter) && !m_enterKeyPressed) { + m_enterKeyPressed = true; + playSelectSound(); + if (static_cast(m_audioSelectedIndex) == AudioItem::BACK) { + exitSubMenu(); + } + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Enter)) { + m_enterKeyPressed = false; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Escape) && !m_escKeyPressed) { + m_escKeyPressed = true; + playSelectSound(); + exitSubMenu(); + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Escape)) { + m_escKeyPressed = false; + } + return; + } + + if (m_inCommandsMenu) { + if (m_waitingForRebind) { + processRebind(); + return; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Up) && !m_upKeyPressed) { + m_upKeyPressed = true; + playSelectSound(); + m_commandsSelectedIndex--; + if (m_commandsSelectedIndex < 0) { + m_commandsSelectedIndex = static_cast(CommandsItem::COUNT) - 1; + } + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Up)) { + m_upKeyPressed = false; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Down) && !m_downKeyPressed) { + m_downKeyPressed = true; + playSelectSound(); + m_commandsSelectedIndex++; + if (m_commandsSelectedIndex >= static_cast(CommandsItem::COUNT)) { + m_commandsSelectedIndex = 0; + } + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Down)) { + m_downKeyPressed = false; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Enter) && !m_enterKeyPressed) { + m_enterKeyPressed = true; + playSelectSound(); + if (static_cast(m_commandsSelectedIndex) == CommandsItem::BACK) { + exitSubMenu(); + } else if (m_commandsSelectedIndex >= 0 && m_commandsSelectedIndex < 5) { + startRebind(m_commandsSelectedIndex); + } + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Enter)) { + m_enterKeyPressed = false; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Escape) && !m_escKeyPressed) { + m_escKeyPressed = true; + playSelectSound(); + exitSubMenu(); + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Escape)) { + m_escKeyPressed = false; + } + return; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Up) && !m_upKeyPressed) { + m_upKeyPressed = true; + playSelectSound(); + m_selectedIndex--; + if (m_selectedIndex < 0) { + m_selectedIndex = static_cast(SettingsItem::COUNT) - 1; + } + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Up)) { + m_upKeyPressed = false; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Down) && !m_downKeyPressed) { + m_downKeyPressed = true; + playSelectSound(); + m_selectedIndex++; + if (m_selectedIndex >= static_cast(SettingsItem::COUNT)) { + m_selectedIndex = 0; + } + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Down)) { + m_downKeyPressed = false; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Enter) && !m_enterKeyPressed) { + m_enterKeyPressed = true; + playSelectSound(); + + switch (static_cast(m_selectedIndex)) { + case SettingsItem::COLOUR_BLIND: + toggleColourBlindMode(); + break; + + case SettingsItem::SCREEN: + enterSubMenu(true); + break; + + case SettingsItem::AUDIO: + enterSubMenu(false); + break; + + case SettingsItem::COMMANDS: + enterCommandsMenu(); + break; + + case SettingsItem::BACK: + m_machine.PopState(); + break; + + default: + break; + } + + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Enter)) { + m_enterKeyPressed = false; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Escape) && !m_escKeyPressed) { + m_escKeyPressed = true; + playSelectSound(); + m_machine.PopState(); + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Escape)) { + m_escKeyPressed = false; + } + } + + void SettingsState::Update(float dt) { + updateAnimations(dt); + updateMenuSelection(); + + if (m_audioSystem) { + m_audioSystem->Update(m_registry, dt); + } + } + + void SettingsState::Draw() { + m_renderer->Clear({0.05f, 0.05f, 0.1f, 1.0f}); + + m_renderingSystem->Update(m_registry, 0.0f); + m_textSystem->Update(m_registry, 0.0f); + } + + void SettingsState::toggleColourBlindMode() { + m_colourBlindMode = !m_colourBlindMode; + s_colourBlindMode = m_colourBlindMode; + SetColourBlindMode(m_colourBlindMode); + saveSettings(); + std::cout << "[SettingsState] Colour-blind mode: " << (m_colourBlindMode ? "ON" : "OFF") << std::endl; + } + + void SettingsState::saveSettings() { + try { + json j; + j["colourBlindMode"] = m_colourBlindMode; + j["screenWidth"] = m_screenWidth; + j["screenHeight"] = m_screenHeight; + j["fullscreen"] = m_fullscreen; + j["targetFramerate"] = m_targetFramerate; + j["masterVolume"] = m_masterVolume; + j["muted"] = m_muted; + + json inputMappings; + std::vector actionKeys = {"MOVE_UP", "MOVE_DOWN", "MOVE_LEFT", "MOVE_RIGHT", "SHOOT"}; + for (const auto& action : actionKeys) { + Renderer::Key key = Core::InputMapping::GetKey(action); + if (key != Renderer::Key::Unknown) { + inputMappings[action] = keyToString(key); + } + } + j["inputMappings"] = inputMappings; + + std::string filePath = SETTINGS_FILE_PATH; + std::ofstream file(filePath); + if (!file.is_open()) { + filePath = "../" + SETTINGS_FILE_PATH; + file.open(filePath); + } + + if (file.is_open()) { + file << j.dump(4); + file.close(); + Core::Logger::Info("[SettingsState] Settings saved to {}", filePath); + } else { + Core::Logger::Warning("[SettingsState] Failed to save settings to {}", SETTINGS_FILE_PATH); + } + } catch (const std::exception& e) { + Core::Logger::Error("[SettingsState] Error saving settings: {}", e.what()); + } + } + + void SettingsState::loadSettings() { + m_colourBlindMode = s_colourBlindMode; + m_screenWidth = s_screenWidth; + m_screenHeight = s_screenHeight; + m_fullscreen = s_fullscreen; + m_targetFramerate = s_targetFramerate; + m_masterVolume = s_masterVolume; + m_muted = s_muted; + m_volumeBeforeMute = m_masterVolume; + + if (m_context.audio) { + if (m_muted) { + m_context.audio->SetMasterVolume(0.0f); + } else { + m_context.audio->SetMasterVolume(m_masterVolume); + } + } + + m_resolutionIndex = 0; + for (size_t i = 0; i < RESOLUTIONS.size(); i++) { + if (RESOLUTIONS[i].width == m_screenWidth && RESOLUTIONS[i].height == m_screenHeight) { + m_resolutionIndex = static_cast(i); + break; + } + } + + m_framerateIndex = 2; + for (size_t i = 0; i < FRAMERATES.size(); i++) { + if (FRAMERATES[i] == m_targetFramerate) { + m_framerateIndex = static_cast(i); + break; + } + } + } + + void SettingsState::LoadSettingsFromFile() { + try { + std::string filePath = SETTINGS_FILE_PATH; + std::ifstream file(filePath); + if (!file.is_open()) { + filePath = "../" + SETTINGS_FILE_PATH; + file.open(filePath); + } + + if (!file.is_open()) { + Core::Logger::Info("[SettingsState] Settings file not found, using defaults"); + s_colourBlindMode = false; + SetColourBlindMode(false); + s_screenWidth = 1280; + s_screenHeight = 720; + s_fullscreen = false; + s_targetFramerate = 60; + s_masterVolume = 1.0f; + s_muted = false; + return; + } + + json j; + file >> j; + file.close(); + + if (j.contains("colourBlindMode")) { + s_colourBlindMode = j["colourBlindMode"].get(); + SetColourBlindMode(s_colourBlindMode); + } else { + s_colourBlindMode = false; + SetColourBlindMode(false); + } + + if (j.contains("screenWidth")) { + s_screenWidth = j["screenWidth"].get(); + } else { + s_screenWidth = 1280; + } + if (j.contains("screenHeight")) { + s_screenHeight = j["screenHeight"].get(); + } else { + s_screenHeight = 720; + } + if (j.contains("fullscreen")) { + s_fullscreen = j["fullscreen"].get(); + } else { + s_fullscreen = false; + } + if (j.contains("targetFramerate")) { + s_targetFramerate = j["targetFramerate"].get(); + } else { + s_targetFramerate = 60; + } + + if (j.contains("masterVolume")) { + s_masterVolume = j["masterVolume"].get(); + } else { + s_masterVolume = 1.0f; + } + + if (j.contains("muted")) { + s_muted = j["muted"].get(); + } else { + s_muted = false; + } + + if (j.contains("inputMappings") && j["inputMappings"].is_object()) { + for (auto& [action, keyStr] : j["inputMappings"].items()) { + Renderer::Key key = stringToKey(keyStr.get()); + if (key != Renderer::Key::Unknown) { + Core::InputMapping::SetKey(action, key); + } + } + } + + Core::Logger::Info("[SettingsState] Settings loaded: colourBlindMode={}, resolution={}x{}, fullscreen={}, framerate={}, masterVolume={}, muted={}", + s_colourBlindMode, s_screenWidth, s_screenHeight, s_fullscreen, s_targetFramerate, s_masterVolume, s_muted); + } catch (const std::exception& e) { + Core::Logger::Error("[SettingsState] Error loading settings: {}", e.what()); + s_colourBlindMode = false; + SetColourBlindMode(false); + s_screenWidth = 1280; + s_screenHeight = 720; + s_fullscreen = false; + s_targetFramerate = 60; + s_masterVolume = 1.0f; + s_muted = false; + } + } + + bool SettingsState::IsColourBlindModeEnabled() { + return s_colourBlindMode; + } + + void SettingsState::SetColourBlindMode(bool enabled) { + s_colourBlindMode = enabled; + Core::ColorFilter::SetColourBlindMode(enabled); + } + + void SettingsState::clearMenuUI() { + for (auto it = m_entities.begin(); it != m_entities.end();) { + Entity entity = *it; + if (m_registry.IsEntityAlive(entity)) { + bool isBackground = false; + if (m_registry.HasComponent(entity)) { + const auto& drawable = m_registry.GetComponent(entity); + if (drawable.layer == -10) { + isBackground = true; + } + } + if (!isBackground) { + m_registry.DestroyEntity(entity); + it = m_entities.erase(it); + } else { + ++it; + } + } else { + it = m_entities.erase(it); + } + } + } + + void SettingsState::enterSubMenu(bool screen) { + for (Entity entity : m_menuItems) { + if (m_registry.IsEntityAlive(entity)) { + m_registry.DestroyEntity(entity); + } + } + m_menuItems.clear(); + + if (m_titleEntity != RType::ECS::NULL_ENTITY && m_registry.IsEntityAlive(m_titleEntity)) { + m_registry.DestroyEntity(m_titleEntity); + m_titleEntity = RType::ECS::NULL_ENTITY; + } + + clearMenuUI(); + + if (screen) { + m_inScreenMenu = true; + m_screenSelectedIndex = 0; + createScreenUI(); + } else { + m_inAudioMenu = true; + m_audioSelectedIndex = 0; + createAudioUI(); + } + } + + void SettingsState::exitSubMenu() { + std::vector* menuItems = nullptr; + if (m_inScreenMenu) { + menuItems = &m_screenMenuItems; + } else if (m_inAudioMenu) { + menuItems = &m_audioMenuItems; + } else if (m_inCommandsMenu) { + menuItems = &m_commandsMenuItems; + } + + if (menuItems) { + for (Entity entity : *menuItems) { + if (m_registry.IsEntityAlive(entity)) { + m_registry.DestroyEntity(entity); + } + } + menuItems->clear(); + } + + clearMenuUI(); + + int savedSelectedIndex = m_selectedIndex; + if (m_inScreenMenu) { + savedSelectedIndex = static_cast(SettingsItem::SCREEN); + } else if (m_inAudioMenu) { + savedSelectedIndex = static_cast(SettingsItem::AUDIO); + } else if (m_inCommandsMenu) { + savedSelectedIndex = static_cast(SettingsItem::COMMANDS); + } + + m_inScreenMenu = false; + m_inAudioMenu = false; + m_inCommandsMenu = false; + m_waitingForRebind = false; + m_rebindingActionIndex = -1; + m_screenSelectedIndex = 0; + m_audioSelectedIndex = 0; + m_commandsSelectedIndex = 0; + m_selectedIndex = savedSelectedIndex; + createUI(); + updateMenuSelection(); + } + + void SettingsState::enterCommandsMenu() { + clearMenuUI(); + m_inCommandsMenu = true; + m_commandsSelectedIndex = 0; + m_waitingForRebind = false; + m_rebindingActionIndex = -1; + createCommandsUI(); + } + + void SettingsState::startRebind(int actionIndex) { + m_waitingForRebind = true; + m_rebindingActionIndex = actionIndex; + m_enterKeyPressed = true; + createCommandsUI(); + } + + void SettingsState::processRebind() { + if (m_renderer->IsKeyPressed(Renderer::Key::Enter)) { + m_enterKeyPressed = true; + return; + } + + if (m_enterKeyPressed && !m_renderer->IsKeyPressed(Renderer::Key::Enter)) { + m_enterKeyPressed = false; + return; + } + + for (Renderer::Key key : REBINDABLE_KEYS) { + if (m_renderer->IsKeyPressed(key)) { + if (key == Renderer::Key::Escape) { + m_waitingForRebind = false; + m_rebindingActionIndex = -1; + m_enterKeyPressed = false; + createCommandsUI(); + return; + } + + if (m_rebindingActionIndex >= 0 && m_rebindingActionIndex < static_cast(ACTION_KEYS.size())) { + Core::InputMapping::SetKey(ACTION_KEYS[m_rebindingActionIndex], key); + saveSettings(); + m_waitingForRebind = false; + m_rebindingActionIndex = -1; + m_enterKeyPressed = false; + createCommandsUI(); + } + return; + } + } + } + + std::string SettingsState::keyToString(Renderer::Key key) { + auto it = KEY_TO_STRING.find(key); + return (it != KEY_TO_STRING.end()) ? it->second : "UNKNOWN"; + } + + Renderer::Key SettingsState::stringToKey(const std::string& str) { + auto it = STRING_TO_KEY.find(str); + return (it != STRING_TO_KEY.end()) ? it->second : Renderer::Key::Unknown; + } + + + void SettingsState::changeResolution(int direction) { + m_resolutionIndex += direction; + if (m_resolutionIndex < 0) { + m_resolutionIndex = static_cast(RESOLUTIONS.size()) - 1; + } else if (m_resolutionIndex >= static_cast(RESOLUTIONS.size())) { + m_resolutionIndex = 0; + } + m_screenWidth = RESOLUTIONS[m_resolutionIndex].width; + m_screenHeight = RESOLUTIONS[m_resolutionIndex].height; + applyScreenSettings(); + } + + void SettingsState::toggleFullscreen() { + m_fullscreen = !m_fullscreen; + applyScreenSettings(); + } + + void SettingsState::changeFrameRate(int direction) { + m_framerateIndex += direction; + if (m_framerateIndex < 0) { + m_framerateIndex = static_cast(FRAMERATES.size()) - 1; + } else if (m_framerateIndex >= static_cast(FRAMERATES.size())) { + m_framerateIndex = 0; + } + m_targetFramerate = FRAMERATES[m_framerateIndex]; + applyScreenSettings(); + } + + void SettingsState::changeMasterVolume(int direction) { + if (m_muted) { + return; + } + + float step = 0.05f; + m_masterVolume += direction * step; + m_masterVolume = std::clamp(m_masterVolume, 0.0f, 1.0f); + + if (m_context.audio) { + m_context.audio->SetMasterVolume(m_masterVolume); + } + + s_masterVolume = m_masterVolume; + saveSettings(); + } + + void SettingsState::toggleMute() { + m_muted = !m_muted; + + if (m_context.audio) { + if (m_muted) { + m_volumeBeforeMute = m_masterVolume; + m_context.audio->SetMasterVolume(0.0f); + } else { + m_masterVolume = m_volumeBeforeMute; + m_context.audio->SetMasterVolume(m_masterVolume); + } + } + + s_muted = m_muted; + s_masterVolume = m_masterVolume; + saveSettings(); + } + + void SettingsState::applyScreenSettings() { + Renderer::WindowConfig config; + config.title = "R-Type - " + m_context.playerName; + config.width = m_screenWidth; + config.height = m_screenHeight; + config.fullscreen = m_fullscreen; + config.resizable = !m_fullscreen; + config.targetFramerate = m_targetFramerate; + + s_screenWidth = m_screenWidth; + s_screenHeight = m_screenHeight; + s_fullscreen = m_fullscreen; + s_targetFramerate = m_targetFramerate; + + m_renderer->Destroy(); + if (!m_renderer->CreateWindow(config)) { + Core::Logger::Error("[SettingsState] Failed to recreate window with new settings"); + m_screenWidth = 1280; + m_screenHeight = 720; + m_fullscreen = false; + m_targetFramerate = 60; + m_resolutionIndex = 0; + m_framerateIndex = 2; + s_screenWidth = 1280; + s_screenHeight = 720; + s_fullscreen = false; + s_targetFramerate = 60; + config.width = 1280; + config.height = 720; + config.fullscreen = false; + config.targetFramerate = 60; + m_renderer->CreateWindow(config); + } + + saveSettings(); + } + + std::uint32_t SettingsState::GetScreenWidth() { + return s_screenWidth; + } + + std::uint32_t SettingsState::GetScreenHeight() { + return s_screenHeight; + } + + bool SettingsState::GetFullscreen() { + return s_fullscreen; + } + + std::uint32_t SettingsState::GetTargetFramerate() { + return s_targetFramerate; + } + + float SettingsState::GetMasterVolume() { + return s_masterVolume; + } + + bool SettingsState::IsMuted() { + return s_muted; + } + + } +} + diff --git a/games/rtype/client/src/editor/EditorAssetLibrary.cpp b/games/rtype/client/src/editor/EditorAssetLibrary.cpp new file mode 100644 index 0000000..d06c885 --- /dev/null +++ b/games/rtype/client/src/editor/EditorAssetLibrary.cpp @@ -0,0 +1,119 @@ +#include "editor/EditorAssetLibrary.hpp" +#include "Core/Logger.hpp" + +namespace RType { + namespace Client { + + namespace { + std::string ResolvePath(const std::string& relativePath) { + return "../" + relativePath; + } + } + + EditorAssetLibrary::EditorAssetLibrary(Renderer::IRenderer* renderer) + : m_renderer(renderer) + { + } + + bool EditorAssetLibrary::Initialize() { + auto definitions = buildDefaultDefinitions(); + bool anyLoaded = false; + + for (auto& def : definitions) { + EditorAssetResource resource; + resource.definition = std::move(def); + if (loadResource(resource)) { + const std::string& id = resource.definition.id; + m_resourcesById.emplace(id, std::move(resource)); + anyLoaded = true; + } else { + Core::Logger::Warning("[EditorAssetLibrary] Failed to load asset '{}'", resource.definition.id); + } + } + + m_resourcesByType.clear(); + for (auto& [id, resource] : m_resourcesById) { + m_resourcesByType[static_cast(resource.definition.type)].push_back(&resource); + } + + return anyLoaded; + } + + const EditorAssetResource* EditorAssetLibrary::GetResource(const std::string& id) const { + auto it = m_resourcesById.find(id); + if (it == m_resourcesById.end()) { + return nullptr; + } + return &it->second; + } + + const std::vector& EditorAssetLibrary::GetResources(EditorEntityType type) const { + static const std::vector empty; + auto it = m_resourcesByType.find(static_cast(type)); + if (it == m_resourcesByType.end()) { + return empty; + } + return it->second; + } + + bool EditorAssetLibrary::loadResource(EditorAssetResource& resource) { + if (!m_renderer) { + return false; + } + + const std::string fullPath = ResolvePath(resource.definition.texturePath); + resource.textureId = m_renderer->LoadTexture(fullPath); + if (resource.textureId == Renderer::INVALID_TEXTURE_ID) { + resource.textureId = m_renderer->LoadTexture(resource.definition.texturePath); + } + + if (resource.textureId == Renderer::INVALID_TEXTURE_ID) { + return false; + } + + resource.spriteId = m_renderer->CreateSprite(resource.textureId, {}); + resource.textureSize = m_renderer->GetTextureSize(resource.textureId); + + if (resource.definition.defaultSize.x <= 0.0f || resource.definition.defaultSize.y <= 0.0f) { + resource.definition.defaultSize = resource.textureSize; + } + + return resource.spriteId != Renderer::INVALID_SPRITE_ID; + } + + std::vector EditorAssetLibrary::buildDefaultDefinitions() const { + return { + // Obstacles + {"obstacle1", "Obstacle 1", EditorEntityType::OBSTACLE, "assets/backgrounds/obstacles/middle_obstacle.png", {441.0f, 200.0f}, -50.0f, 1, "", ""}, + {"obstacle2", "Obstacle 2", EditorEntityType::OBSTACLE, "assets/backgrounds/obstacles/obstacle_bas.png", {441.0f, 120.0f}, -50.0f, 1, "", ""}, + {"obstacle3", "Obstacle 3", EditorEntityType::OBSTACLE, "assets/backgrounds/obstacles/little_obstacle.png", {250.0f, 150.0f}, -50.0f, 1, "", ""}, + {"obstacle4", "Obstacle 4", EditorEntityType::OBSTACLE, "assets/backgrounds/obstacles/obstacle_square.png", {300.0f, 300.0f}, -50.0f, 1, "", ""}, + {"obstacle5", "Obstacle 5", EditorEntityType::OBSTACLE, "assets/backgrounds/obstacles/obstacle_bas_two.png", {441.0f, 120.0f}, -50.0f, 1, "", ""}, + {"obstacle6", "Obstacle 6", EditorEntityType::OBSTACLE, "assets/backgrounds/obstacles/obstacle_haut_two.png", {441.0f, 100.0f}, -50.0f, 1, "", ""}, + {"obstacle7", "Obstacle 7", EditorEntityType::OBSTACLE, "assets/backgrounds/obstacles/obstacle_yellow.png", {350.0f, 220.0f}, -50.0f, 1, "", ""}, + {"obstacle8", "Obstacle 8", EditorEntityType::OBSTACLE, "assets/backgrounds/obstacles/little_obstacle_haut.png", {250.0f, 120.0f}, -50.0f, 1, "", ""}, + + // Enemies + {"enemy_blue", "Enemy Blue", EditorEntityType::ENEMY, "assets/spaceships/enemy-blue.png", {96.0f, 96.0f}, -10.0f, 5, "BASIC", ""}, + {"enemy_red", "Enemy Red", EditorEntityType::ENEMY, "assets/spaceships/enemy-red.png", {96.0f, 96.0f}, -10.0f, 5, "FAST", ""}, + {"enemy_green", "Enemy Green", EditorEntityType::ENEMY, "assets/spaceships/enemy-green.png", {96.0f, 96.0f}, -10.0f, 5, "TANK", ""}, + {"enemy_boss", "Enemy Boss", EditorEntityType::ENEMY, "assets/spaceships/enemy-purple-boss.png", {200.0f, 200.0f}, -15.0f, 5, "BOSS", ""}, + + // Power-ups + {"power_spread", "Power Spread", EditorEntityType::POWERUP, "assets/powerups/spread.png", {64.0f, 64.0f}, -50.0f, 2, "", "SPREAD_SHOT"}, + {"power_laser", "Power Laser", EditorEntityType::POWERUP, "assets/powerups/laser.png", {64.0f, 64.0f}, -50.0f, 2, "", "LASER_BEAM"}, + {"power_force", "Power Force Pod", EditorEntityType::POWERUP, "assets/powerups/force_pod.png", {64.0f, 64.0f}, -50.0f, 2, "", "FORCE_POD"}, + {"power_speed", "Power Speed", EditorEntityType::POWERUP, "assets/powerups/speed.png", {64.0f, 64.0f}, -50.0f, 2, "", "SPEED_BOOST"}, + {"power_shield", "Power Shield", EditorEntityType::POWERUP, "assets/powerups/shield.png", {64.0f, 64.0f}, -50.0f, 2, "", "SHIELD"}, + + // Player spawn + {"player_spawn", "Player Spawn", EditorEntityType::PLAYER_SPAWN, "assets/spaceships/player_blue.png", {96.0f, 96.0f}, 0.0f, 10, "", ""}, + + // Background + {"background_space", "Space Background", EditorEntityType::BACKGROUND, "assets/backgrounds/Cave_one.png", {1280.0f, 720.0f}, -20.0f, -100, "", ""}, + }; + } + + } +} + diff --git a/games/rtype/client/src/editor/EditorCanvasManager.cpp b/games/rtype/client/src/editor/EditorCanvasManager.cpp new file mode 100644 index 0000000..0478121 --- /dev/null +++ b/games/rtype/client/src/editor/EditorCanvasManager.cpp @@ -0,0 +1,116 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** EditorCanvasManager +*/ + +#include "editor/EditorCanvasManager.hpp" +#include "editor/EditorConstants.hpp" +#include +#include + +using namespace RType::Client::EditorConstants; + +namespace RType { + namespace Client { + + EditorCanvasManager::EditorCanvasManager(Renderer::IRenderer* renderer) + : m_renderer(renderer) + { + } + + void EditorCanvasManager::HandleCameraInput() { + const float panSpeed = Camera::PAN_SPEED; + const float zoomSpeed = Camera::ZOOM_SPEED; + + // Camera panning (continuous while key is held) + if (m_renderer->IsKeyPressed(Renderer::Key::Up)) { + m_camera.y -= panSpeed * (1.0f / 60.0f) / m_camera.zoom; + } + if (m_renderer->IsKeyPressed(Renderer::Key::Down)) { + m_camera.y += panSpeed * (1.0f / 60.0f) / m_camera.zoom; + } + if (m_renderer->IsKeyPressed(Renderer::Key::Left)) { + m_camera.x -= panSpeed * (1.0f / 60.0f) / m_camera.zoom; + } + if (m_renderer->IsKeyPressed(Renderer::Key::Right)) { + m_camera.x += panSpeed * (1.0f / 60.0f) / m_camera.zoom; + } + + static bool zoomInKeyPressed = false; + static bool zoomOutKeyPressed = false; + + bool isZoomInPressed = m_renderer->IsKeyPressed(Renderer::Key::X); + if (isZoomInPressed && !zoomInKeyPressed) { + zoomInKeyPressed = true; + m_camera.zoom += zoomSpeed; + } else if (!isZoomInPressed) { + zoomInKeyPressed = false; + } + + bool isZoomOutPressed = m_renderer->IsKeyPressed(Renderer::Key::C); + if (isZoomOutPressed && !zoomOutKeyPressed) { + zoomOutKeyPressed = true; + m_camera.zoom -= zoomSpeed; + } else if (!isZoomOutPressed) { + zoomOutKeyPressed = false; + } + + m_camera.x = std::clamp(m_camera.x, m_camera.minX, m_camera.maxX); + m_camera.y = std::clamp(m_camera.y, m_camera.minY, m_camera.maxY); + m_camera.zoom = std::clamp(m_camera.zoom, m_camera.minZoom, m_camera.maxZoom); + } + + void EditorCanvasManager::ApplyCamera() { + Renderer::Camera2D cam; + cam.center = {m_camera.x, m_camera.y}; + cam.size = {1280.0f / m_camera.zoom, 720.0f / m_camera.zoom}; + m_renderer->SetCamera(cam); + } + + void EditorCanvasManager::DrawGrid() { + if (!m_grid.enabled) { + return; + } + + Math::Vector2 topLeft = ScreenToWorld({0.0f, 0.0f}); + Math::Vector2 bottomRight = ScreenToWorld({1280.0f, 720.0f}); + + float startX = std::floor(topLeft.x / m_grid.cellSize) * m_grid.cellSize; + float startY = std::floor(topLeft.y / m_grid.cellSize) * m_grid.cellSize; + float endX = std::ceil(bottomRight.x / m_grid.cellSize) * m_grid.cellSize; + float endY = std::ceil(bottomRight.y / m_grid.cellSize) * m_grid.cellSize; + + for (float x = startX; x <= endX; x += m_grid.cellSize) { + Math::Rectangle rect = {{x, startY}, {1.0f, endY - startY}}; + m_renderer->DrawRectangle(rect, m_grid.gridColor); + } + + for (float y = startY; y <= endY; y += m_grid.cellSize) { + Math::Rectangle rect = {{startX, y}, {endX - startX, 1.0f}}; + m_renderer->DrawRectangle(rect, m_grid.gridColor); + } + } + + Math::Vector2 EditorCanvasManager::ScreenToWorld(Math::Vector2 screenPos) const { + float worldX = (screenPos.x - 640.0f) / m_camera.zoom + m_camera.x; + float worldY = (screenPos.y - 360.0f) / m_camera.zoom + m_camera.y; + return {worldX, worldY}; + } + + Math::Vector2 EditorCanvasManager::WorldToScreen(Math::Vector2 worldPos) const { + float screenX = (worldPos.x - m_camera.x) * m_camera.zoom + 640.0f; + float screenY = (worldPos.y - m_camera.y) * m_camera.zoom + 360.0f; + return {screenX, screenY}; + } + + float EditorCanvasManager::SnapToGrid(float value) const { + if (!m_grid.snapToGrid) { + return value; + } + return std::round(value / m_grid.cellSize) * m_grid.cellSize; + } + + } +} diff --git a/games/rtype/client/src/editor/EditorColliderManager.cpp b/games/rtype/client/src/editor/EditorColliderManager.cpp new file mode 100644 index 0000000..8e474d6 --- /dev/null +++ b/games/rtype/client/src/editor/EditorColliderManager.cpp @@ -0,0 +1,189 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** EditorColliderManager +*/ + +#include "editor/EditorColliderManager.hpp" +#include "editor/EditorConstants.hpp" +#include "editor/EditorGeometry.hpp" +#include "editor/EditorDrawing.hpp" +#include + +using namespace RType::Client::EditorConstants; + +namespace RType { + namespace Client { + + EditorColliderManager::EditorColliderManager(Renderer::IRenderer* renderer) + : m_renderer(renderer) + { + } + + void EditorColliderManager::DrawColliders(const EditorEntityData* entity, int selectedIndex) const { + if (!entity || !m_renderer) { + return; + } + + for (size_t i = 0; i < entity->colliders.size(); ++i) { + const auto& collider = entity->colliders[i]; + bool isSelected = (static_cast(i) == selectedIndex); + Math::Color color = isSelected ? Collider::COLLIDER_SELECTED : Collider::COLLIDER_NORMAL; + drawCollider(collider, color); + } + } + + void EditorColliderManager::DrawHandles(const EditorEntityData* entity, int colliderIndex) const { + if (!entity || !m_renderer || colliderIndex < 0 || colliderIndex >= static_cast(entity->colliders.size())) { + return; + } + + const auto& collider = entity->colliders[static_cast(colliderIndex)]; + + // Draw handles at the four corners + drawHandle({collider.x, collider.y}); + drawHandle({collider.x + collider.width, collider.y}); + drawHandle({collider.x, collider.y + collider.height}); + drawHandle({collider.x + collider.width, collider.y + collider.height}); + } + + ColliderHandle EditorColliderManager::GetHandleAt(const EditorEntityData* entity, + const Math::Vector2& worldPos) const { + if (!entity) { + return {}; + } + + const float hs = Collider::HANDLE_SIZE / 2.0f; + + for (size_t i = 0; i < entity->colliders.size(); ++i) { + const auto& collider = entity->colliders[i]; + + // Check corner handles first (they have priority over body) + Math::Rectangle tlHandle = EditorGeometry::BuildRect( + collider.x - hs, collider.y - hs, Collider::HANDLE_SIZE, Collider::HANDLE_SIZE); + Math::Rectangle trHandle = EditorGeometry::BuildRect( + collider.x + collider.width - hs, collider.y - hs, Collider::HANDLE_SIZE, Collider::HANDLE_SIZE); + Math::Rectangle blHandle = EditorGeometry::BuildRect( + collider.x - hs, collider.y + collider.height - hs, Collider::HANDLE_SIZE, Collider::HANDLE_SIZE); + Math::Rectangle brHandle = EditorGeometry::BuildRect( + collider.x + collider.width - hs, collider.y + collider.height - hs, Collider::HANDLE_SIZE, Collider::HANDLE_SIZE); + + if (EditorGeometry::PointInRect(worldPos, tlHandle)) { + return {static_cast(i), ColliderHandle::Type::TOP_LEFT}; + } + if (EditorGeometry::PointInRect(worldPos, trHandle)) { + return {static_cast(i), ColliderHandle::Type::TOP_RIGHT}; + } + if (EditorGeometry::PointInRect(worldPos, blHandle)) { + return {static_cast(i), ColliderHandle::Type::BOTTOM_LEFT}; + } + if (EditorGeometry::PointInRect(worldPos, brHandle)) { + return {static_cast(i), ColliderHandle::Type::BOTTOM_RIGHT}; + } + + // Check body last + Math::Rectangle bodyRect = getColliderRect(collider); + if (EditorGeometry::PointInRect(worldPos, bodyRect)) { + return {static_cast(i), ColliderHandle::Type::BODY}; + } + } + + return {}; + } + + int EditorColliderManager::AddCollider(EditorEntityData& entity, const Math::Vector2& worldPos) { + // Create a default-sized collider at the specified position + ECS::ColliderDef newCollider; + newCollider.x = worldPos.x - 25.0f; + newCollider.y = worldPos.y - 25.0f; + newCollider.width = 50.0f; + newCollider.height = 50.0f; + + entity.colliders.push_back(newCollider); + + return static_cast(entity.colliders.size()) - 1; + } + + bool EditorColliderManager::RemoveCollider(EditorEntityData& entity, int colliderIndex) { + if (colliderIndex < 0 || colliderIndex >= static_cast(entity.colliders.size())) { + return false; + } + + entity.colliders.erase(entity.colliders.begin() + colliderIndex); + return true; + } + + void EditorColliderManager::ResizeCollider(EditorEntityData& entity, int colliderIndex, + ColliderHandle::Type handleType, + const Math::Vector2& worldPos) { + if (colliderIndex < 0 || colliderIndex >= static_cast(entity.colliders.size())) { + return; + } + + auto& collider = entity.colliders[static_cast(colliderIndex)]; + const float minSize = Collider::MIN_COLLIDER_SIZE; + + float originalRight = collider.x + collider.width; + float originalBottom = collider.y + collider.height; + + switch (handleType) { + case ColliderHandle::Type::TOP_LEFT: + collider.x = std::min(worldPos.x, originalRight - minSize); + collider.y = std::min(worldPos.y, originalBottom - minSize); + collider.width = originalRight - collider.x; + collider.height = originalBottom - collider.y; + break; + + case ColliderHandle::Type::TOP_RIGHT: + collider.y = std::min(worldPos.y, originalBottom - minSize); + collider.width = std::max(worldPos.x - collider.x, minSize); + collider.height = originalBottom - collider.y; + break; + + case ColliderHandle::Type::BOTTOM_LEFT: + collider.x = std::min(worldPos.x, originalRight - minSize); + collider.width = originalRight - collider.x; + collider.height = std::max(worldPos.y - collider.y, minSize); + break; + + case ColliderHandle::Type::BOTTOM_RIGHT: + collider.width = std::max(worldPos.x - collider.x, minSize); + collider.height = std::max(worldPos.y - collider.y, minSize); + break; + + case ColliderHandle::Type::BODY: { + Math::Vector2 delta = {worldPos.x - m_dragStart.x, worldPos.y - m_dragStart.y}; + collider.x += delta.x; + collider.y += delta.y; + m_dragStart = worldPos; + break; + } + + case ColliderHandle::Type::NONE: + break; + } + } + + void EditorColliderManager::drawCollider(const ECS::ColliderDef& collider, const Math::Color& color) const { + // Draw semi-transparent fill + Math::Rectangle colliderRect = EditorGeometry::BuildRect( + collider.x, collider.y, collider.width, collider.height); + Math::Color fillColor = color; + fillColor.a = 0.2f; + m_renderer->DrawRectangle(colliderRect, fillColor); + + // Draw outline using utility + EditorDrawing::DrawCollider(m_renderer, collider, color, Collider::COLLIDER_LINE_THICKNESS); + } + + void EditorColliderManager::drawHandle(const Math::Vector2& pos) const { + EditorDrawing::DrawHandle(m_renderer, pos, Collider::COLLIDER_HANDLE, Collider::HANDLE_SIZE); + } + + Math::Rectangle EditorColliderManager::getColliderRect(const ECS::ColliderDef& collider) const { + return EditorGeometry::BuildRect(collider.x, collider.y, collider.width, collider.height); + } + + } +} diff --git a/games/rtype/client/src/editor/EditorDrawing.cpp b/games/rtype/client/src/editor/EditorDrawing.cpp new file mode 100644 index 0000000..27acb77 --- /dev/null +++ b/games/rtype/client/src/editor/EditorDrawing.cpp @@ -0,0 +1,79 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** EditorDrawing +*/ + +#include "editor/EditorDrawing.hpp" +#include "editor/EditorGeometry.hpp" + +namespace RType { + namespace Client { + namespace EditorDrawing { + + void DrawRectangleOutline(Renderer::IRenderer* renderer, + const Math::Rectangle& rect, + const Math::Color& color, + float thickness) { + if (!renderer) { + return; + } + + // Draw four rectangles for the border (top, bottom, left, right) + Math::Rectangle top = EditorGeometry::BuildRect( + rect.position.x, rect.position.y, rect.size.x, thickness); + Math::Rectangle bottom = EditorGeometry::BuildRect( + rect.position.x, rect.position.y + rect.size.y - thickness, rect.size.x, thickness); + Math::Rectangle left = EditorGeometry::BuildRect( + rect.position.x, rect.position.y, thickness, rect.size.y); + Math::Rectangle right = EditorGeometry::BuildRect( + rect.position.x + rect.size.x - thickness, rect.position.y, thickness, rect.size.y); + + renderer->DrawRectangle(top, color); + renderer->DrawRectangle(bottom, color); + renderer->DrawRectangle(left, color); + renderer->DrawRectangle(right, color); + } + + void DrawCollider(Renderer::IRenderer* renderer, + const ECS::ColliderDef& collider, + const Math::Color& color, + float lineThickness) { + if (!renderer) { + return; + } + + // Draw collider as an outline (same pattern as DrawRectangleOutline) + Math::Rectangle top = EditorGeometry::BuildRect( + collider.x, collider.y, collider.width, lineThickness); + Math::Rectangle bottom = EditorGeometry::BuildRect( + collider.x, collider.y + collider.height - lineThickness, collider.width, lineThickness); + Math::Rectangle left = EditorGeometry::BuildRect( + collider.x, collider.y, lineThickness, collider.height); + Math::Rectangle right = EditorGeometry::BuildRect( + collider.x + collider.width - lineThickness, collider.y, lineThickness, collider.height); + + renderer->DrawRectangle(top, color); + renderer->DrawRectangle(bottom, color); + renderer->DrawRectangle(left, color); + renderer->DrawRectangle(right, color); + } + + void DrawHandle(Renderer::IRenderer* renderer, + const Math::Vector2& position, + const Math::Color& color, + float size) { + if (!renderer) { + return; + } + + const float halfSize = size / 2.0f; + Math::Rectangle handle = EditorGeometry::BuildRect( + position.x - halfSize, position.y - halfSize, size, size); + renderer->DrawRectangle(handle, color); + } + + } + } +} diff --git a/games/rtype/client/src/editor/EditorEntityManager.cpp b/games/rtype/client/src/editor/EditorEntityManager.cpp new file mode 100644 index 0000000..a67e310 --- /dev/null +++ b/games/rtype/client/src/editor/EditorEntityManager.cpp @@ -0,0 +1,334 @@ +#include "editor/EditorEntityManager.hpp" +#include "editor/EditorConstants.hpp" +#include "editor/EditorGeometry.hpp" +#include "editor/EditorDrawing.hpp" +#include "editor/EditorColliderManager.hpp" +#include "ECS/Component.hpp" +#include "Core/Logger.hpp" +#include +#include + +using namespace RType::Client::EditorConstants; + +namespace RType { + namespace Client { + + EditorEntityManager::EditorEntityManager(Renderer::IRenderer* renderer, + RType::ECS::Registry& registry, + EditorAssetLibrary& assets) + : m_renderer(renderer) + , m_registry(registry) + , m_assets(assets) + , m_colliderManager(std::make_unique(renderer)) + { + } + + EditorEntityManager::~EditorEntityManager() = default; + + EditorEntityData* EditorEntityManager::PlaceEntity(const EditorPaletteSelection& selection, const Math::Vector2& worldPos) { + auto* resource = getResource(selection.subtype); + if (!resource) { + Core::Logger::Warning("[EditorEntityManager] Unknown asset '{}'", selection.subtype); + return nullptr; + } + + EditorEntityData data; + data.type = selection.entityType; + data.presetId = resource->definition.id; + data.textureKey = resource->definition.id; + data.scaleWidth = resource->definition.defaultSize.x; + data.scaleHeight = resource->definition.defaultSize.y; + data.x = worldPos.x; + data.y = worldPos.y; + data.scrollSpeed = resource->definition.defaultScrollSpeed; + data.layer = resource->definition.defaultLayer; + data.enemyType = resource->definition.enemyType; + data.powerUpType = resource->definition.powerUpType; + + initializeDefaultCollider(data); + + m_entities.push_back(data); + EditorEntityData& stored = m_entities.back(); + applyEntityToComponents(stored); + + m_selectedIndex = static_cast(m_entities.size()) - 1; + stored.isSelected = true; + + return &stored; + } + + void EditorEntityManager::DrawPlacementPreview(EditorMode mode, + EditorEntityType type, + const std::string& identifier, + const Math::Vector2& worldPos) const { + if (!m_renderer || mode == EditorMode::SELECT) { + return; + } + + Math::Vector2 size = getDefaultSize(type); + if (!identifier.empty()) { + if (const auto* resource = getResource(identifier)) { + size = resource->definition.defaultSize; + } + } + Math::Rectangle rect; + rect.position = {worldPos.x - size.x / 2.0f, worldPos.y - size.y / 2.0f}; + rect.size = {size.x, size.y}; + + Math::Color color = getColor(type); + color.a = 0.35f; + m_renderer->DrawRectangle(rect, color); + } + + void EditorEntityManager::DrawSelectionOutline() const { + if (!m_renderer || !GetSelectedEntity()) { + return; + } + drawOutline(*GetSelectedEntity()); + } + + bool EditorEntityManager::SelectAt(const Math::Vector2& worldPos) { + bool found = false; + for (auto& entity : m_entities) { + entity.isSelected = false; + } + + for (int i = static_cast(m_entities.size()) - 1; i >= 0; --i) { + const auto& entity = m_entities[static_cast(i)]; + if (EditorGeometry::PointInEntityBounds(worldPos, entity.x, entity.y, entity.scaleWidth, entity.scaleHeight)) { + m_entities[static_cast(i)].isSelected = true; + m_selectedIndex = i; + found = true; + break; + } + } + + if (!found) { + m_selectedIndex = -1; + } + + return found; + } + + void EditorEntityManager::ClearSelection() { + for (auto& entity : m_entities) { + entity.isSelected = false; + } + m_selectedIndex = -1; + } + + bool EditorEntityManager::DeleteSelected() { + if (!GetSelectedEntity()) { + return false; + } + + destroyEntity(m_entities[static_cast(m_selectedIndex)]); + m_entities.erase(m_entities.begin() + m_selectedIndex); + + if (m_entities.empty()) { + m_selectedIndex = -1; + } else { + m_selectedIndex = std::min(m_selectedIndex, static_cast(m_entities.size()) - 1); + if (m_selectedIndex >= 0) { + m_entities[static_cast(m_selectedIndex)].isSelected = true; + } + } + + return true; + } + + EditorEntityData* EditorEntityManager::GetSelectedEntity() { + if (m_selectedIndex < 0 || m_selectedIndex >= static_cast(m_entities.size())) { + return nullptr; + } + return &m_entities[static_cast(m_selectedIndex)]; + } + + const EditorEntityData* EditorEntityManager::GetSelectedEntity() const { + if (m_selectedIndex < 0 || m_selectedIndex >= static_cast(m_entities.size())) { + return nullptr; + } + return &m_entities[static_cast(m_selectedIndex)]; + } + + void EditorEntityManager::SyncEntity(EditorEntityData& data) { + applyEntityToComponents(data); + } + + void EditorEntityManager::RebuildDefaultCollider(EditorEntityData& data) { + initializeDefaultCollider(data); + } + + const EditorAssetResource* EditorEntityManager::getResource(const std::string& id) const { + return m_assets.GetResource(id); + } + + void EditorEntityManager::applyEntityToComponents(EditorEntityData& data) { + destroyEntity(data); + + auto* resource = getResource(data.presetId.empty() ? data.textureKey : data.presetId); + if (!resource || !m_renderer) { + return; + } + + float left = data.x - data.scaleWidth / 2.0f; + float top = data.y - data.scaleHeight / 2.0f; + + data.entity = m_registry.CreateEntity(); + m_registry.AddComponent(data.entity, ECS::Position{left, top}); + + auto& drawable = m_registry.AddComponent(data.entity, ECS::Drawable(resource->spriteId, data.layer)); + drawable.scale = {data.scaleWidth / resource->textureSize.x, data.scaleHeight / resource->textureSize.y}; + drawable.origin = {0.0f, 0.0f}; + + if (data.scrollSpeed != 0.0f) { + m_registry.AddComponent(data.entity, ECS::Scrollable(data.scrollSpeed)); + } + + createColliderEntities(data); + } + + void EditorEntityManager::destroyEntity(EditorEntityData& data) { + if (data.entity != ECS::NULL_ENTITY && m_registry.IsEntityAlive(data.entity)) { + m_registry.DestroyEntity(data.entity); + } + data.entity = ECS::NULL_ENTITY; + + for (ECS::Entity collider : data.colliderEntities) { + if (m_registry.IsEntityAlive(collider)) { + m_registry.DestroyEntity(collider); + } + } + data.colliderEntities.clear(); + } + + void EditorEntityManager::drawOutline(const EditorEntityData& entity) const { + Math::Rectangle rect = EditorGeometry::BuildRect( + entity.x - entity.scaleWidth / 2.0f, + entity.y - entity.scaleHeight / 2.0f, + entity.scaleWidth, + entity.scaleHeight); + + EditorDrawing::DrawRectangleOutline(m_renderer, rect, + Selection::OUTLINE_COLOR, Selection::OUTLINE_THICKNESS); + } + + Math::Vector2 EditorEntityManager::getDefaultSize(EditorEntityType type) const { + switch (type) { + case EditorEntityType::ENEMY: + return {80.0f, 60.0f}; + case EditorEntityType::POWERUP: + return {50.0f, 50.0f}; + case EditorEntityType::PLAYER_SPAWN: + return {40.0f, 40.0f}; + case EditorEntityType::BACKGROUND: + return {1280.0f, 720.0f}; + case EditorEntityType::OBSTACLE: + default: + return {120.0f, 80.0f}; + } + } + + Math::Color EditorEntityManager::getColor(EditorEntityType type) const { + switch (type) { + case EditorEntityType::ENEMY: + return {0.95f, 0.35f, 0.35f, 1.0f}; + case EditorEntityType::POWERUP: + return {0.4f, 0.9f, 0.6f, 1.0f}; + case EditorEntityType::PLAYER_SPAWN: + return {0.9f, 0.9f, 0.1f, 1.0f}; + case EditorEntityType::BACKGROUND: + return {0.4f, 0.5f, 0.95f, 1.0f}; + case EditorEntityType::OBSTACLE: + default: + return {0.6f, 0.7f, 0.95f, 1.0f}; + } + } + + void EditorEntityManager::DrawColliders(int selectedColliderIndex) const { + if (m_colliderManager) { + m_colliderManager->DrawColliders(GetSelectedEntity(), selectedColliderIndex); + } + } + + void EditorEntityManager::DrawColliderHandles(int colliderIndex) const { + if (m_colliderManager) { + m_colliderManager->DrawHandles(GetSelectedEntity(), colliderIndex); + } + } + + ColliderHandle EditorEntityManager::GetColliderHandleAt(const Math::Vector2& worldPos) const { + if (m_colliderManager) { + return m_colliderManager->GetHandleAt(GetSelectedEntity(), worldPos); + } + return {}; + } + + void EditorEntityManager::AddCollider(const Math::Vector2& worldPos) { + auto* entity = GetSelectedEntity(); + if (!entity || !m_colliderManager) { + return; + } + + m_selectedColliderIndex = m_colliderManager->AddCollider(*entity, worldPos); + SyncEntity(*entity); + } + + bool EditorEntityManager::RemoveCollider(int colliderIndex) { + auto* entity = GetSelectedEntity(); + if (!entity || !m_colliderManager) { + return false; + } + + if (m_colliderManager->RemoveCollider(*entity, colliderIndex)) { + SyncEntity(*entity); + + // Update selected collider index + if (m_selectedColliderIndex == colliderIndex) { + m_selectedColliderIndex = -1; + } else if (m_selectedColliderIndex > colliderIndex) { + m_selectedColliderIndex--; + } + + return true; + } + + return false; + } + + void EditorEntityManager::ResizeCollider(int colliderIndex, ColliderHandle::Type handleType, const Math::Vector2& worldPos) { + auto* entity = GetSelectedEntity(); + if (!entity || !m_colliderManager) { + return; + } + + m_colliderManager->ResizeCollider(*entity, colliderIndex, handleType, worldPos); + SyncEntity(*entity); + } + + void EditorEntityManager::initializeDefaultCollider(EditorEntityData& data) const { + data.colliders.clear(); + ECS::ColliderDef collider; + collider.x = data.x - data.scaleWidth / 2.0f; + collider.y = data.y - data.scaleHeight / 2.0f; + collider.width = data.scaleWidth; + collider.height = data.scaleHeight; + data.colliders.push_back(collider); + } + + void EditorEntityManager::createColliderEntities(EditorEntityData& data) { + data.colliderEntities.clear(); + for (const auto& collider : data.colliders) { + ECS::Entity colliderEntity = m_registry.CreateEntity(); + m_registry.AddComponent(colliderEntity, ECS::Position{collider.x, collider.y}); + m_registry.AddComponent(colliderEntity, ECS::BoxCollider{collider.width, collider.height}); + m_registry.AddComponent(colliderEntity, ECS::Scrollable(data.scrollSpeed)); + m_registry.AddComponent(colliderEntity, ECS::Obstacle(true)); + m_registry.AddComponent(colliderEntity, ECS::CollisionLayer(ECS::CollisionLayers::OBSTACLE, ECS::CollisionLayers::ALL)); + data.colliderEntities.push_back(colliderEntity); + } + } + + } +} + diff --git a/games/rtype/client/src/editor/EditorFileManager.cpp b/games/rtype/client/src/editor/EditorFileManager.cpp new file mode 100644 index 0000000..9eec9a1 --- /dev/null +++ b/games/rtype/client/src/editor/EditorFileManager.cpp @@ -0,0 +1,269 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** EditorFileManager - Save/Load level files implementation +*/ + +#include "editor/EditorFileManager.hpp" +#include "ECS/Component.hpp" +#include "Core/Logger.hpp" +#include +#include + +namespace RType { + namespace Client { + + EditorFileManager::EditorFileManager(EditorAssetLibrary& assets) + : m_assets(assets), m_lastError("") { + } + + bool EditorFileManager::SaveLevel(const std::string& path, + const std::vector& entities, + const std::string& levelName) { + m_lastError.clear(); + ECS::LevelData levelData = GatherLevelData(entities, levelName); + + std::vector candidatePaths; + candidatePaths.emplace_back(std::filesystem::path("..") / path); + candidatePaths.emplace_back(path); + + std::string lastFailure; + + for (const auto& candidate : candidatePaths) { + try { + if (candidate.has_parent_path()) { + std::error_code ec; + std::filesystem::create_directories(candidate.parent_path(), ec); + if (ec) { + throw std::runtime_error("Failed to create directories for " + + candidate.parent_path().string() + ": " + ec.message()); + } + } + + ECS::LevelLoader::SaveToFile(levelData, candidate.string()); + + Core::Logger::Info("[EditorFileManager] Level '{}' saved to {}", + levelName, candidate.string()); + return true; + + } catch (const std::exception& e) { + lastFailure = e.what(); + Core::Logger::Warning("[EditorFileManager] Save attempt failed for {}: {}", + candidate.string(), e.what()); + } + } + + m_lastError = lastFailure.empty() + ? "Save failed: no valid path candidates" + : std::string("Save failed: ") + lastFailure; + Core::Logger::Error("[EditorFileManager] {}", m_lastError); + return false; + } + + std::vector EditorFileManager::LoadLevel(const std::string& path, + ECS::Registry& registry, + Renderer::IRenderer* renderer) { + std::vector result; + + try { + m_lastError.clear(); + + ECS::LevelData levelData = ECS::LevelLoader::LoadFromFile(path); + + if (!levelData.background.texture.empty()) { + result.push_back(ConvertBackgroundToEditor(levelData.background, registry, renderer)); + } + + for (const auto& obs : levelData.obstacles) { + result.push_back(ConvertObstacleToEditor(obs, registry, renderer)); + } + + for (const auto& enemy : levelData.enemies) { + result.push_back(ConvertEnemyToEditor(enemy, registry, renderer)); + } + + for (const auto& spawn : levelData.playerSpawns) { + result.push_back(ConvertSpawnToEditor(spawn, registry, renderer)); + } + + Core::Logger::Info("[EditorFileManager] Successfully loaded {} entities from {}", + result.size(), path); + + } catch (const std::exception& e) { + m_lastError = std::string("Load failed: ") + e.what(); + Core::Logger::Error("[EditorFileManager] {}", m_lastError); + result.clear(); + } + + return result; + } + + ECS::LevelData EditorFileManager::GatherLevelData(const std::vector& entities, + const std::string& levelName) { + ECS::LevelData level; + level.name = levelName; + + level.textures["background"] = "assets/backgrounds/Cave_one.png"; + level.textures["obstacle1"] = "assets/backgrounds/obstacles/middle_obstacle.png"; + level.textures["obstacle2"] = "assets/backgrounds/obstacles/obstacle_bas.png"; + level.textures["obstacle3"] = "assets/backgrounds/obstacles/little_obstacle.png"; + level.textures["obstacle4"] = "assets/backgrounds/obstacles/obstacle_square.png"; + level.textures["obstacle5"] = "assets/backgrounds/obstacles/obstacle_bas_two.png"; + level.textures["obstacle6"] = "assets/backgrounds/obstacles/obstacle_haut_two.png"; + level.textures["obstacle7"] = "assets/backgrounds/obstacles/obstacle_yellow.png"; + level.textures["obstacle8"] = "assets/backgrounds/obstacles/little_obstacle_haut.png"; + level.textures["player_blue"] = "assets/spaceships/player_blue.png"; + level.textures["player_green"] = "assets/spaceships/player_green.png"; + level.textures["player_red"] = "assets/spaceships/player_red.png"; + level.textures["enemy-green"] = "assets/spaceships/enemy-green.png"; + level.textures["enemy-red"] = "assets/spaceships/enemy-red.png"; + level.textures["enemy-blue"] = "assets/spaceships/enemy-blue.png"; + level.textures["bullet"] = "assets/projectiles/bullet.png"; + level.textures["powerup-spread"] = "assets/powerups/spread.png"; + level.textures["powerup-laser"] = "assets/powerups/laser.png"; + level.textures["powerup-force-pod"] = "assets/powerups/force_pod.png"; + level.textures["powerup-speed"] = "assets/powerups/speed.png"; + level.textures["powerup-shield"] = "assets/powerups/shield.png"; + + level.fonts["main"] = {"assets/fonts/PressStart2P-Regular.ttf", 16}; + level.fonts["small"] = {"assets/fonts/PressStart2P-Regular.ttf", 12}; + + bool hasBackground = false; + + for (const auto& entity : entities) { + switch (entity.type) { + case EditorEntityType::BACKGROUND: { + if (!hasBackground) { + level.background.texture = entity.textureKey; + level.background.scrollSpeed = entity.scrollSpeed; + level.background.copies = 3; + level.background.layer = entity.layer; + hasBackground = true; + } + break; + } + + case EditorEntityType::OBSTACLE: { + ECS::ObstacleDef obs; + obs.texture = entity.textureKey; + obs.x = entity.x; + obs.y = entity.y; + obs.scaleWidth = entity.scaleWidth; + obs.scaleHeight = entity.scaleHeight; + obs.scrollSpeed = entity.scrollSpeed; + obs.layer = entity.layer; + obs.colliders = entity.colliders; + level.obstacles.push_back(obs); + break; + } + + case EditorEntityType::ENEMY: { + ECS::EnemyDef enemy; + enemy.type = entity.enemyType; + enemy.x = entity.x; + enemy.y = entity.y; + level.enemies.push_back(enemy); + break; + } + + case EditorEntityType::PLAYER_SPAWN: { + ECS::PlayerSpawnDef spawn; + spawn.x = entity.x; + spawn.y = entity.y; + level.playerSpawns.push_back(spawn); + break; + } + + case EditorEntityType::POWERUP: + break; + } + } + + if (!hasBackground) { + level.background.texture = "background"; + level.background.scrollSpeed = -50.0f; + level.background.copies = 3; + level.background.layer = -100; + } + + if (level.playerSpawns.empty()) { + level.playerSpawns.push_back({100.0f, 200.0f}); + level.playerSpawns.push_back({100.0f, 360.0f}); + level.playerSpawns.push_back({100.0f, 520.0f}); + level.playerSpawns.push_back({100.0f, 680.0f}); + } + + return level; + } + + EditorEntityData EditorFileManager::ConvertObstacleToEditor(const ECS::ObstacleDef& obs, + ECS::Registry&, + Renderer::IRenderer*) { + EditorEntityData data; + data.type = EditorEntityType::OBSTACLE; + data.textureKey = obs.texture; + data.presetId = obs.texture; + data.x = obs.x; + data.y = obs.y; + data.scaleWidth = obs.scaleWidth; + data.scaleHeight = obs.scaleHeight; + data.scrollSpeed = obs.scrollSpeed; + data.layer = obs.layer; + data.colliders = obs.colliders; + data.entity = ECS::NULL_ENTITY; + + return data; + } + + EditorEntityData EditorFileManager::ConvertEnemyToEditor(const ECS::EnemyDef& enemy, + ECS::Registry& registry, + Renderer::IRenderer*) { + EditorEntityData data; + data.type = EditorEntityType::ENEMY; + data.enemyType = enemy.type; + data.x = enemy.x; + data.y = enemy.y; + data.scaleWidth = 50.0f; + data.scaleHeight = 50.0f; + data.layer = 10; + data.entity = ECS::NULL_ENTITY; + + return data; + } + + EditorEntityData EditorFileManager::ConvertSpawnToEditor(const ECS::PlayerSpawnDef& spawn, + ECS::Registry&, + Renderer::IRenderer*) { + EditorEntityData data; + data.type = EditorEntityType::PLAYER_SPAWN; + data.x = spawn.x; + data.y = spawn.y; + data.scaleWidth = 40.0f; + data.scaleHeight = 40.0f; + data.layer = 5; + data.entity = ECS::NULL_ENTITY; + + return data; + } + + EditorEntityData EditorFileManager::ConvertBackgroundToEditor(const ECS::BackgroundDef& bg, + ECS::Registry&, + Renderer::IRenderer*) { + EditorEntityData data; + data.type = EditorEntityType::BACKGROUND; + data.textureKey = bg.texture; + data.presetId = bg.texture; + data.x = 0.0f; + data.y = 0.0f; + data.scaleWidth = 1280.0f; + data.scaleHeight = 720.0f; + data.scrollSpeed = bg.scrollSpeed; + data.layer = bg.layer; + data.entity = ECS::NULL_ENTITY; + + return data; + } + + } +} diff --git a/games/rtype/client/src/editor/EditorInputHandler.cpp b/games/rtype/client/src/editor/EditorInputHandler.cpp new file mode 100644 index 0000000..721a3dc --- /dev/null +++ b/games/rtype/client/src/editor/EditorInputHandler.cpp @@ -0,0 +1,41 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** EditorInputHandler +*/ + +#include "editor/EditorInputHandler.hpp" + +namespace RType { + namespace Client { + + EditorInputHandler::EditorInputHandler(Renderer::IRenderer* renderer) + : m_renderer(renderer) + { + } + + void EditorInputHandler::HandleKeyPress(Renderer::Key key, const std::function& action) { + if (!m_renderer) { + return; + } + + bool isCurrentlyPressed = m_renderer->IsKeyPressed(key); + bool wasPreviouslyPressed = m_keyStates[key]; + + // Action triggers only on transition from not pressed to pressed + if (isCurrentlyPressed && !wasPreviouslyPressed) { + action(); + } + + m_keyStates[key] = isCurrentlyPressed; + } + + void EditorInputHandler::Update() { + // Update is now handled within HandleKeyPress itself + // This method is kept for potential future frame-based reset logic + // if needed, but currently the state is updated immediately + } + + } +} diff --git a/games/rtype/client/src/editor/EditorPropertyManager.cpp b/games/rtype/client/src/editor/EditorPropertyManager.cpp new file mode 100644 index 0000000..04678c3 --- /dev/null +++ b/games/rtype/client/src/editor/EditorPropertyManager.cpp @@ -0,0 +1,219 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** EditorPropertyManager +*/ + +#include "editor/EditorPropertyManager.hpp" +#include "editor/EditorConstants.hpp" +#include + +using namespace RType::Client::EditorConstants; + +namespace RType { + namespace Client { + + EditorPropertyManager::EditorPropertyManager(Renderer::IRenderer* renderer) + : m_renderer(renderer) + { + } + + bool EditorPropertyManager::HandleInput(EditorEntityData* selectedEntity, + std::function)> handleKeyPress) { + if (!selectedEntity) { + return false; + } + + bool consumedDirectional = m_renderer->IsKeyPressed(Renderer::Key::Up) || + m_renderer->IsKeyPressed(Renderer::Key::Down); + + // Tab: cycle through properties + handleKeyPress(Renderer::Key::Tab, [this]() { + cycleProperty(); + }); + + // Arrow keys: adjust property values + handleKeyPress(Renderer::Key::Up, [this, selectedEntity]() { + applyPropertyDelta(*selectedEntity, getPropertyStep()); + }); + handleKeyPress(Renderer::Key::Down, [this, selectedEntity]() { + applyPropertyDelta(*selectedEntity, -getPropertyStep()); + }); + + // Number keys: direct value input + const Renderer::Key digitKeys[10] = { + Renderer::Key::Num0, Renderer::Key::Num1, Renderer::Key::Num2, Renderer::Key::Num3, Renderer::Key::Num4, + Renderer::Key::Num5, Renderer::Key::Num6, Renderer::Key::Num7, Renderer::Key::Num8, Renderer::Key::Num9 + }; + + for (int i = 0; i < 10; ++i) { + handleKeyPress(digitKeys[i], [this, selectedEntity, key = digitKeys[i]]() { + handleNumberInput(key); + // Apply the new value immediately if buffer is not empty + if (!m_inputBuffer.empty()) { + setPropertyValue(*selectedEntity, std::stof(m_inputBuffer)); + } + }); + } + + // Backspace: remove digit or delete entity + handleKeyPress(Renderer::Key::Backspace, [this, selectedEntity]() { + handleBackspace(selectedEntity); + }); + + // Enter: commit current value + handleKeyPress(Renderer::Key::Enter, [this]() { + ClearInput(); + }); + + return consumedDirectional; + } + + void EditorPropertyManager::ClearInput() { + m_inputBuffer.clear(); + } + + void EditorPropertyManager::SetOnPropertyChanged(std::function callback) { + m_onPropertyChanged = callback; + } + + void EditorPropertyManager::SetOnEntityDeleted(std::function callback) { + m_onEntityDeleted = callback; + } + + void EditorPropertyManager::SetOnPropertyCycled(std::function callback) { + m_onPropertyCycled = callback; + } + + void EditorPropertyManager::cycleProperty() { + // Only cycle through the 6 visible properties (POSITION_X to SCROLL_SPEED) + // Skip collider properties (COLLIDER_X to COLLIDER_HEIGHT) as they have their own UI + const int numVisibleProperties = static_cast(EditableProperty::SCROLL_SPEED) + 1; + int current = static_cast(m_activeProperty); + current = (current + 1) % numVisibleProperties; + m_activeProperty = static_cast(current); + ClearInput(); + + if (m_onPropertyCycled) { + m_onPropertyCycled(); + } + } + + void EditorPropertyManager::applyPropertyDelta(EditorEntityData& entity, float delta) { + float newValue = getPropertyValue(entity) + delta; + setPropertyValue(entity, newValue); + ClearInput(); + } + + void EditorPropertyManager::setPropertyValue(EditorEntityData& entity, float value) { + switch (m_activeProperty) { + case EditableProperty::POSITION_X: + entity.x = value; + break; + case EditableProperty::POSITION_Y: + entity.y = value; + break; + case EditableProperty::SCALE_WIDTH: + entity.scaleWidth = std::max(PropertySteps::MIN_SCALE, value); + break; + case EditableProperty::SCALE_HEIGHT: + entity.scaleHeight = std::max(PropertySteps::MIN_SCALE, value); + break; + case EditableProperty::LAYER: + entity.layer = static_cast(value); + break; + case EditableProperty::SCROLL_SPEED: + entity.scrollSpeed = value; + break; + case EditableProperty::COUNT: + break; + } + + // Notify that property changed (for collider rebuild and UI update) + if (m_onPropertyChanged) { + m_onPropertyChanged(entity); + } + } + + float EditorPropertyManager::getPropertyValue(const EditorEntityData& entity) const { + switch (m_activeProperty) { + case EditableProperty::POSITION_X: + return entity.x; + case EditableProperty::POSITION_Y: + return entity.y; + case EditableProperty::SCALE_WIDTH: + return entity.scaleWidth; + case EditableProperty::SCALE_HEIGHT: + return entity.scaleHeight; + case EditableProperty::LAYER: + return static_cast(entity.layer); + case EditableProperty::SCROLL_SPEED: + return entity.scrollSpeed; + case EditableProperty::COUNT: + break; + } + return 0.0f; + } + + float EditorPropertyManager::getPropertyStep() const { + switch (m_activeProperty) { + case EditableProperty::POSITION_X: + case EditableProperty::POSITION_Y: + return PropertySteps::POSITION_STEP; + case EditableProperty::SCALE_WIDTH: + case EditableProperty::SCALE_HEIGHT: + return PropertySteps::SCALE_STEP; + case EditableProperty::LAYER: + return PropertySteps::LAYER_STEP; + case EditableProperty::SCROLL_SPEED: + return PropertySteps::SCROLL_SPEED_STEP; + case EditableProperty::COUNT: + break; + } + return PropertySteps::LAYER_STEP; + } + + void EditorPropertyManager::handleNumberInput(Renderer::Key key) { + if (m_inputBuffer.size() >= Input::MAX_INPUT_BUFFER_SIZE) { + return; + } + + char digit = '0'; + switch (key) { + case Renderer::Key::Num0: digit = '0'; break; + case Renderer::Key::Num1: digit = '1'; break; + case Renderer::Key::Num2: digit = '2'; break; + case Renderer::Key::Num3: digit = '3'; break; + case Renderer::Key::Num4: digit = '4'; break; + case Renderer::Key::Num5: digit = '5'; break; + case Renderer::Key::Num6: digit = '6'; break; + case Renderer::Key::Num7: digit = '7'; break; + case Renderer::Key::Num8: digit = '8'; break; + case Renderer::Key::Num9: digit = '9'; break; + default: + return; + } + + m_inputBuffer.push_back(digit); + // Note: We don't have access to selectedEntity here, so we rely on the + // callback pattern where EditorState will call setPropertyValue + } + + void EditorPropertyManager::handleBackspace(EditorEntityData* entity) { + if (!m_inputBuffer.empty()) { + m_inputBuffer.pop_back(); + if (!m_inputBuffer.empty() && entity) { + // Update property with new buffer value + setPropertyValue(*entity, std::stof(m_inputBuffer)); + } + } else { + // Empty buffer + backspace = delete entity + if (m_onEntityDeleted) { + m_onEntityDeleted(); + } + } + } + + } +} diff --git a/games/rtype/client/src/editor/EditorUIManager.cpp b/games/rtype/client/src/editor/EditorUIManager.cpp new file mode 100644 index 0000000..bb2f344 --- /dev/null +++ b/games/rtype/client/src/editor/EditorUIManager.cpp @@ -0,0 +1,433 @@ +#include "editor/EditorUIManager.hpp" +#include "editor/EditorConstants.hpp" +#include "editor/EditorGeometry.hpp" +#include "ECS/Components/TextLabel.hpp" +#include + +using namespace RType::Client::EditorConstants; + +namespace RType { + namespace Client { + + EditorUIManager::EditorUIManager(Renderer::IRenderer* renderer, + EditorAssetLibrary& assets, + ECS::Registry& registry, + std::vector& trackedEntities, + Renderer::FontId fontSmall, + Renderer::FontId fontMedium) + : m_renderer(renderer) + , m_assets(assets) + , m_registry(registry) + , m_trackedEntities(trackedEntities) + , m_fontSmall(fontSmall) + , m_fontMedium(fontMedium) + { + } + + void EditorUIManager::InitializePalette() { + m_entries.clear(); + m_actionButtons.clear(); + float cursorY = UI::PALETTE_START_Y; + m_activeSelection = EditorPaletteSelection{}; + + InitializeToolbar(); + + createCategoryLabel("TOOLS", cursorY); + cursorY += UI::PALETTE_BUTTON_HEIGHT; + PaletteEntry selectEntry; + selectEntry.label = "SELECT"; + selectEntry.mode = EditorMode::SELECT; + selectEntry.entityType = EditorEntityType::OBSTACLE; + createPaletteButton(selectEntry, cursorY); + cursorY += UI::PALETTE_BUTTON_HEIGHT * 1.5f; + + auto createSection = [&](const std::string& label, EditorEntityType type, EditorMode mode) { + createCategoryLabel(label, cursorY); + cursorY += UI::PALETTE_BUTTON_HEIGHT; + const auto& resources = m_assets.GetResources(type); + for (const auto* resource : resources) { + PaletteEntry entry; + entry.label = resource->definition.displayName; + entry.mode = mode; + entry.entityType = type; + entry.subtype = resource->definition.id; + entry.resource = resource; + createPaletteButton(entry, cursorY); + cursorY += UI::PALETTE_BUTTON_HEIGHT; + } + cursorY += UI::PALETTE_BUTTON_HEIGHT * 0.5f; + }; + + createSection("ENEMIES", EditorEntityType::ENEMY, EditorMode::PLACE_ENEMY); + createSection("OBSTACLES", EditorEntityType::OBSTACLE, EditorMode::PLACE_OBSTACLE); + createSection("POWERUPS", EditorEntityType::POWERUP, EditorMode::PLACE_POWERUP); + createSection("PLAYER SPAWNS", EditorEntityType::PLAYER_SPAWN, EditorMode::PLACE_PLAYER_SPAWN); + createSection("BACKGROUNDS", EditorEntityType::BACKGROUND, EditorMode::PLACE_BACKGROUND); + + SetActiveSelection(m_activeSelection); + InitializePropertiesPanel(); + InitializeColliderPanel(); + } + + void EditorUIManager::InitializeColliderPanel() { + float startY = UI::PROPERTY_PANEL_START_Y + 320.0f; + + m_colliderPanelHeader = m_registry.CreateEntity(); + m_trackedEntities.push_back(m_colliderPanelHeader); + m_registry.AddComponent(m_colliderPanelHeader, ECS::Position{UI::PROPERTY_PANEL_X, startY}); + + ECS::TextLabel headerLabel("COLLIDERS", m_fontMedium, 18); + headerLabel.centered = false; + headerLabel.color = Colors::UI_HEADER; + m_registry.AddComponent(m_colliderPanelHeader, std::move(headerLabel)); + + m_colliderCountEntity = m_registry.CreateEntity(); + m_trackedEntities.push_back(m_colliderCountEntity); + m_registry.AddComponent(m_colliderCountEntity, ECS::Position{UI::PROPERTY_PANEL_X, startY + 32.0f}); + + ECS::TextLabel countLabel("No colliders", m_fontSmall, 13); + countLabel.centered = false; + countLabel.color = Colors::UI_TEXT; + m_registry.AddComponent(m_colliderCountEntity, std::move(countLabel)); + + m_colliderHintEntity = m_registry.CreateEntity(); + m_trackedEntities.push_back(m_colliderHintEntity); + m_registry.AddComponent(m_colliderHintEntity, ECS::Position{UI::PROPERTY_PANEL_X, startY + 120.0f}); + + ECS::TextLabel hintLabel("Shown in green", m_fontSmall, 11); + hintLabel.centered = false; + hintLabel.color = Colors::UI_HINT; + m_registry.AddComponent(m_colliderHintEntity, std::move(hintLabel)); + } + + void EditorUIManager::InitializePropertiesPanel() { + m_propertyFields.clear(); + + float headerY = UI::PROPERTY_PANEL_START_Y; + m_propertiesHeader = m_registry.CreateEntity(); + m_trackedEntities.push_back(m_propertiesHeader); + m_registry.AddComponent(m_propertiesHeader, ECS::Position{UI::PROPERTY_PANEL_X, headerY}); + + ECS::TextLabel headerLabel("PROPERTIES", m_fontMedium, 18); + headerLabel.centered = false; + headerLabel.color = Colors::UI_HEADER; + m_registry.AddComponent(m_propertiesHeader, std::move(headerLabel)); + + m_selectedInfoEntity = m_registry.CreateEntity(); + m_trackedEntities.push_back(m_selectedInfoEntity); + m_registry.AddComponent(m_selectedInfoEntity, ECS::Position{UI::PROPERTY_PANEL_X, headerY + 32.0f}); + + ECS::TextLabel infoLabel("No entity selected", m_fontSmall, 14); + infoLabel.centered = false; + infoLabel.color = Colors::UI_TEXT; + m_registry.AddComponent(m_selectedInfoEntity, std::move(infoLabel)); + + m_propertyHintEntity = m_registry.CreateEntity(); + m_trackedEntities.push_back(m_propertyHintEntity); + m_registry.AddComponent(m_propertyHintEntity, ECS::Position{UI::PROPERTY_PANEL_X, headerY + 240.0f}); + + ECS::TextLabel hintLabel("Tab | ↑↓ | 0-9 | Backspace", m_fontSmall, 11); + hintLabel.centered = false; + hintLabel.color = Colors::UI_HINT; + m_registry.AddComponent(m_propertyHintEntity, std::move(hintLabel)); + + const std::vector> propertyRows = { + {"POS X", EditableProperty::POSITION_X}, + {"POS Y", EditableProperty::POSITION_Y}, + {"WIDTH", EditableProperty::SCALE_WIDTH}, + {"HEIGHT", EditableProperty::SCALE_HEIGHT}, + {"LAYER", EditableProperty::LAYER}, + {"SCROLL", EditableProperty::SCROLL_SPEED}, + }; + + float rowY = headerY + 80.0f; + for (const auto& [label, property] : propertyRows) { + PropertyField field; + field.property = property; + + field.nameEntity = m_registry.CreateEntity(); + m_trackedEntities.push_back(field.nameEntity); + m_registry.AddComponent(field.nameEntity, ECS::Position{UI::PROPERTY_PANEL_X, rowY}); + + ECS::TextLabel nameLabel(label, m_fontSmall, 13); + nameLabel.centered = false; + nameLabel.color = Colors::UI_TEXT; + m_registry.AddComponent(field.nameEntity, std::move(nameLabel)); + + field.valueEntity = m_registry.CreateEntity(); + m_trackedEntities.push_back(field.valueEntity); + m_registry.AddComponent(field.valueEntity, ECS::Position{UI::PROPERTY_PANEL_X + UI::PROPERTY_VALUE_OFFSET_X, rowY}); + + ECS::TextLabel valueLabel("--", m_fontSmall, 13); + valueLabel.centered = false; + valueLabel.color = Colors::UI_TEXT; + m_registry.AddComponent(field.valueEntity, std::move(valueLabel)); + + m_propertyFields.push_back(field); + rowY += UI::PROPERTY_ROW_HEIGHT; + } + } + + void EditorUIManager::UpdateHover(Math::Vector2 mouseScreen) { + bool hoverChanged = false; + for (auto& entry : m_entries) { + bool inside = EditorGeometry::PointInRect(mouseScreen, entry.bounds); + if (entry.hovered != inside) { + entry.hovered = inside; + hoverChanged = true; + } + } + + for (auto& button : m_actionButtons) { + bool inside = EditorGeometry::PointInRect(mouseScreen, button.bounds); + if (button.hovered != inside) { + button.hovered = inside; + hoverChanged = true; + } + } + + if (hoverChanged) { + refreshPaletteVisuals(); + refreshActionButtons(); + } + } + + bool EditorUIManager::HandleActionClick(Math::Vector2 mouseScreen) { + for (const auto& button : m_actionButtons) { + if (EditorGeometry::PointInRect(mouseScreen, button.bounds)) { + if (button.onClick) { + button.onClick(); + } + return true; + } + } + return false; + } + + std::optional EditorUIManager::HandleClick(Math::Vector2 mouseScreen) { + for (const auto& entry : m_entries) { + if (EditorGeometry::PointInRect(mouseScreen, entry.bounds)) { + EditorPaletteSelection selection; + selection.mode = entry.mode; + selection.entityType = entry.entityType; + selection.subtype = entry.subtype; + SetActiveSelection(selection); + return selection; + } + } + + return std::nullopt; + } + + void EditorUIManager::SetActiveSelection(const EditorPaletteSelection& selection) { + m_activeSelection = selection; + refreshPaletteVisuals(); + refreshActionButtons(); + } + + void EditorUIManager::UpdatePropertyPanel(const EditorEntityData* selected, + EditableProperty activeProperty, + const std::string& inputBuffer) { + std::string infoText = "No entity selected"; + if (selected) { + std::ostringstream oss; + oss << "Type: "; + switch (selected->type) { + case EditorEntityType::ENEMY: + oss << "Enemy (" << (selected->enemyType.empty() ? "BASIC" : selected->enemyType) << ")"; + break; + case EditorEntityType::POWERUP: + oss << "PowerUp (" << (selected->powerUpType.empty() ? "DEFAULT" : selected->powerUpType) << ")"; + break; + case EditorEntityType::PLAYER_SPAWN: + oss << "Player Spawn"; + break; + case EditorEntityType::BACKGROUND: + oss << "Background"; + break; + case EditorEntityType::OBSTACLE: + default: + oss << "Obstacle"; + break; + } + infoText = oss.str(); + } + + if (m_selectedInfoEntity != ECS::NULL_ENTITY && m_registry.IsEntityAlive(m_selectedInfoEntity)) { + auto& label = m_registry.GetComponent(m_selectedInfoEntity); + label.text = infoText; + } + + for (auto& field : m_propertyFields) { + if (!m_registry.IsEntityAlive(field.valueEntity)) { + continue; + } + + auto& valueLabel = m_registry.GetComponent(field.valueEntity); + if (!selected) { + valueLabel.text = "--"; + valueLabel.color = {0.5f, 0.5f, 0.5f, 0.7f}; + continue; + } + + float value = 0.0f; + switch (field.property) { + case EditableProperty::POSITION_X: value = selected->x; break; + case EditableProperty::POSITION_Y: value = selected->y; break; + case EditableProperty::SCALE_WIDTH: value = selected->scaleWidth; break; + case EditableProperty::SCALE_HEIGHT: value = selected->scaleHeight; break; + case EditableProperty::LAYER: value = static_cast(selected->layer); break; + case EditableProperty::SCROLL_SPEED: value = selected->scrollSpeed; break; + case EditableProperty::COUNT: break; + } + + if (!inputBuffer.empty() && field.property == activeProperty) { + valueLabel.text = inputBuffer; + } else { + std::ostringstream oss; + oss.setf(std::ios::fixed); + oss.precision(field.property == EditableProperty::LAYER ? 0 : 1); + oss << value; + valueLabel.text = oss.str(); + } + + if (field.property == activeProperty) { + valueLabel.color = {1.0f, 0.85f, 0.35f, 1.0f}; + } else { + valueLabel.color = {0.95f, 0.95f, 0.95f, 1.0f}; + } + } + } + + void EditorUIManager::createCategoryLabel(const std::string& label, float y) { + float textX = UI::PALETTE_PANEL_LEFT + 10.0f; + ECS::Entity entity = m_registry.CreateEntity(); + m_trackedEntities.push_back(entity); + m_registry.AddComponent(entity, ECS::Position{textX, y}); + + ECS::TextLabel textLabel(label, m_fontSmall, 12); + textLabel.centered = false; + textLabel.color = Colors::UI_HEADER; + m_registry.AddComponent(entity, std::move(textLabel)); + } + + void EditorUIManager::createPaletteButton(PaletteEntry entry, float y) { + entry.bounds.position = {UI::PALETTE_PANEL_LEFT, y - (UI::PALETTE_BUTTON_HEIGHT / 2.0f)}; + entry.bounds.size = {UI::PALETTE_PANEL_WIDTH, UI::PALETTE_BUTTON_HEIGHT}; + + float textX = UI::PALETTE_PANEL_LEFT + 10.0f; + ECS::Entity entity = m_registry.CreateEntity(); + m_trackedEntities.push_back(entity); + m_registry.AddComponent(entity, ECS::Position{textX, y}); + + std::string labelText = entry.label; + if (entry.resource) { + labelText = entry.resource->definition.displayName; + } + + ECS::TextLabel label(labelText, m_fontSmall, 13); + label.centered = false; + label.color = {0.75f, 0.75f, 0.8f, 1.0f}; + m_registry.AddComponent(entity, std::move(label)); + + entry.textEntity = entity; + m_entries.push_back(std::move(entry)); + } + + void EditorUIManager::InitializeToolbar() { + float buttonY = UI::TOOLBAR_START_Y; + ActionButton saveButton; + saveButton.label = "SAVE LEVEL"; + saveButton.bounds.position = {UI::TOOLBAR_RIGHT_X, buttonY - (UI::TOOLBAR_BUTTON_HEIGHT / 2.0f)}; + saveButton.bounds.size = {UI::TOOLBAR_RIGHT_WIDTH, UI::TOOLBAR_BUTTON_HEIGHT}; + + float textX = UI::TOOLBAR_RIGHT_X + 10.0f; + ECS::Entity entity = m_registry.CreateEntity(); + m_trackedEntities.push_back(entity); + m_registry.AddComponent(entity, ECS::Position{textX, buttonY}); + + ECS::TextLabel label(saveButton.label, m_fontMedium, 14); + label.centered = false; + label.color = Colors::UI_TEXT; + m_registry.AddComponent(entity, std::move(label)); + + saveButton.textEntity = entity; + saveButton.onClick = [this]() { + if (m_onSaveRequested) { + m_onSaveRequested(); + } + }; + + m_actionButtons.push_back(std::move(saveButton)); + } + + + void EditorUIManager::refreshPaletteVisuals() { + for (auto& entry : m_entries) { + if (!m_registry.IsEntityAlive(entry.textEntity)) { + continue; + } + + auto& label = m_registry.GetComponent(entry.textEntity); + bool isSelected = entry.mode == m_activeSelection.mode; + if (!entry.subtype.empty() || !m_activeSelection.subtype.empty()) { + isSelected = isSelected && entry.subtype == m_activeSelection.subtype; + } + + if (isSelected) { + label.color = {1.0f, 0.85f, 0.35f, 1.0f}; + } else if (entry.hovered) { + label.color = {0.95f, 0.95f, 0.95f, 1.0f}; + } else { + label.color = {0.75f, 0.75f, 0.8f, 1.0f}; + } + } + } + + void EditorUIManager::refreshActionButtons() { + for (auto& button : m_actionButtons) { + if (!m_registry.IsEntityAlive(button.textEntity)) { + continue; + } + auto& label = m_registry.GetComponent(button.textEntity); + label.color = button.hovered ? Colors::UI_HOVER : Colors::UI_TEXT; + } + } + + void EditorUIManager::SetOnSaveRequested(const std::function& callback) { + m_onSaveRequested = callback; + } + + void EditorUIManager::UpdateColliderPanel(const EditorEntityData* selected, int selectedColliderIndex) { + if (!m_registry.IsEntityAlive(m_colliderCountEntity)) { + return; + } + + auto& countLabel = m_registry.GetComponent(m_colliderCountEntity); + + if (!selected || selected->colliders.empty()) { + countLabel.text = "No colliders"; + countLabel.color = Colors::UI_TEXT; + return; + } + + std::ostringstream oss; + oss << selected->colliders.size() << " collider"; + if (selected->colliders.size() > 1) { + oss << "s"; + } + + if (selectedColliderIndex >= 0 && selectedColliderIndex < static_cast(selected->colliders.size())) { + const auto& collider = selected->colliders[static_cast(selectedColliderIndex)]; + oss << "\n\nSelected: #" << selectedColliderIndex; + oss << "\nPos: (" << static_cast(collider.x) << ", " << static_cast(collider.y) << ")"; + oss << "\nSize: " << static_cast(collider.width) << "x" << static_cast(collider.height); + } + + countLabel.text = oss.str(); + countLabel.color = Colors::UI_TEXT; + } + + } +} + diff --git a/games/rtype/client/src/game/GameStateHelpers.cpp b/games/rtype/client/src/game/GameStateHelpers.cpp new file mode 100644 index 0000000..9d42a42 --- /dev/null +++ b/games/rtype/client/src/game/GameStateHelpers.cpp @@ -0,0 +1,513 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** GameState - Helper functions (sprite configs, power-ups, player labels) +*/ + +#include "../../include/GameState.hpp" + +#include "ECS/Components/TextLabel.hpp" +#include "ECS/Component.hpp" +#include "Core/Logger.hpp" + +using namespace RType::ECS; + +namespace RType { + namespace Client { + + InGameState::EnemySpriteConfig InGameState::GetEnemySpriteConfig(uint8_t enemyType) const { + const Renderer::SpriteId* sprites[] = { + &m_enemyGreenSprite, + &m_enemyRedSprite, + &m_enemyBlueSprite, + &m_enemyGreenSprite, + &m_enemyGreenSprite}; + + static const Math::Color tints[] = { + {1.0f, 1.0f, 1.0f, 1.0f}, + {1.0f, 1.0f, 1.0f, 1.0f}, + {1.0f, 1.0f, 1.0f, 1.0f}, + {1.0f, 1.0f, 1.0f, 1.0f}, + {1.0f, 1.0f, 1.0f, 1.0f}}; + static const float rotations[] = { + 180.0f, + 180.0f, + 180.0f, + 180.0f, + 180.0f}; + + size_t index = (enemyType < 5) ? enemyType : 0; + EnemySpriteConfig result; + result.sprite = *sprites[index]; + result.tint = tints[index]; + result.rotation = rotations[index]; + return result; + } + + Renderer::SpriteId InGameState::GetPowerUpSprite(ECS::PowerUpType type) const { + switch (type) { + case ECS::PowerUpType::FIRE_RATE_BOOST: + case ECS::PowerUpType::SPREAD_SHOT: + return m_powerupSpreadSprite; + case ECS::PowerUpType::LASER_BEAM: + return m_powerupLaserSprite; + case ECS::PowerUpType::FORCE_POD: + return m_powerupForcePodSprite; + case ECS::PowerUpType::SPEED_BOOST: + return m_powerupSpeedSprite; + case ECS::PowerUpType::SHIELD: + return m_powerupShieldSprite; + default: + return m_powerupSpreadSprite; + } + } + + InGameState::EnemyBulletSpriteConfig InGameState::GetEnemyBulletSpriteConfig(uint8_t enemyType) const { + const Renderer::SpriteId* sprites[] = { + &m_enemyBulletGreenSprite, + &m_enemyBulletYellowSprite, + &m_enemyBulletPurpleSprite}; + + static const Math::Color tints[] = { + {1.0f, 1.0f, 1.0f, 1.0f}, + {1.0f, 0.2f, 0.2f, 1.0f}, + {0.8f, 0.3f, 1.0f, 1.0f}}; + + static const float scales[] = { + 0.14f, + 0.09f, + 0.18f}; + + size_t index = (enemyType < 3) ? enemyType : 0; + EnemyBulletSpriteConfig result; + result.sprite = *sprites[index]; + result.tint = tints[index]; + result.scale = scales[index]; + return result; + } + + void InGameState::ApplyPowerUpStateToPlayer(ECS::Entity playerEntity, const network::EntityState& entityState) { + using namespace RType::ECS; + using namespace network; + + if (!m_registry.IsEntityAlive(playerEntity)) { + return; + } + + if (!m_registry.HasComponent(playerEntity)) { + m_registry.AddComponent(playerEntity, ActivePowerUps()); + } + auto& activePowerUps = m_registry.GetComponent(playerEntity); + + bool gainedPowerUp = false; + bool isLocal = (playerEntity == m_localPlayerEntity); + + if (isLocal) { + bool newFireRate = (entityState.powerUpFlags & PowerUpFlags::POWERUP_FIRE_RATE_BOOST) != 0; + bool newSpread = (entityState.powerUpFlags & PowerUpFlags::POWERUP_SPREAD_SHOT) != 0; + bool newLaser = (entityState.powerUpFlags & PowerUpFlags::POWERUP_LASER_BEAM) != 0; + bool newShield = (entityState.powerUpFlags & PowerUpFlags::POWERUP_SHIELD) != 0; + + if (!activePowerUps.hasFireRateBoost && newFireRate) gainedPowerUp = true; + if (!activePowerUps.hasSpreadShot && newSpread) gainedPowerUp = true; + if (!activePowerUps.hasLaserBeam && newLaser) gainedPowerUp = true; + if (!activePowerUps.hasShield && newShield) gainedPowerUp = true; + + } + + activePowerUps.hasFireRateBoost = (entityState.powerUpFlags & PowerUpFlags::POWERUP_FIRE_RATE_BOOST) != 0; + activePowerUps.hasSpreadShot = (entityState.powerUpFlags & PowerUpFlags::POWERUP_SPREAD_SHOT) != 0; + activePowerUps.hasLaserBeam = (entityState.powerUpFlags & PowerUpFlags::POWERUP_LASER_BEAM) != 0; + activePowerUps.hasShield = (entityState.powerUpFlags & PowerUpFlags::POWERUP_SHIELD) != 0; + + float speedMult = static_cast(entityState.speedMultiplier) / 10.0f; + activePowerUps.speedMultiplier = speedMult; + + if (m_registry.HasComponent(playerEntity)) { + auto& controllable = m_registry.GetComponent(playerEntity); + controllable.speed = 200.0f * speedMult; + } + + WeaponType weaponType = static_cast(entityState.weaponType); + + if (isLocal) { + bool hadSpecialWeapon = m_registry.HasComponent(playerEntity); + bool hasSpecialWeaponNow = (weaponType != WeaponType::STANDARD); + + + if (!hadSpecialWeapon && hasSpecialWeaponNow) { + std::cout << "[DEBUG] PowerUp: Acquired special weapon!" << std::endl; + gainedPowerUp = true; + } else if (hadSpecialWeapon && hasSpecialWeaponNow) { + auto& currentWeapon = m_registry.GetComponent(playerEntity); + if (currentWeapon.type != weaponType) { + std::cout << "[DEBUG] PowerUp: Changed special weapon!" << std::endl; + gainedPowerUp = true; + } + } + + bool newFireRate = (entityState.powerUpFlags & PowerUpFlags::POWERUP_FIRE_RATE_BOOST) != 0; + bool newSpread = (entityState.powerUpFlags & PowerUpFlags::POWERUP_SPREAD_SHOT) != 0; + if (!activePowerUps.hasFireRateBoost && newFireRate) { + std::cout << "[DEBUG] PowerUp: FireRate Boost!" << std::endl; + gainedPowerUp = true; + } + if (!activePowerUps.hasSpreadShot && newSpread) { + std::cout << "[DEBUG] PowerUp: Spread Shot!" << std::endl; + gainedPowerUp = true; + } + } + + if (gainedPowerUp) { + std::cout << "[DEBUG] PowerUp GAINED! Playing sound..." << std::endl; + if (m_context.audio) { + if (m_powerUpMusic != Audio::INVALID_MUSIC_ID) { + Audio::PlaybackOptions opts; + opts.volume = 1.0f; + opts.loop = false; + m_context.audio->PlayMusic(m_powerUpMusic, opts); + } else if (m_powerUpSound != Audio::INVALID_SOUND_ID) { + Audio::PlaybackOptions opts; + opts.volume = 1.0f; + m_context.audio->PlaySound(m_powerUpSound, opts); + } + } + } + + float fireRate = static_cast(entityState.fireRate) / 10.0f; + + if (weaponType != WeaponType::STANDARD) { + if (m_registry.HasComponent(playerEntity)) { + auto& weaponSlot = m_registry.GetComponent(playerEntity); + weaponSlot.type = weaponType; + weaponSlot.fireRate = fireRate; + } else { + int damage = (weaponType == WeaponType::LASER) ? 40 : 20; + m_registry.AddComponent(playerEntity, WeaponSlot(weaponType, fireRate, damage)); + } + activePowerUps.hasSpreadShot = (weaponType == WeaponType::SPREAD); + activePowerUps.hasLaserBeam = (weaponType == WeaponType::LASER); + } else { + if (m_registry.HasComponent(playerEntity)) { + m_registry.RemoveComponent(playerEntity); + } + activePowerUps.hasSpreadShot = false; + activePowerUps.hasLaserBeam = false; + } + + if (!m_registry.HasComponent(playerEntity) && m_registry.HasComponent(playerEntity)) { + auto& shooter = m_registry.GetComponent(playerEntity); + shooter.fireRate = fireRate; + } + + constexpr float SHIELD_DURATION_SECONDS = 5.0f; + if (activePowerUps.hasShield) { + if (!m_registry.HasComponent(playerEntity)) { + m_registry.AddComponent(playerEntity, Shield(SHIELD_DURATION_SECONDS)); + } else { + auto& shield = m_registry.GetComponent(playerEntity); + if (shield.timeRemaining <= 0.0f) { + shield.timeRemaining = SHIELD_DURATION_SECONDS; + } + } + } else { + if (m_registry.HasComponent(playerEntity)) { + m_registry.RemoveComponent(playerEntity); + if (m_registry.HasComponent(playerEntity)) { + auto& drawable = m_registry.GetComponent(playerEntity); + drawable.tint = Math::Color(1.0f, 1.0f, 1.0f, 1.0f); + } + } + } + + } + + void InGameState::createBeamEntity() { + if (m_localPlayerEntity == ECS::NULL_ENTITY || !m_registry.IsEntityAlive(m_localPlayerEntity)) { + return; + } + + if (!m_registry.HasComponent(m_localPlayerEntity) || + !m_registry.HasComponent(m_localPlayerEntity) || + !m_effectFactory) { + return; + } + + const auto& playerPos = m_registry.GetComponent(m_localPlayerEntity); + const auto& shooter = m_registry.GetComponent(m_localPlayerEntity); + + float beamX = playerPos.x + shooter.offsetX; + float beamY = playerPos.y + shooter.offsetY; + + float screenWidth = 1280.0f; + if (m_levelData.config.screenWidth > 0.0f) { + screenWidth = m_levelData.config.screenWidth; + } + + float beamHeight = 40.0f; + if (m_registry.HasComponent(m_localPlayerEntity)) { + const auto& playerDrawable = m_registry.GetComponent(m_localPlayerEntity); + beamHeight = 80.0f * playerDrawable.scale.y; + } + + float savedChargeTime = m_chargeTime; + m_beamEntity = m_effectFactory->CreateBeam(m_registry, beamX, beamY, m_localPlayerEntity, savedChargeTime, screenWidth, beamHeight); + } + + void InGameState::updateBeam(float dt) { + if (m_beamEntity == ECS::NULL_ENTITY) { + return; + } + + if (!m_registry.IsEntityAlive(m_beamEntity)) { + m_beamEntity = ECS::NULL_ENTITY; + return; + } + + // Update beam width dynamically as player moves + if (m_registry.HasComponent(m_beamEntity)) { + const auto& beamPos = m_registry.GetComponent(m_beamEntity); + + float screenWidth = 1280.0f; + if (m_levelData.config.screenWidth > 0.0f) { + screenWidth = m_levelData.config.screenWidth; + } + + // Get frame width from config or use default + float frameWidth = 200.0f; + if (m_effectFactory) { + const auto& config = m_effectFactory->GetConfig(); + if (config.beamFirstFrameRegion.size.x > 0.0f) { + frameWidth = config.beamFirstFrameRegion.size.x; + } + } + + // Calculate new beam width (extend from current position to screen edge) + float newBeamWidth = screenWidth - beamPos.x; + if (newBeamWidth < frameWidth) { + newBeamWidth = frameWidth; + } + + // Update Drawable scale if it exists + if (m_registry.HasComponent(m_beamEntity)) { + auto& drawable = m_registry.GetComponent(m_beamEntity); + float newScaleX = newBeamWidth / frameWidth; + // Preserve the Y scale + drawable.scale.x = newScaleX; + } + + // Update BoxCollider width if it exists + if (m_registry.HasComponent(m_beamEntity)) { + auto& collider = m_registry.GetComponent(m_beamEntity); + // Get frame height to calculate the scaled height + float frameHeight = 64.0f; + if (m_effectFactory) { + const auto& config = m_effectFactory->GetConfig(); + if (config.beamFirstFrameRegion.size.y > 0.0f) { + frameHeight = config.beamFirstFrameRegion.size.y; + } + } + + // Get current scale Y from drawable or use default + float scaleY = 1.0f; + if (m_registry.HasComponent(m_beamEntity)) { + scaleY = m_registry.GetComponent(m_beamEntity).scale.y; + } + + collider.width = newBeamWidth; + collider.height = frameHeight * scaleY; + } + } + + if (m_registry.HasComponent(m_beamEntity)) { + auto& anim = m_registry.GetComponent(m_beamEntity); + if (anim.looping && m_animationSystem) { + anim.destroyOnComplete = false; + } + } + + m_beamDuration -= dt; + if (m_beamDuration <= 0.0f) { + if (m_registry.IsEntityAlive(m_beamEntity)) { + m_registry.DestroyEntity(m_beamEntity); + } + m_beamEntity = ECS::NULL_ENTITY; + m_beamDuration = 0.0f; + } + } + + std::pair InGameState::FindPlayerNameAndNumber(uint64_t ownerHash, const std::unordered_set& assignedNumbers) const { + std::string playerName = "Player"; + uint8_t playerNum = 0; + + if (ownerHash != 0) { + auto nameIt = m_playerNameMap.find(ownerHash); + if (nameIt != m_playerNameMap.end()) { + playerName = nameIt->second; + } + for (const auto& p : m_context.allPlayers) { + if (p.hash == ownerHash) { + playerNum = p.number; + break; + } + } + } + + if (playerName == "Player" || playerNum == 0) { + for (const auto& p : m_context.allPlayers) { + if (p.number > 0 && p.number <= MAX_PLAYERS) { + if (assignedNumbers.find(p.number) == assignedNumbers.end()) { + auto fallbackIt = m_playerNameMap.find(static_cast(p.number)); + if (fallbackIt != m_playerNameMap.end() && !fallbackIt->second.empty()) { + playerName = fallbackIt->second; + playerNum = p.number; + break; + } + } + } + } + } + + return {playerName, playerNum}; + } + + void InGameState::CreatePlayerNameLabel(Entity playerEntity, const std::string& playerName, float x, float y) { + Renderer::FontId nameFont = (m_hudFontSmall != Renderer::INVALID_FONT_ID) ? m_hudFontSmall : m_hudFont; + if (nameFont == Renderer::INVALID_FONT_ID) { + return; + } + + std::string displayName = playerName; + if (displayName.length() > 16) { + displayName = displayName.substr(0, 16); + } + + Entity nameLabelEntity = m_registry.CreateEntity(); + m_registry.AddComponent(nameLabelEntity, Position{x, y}); + TextLabel nameLabel(displayName, nameFont, 12); + nameLabel.color = {1.0f, 1.0f, 1.0f, 1.0f}; + nameLabel.centered = true; + nameLabel.offsetY = -30.0f; + nameLabel.offsetX = 0.0f; + m_registry.AddComponent(nameLabelEntity, std::move(nameLabel)); + m_playerNameLabels[playerEntity] = nameLabelEntity; + } + + void InGameState::UpdatePlayerNameLabelPosition(Entity playerEntity, float x, float y) { + auto labelIt = m_playerNameLabels.find(playerEntity); + if (labelIt == m_playerNameLabels.end()) { + return; + } + + Entity labelEntity = labelIt->second; + if (!m_registry.IsEntityAlive(labelEntity) || !m_registry.HasComponent(labelEntity)) { + return; + } + + auto& labelPos = m_registry.GetComponent(labelEntity); + labelPos.x = x; + labelPos.y = y; + } + + void InGameState::DestroyPlayerNameLabel(Entity playerEntity) { + auto labelIt = m_playerNameLabels.find(playerEntity); + if (labelIt == m_playerNameLabels.end()) { + return; + } + + Entity labelEntity = labelIt->second; + if (m_registry.IsEntityAlive(labelEntity)) { + m_registry.DestroyEntity(labelEntity); + } + m_playerNameLabels.erase(labelIt); + } + + void InGameState::CleanupInvalidComponents(ECS::Entity entity, network::EntityType expectedType) { + if (!m_registry.IsEntityAlive(entity)) { + return; + } + + if (expectedType == network::EntityType::OBSTACLE) { + if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); + if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); + if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); + if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); + if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); + if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); + } else if (expectedType == network::EntityType::BULLET) { + if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); + if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); + if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); + if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); + if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); + if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); + if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); + if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); + } else { + if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); + if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); + if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); + if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); + + if (expectedType == network::EntityType::PLAYER || expectedType == network::EntityType::POWERUP) { + if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); + } + if (expectedType == network::EntityType::ENEMY || expectedType == network::EntityType::POWERUP) { + if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); + } + if (expectedType == network::EntityType::BOSS) { + if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); + } + } + } + + void InGameState::cleanupForLevelTransition() { + Core::Logger::Info("[GameState] Cleaning up for level transition..."); + + auto positionEntities = m_registry.GetEntitiesWithComponent(); + std::vector entitiesToDestroy(positionEntities.begin(), positionEntities.end()); + + auto drawableEntities = m_registry.GetEntitiesWithComponent(); + for (auto e : drawableEntities) { + if (std::find(entitiesToDestroy.begin(), entitiesToDestroy.end(), e) == entitiesToDestroy.end()) { + entitiesToDestroy.push_back(e); + } + } + + Core::Logger::Info("[GameState] Destroying {} entities from previous level", entitiesToDestroy.size()); + for (Entity entity : entitiesToDestroy) { + if (m_registry.IsEntityAlive(entity)) { + m_registry.DestroyEntity(entity); + } + } + + m_networkEntityMap.clear(); + m_obstacleColliderEntities.clear(); + m_obstacleIdToCollider.clear(); + m_playerNameLabels.clear(); + + for (auto& hud : m_playersHUD) { + hud = PlayerHUDData{}; + } + + m_bossHealthBar.active = false; + m_bossHealthBar.currentHealth = 0; + m_bossHealthBar.maxHealth = 1000; + m_bossHealthBar.bossNetworkId = 0; + m_bossHealthBar.titleEntity = NULL_ENTITY; + m_bossHealthBar.barBackgroundEntity = NULL_ENTITY; + m_bossHealthBar.barForegroundEntity = NULL_ENTITY; + + m_bossWarningActive = false; + m_bossWarningTriggered = false; + m_bossWarningTimer = 0.0f; + + m_isGameOver = false; + m_localScrollOffset = 0.0f; + + Core::Logger::Info("[GameState] Level transition cleanup complete"); + } + + } +} diff --git a/games/rtype/client/src/game/GameStateInit.cpp b/games/rtype/client/src/game/GameStateInit.cpp new file mode 100644 index 0000000..8f5cbb9 --- /dev/null +++ b/games/rtype/client/src/game/GameStateInit.cpp @@ -0,0 +1,782 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** GameState - Initialization functions +*/ + +#include "../../include/GameState.hpp" + +#include "ECS/Components/TextLabel.hpp" +#include "ECS/Component.hpp" +#include "Core/Logger.hpp" +#include "ECS/PlayerFactory.hpp" +#include "Animation/AnimationTypes.hpp" + +using namespace RType::ECS; +using namespace Animation; + +namespace RType { + namespace Client { + + InGameState::InGameState(GameStateMachine& machine, GameContext& context, uint32_t seed, const std::string& levelPath) + : m_machine(machine), m_context(context), m_gameSeed(seed), m_currentLevelPath(levelPath) { + m_renderer = context.renderer; + } + + void InGameState::Init() { + Core::Logger::Info("[GameState] Initializing game"); + + if (!m_networkEntityMap.empty() || !m_obstacleColliderEntities.empty()) { + Core::Logger::Info("[GameState] Detected level transition, cleaning up previous level..."); + cleanupForLevelTransition(); + } + + if (m_context.audio) { + m_audioSystem = std::make_unique(m_context.audio.get()); + + m_shootMusic = m_context.audio->LoadMusic("assets/sounds/players_shoot.flac"); + if (m_shootMusic == Audio::INVALID_MUSIC_ID) { + m_shootMusic = m_context.audio->LoadMusic("../assets/sounds/players_shoot.flac"); + } + + m_powerUpSound = m_context.audio->LoadSound("assets/sounds/powerup.flac"); + if (m_powerUpSound == Audio::INVALID_SOUND_ID) { + m_powerUpSound = m_context.audio->LoadSound("../assets/sounds/powerup.flac"); + } + m_powerUpMusic = m_context.audio->LoadMusic("assets/sounds/powerup.flac"); + if (m_powerUpMusic == Audio::INVALID_MUSIC_ID) { + m_powerUpMusic = m_context.audio->LoadMusic("../assets/sounds/powerup.flac"); + } + + std::string musicPath = "assets/sounds/stage1.flac"; + if (m_currentLevelPath.find("level2.json") != std::string::npos) { + musicPath = "assets/sounds/stage2.flac"; + } else if (m_currentLevelPath.find("level3.json") != std::string::npos) { + musicPath = "assets/sounds/stage3.flac"; + } + + m_gameMusic = m_context.audio->LoadMusic(musicPath); + if (m_gameMusic == Audio::INVALID_MUSIC_ID) { + m_gameMusic = m_context.audio->LoadMusic("../" + musicPath); + } + + m_bossMusic = m_context.audio->LoadMusic("assets/sounds/BOSS.flac"); + if (m_bossMusic == Audio::INVALID_MUSIC_ID) { + m_bossMusic = m_context.audio->LoadMusic("../assets/sounds/BOSS.flac"); + } + + m_gameOverMusic = m_context.audio->LoadMusic("assets/sounds/gameover.flac"); + if (m_gameOverMusic == Audio::INVALID_MUSIC_ID) { + m_gameOverMusic = m_context.audio->LoadMusic("../assets/sounds/gameover.flac"); + } + + m_victoryMusic = m_context.audio->LoadMusic("assets/sounds/victory.flac"); + if (m_victoryMusic == Audio::INVALID_MUSIC_ID) { + m_victoryMusic = m_context.audio->LoadMusic("../assets/sounds/victory.flac"); + } + + if (m_gameMusic != Audio::INVALID_MUSIC_ID) { + auto cmd = m_registry.CreateEntity(); + auto& me = m_registry.AddComponent(cmd, MusicEffect(m_gameMusic)); + me.play = true; + me.stop = false; + me.loop = true; + me.volume = 0.35f; + me.pitch = 1.0f; + m_gameMusicPlaying = true; + } + } + + if (m_context.networkClient) { + m_context.networkClient->SetStateCallback([this](uint32_t tick, const std::vector& entities, const std::vector& inputAcks) { this->OnServerStateUpdate(tick, entities, inputAcks); }); + m_context.networkClient->SetLevelCompleteCallback([this](uint8_t completedLevel, uint8_t nextLevel) { this->OnLevelComplete(completedLevel, nextLevel); }); + + if (!m_context.networkClient->IsConnected()) { + Core::Logger::Info("[GameState] Reconnecting to server after level transition..."); + if (!m_context.networkClient->ConnectToServer()) { + Core::Logger::Error("[GameState] Failed to reconnect to server!"); + } else { + Core::Logger::Info("[GameState] Reconnected to server successfully"); + } + } + } else { + Core::Logger::Warning("[GameState] No network client available"); + } + + m_isNetworkSession = (m_context.networkClient != nullptr); + + for (const auto& player : m_context.allPlayers) { + if (player.hash != 0) { + m_playerNameMap[player.hash] = player.name; + } + m_playerNameMap[static_cast(player.number)] = player.name; + } + + loadLevel(m_currentLevelPath); + createSystems(); + + if (m_shootingSystem && m_playerShootSound != Audio::INVALID_SOUND_ID) { + m_shootingSystem->SetShootSound(m_playerShootSound); + } + + if (m_powerUpCollisionSystem && m_powerUpSound != Audio::INVALID_SOUND_ID) { + m_powerUpCollisionSystem->SetPowerUpSound(m_powerUpSound); + } + + initializeFromLevel(); + if (!m_isNetworkSession) { + initializePlayers(); + } + initializeUI(); + + Core::Logger::Info("[GameState] Initialization complete"); + } + + void InGameState::loadLevel(const std::string& levelPath) { + m_levelData = ECS::LevelLoader::LoadFromFile(levelPath); + m_levelAssets = ECS::LevelLoader::LoadAssets(m_levelData, m_renderer.get()); + Core::Logger::Info("[GameState] Loaded level '{}' with {} textures, {} obstacle definitions", m_levelData.name, m_levelAssets.textures.size(), m_levelData.obstacles.size()); + + // Load explosion spritesheet for death animations + m_explosionTexture = m_renderer->LoadTexture("assets/SFX/explosion.png"); + if (m_explosionTexture == Renderer::INVALID_TEXTURE_ID) { + m_explosionTexture = m_renderer->LoadTexture("../assets/SFX/explosion.png"); + } + if (m_explosionTexture != Renderer::INVALID_TEXTURE_ID) { + m_explosionSprite = m_renderer->CreateSprite(m_explosionTexture, {}); + Core::Logger::Info("[GameState] Explosion spritesheet loaded"); + } else { + Core::Logger::Warning("[GameState] Failed to load explosion spritesheet"); + } + + m_shootingTexture = m_renderer->LoadTexture("assets/SFX/shooting.png"); + if (m_shootingTexture == Renderer::INVALID_TEXTURE_ID) { + m_shootingTexture = m_renderer->LoadTexture("../assets/SFX/shooting.png"); + } + if (m_shootingTexture != Renderer::INVALID_TEXTURE_ID) { + m_shootingSprite = m_renderer->CreateSprite(m_shootingTexture, {}); + Core::Logger::Info("[GameState] Shooting animation spritesheet loaded"); + } else { + Core::Logger::Warning("[GameState] Failed to load shooting animation spritesheet"); + } + + m_forcePodTexture = m_renderer->LoadTexture("assets/SFX/forcepod.png"); + if (m_forcePodTexture == Renderer::INVALID_TEXTURE_ID) { + m_forcePodTexture = m_renderer->LoadTexture("../assets/SFX/forcepod.png"); + } + if (m_forcePodTexture != Renderer::INVALID_TEXTURE_ID) { + m_forcePodSprite = m_renderer->CreateSprite(m_forcePodTexture, {}); + Core::Logger::Info("[GameState] Force pod animation spritesheet loaded"); + } else { + Core::Logger::Warning("[GameState] Failed to load force pod animation spritesheet"); + } + + m_beamTexture = m_renderer->LoadTexture("assets/SFX/beam.png"); + if (m_beamTexture == Renderer::INVALID_TEXTURE_ID) { + m_beamTexture = m_renderer->LoadTexture("../assets/SFX/beam.png"); + } + if (m_beamTexture != Renderer::INVALID_TEXTURE_ID) { + m_beamSprite = m_renderer->CreateSprite(m_beamTexture, {}); + Core::Logger::Info("[GameState] Beam animation spritesheet loaded"); + } else { + Core::Logger::Warning("[GameState] Failed to load beam animation spritesheet"); + } + + m_enemyBulletGreenTexture = m_renderer->LoadTexture("assets/projectiles/bullet-green.png"); + if (m_enemyBulletGreenTexture == Renderer::INVALID_TEXTURE_ID) { + m_enemyBulletGreenTexture = m_renderer->LoadTexture("../assets/projectiles/bullet-green.png"); + } + if (m_enemyBulletGreenTexture != Renderer::INVALID_TEXTURE_ID) { + m_enemyBulletGreenSprite = m_renderer->CreateSprite(m_enemyBulletGreenTexture, {}); + Core::Logger::Info("[GameState] Enemy bullet green sprite loaded"); + } + + m_enemyBulletYellowTexture = m_renderer->LoadTexture("assets/projectiles/bullet-yellow.png"); + if (m_enemyBulletYellowTexture == Renderer::INVALID_TEXTURE_ID) { + m_enemyBulletYellowTexture = m_renderer->LoadTexture("../assets/projectiles/bullet-yellow.png"); + } + if (m_enemyBulletYellowTexture != Renderer::INVALID_TEXTURE_ID) { + m_enemyBulletYellowSprite = m_renderer->CreateSprite(m_enemyBulletYellowTexture, {}); + Core::Logger::Info("[GameState] Enemy bullet yellow sprite loaded"); + } + + m_enemyBulletPurpleTexture = m_renderer->LoadTexture("assets/projectiles/bullet-purple.png"); + if (m_enemyBulletPurpleTexture == Renderer::INVALID_TEXTURE_ID) { + m_enemyBulletPurpleTexture = m_renderer->LoadTexture("../assets/projectiles/bullet-purple.png"); + } + if (m_enemyBulletPurpleTexture != Renderer::INVALID_TEXTURE_ID) { + m_enemyBulletPurpleSprite = m_renderer->CreateSprite(m_enemyBulletPurpleTexture, {}); + Core::Logger::Info("[GameState] Enemy bullet purple sprite loaded"); + } + + auto enemyGreenIt = m_levelAssets.sprites.find("enemy-green"); + if (enemyGreenIt != m_levelAssets.sprites.end()) { + m_enemyGreenSprite = enemyGreenIt->second; + } else { + auto textureIt = m_levelAssets.textures.find("enemy-green"); + if (textureIt != m_levelAssets.textures.end()) { + m_enemyGreenSprite = m_renderer->CreateSprite(textureIt->second, {}); + } + } + + auto enemyRedIt = m_levelAssets.sprites.find("enemy-red"); + if (enemyRedIt != m_levelAssets.sprites.end()) { + m_enemyRedSprite = enemyRedIt->second; + } else { + auto textureIt = m_levelAssets.textures.find("enemy-red"); + if (textureIt != m_levelAssets.textures.end()) { + m_enemyRedSprite = m_renderer->CreateSprite(textureIt->second, {}); + } + } + + auto enemyBlueIt = m_levelAssets.sprites.find("enemy-blue"); + if (enemyBlueIt != m_levelAssets.sprites.end()) { + m_enemyBlueSprite = enemyBlueIt->second; + } else { + auto textureIt = m_levelAssets.textures.find("enemy-blue"); + if (textureIt != m_levelAssets.textures.end()) { + m_enemyBlueSprite = m_renderer->CreateSprite(textureIt->second, {}); + } + } + + auto ensureEnemySprite = [this](Renderer::SpriteId& target, Renderer::SpriteId fallbackPlayerSprite, const std::string& path) { + if (target != Renderer::INVALID_SPRITE_ID) { + return; + } + Renderer::TextureId textureId = m_renderer->LoadTexture(path); + if (textureId == Renderer::INVALID_TEXTURE_ID) { + textureId = m_renderer->LoadTexture("../" + path); + } + if (textureId != Renderer::INVALID_TEXTURE_ID) { + target = m_renderer->CreateSprite(textureId, {}); + } else if (fallbackPlayerSprite != Renderer::INVALID_SPRITE_ID) { + target = fallbackPlayerSprite; + } + }; + + ensureEnemySprite(m_enemyGreenSprite, m_playerGreenSprite, "assets/spaceships/enemy-green.png"); + ensureEnemySprite(m_enemyRedSprite, m_playerRedSprite, "assets/spaceships/enemy-red.png"); + ensureEnemySprite(m_enemyBlueSprite, m_playerBlueSprite, "assets/spaceships/enemy-blue.png"); + + auto powerupSpreadIt = m_levelAssets.sprites.find("powerup-spread"); + if (powerupSpreadIt != m_levelAssets.sprites.end()) { + m_powerupSpreadSprite = powerupSpreadIt->second; + } else { + auto textureIt = m_levelAssets.textures.find("powerup-spread"); + if (textureIt != m_levelAssets.textures.end()) { + m_powerupSpreadSprite = m_renderer->CreateSprite(textureIt->second, {}); + } + } + + auto powerupLaserIt = m_levelAssets.sprites.find("powerup-laser"); + if (powerupLaserIt != m_levelAssets.sprites.end()) { + m_powerupLaserSprite = powerupLaserIt->second; + } else { + auto textureIt = m_levelAssets.textures.find("powerup-laser"); + if (textureIt != m_levelAssets.textures.end()) { + m_powerupLaserSprite = m_renderer->CreateSprite(textureIt->second, {}); + } + } + + auto powerupForcePodIt = m_levelAssets.sprites.find("powerup-force-pod"); + if (powerupForcePodIt != m_levelAssets.sprites.end()) { + m_powerupForcePodSprite = powerupForcePodIt->second; + } else { + auto textureIt = m_levelAssets.textures.find("powerup-force-pod"); + if (textureIt != m_levelAssets.textures.end()) { + m_powerupForcePodSprite = m_renderer->CreateSprite(textureIt->second, {}); + } + } + + auto powerupSpeedIt = m_levelAssets.sprites.find("powerup-speed"); + if (powerupSpeedIt != m_levelAssets.sprites.end()) { + m_powerupSpeedSprite = powerupSpeedIt->second; + } else { + auto textureIt = m_levelAssets.textures.find("powerup-speed"); + if (textureIt != m_levelAssets.textures.end()) { + m_powerupSpeedSprite = m_renderer->CreateSprite(textureIt->second, {}); + } + } + + auto powerupShieldIt = m_levelAssets.sprites.find("powerup-shield"); + if (powerupShieldIt != m_levelAssets.sprites.end()) { + m_powerupShieldSprite = powerupShieldIt->second; + } else { + auto textureIt = m_levelAssets.textures.find("powerup-shield"); + if (textureIt != m_levelAssets.textures.end()) { + m_powerupShieldSprite = m_renderer->CreateSprite(textureIt->second, {}); + } + } + } + + void InGameState::initializeFromLevel() { + m_levelEntities = ECS::LevelLoader::CreateEntities( + m_registry, + m_levelData, + m_levelAssets, + m_renderer.get()); + + m_backgroundEntities = m_levelEntities.backgrounds; + m_obstacleSpriteEntities = m_levelEntities.obstacleVisuals; + m_obstacleColliderEntities = m_levelEntities.obstacleColliders; + m_obstacleIdToCollider.clear(); + + // Initialize collider positions based on their visual entities + for (auto collider : m_obstacleColliderEntities) { + if (!m_registry.IsEntityAlive(collider) || + !m_registry.HasComponent(collider)) { + continue; + } + const auto& metadata = m_registry.GetComponent(collider); + m_obstacleIdToCollider[metadata.uniqueId] = collider; + + // Sync collider to visual entity position on initialization + if (metadata.visualEntity != ECS::NULL_ENTITY && + m_registry.IsEntityAlive(metadata.visualEntity) && + m_registry.HasComponent(metadata.visualEntity) && + m_registry.HasComponent(collider)) { + + const auto& visualPos = m_registry.GetComponent(metadata.visualEntity); + auto& colliderPos = m_registry.GetComponent(collider); + + // Set absolute position = visual position + offset + colliderPos.x = visualPos.x + metadata.offsetX; + colliderPos.y = visualPos.y + metadata.offsetY; + + } + } + + } + + void InGameState::createSystems() { + auto bulletSpriteIt = m_levelAssets.sprites.find("bullet"); + Renderer::SpriteId bulletSprite = (bulletSpriteIt != m_levelAssets.sprites.end()) ? bulletSpriteIt->second : Renderer::INVALID_SPRITE_ID; + + if (bulletSprite == Renderer::INVALID_SPRITE_ID) { + Core::Logger::Error("[GameState] Bullet sprite not found in level assets"); + } + + m_scrollingSystem = std::make_unique(); + m_renderingSystem = std::make_unique(m_renderer.get()); + m_textSystem = std::make_unique(m_renderer.get()); + + // Initialize animation module and system + m_animationModule = std::make_unique(); + + if (m_explosionTexture != Renderer::INVALID_TEXTURE_ID && m_animationModule) { + Animation::GridLayout layout; + layout.columns = 6; + layout.rows = 1; + layout.frameCount = 6; + layout.frameWidth = 45.0f; + layout.frameHeight = 40.0f; + layout.defaultDuration = 0.005f; + + m_explosionClipId = m_animationModule->CreateClipFromGrid( + "explosion_small", + "assets/SFX/explosion.png", + layout, + false); + Core::Logger::Info("[GameState] Created explosion animation clip with {} frames", 6); + } + + if (m_shootingTexture != Renderer::INVALID_TEXTURE_ID && m_animationModule) { + const int frameCount = 8; + + Renderer::Vector2 textureSize = m_renderer->GetTextureSize(m_shootingTexture); + float singleFrameWidth = textureSize.x / static_cast(frameCount); + float frameHeight = textureSize.y; + + Animation::AnimationClipConfig shootingConfig; + shootingConfig.name = "shooting_animation"; + shootingConfig.texturePath = "assets/SFX/shooting.png"; + shootingConfig.looping = false; + shootingConfig.playbackSpeed = 1.0f; + + for (int i = frameCount - 1; i >= 0; --i) { + Animation::FrameDef frame; + frame.region.position.x = static_cast(i) * singleFrameWidth; + frame.region.position.y = 0.0f; + frame.region.size.x = singleFrameWidth; + frame.region.size.y = frameHeight; + frame.duration = 0.05f; + shootingConfig.frames.push_back(frame); + } + + m_shootingClipId = m_animationModule->CreateClip(shootingConfig); + Core::Logger::Info("[GameState] Created shooting animation clip with {} frames (frame size: {}x{}, texture size: {}x{}, reversed for right-to-left)", + frameCount, singleFrameWidth, frameHeight, textureSize.x, textureSize.y); + } + + if (m_forcePodTexture != Renderer::INVALID_TEXTURE_ID && m_animationModule) { + const int totalFramesInSheet = 12; + const int frameCount = 6; + + Renderer::Vector2 textureSize = m_renderer->GetTextureSize(m_forcePodTexture); + float singleFrameWidth = textureSize.x / static_cast(totalFramesInSheet); + float frameHeight = textureSize.y; + + Animation::AnimationClipConfig forcePodConfig; + forcePodConfig.name = "forcepod_rotation"; + forcePodConfig.texturePath = "assets/SFX/forcepod.png"; + forcePodConfig.looping = true; + forcePodConfig.playbackSpeed = 1.0f; + + for (int i = 0; i < frameCount; ++i) { + Animation::FrameDef frame; + frame.region.position.x = static_cast(i) * singleFrameWidth; + frame.region.position.y = 0.0f; + frame.region.size.x = singleFrameWidth; + frame.region.size.y = frameHeight; + frame.duration = 0.1f; + forcePodConfig.frames.push_back(frame); + } + + m_forcePodClipId = m_animationModule->CreateClip(forcePodConfig); + Core::Logger::Info("[GameState] Created force pod rotation animation clip with {} frames (frame size: {}x{}, texture size: {}x{})", + frameCount, singleFrameWidth, frameHeight, textureSize.x, textureSize.y); + } + + if (m_beamTexture != Renderer::INVALID_TEXTURE_ID && m_animationModule) { + const int frameCount = 5; + + Renderer::Vector2 textureSize = m_renderer->GetTextureSize(m_beamTexture); + float singleFrameWidth = textureSize.x / static_cast(frameCount); + float frameHeight = textureSize.y; + + Animation::AnimationClipConfig beamConfig; + beamConfig.name = "beam_animation"; + beamConfig.texturePath = "assets/SFX/beam.png"; + beamConfig.looping = true; + beamConfig.playbackSpeed = 1.0f; + + for (int i = 0; i < frameCount; ++i) { + Animation::FrameDef frame; + frame.region.position.x = static_cast(i) * singleFrameWidth; + frame.region.position.y = 0.0f; + frame.region.size.x = singleFrameWidth; + frame.region.size.y = frameHeight; + frame.duration = 0.1f; + beamConfig.frames.push_back(frame); + } + + m_beamClipId = m_animationModule->CreateClip(beamConfig); + Core::Logger::Info("[GameState] Created beam animation clip with {} frames (frame size: {}x{}, texture size: {}x{})", + frameCount, singleFrameWidth, frameHeight, textureSize.x, textureSize.y); + } + + m_hitTexture = m_renderer->LoadTexture("assets/SFX/hit.png"); + if (m_hitTexture == Renderer::INVALID_TEXTURE_ID) { + m_hitTexture = m_renderer->LoadTexture("../assets/SFX/hit.png"); + } + if (m_hitTexture != Renderer::INVALID_TEXTURE_ID) { + Renderer::Vector2 textureSize = m_renderer->GetTextureSize(m_hitTexture); + m_hitSprite = m_renderer->CreateSprite(m_hitTexture, Renderer::Rectangle{{0.0f, 0.0f}, {textureSize.x, textureSize.y}}); + Core::Logger::Info("[GameState] Hit animation spritesheet loaded (texture size: {}x{})", textureSize.x, textureSize.y); + } else { + Core::Logger::Warning("[GameState] Failed to load hit animation spritesheet"); + } + + if (m_hitTexture != Renderer::INVALID_TEXTURE_ID && m_animationModule) { + const int frameCount = 5; + + Renderer::Vector2 textureSize = m_renderer->GetTextureSize(m_hitTexture); + float singleFrameWidth = textureSize.x / static_cast(frameCount); + float frameHeight = textureSize.y; + + Animation::AnimationClipConfig hitConfig; + hitConfig.name = "hit_animation"; + hitConfig.texturePath = "assets/SFX/hit.png"; + hitConfig.looping = false; + hitConfig.playbackSpeed = 1.0f; + + for (int i = 0; i < frameCount; ++i) { + Animation::FrameDef frame; + frame.region.position.x = static_cast(i) * singleFrameWidth; + frame.region.position.y = 0.0f; + frame.region.size.x = singleFrameWidth; + frame.region.size.y = frameHeight; + frame.duration = 0.1f; + hitConfig.frames.push_back(frame); + } + + m_hitClipId = m_animationModule->CreateClip(hitConfig); + Core::Logger::Info("[GameState] Created hit animation clip with {} frames (frame size: {}x{}, texture size: {}x{})", + frameCount, singleFrameWidth, frameHeight, textureSize.x, textureSize.y); + } + + if (m_animationModule) { + Animation::GridLayout waveLayout; + waveLayout.columns = 3; + waveLayout.rows = 1; + waveLayout.frameCount = 3; + waveLayout.frameWidth = 30.0f; + waveLayout.frameHeight = 40.0f; + waveLayout.defaultDuration = 0.3f; + + m_waveAttackClipId = m_animationModule->CreateClipFromGrid( + "wave_attack", + "assets/boss/attacks/boss 2/wave_attack.png", + waveLayout, + true); + Core::Logger::Info("[GameState] Created wave attack animation clip with {} frames", 3); + } + + if (m_animationModule) { + Animation::GridLayout secondLayout; + secondLayout.columns = 3; + secondLayout.rows = 1; + secondLayout.frameCount = 3; + secondLayout.frameWidth = 19.0f; + secondLayout.frameHeight = 29.0f; + secondLayout.defaultDuration = 0.5f; + + m_secondAttackClipId = m_animationModule->CreateClipFromGrid( + "second_attack", + "assets/boss/attacks/boss_2/second_attack_boss2.png", + secondLayout, + true); + Core::Logger::Info("[GameState] Created second attack animation clip with {} frames", 3); + } + + if (m_animationModule) { + Animation::GridLayout fireLayout; + fireLayout.columns = 2; + fireLayout.rows = 1; + fireLayout.frameCount = 2; + fireLayout.frameWidth = 25.0f; + fireLayout.frameHeight = 25.0f; + fireLayout.defaultDuration = 0.3f; + + m_fireBulletClipId = m_animationModule->CreateClipFromGrid( + "fire_bullet", + "assets/boss/attacks/boss_3/fire.png", + fireLayout, + true); + Core::Logger::Info("[GameState] Created fire bullet animation clip with {} frames", 4); + } + + if (m_animationModule) { + Animation::GridLayout mineLayout; + mineLayout.columns = 2; + mineLayout.rows = 1; + mineLayout.frameCount = 2; + mineLayout.frameWidth = 36.0f; + mineLayout.frameHeight = 42.0f; + mineLayout.defaultDuration = 0.3f; + + m_mineClipId = m_animationModule->CreateClipFromGrid( + "mine", + "assets/boss/attacks/boss_3/mine.png", + mineLayout, + true); + Core::Logger::Info("[GameState] Created mine animation clip with {} frames", 4); + } + + if (m_animationModule) { + Animation::GridLayout explosionLayout; + explosionLayout.columns = 5; + explosionLayout.rows = 1; + explosionLayout.frameCount = 5; + explosionLayout.frameWidth = 54.0f; + explosionLayout.frameHeight = 65.0f; + explosionLayout.defaultDuration = 0.1f; + + m_mineExplosionClipId = m_animationModule->CreateClipFromGrid( + "mine_explosion", + "assets/boss/attacks/boss_3/explosion.png", + explosionLayout, + false); + Core::Logger::Info("[GameState] Created mine explosion animation clip with {} frames", 7); + } + + m_animationSystem = std::make_unique(m_animationModule.get()); + + ECS::EffectConfig effectConfig; + effectConfig.explosionSmall = m_explosionClipId; + effectConfig.effectsTexture = m_explosionTexture; + effectConfig.effectsSprite = m_explosionSprite; + effectConfig.shootingAnimation = m_shootingClipId; + effectConfig.shootingTexture = m_shootingTexture; + effectConfig.shootingSprite = m_shootingSprite; + effectConfig.forcePodAnimation = m_forcePodClipId; + effectConfig.forcePodTexture = m_forcePodTexture; + effectConfig.forcePodSprite = m_forcePodSprite; + effectConfig.beamAnimation = m_beamClipId; + effectConfig.beamTexture = m_beamTexture; + effectConfig.beamSprite = m_beamSprite; + effectConfig.hitAnimation = m_hitClipId; + effectConfig.hitTexture = m_hitTexture; + effectConfig.hitSprite = m_hitSprite; + + if (m_animationModule) { + if (m_explosionClipId != Animation::INVALID_CLIP_ID) { + auto explosionFirstFrame = m_animationModule->GetFrameAtTime(m_explosionClipId, 0.0f, false); + effectConfig.explosionFirstFrameRegion = explosionFirstFrame.region; + } + + if (m_shootingClipId != Animation::INVALID_CLIP_ID) { + auto shootingFirstFrame = m_animationModule->GetFrameAtTime(m_shootingClipId, 0.0f, false); + effectConfig.shootingFirstFrameRegion = shootingFirstFrame.region; + } + + if (m_forcePodClipId != Animation::INVALID_CLIP_ID) { + auto forcePodFirstFrame = m_animationModule->GetFrameAtTime(m_forcePodClipId, 0.0f, true); + effectConfig.forcePodFirstFrameRegion = forcePodFirstFrame.region; + } + + if (m_beamClipId != Animation::INVALID_CLIP_ID) { + auto beamFirstFrame = m_animationModule->GetFrameAtTime(m_beamClipId, 0.0f, true); + effectConfig.beamFirstFrameRegion = beamFirstFrame.region; + } + + if (m_hitClipId != Animation::INVALID_CLIP_ID) { + auto hitFirstFrame = m_animationModule->GetFrameAtTime(m_hitClipId, 0.0f, false); + effectConfig.hitFirstFrameRegion = hitFirstFrame.region; + } + } + + m_effectFactory = std::make_unique(effectConfig); + + m_forcePodSystem = std::make_unique(); + m_shieldSystem = std::make_unique(); + + if (!m_isNetworkSession) { + m_powerUpSpawnSystem = std::make_unique( + m_renderer.get(), + m_levelData.config.screenWidth, + m_levelData.config.screenHeight); + m_powerUpSpawnSystem->SetSpawnInterval(m_levelData.config.powerUpSpawnInterval); + m_powerUpSpawnSystem->SetEffectFactory(m_effectFactory.get()); + m_powerUpCollisionSystem = std::make_unique(m_renderer.get()); + m_collisionDetectionSystem = std::make_unique(); + m_playerResponseSystem = std::make_unique(); + m_shootingSystem = std::make_unique(bulletSprite); + m_shootingSystem->SetEffectFactory(m_effectFactory.get()); + m_movementSystem = std::make_unique(); + m_inputSystem = std::make_unique(m_renderer.get()); + m_bulletResponseSystem = std::make_unique(m_effectFactory.get()); + m_obstacleResponseSystem = std::make_unique(); + m_healthSystem = std::make_unique(); + m_scoreSystem = std::make_unique(); + } else { + // In network sessions, we still need collision systems for visual effects + m_collisionDetectionSystem = std::make_unique(); + m_bulletResponseSystem = std::make_unique(m_effectFactory.get()); + + // These systems are server-authoritative, so we don't need them on client + m_powerUpSpawnSystem.reset(); + m_powerUpCollisionSystem.reset(); + m_playerResponseSystem.reset(); + m_shootingSystem.reset(); + m_movementSystem.reset(); + m_inputSystem.reset(); + m_obstacleResponseSystem.reset(); + m_healthSystem.reset(); + m_scoreSystem.reset(); + } + } + + void InGameState::initializePlayers() { + if (m_isNetworkSession) { + return; + } + + const uint8_t playerNumber = m_context.playerNumber == 0 ? 1 : m_context.playerNumber; + const uint64_t playerHash = m_context.playerHash; + + const auto& spawns = ECS::LevelLoader::GetPlayerSpawns(m_levelData); + uint8_t playerIndex = playerNumber - 1; + + Position spawnPos{100.0f, 360.0f}; + if (!spawns.empty()) { + if (playerIndex < spawns.size()) { + spawnPos = Position{spawns[playerIndex].x, spawns[playerIndex].y}; + Core::Logger::Info("[GameState] Player {} spawning at ({}, {}) from level data", playerNumber, spawnPos.x, spawnPos.y); + } else { + spawnPos = Position{spawns[0].x, spawns[0].y}; + Core::Logger::Warning("[GameState] Player {} index out of range, using first spawn", playerNumber); + } + } else { + Core::Logger::Warning("[GameState] No player spawns in level data, using default position"); + } + + const auto& playerConfig = m_levelData.config.playerDefaults; + + m_localPlayerEntity = ECS::PlayerFactory::CreatePlayer(m_registry, playerNumber, playerHash, spawnPos.x, spawnPos.y, m_renderer.get()); + + if (m_localPlayerEntity == ECS::NULL_ENTITY) { + Core::Logger::Error("[GameState] Failed to create local player entity"); + return; + } + + if (m_registry.HasComponent(m_localPlayerEntity)) { + auto& playerComp = m_registry.GetComponent(m_localPlayerEntity); + playerComp.isLocalPlayer = true; + } else { + m_registry.AddComponent(m_localPlayerEntity, Player{playerNumber, playerHash, true}); + } + + if (m_registry.HasComponent(m_localPlayerEntity)) { + auto& position = m_registry.GetComponent(m_localPlayerEntity); + position = spawnPos; + } else { + m_registry.AddComponent(m_localPlayerEntity, Position{spawnPos.x, spawnPos.y}); + } + + if (!m_registry.HasComponent(m_localPlayerEntity)) { + m_registry.AddComponent(m_localPlayerEntity, Velocity{0.0f, 0.0f}); + } + + if (m_registry.HasComponent(m_localPlayerEntity)) { + auto& controllable = m_registry.GetComponent(m_localPlayerEntity); + controllable.speed = playerConfig.movementSpeed; + } else { + m_registry.AddComponent(m_localPlayerEntity, Controllable{playerConfig.movementSpeed}); + } + + Shooter shooterConfig{ + playerConfig.fireRate, + playerConfig.bulletOffsetX, + playerConfig.bulletOffsetY}; + if (m_registry.HasComponent(m_localPlayerEntity)) { + auto& shooter = m_registry.GetComponent(m_localPlayerEntity); + shooter = shooterConfig; + } else { + m_registry.AddComponent(m_localPlayerEntity, Shooter{shooterConfig.fireRate, shooterConfig.offsetX, shooterConfig.offsetY}); + } + + if (m_registry.HasComponent(m_localPlayerEntity)) { + auto& shootCommand = m_registry.GetComponent(m_localPlayerEntity); + shootCommand = ShootCommand{}; + } else { + m_registry.AddComponent(m_localPlayerEntity, ShootCommand{}); + } + + if (m_registry.HasComponent(m_localPlayerEntity)) { + auto& health = m_registry.GetComponent(m_localPlayerEntity); + health.max = playerConfig.maxHealth; + if (health.current > playerConfig.maxHealth) { + health.current = playerConfig.maxHealth; + } + } else { + m_registry.AddComponent(m_localPlayerEntity, Health{playerConfig.maxHealth}); + } + + auto spriteIt = m_levelAssets.sprites.find("player_blue"); + if (spriteIt != m_levelAssets.sprites.end()) { + if (m_registry.HasComponent(m_localPlayerEntity)) { + auto& drawable = m_registry.GetComponent(m_localPlayerEntity); + drawable.spriteId = spriteIt->second; + drawable.layer = 10; + drawable.scale = {0.5f, 0.5f}; + } else { + auto& drawable = m_registry.AddComponent(m_localPlayerEntity, Drawable(spriteIt->second, 10)); + drawable.scale = {0.5f, 0.5f}; + } + } + } + + } +} diff --git a/games/rtype/client/src/game/GameStateNetwork.cpp b/games/rtype/client/src/game/GameStateNetwork.cpp new file mode 100644 index 0000000..d443c3e --- /dev/null +++ b/games/rtype/client/src/game/GameStateNetwork.cpp @@ -0,0 +1,1059 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** GameState - Network state update functions +*/ + +#include "../../include/GameState.hpp" + +#include "ECS/Components/TextLabel.hpp" +#include "ECS/Component.hpp" +#include "Core/Logger.hpp" +#include "ECS/PowerUpFactory.hpp" +#include "Animation/AnimationModule.hpp" +#include + +using namespace RType::ECS; +using namespace Animation; + +namespace RType { + namespace Client { + + void InGameState::OnServerStateUpdate(uint32_t /*tick*/, const std::vector& entities, const std::vector& inputAcks) { + + if (m_context.networkClient) { + m_serverScrollOffset = m_context.networkClient->GetLastScrollOffset(); + } + + for (const auto& ack : inputAcks) { + if (ack.playerHash == m_context.playerHash) { + ReconcileWithServer(ack); + break; + } + } + + std::unordered_set receivedIds; + std::vector entitiesToRemove; + + { + auto localObstacles = m_registry.GetEntitiesWithComponent(); + for (auto obsEntity : localObstacles) { + if (!m_registry.IsEntityAlive(obsEntity)) { + continue; + } + bool contaminated = m_registry.HasComponent(obsEntity) || + m_registry.HasComponent(obsEntity) || + m_registry.HasComponent(obsEntity) || + m_registry.HasComponent(obsEntity); + if (contaminated) { + + for (auto itMap = m_networkEntityMap.begin(); itMap != m_networkEntityMap.end();) { + if (itMap->second == obsEntity) { + itMap = m_networkEntityMap.erase(itMap); + } else { + ++itMap; + } + } + for (auto itId = m_obstacleIdToCollider.begin(); itId != m_obstacleIdToCollider.end();) { + if (itId->second == obsEntity) { + itId = m_obstacleIdToCollider.erase(itId); + } else { + ++itId; + } + } + auto itColl = std::find(m_obstacleColliderEntities.begin(), m_obstacleColliderEntities.end(), obsEntity); + if (itColl != m_obstacleColliderEntities.end()) { + m_obstacleColliderEntities.erase(itColl); + } + if (m_registry.IsEntityAlive(obsEntity)) { + m_registry.DestroyEntity(obsEntity); + } + } + } + } + + for (const auto& entityState : entities) { + receivedIds.insert(entityState.entityId); + + network::EntityType type = static_cast(entityState.entityType); + + if (auto it = m_networkEntityMap.find(entityState.entityId); it == m_networkEntityMap.end()) { + if (type == network::EntityType::PLAYER) { + if (entityState.ownerHash == m_context.playerHash && m_localPlayerEntity != ECS::NULL_ENTITY) { + continue; + } + + Renderer::SpriteId playerSprite = Renderer::INVALID_SPRITE_ID; + size_t playerIndex = m_networkEntityMap.size() % 3; + + const char* spriteKeys[] = {"player_green", "player_blue", "player_red"}; + auto spriteIt = m_levelAssets.sprites.find(spriteKeys[playerIndex]); + if (spriteIt != m_levelAssets.sprites.end()) { + playerSprite = spriteIt->second; + } + + if (playerSprite == Renderer::INVALID_SPRITE_ID) { + continue; + } + + auto newEntity = m_registry.CreateEntity(); + CleanupInvalidComponents(newEntity, network::EntityType::PLAYER); + + m_registry.AddComponent(newEntity, Position{entityState.x, entityState.y}); + m_registry.AddComponent(newEntity, Velocity{entityState.vx, entityState.vy}); + m_registry.AddComponent(newEntity, Health{static_cast(entityState.health), 300}); + + m_registry.AddComponent(newEntity, Controllable{200.0f}); + m_registry.AddComponent(newEntity, Shooter{0.2f, 50.0f, 20.0f}); + m_registry.AddComponent(newEntity, BoxCollider{25.0f, 25.0f}); + m_registry.AddComponent(newEntity, CircleCollider{12.5f}); + m_registry.AddComponent(newEntity, CollisionLayer(CollisionLayers::PLAYER, CollisionLayers::ENEMY | CollisionLayers::ENEMY_BULLET | CollisionLayers::OBSTACLE)); + + auto& drawable = m_registry.AddComponent(newEntity, Drawable(playerSprite, 10)); + drawable.scale = {0.5f, 0.5f}; + drawable.origin = Math::Vector2(128.0f, 128.0f); + + auto [playerName, playerNum] = FindPlayerNameAndNumber(entityState.ownerHash, m_assignedPlayerNumbers); + uint8_t pNum = (playerNum > 0 && playerNum <= MAX_PLAYERS) ? playerNum : 1; + m_registry.AddComponent(newEntity, Player{pNum, entityState.ownerHash, false}); + if (playerNum > 0 && playerNum <= MAX_PLAYERS) { + m_assignedPlayerNumbers.insert(playerNum); + } + CreatePlayerNameLabel(newEntity, playerName, entityState.x, entityState.y); + + ApplyPowerUpStateToPlayer(newEntity, entityState); + + m_networkEntityMap[entityState.entityId] = newEntity; + + if (entityState.ownerHash == m_context.playerHash) { + m_localPlayerEntity = newEntity; + if (m_registry.HasComponent(newEntity)) { + auto& playerComp = m_registry.GetComponent(newEntity); + playerComp.isLocalPlayer = true; + } + if (!m_registry.HasComponent(newEntity)) { + m_registry.AddComponent(newEntity, ShootCommand{}); + } + if (m_context.playerNumber >= 1 && m_context.playerNumber <= MAX_PLAYERS) { + size_t localPlayerIndex = static_cast(m_context.playerNumber - 1); + m_playersHUD[localPlayerIndex].playerEntity = newEntity; + m_playersHUD[localPlayerIndex].active = true; + m_playersHUD[localPlayerIndex].score = entityState.score; + m_playersHUD[localPlayerIndex].health = static_cast(entityState.health); + m_playersHUD[localPlayerIndex].maxHealth = 300; + m_playerScore = entityState.score; + if (entityState.health > 0) { + m_playersHUD[localPlayerIndex].isDead = false; + } + } + } + if (playerNum > 0 && playerNum <= MAX_PLAYERS) { + size_t hudPlayerIndex = static_cast(playerNum - 1); + m_playersHUD[hudPlayerIndex].active = true; + m_playersHUD[hudPlayerIndex].playerEntity = newEntity; + m_playersHUD[hudPlayerIndex].score = entityState.score; + m_playersHUD[hudPlayerIndex].health = static_cast(entityState.health); + m_playersHUD[hudPlayerIndex].maxHealth = 300; + + if (entityState.health > 0) { + m_playersHUD[hudPlayerIndex].isDead = false; + } + } + } else if (type == network::EntityType::ENEMY) { + uint8_t enemyType = entityState.flags; + EnemySpriteConfig config = GetEnemySpriteConfig(enemyType); + Renderer::SpriteId enemySprite = config.sprite; + Math::Color enemyTint = config.tint; + + if (enemySprite == Renderer::INVALID_SPRITE_ID) { + enemySprite = m_enemyGreenSprite; + } + + if (enemySprite == Renderer::INVALID_SPRITE_ID) { + Core::Logger::Error("[GameState] Missing enemy sprite for type {}, skipping entity {}", static_cast(enemyType), entityState.entityId); + continue; + } + + auto newEntity = m_registry.CreateEntity(); + CleanupInvalidComponents(newEntity, network::EntityType::ENEMY); + + m_registry.AddComponent(newEntity, Position{entityState.x, entityState.y}); + m_registry.AddComponent(newEntity, Velocity{entityState.vx, entityState.vy}); + m_registry.AddComponent(newEntity, Health{static_cast(entityState.health), 100}); + m_registry.AddComponent(newEntity, Enemy{static_cast(enemyType)}); + + // Add collision components for hit effect detection + m_registry.AddComponent(newEntity, BoxCollider(64.0f, 64.0f)); + m_registry.AddComponent(newEntity, + CollisionLayer(CollisionLayers::ENEMY, CollisionLayers::PLAYER | CollisionLayers::PLAYER_BULLET)); + + auto& drawable = m_registry.AddComponent(newEntity, Drawable(enemySprite, 1)); + drawable.scale = {0.5f, 0.5f}; + drawable.origin = Math::Vector2(128.0f, 128.0f); + drawable.rotation = config.rotation; + drawable.tint = enemyTint; + + m_networkEntityMap[entityState.entityId] = newEntity; + } else if (type == network::EntityType::BOSS) { + Renderer::SpriteId bossSprite = Renderer::INVALID_SPRITE_ID; + + std::vector bossNames = {"boss_2", "boss_dragon", "boss", "boss_3"}; + for (const auto& bossName : bossNames) { + auto spriteIt = m_levelAssets.sprites.find(bossName); + if (spriteIt != m_levelAssets.sprites.end()) { + bossSprite = spriteIt->second; + Core::Logger::Info("[GameState] Using boss sprite: {}", bossName); + break; + } + } + + if (bossSprite == Renderer::INVALID_SPRITE_ID) { + Core::Logger::Warning("[GameState] No boss sprite found in level assets"); + continue; + } + + auto newEntity = m_registry.CreateEntity(); + CleanupInvalidComponents(newEntity, network::EntityType::BOSS); + + m_registry.AddComponent(newEntity, Position{entityState.x, entityState.y}); + m_registry.AddComponent(newEntity, Velocity{entityState.vx, entityState.vy}); + m_registry.AddComponent(newEntity, Health{static_cast(entityState.health), 1000}); + m_registry.AddComponent(newEntity, Boss{}); + + // Add collision components for hit effect detection + m_registry.AddComponent(newEntity, BoxCollider(200.0f, 200.0f)); + m_registry.AddComponent(newEntity, + CollisionLayer(CollisionLayers::ENEMY, CollisionLayers::PLAYER | CollisionLayers::PLAYER_BULLET)); + + auto& drawable = m_registry.AddComponent(newEntity, Drawable(bossSprite, 5)); + drawable.scale = {3.0f, 3.0f}; + drawable.origin = Math::Vector2(0.0f, 0.0f); + + m_networkEntityMap[entityState.entityId] = newEntity; + + m_bossHealthBar.currentHealth = static_cast(entityState.health); + m_bossHealthBar.maxHealth = 100; + m_bossHealthBar.bossNetworkId = entityState.entityId; + } else if (type == network::EntityType::BULLET) { + auto newEntity = m_registry.CreateEntity(); + CleanupInvalidComponents(newEntity, network::EntityType::BULLET); + + m_registry.AddComponent(newEntity, Position{entityState.x, entityState.y}); + m_registry.AddComponent(newEntity, Velocity{entityState.vx, entityState.vy}); + + if (entityState.flags == 15) { + auto thirdBulletSpriteIt = m_levelAssets.sprites.find("third_bullet"); + if (thirdBulletSpriteIt != m_levelAssets.sprites.end()) { + auto& d = m_registry.AddComponent(newEntity, Drawable(thirdBulletSpriteIt->second, 12)); + d.scale = {2.5f, 2.5f}; + d.origin = Math::Vector2(16.0f, 16.0f); + } else { + Core::Logger::Warning("[GameState] Missing third bullet sprite (entity {})", entityState.entityId); + m_registry.DestroyEntity(newEntity); + continue; + } + } else if (entityState.flags == 14) { + auto blackOrbSpriteIt = m_levelAssets.sprites.find("black_orb"); + if (blackOrbSpriteIt != m_levelAssets.sprites.end()) { + auto& d = m_registry.AddComponent(newEntity, Drawable(blackOrbSpriteIt->second, 12)); + d.scale = {2.0f, 2.0f}; + d.origin = Math::Vector2(20.0f, 20.0f); + } else { + Core::Logger::Warning("[GameState] Missing black orb sprite (entity {})", entityState.entityId); + m_registry.DestroyEntity(newEntity); + continue; + } + } else if (entityState.flags == 16) { + auto waveAttackSpriteIt = m_levelAssets.sprites.find("wave_attack"); + if (waveAttackSpriteIt != m_levelAssets.sprites.end() && + m_waveAttackClipId != Animation::INVALID_CLIP_ID) { + + auto& d = m_registry.AddComponent(newEntity, Drawable(waveAttackSpriteIt->second, 12)); + d.scale = {3.0f, 3.0f}; + d.origin = Math::Vector2(15.0f, 20.0f); + + if (m_animationModule) { + auto& anim = m_registry.AddComponent(newEntity, ECS::SpriteAnimation{m_waveAttackClipId, true, 1.0f}); + auto firstFrame = m_animationModule->GetFrameAtTime(m_waveAttackClipId, 0.0f, true); + anim.currentRegion = firstFrame.region; + anim.currentFrameIndex = 0; + + auto& animatedSprite = m_registry.AddComponent(newEntity); + animatedSprite.needsUpdate = true; + } + + m_registry.AddComponent(newEntity, Bullet(ECS::NULL_ENTITY)); + m_registry.AddComponent(newEntity, CircleCollider(45.0f)); + m_registry.AddComponent(newEntity, Damage(10)); + m_registry.AddComponent(newEntity, + CollisionLayer(CollisionLayers::ENEMY_BULLET, CollisionLayers::PLAYER)); + } else { + Core::Logger::Warning("[GameState] Missing wave_attack sprite or animation (entity {})", entityState.entityId); + m_registry.DestroyEntity(newEntity); + continue; + } + } else if (entityState.flags == 17) { + auto secondAttackSpriteIt = m_levelAssets.sprites.find("second_attack"); + if (secondAttackSpriteIt != m_levelAssets.sprites.end() && + m_secondAttackClipId != Animation::INVALID_CLIP_ID) { + + auto& d = m_registry.AddComponent(newEntity, Drawable(secondAttackSpriteIt->second, 12)); + d.scale = {2.8f, 2.8f}; + d.origin = Math::Vector2(11.0f, 11.0f); + + if (m_animationModule) { + auto& anim = m_registry.AddComponent(newEntity, ECS::SpriteAnimation{m_secondAttackClipId, true, 1.0f}); + auto firstFrame = m_animationModule->GetFrameAtTime(m_secondAttackClipId, 0.0f, true); + anim.currentRegion = firstFrame.region; + anim.currentFrameIndex = 0; + + auto& animatedSprite = m_registry.AddComponent(newEntity); + animatedSprite.needsUpdate = true; + } + + } else { + Core::Logger::Warning("[GameState] Missing second_attack sprite or animation (entity {})", entityState.entityId); + m_registry.DestroyEntity(newEntity); + continue; + } + } else if (entityState.flags == 18) { + auto fireBulletSpriteIt = m_levelAssets.sprites.find("fire_bullet"); + if (fireBulletSpriteIt != m_levelAssets.sprites.end() && + m_fireBulletClipId != Animation::INVALID_CLIP_ID) { + + auto& d = m_registry.AddComponent(newEntity, Drawable(fireBulletSpriteIt->second, 12)); + d.scale = {2.5f, 2.5f}; + d.origin = Math::Vector2(10.5f, 8.5f); + + if (m_animationModule) { + auto& anim = m_registry.AddComponent(newEntity, ECS::SpriteAnimation{m_fireBulletClipId, true, 1.0f}); + auto firstFrame = m_animationModule->GetFrameAtTime(m_fireBulletClipId, 0.0f, true); + anim.currentRegion = firstFrame.region; + anim.currentFrameIndex = 0; + + auto& animatedSprite = m_registry.AddComponent(newEntity); + animatedSprite.needsUpdate = true; + } + + m_registry.AddComponent(newEntity, ECS::CircleCollider{12.0f}); + m_registry.AddComponent(newEntity, + CollisionLayer(CollisionLayers::ENEMY_BULLET, CollisionLayers::PLAYER)); + } else { + Core::Logger::Warning("[GameState] Missing fire_bullet sprite or animation (entity {})", entityState.entityId); + m_registry.DestroyEntity(newEntity); + continue; + } + } else if (entityState.flags == 20) { + Core::Logger::Debug("[GameState] Received exploding mine entity {} at ({}, {})", entityState.entityId, entityState.x, entityState.y); + auto mineExplosionSpriteIt = m_levelAssets.sprites.find("mine_explosion"); + Core::Logger::Debug("[GameState] Mine explosion sprite found: {}, clipId valid: {}", + mineExplosionSpriteIt != m_levelAssets.sprites.end(), + m_mineExplosionClipId != Animation::INVALID_CLIP_ID); + if (mineExplosionSpriteIt != m_levelAssets.sprites.end() && + m_mineExplosionClipId != Animation::INVALID_CLIP_ID) { + + auto& d = m_registry.AddComponent(newEntity, Drawable(mineExplosionSpriteIt->second, 12)); + d.scale = {2.0f, 2.0f}; + d.origin = Math::Vector2(13.0f, 11.5f); + + if (m_animationModule) { + auto& anim = m_registry.AddComponent(newEntity, ECS::SpriteAnimation{m_mineExplosionClipId, false, 1.0f}); + auto firstFrame = m_animationModule->GetFrameAtTime(m_mineExplosionClipId, 0.0f, false); + anim.currentRegion = firstFrame.region; + anim.currentFrameIndex = 0; + + auto& animatedSprite = m_registry.AddComponent(newEntity); + animatedSprite.needsUpdate = true; + } + } else { + Core::Logger::Warning("[GameState] Missing mine_explosion sprite or animation (entity {})", entityState.entityId); + m_registry.DestroyEntity(newEntity); + continue; + } + } else if (entityState.flags == 19) { + Core::Logger::Debug("[GameState] Received mine entity {} at ({}, {})", entityState.entityId, entityState.x, entityState.y); + auto mineSpriteIt = m_levelAssets.sprites.find("mine"); + Core::Logger::Debug("[GameState] Mine sprite found: {}, clipId valid: {}", + mineSpriteIt != m_levelAssets.sprites.end(), + m_mineClipId != Animation::INVALID_CLIP_ID); + if (mineSpriteIt != m_levelAssets.sprites.end() && + m_mineClipId != Animation::INVALID_CLIP_ID) { + + auto& d = m_registry.AddComponent(newEntity, Drawable(mineSpriteIt->second, 12)); + d.scale = {2.5f, 2.5f}; + d.origin = Math::Vector2(9.5f, 22.5f); + + if (m_animationModule) { + auto& anim = m_registry.AddComponent(newEntity, ECS::SpriteAnimation{m_mineClipId, true, 1.0f}); + auto firstFrame = m_animationModule->GetFrameAtTime(m_mineClipId, 0.0f, true); + anim.currentRegion = firstFrame.region; + anim.currentFrameIndex = 0; + + auto& animatedSprite = m_registry.AddComponent(newEntity); + animatedSprite.needsUpdate = true; + } + + m_registry.AddComponent(newEntity, ECS::CircleCollider{20.0f}); + m_registry.AddComponent(newEntity, + CollisionLayer(CollisionLayers::OBSTACLE, CollisionLayers::PLAYER)); + } else { + Core::Logger::Warning("[GameState] Missing mine sprite or animation (entity {})", entityState.entityId); + m_registry.DestroyEntity(newEntity); + continue; + } + } else if (entityState.flags == 13) { + auto bossBulletSpriteIt = m_levelAssets.sprites.find("boss_bullet"); + if (bossBulletSpriteIt != m_levelAssets.sprites.end()) { + auto& d = m_registry.AddComponent(newEntity, Drawable(bossBulletSpriteIt->second, 12)); + d.scale = {2.0f, 2.0f}; + d.origin = Math::Vector2(8.0f, 4.0f); + } else { + Core::Logger::Warning("[GameState] Missing boss bullet sprite (entity {})", entityState.entityId); + m_registry.DestroyEntity(newEntity); + continue; + } + } else if (entityState.flags >= 10) { + uint8_t enemyType = entityState.flags - 10; + EnemyBulletSpriteConfig config = GetEnemyBulletSpriteConfig(enemyType); + Renderer::SpriteId bulletSprite = config.sprite; + Math::Color bulletTint = config.tint; + float scaleValue = config.scale; + + if (bulletSprite == Renderer::INVALID_SPRITE_ID) { + auto bulletSpriteIt = m_levelAssets.sprites.find("bullet"); + if (bulletSpriteIt != m_levelAssets.sprites.end()) { + bulletSprite = bulletSpriteIt->second; + } + } + + if (bulletSprite == Renderer::INVALID_SPRITE_ID) { + Core::Logger::Warning("[GameState] Missing enemy bullet sprite for type {} (entity {})", static_cast(enemyType), entityState.entityId); + m_registry.DestroyEntity(newEntity); + continue; + } + + auto& d = m_registry.AddComponent(newEntity, Drawable(bulletSprite, 12)); + d.scale = {scaleValue, scaleValue}; + d.origin = Math::Vector2(128.0f, 128.0f); + d.tint = bulletTint; + } else { + auto bulletSpriteIt = m_levelAssets.sprites.find("bullet"); + if (bulletSpriteIt != m_levelAssets.sprites.end()) { + auto& d = m_registry.AddComponent(newEntity, Drawable(bulletSpriteIt->second, 12)); + d.scale = {0.1f, 0.1f}; + d.origin = Math::Vector2(128.0f, 128.0f); + d.tint = {0.2f, 0.8f, 1.0f, 1.0f}; + } + } + + bool isPlayerBullet = (entityState.flags < 10); + + m_registry.AddComponent(newEntity, Bullet(m_localPlayerEntity)); + m_registry.AddComponent(newEntity, BoxCollider(20.0f, 10.0f)); + m_registry.AddComponent(newEntity, Damage(25)); + m_registry.AddComponent(newEntity, + CollisionLayer(CollisionLayers::PLAYER_BULLET, + CollisionLayers::ENEMY | CollisionLayers::OBSTACLE)); + + if (isPlayerBullet && m_localPlayerEntity != ECS::NULL_ENTITY && + m_registry.IsEntityAlive(m_localPlayerEntity) && m_effectFactory && + m_registry.HasComponent(m_localPlayerEntity)) { + const auto& playerPos = m_registry.GetComponent(m_localPlayerEntity); + float bulletX = entityState.x; + float bulletY = entityState.y; + float dx = bulletX - playerPos.x; + float dy = bulletY - playerPos.y; + float distance = std::sqrt(dx * dx + dy * dy); + + if (distance < 80.0f && dx > 0 && std::abs(dy) < 40.0f && entityState.vx > 400.0f) { + m_effectFactory->CreateShootingEffect(m_registry, playerPos.x, playerPos.y, m_localPlayerEntity); + } + } + + m_networkEntityMap[entityState.entityId] = newEntity; + m_bulletFlagsMap[entityState.entityId] = entityState.flags; + } else if (type == network::EntityType::POWERUP) { + uint8_t powerupType = entityState.flags; + ECS::PowerUpType puType = static_cast(powerupType); + + auto newEntity = m_registry.CreateEntity(); + CleanupInvalidComponents(newEntity, network::EntityType::POWERUP); + + m_registry.AddComponent(newEntity, Position{entityState.x, entityState.y}); + m_registry.AddComponent(newEntity, Velocity{entityState.vx, entityState.vy}); + + Math::Color powerupColor = ECS::PowerUpFactory::GetPowerUpColor(puType); + Renderer::SpriteId powerupSprite = Renderer::INVALID_SPRITE_ID; + + powerupSprite = GetPowerUpSprite(puType); + + if (powerupSprite == Renderer::INVALID_SPRITE_ID && puType == ECS::PowerUpType::FORCE_POD) { + auto textureIt = m_levelAssets.textures.find("powerup-force-pod"); + if (textureIt != m_levelAssets.textures.end()) { + powerupSprite = m_renderer->CreateSprite(textureIt->second, {}); + } else { + powerupSprite = GetPowerUpSprite(ECS::PowerUpType::LASER_BEAM); + } + } + + if (powerupSprite != Renderer::INVALID_SPRITE_ID) { + auto& d = m_registry.AddComponent(newEntity, Drawable(powerupSprite, 5)); + float scale = ECS::PowerUpFactory::GetPowerUpScale(puType); + d.scale = {scale, scale}; + d.tint = powerupColor; + + if (puType == ECS::PowerUpType::FORCE_POD && m_effectFactory) { + const auto& config = m_effectFactory->GetConfig(); + if (config.forcePodAnimation != Animation::INVALID_CLIP_ID) { + if (config.forcePodSprite != Renderer::INVALID_SPRITE_ID) { + d.spriteId = config.forcePodSprite; + } + + auto& anim = m_registry.AddComponent(newEntity, + ECS::SpriteAnimation(config.forcePodAnimation, true, 1.0f)); + anim.looping = true; + + if (config.forcePodFirstFrameRegion.size.x > 0.0f && config.forcePodFirstFrameRegion.size.y > 0.0f) { + anim.currentRegion = config.forcePodFirstFrameRegion; + anim.currentFrameIndex = 0; + } else if (m_animationModule) { + auto firstFrame = m_animationModule->GetFrameAtTime(config.forcePodAnimation, 0.0f, true); + anim.currentRegion = firstFrame.region; + anim.currentFrameIndex = m_animationModule->GetFrameIndexAtTime(config.forcePodAnimation, 0.0f, true); + } + auto& animatedSprite = m_registry.AddComponent(newEntity); + animatedSprite.needsUpdate = true; + } else { + auto& glow = m_registry.AddComponent(newEntity); + glow.baseScale = scale; + } + } else { + auto& glow = m_registry.AddComponent(newEntity); + glow.baseScale = scale; + } + } + + m_registry.AddComponent(newEntity, BoxCollider{32.0f, 32.0f}); + m_registry.AddComponent(newEntity, + CollisionLayer(CollisionLayers::POWERUP, + CollisionLayers::PLAYER)); + m_registry.AddComponent(newEntity, ECS::PowerUp(puType, entityState.entityId)); + + m_networkEntityMap[entityState.entityId] = newEntity; + } else if (type == network::EntityType::OBSTACLE) { + uint64_t obstacleId = entityState.ownerHash; + auto colliderIt = m_obstacleIdToCollider.find(obstacleId); + + if (colliderIt == m_obstacleIdToCollider.end()) { + continue; + } + + ECS::Entity colliderEntity = colliderIt->second; + + if (!m_registry.IsEntityAlive(colliderEntity)) { + continue; + } + + if (!m_registry.HasComponent(colliderEntity) || + !m_registry.HasComponent(colliderEntity)) { + continue; + } + + m_networkEntityMap[entityState.entityId] = colliderEntity; + + CleanupInvalidComponents(colliderEntity, network::EntityType::OBSTACLE); + } + } else { + auto ecsEntity = it->second; + + if (type == network::EntityType::BULLET) { + bool contaminated = false; + if (!m_registry.IsEntityAlive(ecsEntity)) { + contaminated = true; + } else if (m_registry.HasComponent(ecsEntity) || + m_registry.HasComponent(ecsEntity)) { + contaminated = true; + } + if (contaminated) { + if (m_registry.IsEntityAlive(ecsEntity)) { + m_registry.DestroyEntity(ecsEntity); + } + m_networkEntityMap.erase(it); + m_bulletFlagsMap.erase(entityState.entityId); + continue; + } + } + + if (type == network::EntityType::BULLET) { + auto flagsIt = m_bulletFlagsMap.find(entityState.entityId); + bool flagsChanged = false; + + if (flagsIt != m_bulletFlagsMap.end()) { + if (flagsIt->second != entityState.flags) { + flagsChanged = true; + } + } + + if (flagsChanged || !m_registry.IsEntityAlive(ecsEntity)) { + if (m_registry.IsEntityAlive(ecsEntity)) { + m_registry.DestroyEntity(ecsEntity); + } + m_networkEntityMap.erase(it); + m_bulletFlagsMap.erase(entityState.entityId); + continue; + } + } + + if (type == network::EntityType::OBSTACLE) { + uint64_t obstacleId = entityState.ownerHash; + auto colliderIt = m_obstacleIdToCollider.find(obstacleId); + + if (colliderIt == m_obstacleIdToCollider.end()) { + continue; + } + + ECS::Entity colliderEntity = colliderIt->second; + + if (!m_registry.IsEntityAlive(colliderEntity)) { + m_networkEntityMap.erase(it); + continue; + } + + if (!m_registry.HasComponent(colliderEntity)) { + CleanupInvalidComponents(colliderEntity, network::EntityType::OBSTACLE); + m_networkEntityMap.erase(it); + continue; + } + + if (!m_registry.HasComponent(colliderEntity)) { + m_networkEntityMap.erase(it); + continue; + } + + auto& colliderPos = m_registry.GetComponent(colliderEntity); + colliderPos.x = entityState.x; + colliderPos.y = entityState.y; + + if (m_registry.HasComponent(colliderEntity)) { + m_registry.RemoveComponent(colliderEntity); + } + + if (m_registry.HasComponent(colliderEntity)) { + const auto& metadata = m_registry.GetComponent(colliderEntity); + if (metadata.visualEntity != ECS::NULL_ENTITY && + m_registry.IsEntityAlive(metadata.visualEntity) && + m_registry.HasComponent(metadata.visualEntity)) { + auto& visualPos = m_registry.GetComponent(metadata.visualEntity); + visualPos.x = entityState.x - metadata.offsetX; + visualPos.y = entityState.y - metadata.offsetY; + + if (m_registry.HasComponent(metadata.visualEntity)) { + m_registry.RemoveComponent(metadata.visualEntity); + } + } + } + + continue; + } + + if (m_registry.HasComponent(ecsEntity)) { + if (type == network::EntityType::PLAYER && ecsEntity == m_localPlayerEntity) { + + } else { + auto& pos = m_registry.GetComponent(ecsEntity); + bool useInterpolation = (type == network::EntityType::PLAYER || + type == network::EntityType::ENEMY || + type == network::EntityType::BOSS); + if (useInterpolation && m_isNetworkSession) { + auto interpIt = m_interpolationStates.find(entityState.entityId); + if (interpIt == m_interpolationStates.end()) { + InterpolationState state; + state.prevX = pos.x; + state.prevY = pos.y; + state.targetX = entityState.x; + state.targetY = entityState.y; + state.interpTime = 0.0f; + state.interpDuration = 1.0f / 60.0f; + m_interpolationStates[entityState.entityId] = state; + } else { + interpIt->second.prevX = pos.x; + interpIt->second.prevY = pos.y; + interpIt->second.targetX = entityState.x; + interpIt->second.targetY = entityState.y; + interpIt->second.interpTime = 0.0f; + } + } else { + pos.x = entityState.x; + pos.y = entityState.y; + } + } + + if (type == network::EntityType::PLAYER) { + auto& pos = m_registry.GetComponent(ecsEntity); + UpdatePlayerNameLabelPosition(ecsEntity, pos.x, pos.y); + ApplyPowerUpStateToPlayer(ecsEntity, entityState); + } + } + + if (m_registry.HasComponent(ecsEntity)) { + auto& vel = m_registry.GetComponent(ecsEntity); + vel.dx = entityState.vx; + vel.dy = entityState.vy; + } + + if (type == network::EntityType::PLAYER) { + for (size_t i = 0; i < MAX_PLAYERS; i++) { + if (m_playersHUD[i].playerEntity == ecsEntity) { + m_playersHUD[i].score = entityState.score; + m_playersHUD[i].health = static_cast(entityState.health); + if (ecsEntity == m_localPlayerEntity) { + m_playerScore = entityState.score; + } + break; + } + } + } + + if (type == network::EntityType::POWERUP) { + if (entityState.health == 0 && m_registry.IsEntityAlive(ecsEntity)) { + m_registry.DestroyEntity(ecsEntity); + entitiesToRemove.push_back(entityState.entityId); + } + continue; + } + + if (m_registry.HasComponent(ecsEntity)) { + auto& health = m_registry.GetComponent(ecsEntity); + int newHealth = static_cast(entityState.health); + if (newHealth < 0) + newHealth = 0; + + bool playerIsDead = false; + size_t playerIndex = MAX_PLAYERS; + bool isPlayerEntity = (type == network::EntityType::PLAYER); + + for (size_t i = 0; i < MAX_PLAYERS; i++) { + if (m_playersHUD[i].playerEntity == ecsEntity) { + playerIsDead = m_playersHUD[i].isDead; + playerIndex = i; + break; + } + } + if (ecsEntity == m_localPlayerEntity && playerIndex == MAX_PLAYERS) { + size_t localPlayerIndex = static_cast(m_context.playerNumber - 1); + if (localPlayerIndex < MAX_PLAYERS) { + playerIsDead = m_playersHUD[localPlayerIndex].isDead; + playerIndex = localPlayerIndex; + } + } + + if (playerIsDead) { + health.current = 0; + if (playerIndex < MAX_PLAYERS) { + m_playersHUD[playerIndex].health = 0; + } + } else { + health.current = newHealth; + + if (isPlayerEntity && playerIndex < MAX_PLAYERS) { + // Player dies when health reaches 0 (all 300 HP gone) + if (newHealth <= 0) { + m_playersHUD[playerIndex].isDead = true; + m_playersHUD[playerIndex].health = 0; + health.current = 0; + + DestroyPlayerNameLabel(ecsEntity); + + for (const auto& p : m_context.allPlayers) { + if (m_playersHUD[playerIndex].playerEntity == ecsEntity) { + if (p.number > 0 && p.number <= MAX_PLAYERS) { + m_assignedPlayerNumbers.erase(p.number); + } + break; + } + } + + if (m_registry.IsEntityAlive(ecsEntity)) { + m_registry.DestroyEntity(ecsEntity); + } + m_playersHUD[playerIndex].playerEntity = NULL_ENTITY; + if (ecsEntity == m_localPlayerEntity) { + m_localPlayerEntity = NULL_ENTITY; + } + entitiesToRemove.push_back(entityState.entityId); + } else { + m_playersHUD[playerIndex].health = newHealth; + } + } + } + } + + if (type == network::EntityType::BOSS) { + if (m_registry.HasComponent(ecsEntity)) { + auto& drawable = m_registry.GetComponent(ecsEntity); + if (entityState.flags == 1) { + drawable.tint = {1.0f, 0.3f, 0.3f, 1.0f}; + } else { + drawable.tint = {1.0f, 1.0f, 1.0f, 1.0f}; + } + } + + if (!m_bossWarningTriggered && entityState.x < 2200.0f && entityState.x > -200.0f) { + Core::Logger::Info("[GameState] Boss warning TRIGGERED at x={}", entityState.x); + m_bossWarningActive = true; + m_bossWarningTriggered = true; + m_bossWarningTimer = 0.0f; + } + + if (!m_bossHealthBar.active && entityState.x < 1300.0f) { + initializeBossHealthBar(); + m_bossHealthBar.maxHealth = 100; + m_bossHealthBar.bossNetworkId = entityState.entityId; + } + + m_bossHealthBar.currentHealth = static_cast(entityState.health); + + if (m_bossHealthBar.active && entityState.health == 0) { + destroyBossHealthBar(); + } else { + updateBossHealthBar(); + } + } + + if (type == network::EntityType::PLAYER) { + ApplyPowerUpStateToPlayer(ecsEntity, entityState); + } + } + } + + + for (uint32_t entityId : entitiesToRemove) { + auto it = m_networkEntityMap.find(entityId); + if (it != m_networkEntityMap.end()) { + m_networkEntityMap.erase(it); + } + m_bulletFlagsMap.erase(entityId); + } + + for (auto it = m_networkEntityMap.begin(); it != m_networkEntityMap.end();) { + if (receivedIds.find(it->first) == receivedIds.end()) { + auto ecsEntity = it->second; + uint32_t networkId = it->first; + + if (m_effectFactory && m_registry.IsEntityAlive(ecsEntity) && + m_registry.HasComponent(ecsEntity) && + m_registry.HasComponent(ecsEntity)) { + const auto& pos = m_registry.GetComponent(ecsEntity); + auto explosionEntity = m_effectFactory->CreateExplosionSmall(m_registry, pos.x, pos.y); + + if (m_explosionSprite != Renderer::INVALID_SPRITE_ID && + m_registry.HasComponent(explosionEntity)) { + auto& drawable = m_registry.GetComponent(explosionEntity); + if (drawable.spriteId == Renderer::INVALID_SPRITE_ID) { + drawable.spriteId = m_explosionSprite; + } + } + + if (m_effectFactory && m_registry.HasComponent(explosionEntity)) { + const auto& config = m_effectFactory->GetConfig(); + auto& anim = m_registry.GetComponent(explosionEntity); + if (anim.clipId == config.explosionSmall && + config.explosionFirstFrameRegion.size.x > 0.0f && + config.explosionFirstFrameRegion.size.y > 0.0f) { + anim.currentRegion = config.explosionFirstFrameRegion; + anim.currentFrameIndex = 0; + if (m_registry.HasComponent(explosionEntity)) { + m_registry.GetComponent(explosionEntity).needsUpdate = true; + } + } + } + } + + // Create hit effect when player bullets are destroyed (server-side collision) + auto bulletFlagsIt = m_bulletFlagsMap.find(networkId); + if (bulletFlagsIt != m_bulletFlagsMap.end() && bulletFlagsIt->second < 10) { + // Player bullet was destroyed - create hit effect at last position + if (m_effectFactory && m_registry.IsEntityAlive(ecsEntity) && + m_registry.HasComponent(ecsEntity)) { + const auto& pos = m_registry.GetComponent(ecsEntity); + m_effectFactory->CreateHitEffect(m_registry, pos.x, pos.y); + Core::Logger::Info("[Network] Created hit effect for destroyed bullet at ({}, {})", pos.x, pos.y); + } + } + + DestroyPlayerNameLabel(ecsEntity); + + if (m_registry.IsEntityAlive(ecsEntity) && m_registry.HasComponent(ecsEntity)) { + for (auto obsIt = m_obstacleIdToCollider.begin(); obsIt != m_obstacleIdToCollider.end(); ) { + if (obsIt->second == ecsEntity) { + obsIt = m_obstacleIdToCollider.erase(obsIt); + } else { + ++obsIt; + } + } + + auto colliderIt = std::find(m_obstacleColliderEntities.begin(), m_obstacleColliderEntities.end(), ecsEntity); + if (colliderIt != m_obstacleColliderEntities.end()) { + m_obstacleColliderEntities.erase(colliderIt); + } + + if (m_registry.HasComponent(ecsEntity)) { + const auto& metadata = m_registry.GetComponent(ecsEntity); + if (metadata.visualEntity != ECS::NULL_ENTITY && + m_registry.IsEntityAlive(metadata.visualEntity)) { + auto spriteIt = std::find(m_obstacleSpriteEntities.begin(), m_obstacleSpriteEntities.end(), metadata.visualEntity); + if (spriteIt != m_obstacleSpriteEntities.end()) { + m_obstacleSpriteEntities.erase(spriteIt); + } + m_registry.DestroyEntity(metadata.visualEntity); + } + } + } + + for (size_t i = 0; i < MAX_PLAYERS; i++) { + if (m_playersHUD[i].playerEntity == ecsEntity) { + m_playersHUD[i].isDead = true; + m_playersHUD[i].health = 0; + m_playersHUD[i].playerEntity = NULL_ENTITY; + for (const auto& p : m_context.allPlayers) { + if (p.number > 0 && p.number <= MAX_PLAYERS && static_cast(p.number - 1) == i) { + m_assignedPlayerNumbers.erase(p.number); + break; + } + } + break; + } + } + if (ecsEntity == m_localPlayerEntity) { + size_t localPlayerIndex = static_cast(m_context.playerNumber - 1); + if (localPlayerIndex < MAX_PLAYERS) { + m_playersHUD[localPlayerIndex].isDead = true; + m_playersHUD[localPlayerIndex].health = 0; + m_playersHUD[localPlayerIndex].playerEntity = NULL_ENTITY; + } + m_localPlayerEntity = NULL_ENTITY; + } + + if (m_bossHealthBar.active && networkId == m_bossHealthBar.bossNetworkId) { + destroyBossHealthBar(); + if (m_context.audio && m_bossMusicPlaying) { + m_context.audio->StopMusic(m_bossMusic); + m_bossMusicPlaying = false; + + if (m_gameMusic != Audio::INVALID_MUSIC_ID && !m_isGameOver) { + Audio::PlaybackOptions opts; + opts.loop = true; + opts.volume = 0.5f; + m_context.audio->PlayMusic(m_gameMusic, opts); + m_gameMusicPlaying = true; + } + } + } + + if (m_registry.IsEntityAlive(ecsEntity)) { + m_registry.DestroyEntity(ecsEntity); + } + it = m_networkEntityMap.erase(it); + m_bulletFlagsMap.erase(networkId); + } else { + ++it; + } + } + } + + void InGameState::ReconcileWithServer(const network::InputAck& ack) { + if (m_localPlayerEntity == ECS::NULL_ENTITY || + !m_registry.IsEntityAlive(m_localPlayerEntity) || + !m_registry.HasComponent(m_localPlayerEntity)) { + return; + } + + if (ack.lastProcessedSeq > m_lastAckedSequence) { + m_lastAckedSequence = ack.lastProcessedSeq; + } + + while (!m_inputHistory.empty() && m_inputHistory.front().sequence <= ack.lastProcessedSeq) { + m_inputHistory.pop_front(); + } + + auto& pos = m_registry.GetComponent(m_localPlayerEntity); + + float serverX = ack.serverPosX; + float serverY = ack.serverPosY; + + constexpr float RECONCILE_THRESHOLD = 1.0f; + float dx = serverX - m_predictedX; + float dy = serverY - m_predictedY; + float errorSq = dx * dx + dy * dy; + + if (m_inputHistory.empty()) { + if (errorSq > RECONCILE_THRESHOLD * RECONCILE_THRESHOLD) { + pos.x = serverX; + pos.y = serverY; + m_predictedX = serverX; + m_predictedY = serverY; + } + return; + } + + constexpr float SNAP_THRESHOLD = 100.0f; + if (errorSq > SNAP_THRESHOLD * SNAP_THRESHOLD) { + pos.x = serverX; + pos.y = serverY; + m_predictedX = serverX; + m_predictedY = serverY; + + m_inputHistory.clear(); + return; + } + + if (errorSq > RECONCILE_THRESHOLD * RECONCILE_THRESHOLD) { + float replayX = serverX; + float replayY = serverY; + + for (const auto& input : m_inputHistory) { + if (input.inputs & network::InputFlags::UP) { + replayY -= PREDICTION_SPEED * input.deltaTime; + } + if (input.inputs & network::InputFlags::DOWN) { + replayY += PREDICTION_SPEED * input.deltaTime; + } + if (input.inputs & network::InputFlags::LEFT) { + replayX -= PREDICTION_SPEED * input.deltaTime; + } + if (input.inputs & network::InputFlags::RIGHT) { + replayX += PREDICTION_SPEED * input.deltaTime; + } + + replayX = std::max(0.0f, std::min(replayX, 1280.0f - 66.0f)); + replayY = std::max(0.0f, std::min(replayY, 720.0f - 32.0f)); + } + + pos.x = replayX; + pos.y = replayY; + m_predictedX = replayX; + m_predictedY = replayY; + } + + UpdatePlayerNameLabelPosition(m_localPlayerEntity, pos.x, pos.y); + } + + void InGameState::OnLevelComplete(uint8_t completedLevel, uint8_t nextLevel) { + Core::Logger::Info("[InGameState] Level {} complete! Starting transition to level {}", + static_cast(completedLevel), + static_cast(nextLevel)); + + m_levelProgress.transitionPhase = TransitionPhase::FADE_OUT; + m_levelProgress.transitionTimer = 0.0f; + m_levelProgress.fadeAlpha = 0.0f; + m_levelProgress.levelComplete = true; + m_levelProgress.bossDefeated = true; + m_levelProgress.currentLevelNumber = static_cast(completedLevel); + m_levelProgress.nextLevelNumber = static_cast(nextLevel); + + Core::Logger::Info("[InGameState] Transition started - staying in GameState, network stays active"); + } + + } +} diff --git a/games/rtype/client/src/game/GameStateTransition.cpp b/games/rtype/client/src/game/GameStateTransition.cpp new file mode 100644 index 0000000..bb098ac --- /dev/null +++ b/games/rtype/client/src/game/GameStateTransition.cpp @@ -0,0 +1,303 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** GameState - Level transition logic +*/ + +#include "../../include/GameState.hpp" +#include "Core/Logger.hpp" +#include +#include + +namespace RType { + namespace Client { + + void InGameState::UpdateLevelTransition(float dt) { + if (m_levelProgress.transitionPhase == TransitionPhase::NONE) { + return; + } + + m_levelProgress.transitionTimer += dt; + + switch (m_levelProgress.transitionPhase) { + case TransitionPhase::FADE_OUT: { + const float FADE_DURATION = 1.0f; + m_levelProgress.fadeAlpha = std::min(1.0f, m_levelProgress.transitionTimer / FADE_DURATION); + + if (m_levelProgress.transitionTimer >= FADE_DURATION) { + m_levelProgress.transitionPhase = TransitionPhase::LOADING; + m_levelProgress.transitionTimer = 0.0f; + Core::Logger::Info("[Transition] FADE_OUT complete, entering LOADING"); + } + break; + } + + case TransitionPhase::LOADING: { + const float LOADING_DURATION = 2.0f; + + if (m_levelProgress.transitionTimer >= LOADING_DURATION) { + if (m_levelProgress.currentLevelNumber >= m_levelProgress.totalLevels) { + m_levelProgress.allLevelsComplete = true; + m_levelProgress.transitionPhase = TransitionPhase::NONE; + m_levelProgress.fadeAlpha = 0.0f; + Core::Logger::Info("[Transition] ALL LEVELS COMPLETE! Victory!"); + } else { + LoadNextLevel(); + + m_levelProgress.transitionPhase = TransitionPhase::FADE_IN; + m_levelProgress.transitionTimer = 0.0f; + Core::Logger::Info("[Transition] LOADING complete, entering FADE_IN"); + } + } + break; + } + + case TransitionPhase::FADE_IN: { + const float FADE_DURATION = 1.0f; + m_levelProgress.fadeAlpha = std::max(0.0f, 1.0f - (m_levelProgress.transitionTimer / FADE_DURATION)); + + if (m_levelProgress.transitionTimer >= FADE_DURATION) { + m_levelProgress.transitionPhase = TransitionPhase::NONE; + m_levelProgress.fadeAlpha = 0.0f; + m_levelProgress.levelComplete = false; + // currentLevelNumber already updated in LoadNextLevel() + Core::Logger::Info("[Transition] FADE_IN complete, transition finished"); + } + break; + } + + default: + break; + } + } + + void InGameState::LoadNextLevel() { + Core::Logger::Info("[Transition] Loading level {}...", m_levelProgress.nextLevelNumber); + + m_levelProgress.currentLevelNumber = m_levelProgress.nextLevelNumber; + + std::unordered_set hudEntities; + + if (m_hudPlayerEntity != ECS::NULL_ENTITY) { + hudEntities.insert(m_hudPlayerEntity); + } + if (m_hudScoreEntity != ECS::NULL_ENTITY) { + hudEntities.insert(m_hudScoreEntity); + } + if (m_hudLivesEntity != ECS::NULL_ENTITY) { + hudEntities.insert(m_hudLivesEntity); + } + if (m_hudScoreboardTitle != ECS::NULL_ENTITY) { + hudEntities.insert(m_hudScoreboardTitle); + } + + for (size_t i = 0; i < MAX_PLAYERS; i++) { + if (m_playersHUD[i].scoreEntity != ECS::NULL_ENTITY) { + hudEntities.insert(m_playersHUD[i].scoreEntity); + } + if (m_playersHUD[i].powerupSpreadEntity != ECS::NULL_ENTITY) { + hudEntities.insert(m_playersHUD[i].powerupSpreadEntity); + } + if (m_playersHUD[i].powerupLaserEntity != ECS::NULL_ENTITY) { + hudEntities.insert(m_playersHUD[i].powerupLaserEntity); + } + if (m_playersHUD[i].powerupSpeedEntity != ECS::NULL_ENTITY) { + hudEntities.insert(m_playersHUD[i].powerupSpeedEntity); + } + if (m_playersHUD[i].powerupShieldEntity != ECS::NULL_ENTITY) { + hudEntities.insert(m_playersHUD[i].powerupShieldEntity); + } + } + + for (const auto& pair : m_playerNameLabels) { + if (pair.second != ECS::NULL_ENTITY) { + hudEntities.insert(pair.second); + } + } + + if (m_gameOverTitleEntity != ECS::NULL_ENTITY) { + hudEntities.insert(m_gameOverTitleEntity); + } + if (m_gameOverScoreEntity != ECS::NULL_ENTITY) { + hudEntities.insert(m_gameOverScoreEntity); + } + if (m_gameOverHintEntity != ECS::NULL_ENTITY) { + hudEntities.insert(m_gameOverHintEntity); + } + + if (m_victoryTitleEntity != ECS::NULL_ENTITY) { + hudEntities.insert(m_victoryTitleEntity); + } + if (m_victoryScoreEntity != ECS::NULL_ENTITY) { + hudEntities.insert(m_victoryScoreEntity); + } + if (m_victoryHintEntity != ECS::NULL_ENTITY) { + hudEntities.insert(m_victoryHintEntity); + } + + std::vector toDestroy; + + auto positionEntities = m_registry.GetEntitiesWithComponent(); + for (auto entity : positionEntities) { + if (entity == m_localPlayerEntity) { + continue; + } + if (hudEntities.find(entity) != hudEntities.end()) { + continue; + } + toDestroy.push_back(entity); + } + + auto colliderEntities = m_registry.GetEntitiesWithComponent(); + for (auto entity : colliderEntities) { + if (entity == m_localPlayerEntity) { + continue; + } + if (hudEntities.find(entity) != hudEntities.end()) { + continue; + } + if (std::find(toDestroy.begin(), toDestroy.end(), entity) == toDestroy.end()) { + toDestroy.push_back(entity); + } + } + + auto drawableEntities = m_registry.GetEntitiesWithComponent(); + for (auto entity : drawableEntities) { + if (entity == m_localPlayerEntity) { + continue; + } + if (hudEntities.find(entity) != hudEntities.end()) { + continue; + } + if (std::find(toDestroy.begin(), toDestroy.end(), entity) == toDestroy.end()) { + toDestroy.push_back(entity); + } + } + + for (auto entity : toDestroy) { + if (m_registry.IsEntityAlive(entity)) { + m_registry.DestroyEntity(entity); + } + } + + Core::Logger::Info("[Transition] Destroyed {} entities (preserved player + {} HUD entities)", + toDestroy.size(), hudEntities.size()); + + std::vector networkIdsToRemove; + for (const auto& [networkId, localEntity] : m_networkEntityMap) { + if (!m_registry.IsEntityAlive(localEntity)) { + networkIdsToRemove.push_back(networkId); + } + } + for (uint32_t networkId : networkIdsToRemove) { + m_networkEntityMap.erase(networkId); + } + Core::Logger::Info("[Transition] Cleaned network map: removed {} dead entities, kept {} alive", + networkIdsToRemove.size(), m_networkEntityMap.size()); + + m_obstacleColliderEntities.clear(); + m_obstacleSpriteEntities.clear(); + m_obstacleIdToCollider.clear(); + m_backgroundEntities.clear(); + + m_serverScrollOffset = 0.0f; + m_localScrollOffset = 0.0f; + + if (m_bossHealthBar.titleEntity != ECS::NULL_ENTITY && m_registry.IsEntityAlive(m_bossHealthBar.titleEntity)) { + m_registry.DestroyEntity(m_bossHealthBar.titleEntity); + } + if (m_bossHealthBar.barBackgroundEntity != ECS::NULL_ENTITY && m_registry.IsEntityAlive(m_bossHealthBar.barBackgroundEntity)) { + m_registry.DestroyEntity(m_bossHealthBar.barBackgroundEntity); + } + if (m_bossHealthBar.barForegroundEntity != ECS::NULL_ENTITY && m_registry.IsEntityAlive(m_bossHealthBar.barForegroundEntity)) { + m_registry.DestroyEntity(m_bossHealthBar.barForegroundEntity); + } + + m_bossHealthBar.bossNetworkId = 0; + m_bossHealthBar.currentHealth = 0; + m_bossHealthBar.maxHealth = 0; + m_bossHealthBar.active = false; + m_bossHealthBar.titleEntity = ECS::NULL_ENTITY; + m_bossHealthBar.barBackgroundEntity = ECS::NULL_ENTITY; + m_bossHealthBar.barForegroundEntity = ECS::NULL_ENTITY; + + m_bossWarningActive = false; + m_bossWarningTriggered = false; + m_bossWarningTimer = 0.0f; + + std::string levelPath = "assets/levels/level" + std::to_string(m_levelProgress.nextLevelNumber) + ".json"; + m_currentLevelPath = levelPath; + + try { + m_levelData = ECS::LevelLoader::LoadFromFile(levelPath); + m_levelAssets = ECS::LevelLoader::LoadAssets(m_levelData, m_renderer.get()); + + m_levelEntities = ECS::LevelLoader::CreateEntities(m_registry, m_levelData, m_levelAssets, m_renderer.get()); + m_backgroundEntities = m_levelEntities.backgrounds; + m_obstacleSpriteEntities = m_levelEntities.obstacleVisuals; + m_obstacleColliderEntities = m_levelEntities.obstacleColliders; + + Core::Logger::Info("[Transition] Level {} loaded: {} textures, {} sprites", + m_levelProgress.nextLevelNumber, + m_levelAssets.textures.size(), + m_levelAssets.sprites.size()); + + for (auto collider : m_obstacleColliderEntities) { + if (!m_registry.IsEntityAlive(collider) || + !m_registry.HasComponent(collider)) { + continue; + } + const auto& metadata = m_registry.GetComponent(collider); + m_obstacleIdToCollider[metadata.uniqueId] = collider; + + if (metadata.visualEntity != ECS::NULL_ENTITY && + m_registry.IsEntityAlive(metadata.visualEntity) && + m_registry.HasComponent(metadata.visualEntity) && + m_registry.HasComponent(collider)) { + + const auto& visualPos = m_registry.GetComponent(metadata.visualEntity); + auto& colliderPos = m_registry.GetComponent(collider); + + colliderPos.x = visualPos.x + metadata.offsetX; + colliderPos.y = visualPos.y + metadata.offsetY; + } + } + + Core::Logger::Info("[Transition] Rebuilt obstacle mapping: {} obstacles tracked", + m_obstacleIdToCollider.size()); + + } catch (const std::exception& e) { + Core::Logger::Error("[Transition] Failed to load level {}: {}", m_levelProgress.nextLevelNumber, e.what()); + } + + if (m_context.audio) { + if (m_gameMusic != Audio::INVALID_MUSIC_ID) { + m_context.audio->StopMusic(m_gameMusic); + m_context.audio->UnloadMusic(m_gameMusic); + } + + std::string musicPath = "assets/sounds/stage1.flac"; + if (m_levelProgress.nextLevelNumber == 2) { + musicPath = "assets/sounds/stage2.flac"; + } else if (m_levelProgress.nextLevelNumber == 3) { + musicPath = "assets/sounds/stage3.flac"; + } + + m_gameMusic = m_context.audio->LoadMusic(musicPath); + if (m_gameMusic == Audio::INVALID_MUSIC_ID) { + m_gameMusic = m_context.audio->LoadMusic("../" + musicPath); + } + + if (m_gameMusic != Audio::INVALID_MUSIC_ID) { + Audio::PlaybackOptions opts; + opts.loop = true; + opts.volume = 0.35f; + m_context.audio->PlayMusic(m_gameMusic, opts); + m_gameMusicPlaying = true; + } + } + } + + } +} diff --git a/games/rtype/client/src/game/GameStateUI.cpp b/games/rtype/client/src/game/GameStateUI.cpp new file mode 100644 index 0000000..e52d1d7 --- /dev/null +++ b/games/rtype/client/src/game/GameStateUI.cpp @@ -0,0 +1,605 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** GameState - UI and HUD rendering functions +*/ + +#include "../../include/GameState.hpp" + +#include "ECS/Components/TextLabel.hpp" +#include "ECS/Component.hpp" +#include "Core/Logger.hpp" +#include +#include +#include + +using namespace RType::ECS; + +namespace RType { + namespace Client { + + void InGameState::initializeUI() { + m_hudFont = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 16); + if (m_hudFont == Renderer::INVALID_FONT_ID) { + m_hudFont = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 16); + } + + m_hudFontSmall = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 12); + if (m_hudFontSmall == Renderer::INVALID_FONT_ID) { + m_hudFontSmall = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 12); + } + + m_gameOverFontLarge = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 48); + if (m_gameOverFontLarge == Renderer::INVALID_FONT_ID) { + m_gameOverFontLarge = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 48); + } + + m_gameOverFontMedium = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 20); + if (m_gameOverFontMedium == Renderer::INVALID_FONT_ID) { + m_gameOverFontMedium = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 20); + } + + if (m_hudFont == Renderer::INVALID_FONT_ID) { + Core::Logger::Error("[GameState] Failed to load HUD font"); + return; + } + + m_hudPlayerEntity = m_registry.CreateEntity(); + m_registry.AddComponent(m_hudPlayerEntity, Position{20.0f, 680.0f}); + std::string playerText = "P" + std::to_string(m_context.playerNumber); + TextLabel playerLabel(playerText, m_hudFont, 16); + playerLabel.color = {0.0f, 1.0f, 1.0f, 1.0f}; + m_registry.AddComponent(m_hudPlayerEntity, std::move(playerLabel)); + + m_hudLivesEntity = m_registry.CreateEntity(); + m_registry.AddComponent(m_hudLivesEntity, Position{350.0f, 680.0f}); + TextLabel livesLabel("LIVES x3", m_hudFontSmall != Renderer::INVALID_FONT_ID ? m_hudFontSmall : m_hudFont, 12); + livesLabel.color = {1.0f, 0.8f, 0.0f, 1.0f}; + m_registry.AddComponent(m_hudLivesEntity, std::move(livesLabel)); + + m_hudScoreboardTitle = m_registry.CreateEntity(); + m_registry.AddComponent(m_hudScoreboardTitle, Position{1050.0f, 620.0f}); + TextLabel titleLabel("SCORES", m_hudFontSmall != Renderer::INVALID_FONT_ID ? m_hudFontSmall : m_hudFont, 12); + titleLabel.color = {0.0f, 1.0f, 1.0f, 1.0f}; + m_registry.AddComponent(m_hudScoreboardTitle, std::move(titleLabel)); + + for (size_t i = 0; i < MAX_PLAYERS; i++) { + m_playersHUD[i].active = false; + m_playersHUD[i].score = 0; + m_playersHUD[i].lives = 3; + m_playersHUD[i].health = 100; + m_playersHUD[i].maxHealth = 100; + m_playersHUD[i].playerEntity = NULL_ENTITY; + + m_playersHUD[i].scoreEntity = m_registry.CreateEntity(); + float yPos = 645.0f + (i * 20.0f); + m_registry.AddComponent(m_playersHUD[i].scoreEntity, Position{1050.0f, yPos}); + + std::string scoreText = "P" + std::to_string(i + 1) + " --------"; + TextLabel label(scoreText, m_hudFontSmall != Renderer::INVALID_FONT_ID ? m_hudFontSmall : m_hudFont, 12); + label.color = {0.4f, 0.4f, 0.4f, 0.6f}; + m_registry.AddComponent(m_playersHUD[i].scoreEntity, std::move(label)); + } + + if (m_context.playerNumber >= 1 && m_context.playerNumber <= MAX_PLAYERS) { + m_playersHUD[m_context.playerNumber - 1].active = true; + } + + } + + void InGameState::updateHUD() { + if (m_localPlayerEntity != ECS::NULL_ENTITY && + m_registry.IsEntityAlive(m_localPlayerEntity) && + m_registry.HasComponent(m_localPlayerEntity)) { + const auto& scoreComp = m_registry.GetComponent(m_localPlayerEntity); + m_playerScore = scoreComp.points; + } + + if (m_hudScoreEntity != NULL_ENTITY && m_registry.IsEntityAlive(m_hudScoreEntity) && m_registry.HasComponent(m_hudScoreEntity)) { + auto& scoreLabel = m_registry.GetComponent(m_hudScoreEntity); + std::ostringstream ss; + ss << std::setw(8) << std::setfill('0') << m_playerScore; + scoreLabel.text = ss.str(); + } + + if (m_hudLivesEntity != NULL_ENTITY && m_registry.IsEntityAlive(m_hudLivesEntity) && m_registry.HasComponent(m_hudLivesEntity)) { + auto& livesLabel = m_registry.GetComponent(m_hudLivesEntity); + + // Calculate visual lives from real health (0-300) + // 201-300 HP = 3 lives, 101-200 HP = 2 lives, 1-100 HP = 1 life, 0 HP = 0 lives + int realHealth = 0; + if (m_context.playerNumber >= 1 && m_context.playerNumber <= MAX_PLAYERS) { + realHealth = m_playersHUD[m_context.playerNumber - 1].health; + } + + int visualLives = 0; + if (realHealth > 200) { + visualLives = 3; + } else if (realHealth > 100) { + visualLives = 2; + } else if (realHealth > 0) { + visualLives = 1; + } + + livesLabel.text = "LIVES x" + std::to_string(visualLives); + + if (visualLives <= 1) { + livesLabel.color = {1.0f, 0.2f, 0.2f, 1.0f}; + } else if (visualLives <= 2) { + livesLabel.color = {1.0f, 0.6f, 0.0f, 1.0f}; + } else { + livesLabel.color = {1.0f, 0.8f, 0.0f, 1.0f}; + } + } + + if (m_context.playerNumber >= 1 && m_context.playerNumber <= MAX_PLAYERS) { + m_playersHUD[m_context.playerNumber - 1].score = m_playerScore; + m_playersHUD[m_context.playerNumber - 1].lives = m_playerLives; + } + + const Math::Color playerColors[MAX_PLAYERS] = { + {0.2f, 1.0f, 0.2f, 1.0f}, // P1 - green + {0.2f, 0.6f, 1.0f, 1.0f}, // P2 - blue + {1.0f, 0.3f, 0.3f, 1.0f}, // P3 - Red + {1.0f, 1.0f, 0.2f, 1.0f} // P4 - yellow + }; + + for (size_t i = 0; i < MAX_PLAYERS; i++) { + if (m_playersHUD[i].scoreEntity == NULL_ENTITY || + !m_registry.IsEntityAlive(m_playersHUD[i].scoreEntity) || + !m_registry.HasComponent(m_playersHUD[i].scoreEntity)) { + continue; + } + + auto& label = m_registry.GetComponent(m_playersHUD[i].scoreEntity); + + if (m_playersHUD[i].active) { + if (m_playersHUD[i].isDead) { + m_playersHUD[i].health = 0; + } else { + bool entityExists = false; + if (m_playersHUD[i].playerEntity != NULL_ENTITY && + m_registry.IsEntityAlive(m_playersHUD[i].playerEntity) && + m_registry.HasComponent(m_playersHUD[i].playerEntity)) { + const auto& health = m_registry.GetComponent(m_playersHUD[i].playerEntity); + if (health.current > 0) { + m_playersHUD[i].health = health.current; + m_playersHUD[i].maxHealth = health.max; + } else { + m_playersHUD[i].isDead = true; + m_playersHUD[i].health = 0; + } + if (m_registry.HasComponent(m_playersHUD[i].playerEntity)) { + const auto& scoreComp = m_registry.GetComponent(m_playersHUD[i].playerEntity); + m_playersHUD[i].score = scoreComp.points; + } + entityExists = true; + } else if (i == static_cast(m_context.playerNumber - 1) && m_localPlayerEntity != ECS::NULL_ENTITY && m_registry.IsEntityAlive(m_localPlayerEntity) && m_registry.HasComponent(m_localPlayerEntity)) { + const auto& health = m_registry.GetComponent(m_localPlayerEntity); + if (health.current > 0) { + m_playersHUD[i].health = health.current; + m_playersHUD[i].maxHealth = health.max; + } else { + m_playersHUD[i].isDead = true; + m_playersHUD[i].health = 0; + } + if (m_registry.HasComponent(m_localPlayerEntity)) { + const auto& scoreComp = m_registry.GetComponent(m_localPlayerEntity); + m_playersHUD[i].score = scoreComp.points; + } + entityExists = true; + + if (m_playersHUD[i].playerEntity == NULL_ENTITY) { + m_playersHUD[i].playerEntity = m_localPlayerEntity; + } + } + + if (!entityExists) { + if (!m_isNetworkSession) { + m_playersHUD[i].isDead = true; + m_playersHUD[i].health = 0; + } + } + } + + std::string playerDisplayName = "P" + std::to_string(i + 1); + uint8_t playerNum = static_cast(i + 1); + + for (const auto& p : m_context.allPlayers) { + if (p.number == playerNum && p.name[0] != '\0') { + playerDisplayName = std::string(p.name); + break; + } + } + + if (playerDisplayName == "P" + std::to_string(i + 1)) { + auto nameIt = m_playerNameMap.find(static_cast(playerNum)); + if (nameIt != m_playerNameMap.end() && !nameIt->second.empty()) { + playerDisplayName = nameIt->second; + } + } + + size_t originalLength = playerDisplayName.length(); + bool nameTooLong = originalLength > 16; + + if (nameTooLong) { + playerDisplayName = playerDisplayName.substr(0, 16); + } + + if (nameTooLong && m_registry.HasComponent(m_playersHUD[i].scoreEntity)) { + auto& pos = m_registry.GetComponent(m_playersHUD[i].scoreEntity); + float shiftAmount = static_cast(originalLength - 16) * 38.0f; + pos.x = 1050.0f - shiftAmount; + } else if (m_registry.HasComponent(m_playersHUD[i].scoreEntity)) { + auto& pos = m_registry.GetComponent(m_playersHUD[i].scoreEntity); + pos.x = 1050.0f; + } + + std::ostringstream ss; + ss << playerDisplayName << " " << std::setw(8) << std::setfill('0') << m_playersHUD[i].score; + label.text = ss.str(); + label.color = playerColors[i]; + + if (i == static_cast(m_context.playerNumber - 1)) { + label.color.a = 1.0f; + } else { + label.color.a = 0.85f; + } + } else { + label.text = "P" + std::to_string(i + 1) + " --------"; + label.color = {0.4f, 0.4f, 0.4f, 0.5f}; + } + } + + updatePowerUpIcons(); + } + + void InGameState::updatePowerUpIcons() { + if (m_context.playerNumber < 1 || m_context.playerNumber > MAX_PLAYERS) { + return; + } + + size_t playerIndex = static_cast(m_context.playerNumber - 1); + if (!m_playersHUD[playerIndex].active) { + return; + } + + ECS::Entity playerEntity = m_playersHUD[playerIndex].playerEntity; + if (playerEntity == ECS::NULL_ENTITY) { + playerEntity = m_localPlayerEntity; + } + + bool hasSpreadShot = false; + bool hasLaserBeam = false; + bool hasSpeedBoost = false; + bool hasShield = false; + + if (playerEntity != ECS::NULL_ENTITY && m_registry.IsEntityAlive(playerEntity) && + m_registry.HasComponent(playerEntity)) { + const auto& powerUps = m_registry.GetComponent(playerEntity); + hasSpreadShot = powerUps.hasSpreadShot; + hasLaserBeam = powerUps.hasLaserBeam; + hasSpeedBoost = powerUps.speedMultiplier > 1.0f; + hasShield = powerUps.hasShield; + } + + const float textSpacing = 25.0f; + const float columnSpacing = 85.0f; + const float startX = 800.0f; + const float startY = 675.0f; + float currentY = startY; + + updatePowerUpText(m_playersHUD[playerIndex].powerupSpreadEntity, "SPREAD", + hasSpreadShot, startX, currentY); + currentY += textSpacing; + + updatePowerUpText(m_playersHUD[playerIndex].powerupLaserEntity, "LASER", + hasLaserBeam, startX, currentY); + currentY += textSpacing; + + currentY = startY; + updatePowerUpText(m_playersHUD[playerIndex].powerupSpeedEntity, "SPEED", + hasSpeedBoost, startX + columnSpacing, currentY); + currentY += textSpacing; + + updatePowerUpText(m_playersHUD[playerIndex].powerupShieldEntity, "SHIELD", + hasShield, startX + columnSpacing, currentY); + } + + void InGameState::updatePowerUpText(ECS::Entity& textEntity, const std::string& text, + bool isActive, float x, float y) { + if (textEntity == ECS::NULL_ENTITY || !m_registry.IsEntityAlive(textEntity)) { + textEntity = m_registry.CreateEntity(); + m_registry.AddComponent(textEntity, Position{x, y}); + Renderer::FontId fontId = (m_hudFontSmall != Renderer::INVALID_FONT_ID) ? m_hudFontSmall : m_hudFont; + TextLabel label(text, fontId, 10); + label.color = isActive ? Math::Color{1.0f, 1.0f, 1.0f, 1.0f} : Math::Color{0.5f, 0.5f, 0.5f, 0.7f}; + m_registry.AddComponent(textEntity, std::move(label)); + } else { + if (m_registry.HasComponent(textEntity)) { + auto& pos = m_registry.GetComponent(textEntity); + pos.x = x; + pos.y = y; + } + if (m_registry.HasComponent(textEntity)) { + auto& label = m_registry.GetComponent(textEntity); + label.color = isActive ? Math::Color{1.0f, 1.0f, 1.0f, 1.0f} : Math::Color{0.5f, 0.5f, 0.5f, 0.7f}; + } + } + } + + void InGameState::Draw() { + m_renderingSystem->Update(m_registry, 0.0f); + m_textSystem->Update(m_registry, 0.0f); + + renderChargeBar(); + renderHealthBars(); + renderBossHealthBar(); + renderGameOverOverlay(); + renderVictoryOverlay(); + renderLevelTransition(); + renderBossWarning(); + } + + void InGameState::renderChargeBar() { + const float barX = 500.0f; + const float barY = 675.0f; + const float barWidth = 200.0f; + const float barHeight = 20.0f; + + Renderer::Rectangle bgRect; + bgRect.position = Renderer::Vector2(barX - 2, barY - 2); + bgRect.size = Renderer::Vector2(barWidth + 4, barHeight + 4); + m_renderer->DrawRectangle(bgRect, Renderer::Color(0.2f, 0.2f, 0.2f, 0.8f)); + + Renderer::Rectangle innerBgRect; + innerBgRect.position = Renderer::Vector2(barX, barY); + innerBgRect.size = Renderer::Vector2(barWidth, barHeight); + m_renderer->DrawRectangle(innerBgRect, Renderer::Color(0.1f, 0.1f, 0.1f, 0.9f)); + + float chargePercent = m_chargeTime / MAX_CHARGE_TIME; + float filledWidth = barWidth * chargePercent; + + Renderer::Color barColor; + if (chargePercent < 0.5f) { + float blend = chargePercent * 2.0f; + barColor = Renderer::Color(blend, 1.0f, 1.0f - blend, 1.0f); + } else { + float blend = (chargePercent - 0.5f) * 2.0f; + barColor = Renderer::Color(1.0f, 1.0f - blend * 0.5f, 0.0f, 1.0f); + } + + if (filledWidth > 0) { + Renderer::Rectangle fillRect; + fillRect.position = Renderer::Vector2(barX, barY); + fillRect.size = Renderer::Vector2(filledWidth, barHeight); + m_renderer->DrawRectangle(fillRect, barColor); + } + + if (m_isCharging && m_hudFontSmall != Renderer::INVALID_FONT_ID) { + Renderer::TextParams textParams; + textParams.position = Renderer::Vector2(barX + barWidth + 10, barY + 2); + textParams.color = Renderer::Color(0.0f, 1.0f, 1.0f, 1.0f); + textParams.scale = 1.0f; + + if (m_chargeTime >= MAX_CHARGE_TIME) { + float pulse = 0.7f + 0.3f * std::sin(m_chargeTime * 10.0f); + textParams.color = Renderer::Color(1.0f, pulse, 0.0f, 1.0f); + m_renderer->DrawText(m_hudFontSmall, "MAX!", textParams); + } else { + m_renderer->DrawText(m_hudFontSmall, "BEAM", textParams); + } + } + } + + void InGameState::renderHealthBars() { + if (m_context.playerNumber < 1 || m_context.playerNumber > MAX_PLAYERS) { + return; + } + + size_t playerIndex = static_cast(m_context.playerNumber - 1); + if (!m_playersHUD[playerIndex].active) { + return; + } + + const float barWidth = 150.0f; + const float barHeight = 12.0f; + const float barX = 180.0f; + const float barY = 680.0f; + + Renderer::Rectangle bgRect; + bgRect.position = Renderer::Vector2(barX - 2, barY - 2); + bgRect.size = Renderer::Vector2(barWidth + 4, barHeight + 4); + m_renderer->DrawRectangle(bgRect, Renderer::Color(0.1f, 0.1f, 0.1f, 0.9f)); + + Renderer::Rectangle innerBgRect; + innerBgRect.position = Renderer::Vector2(barX, barY); + innerBgRect.size = Renderer::Vector2(barWidth, barHeight); + m_renderer->DrawRectangle(innerBgRect, Renderer::Color(0.3f, 0.1f, 0.1f, 0.8f)); + + Renderer::TextParams textParams; + textParams.position = Renderer::Vector2(barX, barY - barHeight - 2); + textParams.color = Renderer::Color(0.0f, 1.0f, 1.0f, 1.0f); + textParams.scale = 1.0f; + m_renderer->DrawText(m_hudFontSmall, "HEALTH", textParams); + + // Calculate visual health (0-100) from real health (0-300) + // Each "life" represents 100 HP + int realHealth = m_playersHUD[playerIndex].health; + int visualHealth = 0; + + if (m_playersHUD[playerIndex].isDead || realHealth <= 0) { + visualHealth = 0; + } else if (realHealth > 200) { + visualHealth = realHealth - 200; // 201-300 -> 1-100 + } else if (realHealth > 100) { + visualHealth = realHealth - 100; // 101-200 -> 1-100 + } else { + visualHealth = realHealth; // 1-100 -> 1-100 + } + + float healthPercent = static_cast(visualHealth) / 100.0f; + healthPercent = std::max(0.0f, std::min(1.0f, healthPercent)); + + float filledWidth = barWidth * healthPercent; + + Renderer::Color healthColor; + if (healthPercent <= 0.3f) { + healthColor = Renderer::Color(1.0f, 0.2f, 0.2f, 1.0f); + } else if (healthPercent <= 0.6f) { + float blend = (healthPercent - 0.3f) / 0.3f; + healthColor = Renderer::Color(1.0f, 0.2f + blend * 0.8f, 0.2f, 1.0f); + } else { + healthColor = Renderer::Color(0.2f, 1.0f, 0.2f, 1.0f); + } + + if (filledWidth > 0) { + Renderer::Rectangle fillRect; + fillRect.position = Renderer::Vector2(barX, barY); + fillRect.size = Renderer::Vector2(filledWidth, barHeight); + m_renderer->DrawRectangle(fillRect, healthColor); + } + } + + void InGameState::renderGameOverOverlay() { + if (!m_isGameOver) { + return; + } + + Renderer::Rectangle rect; + rect.position = Renderer::Vector2(0.0f, 0.0f); + rect.size = Renderer::Vector2(1280.0f, 720.0f); + m_renderer->DrawRectangle(rect, Renderer::Color(0.0f, 0.0f, 0.0f, 0.45f)); + } + + void InGameState::renderVictoryOverlay() { + if (!m_levelProgress.allLevelsComplete) { + return; + } + + Renderer::Rectangle bgRect; + bgRect.position = Renderer::Vector2(0.0f, 0.0f); + bgRect.size = Renderer::Vector2(1280.0f, 720.0f); + m_renderer->DrawRectangle(bgRect, Renderer::Color(0.0f, 0.0f, 0.0f, 0.45f)); + } + + void InGameState::initializeBossHealthBar() { + if (m_bossHealthBar.active) { + return; + } + + m_bossHealthBar.active = true; + + m_bossHealthBar.titleEntity = m_registry.CreateEntity(); + m_registry.AddComponent(m_bossHealthBar.titleEntity, Position{640.0f - 80.0f, 10.0f}); + + std::string bossTitle = "BOSS - LEVEL " + std::to_string(m_levelProgress.currentLevelNumber); + m_registry.AddComponent(m_bossHealthBar.titleEntity, + TextLabel(bossTitle, m_hudFontSmall != Renderer::INVALID_FONT_ID ? m_hudFontSmall : m_hudFont, 12)); + + Core::Logger::Info("[GameState] Boss health bar initialized for level {}", m_levelProgress.currentLevelNumber); + } + + void InGameState::destroyBossHealthBar() { + if (!m_bossHealthBar.active) { + return; + } + + if (m_bossHealthBar.titleEntity != NULL_ENTITY && m_registry.IsEntityAlive(m_bossHealthBar.titleEntity)) { + m_registry.DestroyEntity(m_bossHealthBar.titleEntity); + } + + m_bossHealthBar.active = false; + m_bossHealthBar.titleEntity = NULL_ENTITY; + m_bossHealthBar.bossNetworkId = 0; + + Core::Logger::Info("[GameState] Boss health bar destroyed"); + } + + void InGameState::updateBossHealthBar() { + if (!m_bossHealthBar.active) { + return; + } + + float healthPercent = static_cast(m_bossHealthBar.currentHealth) / + static_cast(m_bossHealthBar.maxHealth); + if (healthPercent < 0.0f) healthPercent = 0.0f; + if (healthPercent > 1.0f) healthPercent = 1.0f; + + if (m_bossHealthBar.currentHealth <= 0) { + destroyBossHealthBar(); + } + } + + void InGameState::renderBossHealthBar() { + if (!m_bossHealthBar.active) { + return; + } + + const float barWidth = 400.0f; + const float barHeight = 15.0f; + const float barX = 440.0f; + const float barY = 35.0f; + + Renderer::Rectangle bgRect; + bgRect.position = Renderer::Vector2(barX, barY); + bgRect.size = Renderer::Vector2(barWidth, barHeight); + m_renderer->DrawRectangle(bgRect, Renderer::Color(0.3f, 0.3f, 0.3f, 1.0f)); + + float healthPercent = static_cast(m_bossHealthBar.currentHealth) / + static_cast(m_bossHealthBar.maxHealth); + if (healthPercent < 0.0f) healthPercent = 0.0f; + if (healthPercent > 1.0f) healthPercent = 1.0f; + + Renderer::Rectangle fgRect; + fgRect.position = Renderer::Vector2(barX, barY); + fgRect.size = Renderer::Vector2(barWidth * healthPercent, barHeight); + + float red = 1.0f - (healthPercent * 0.5f); + float green = healthPercent * 0.8f; + m_renderer->DrawRectangle(fgRect, Renderer::Color(red, green, 0.0f, 1.0f)); + } + + void InGameState::renderLevelTransition() { + if (m_levelProgress.transitionPhase == TransitionPhase::NONE) { + return; + } + + if (m_levelProgress.fadeAlpha > 0.0f) { + Renderer::Rectangle fadeOverlay; + fadeOverlay.position = Renderer::Vector2(0.0f, 0.0f); + fadeOverlay.size = Renderer::Vector2(1280.0f, 720.0f); + m_renderer->DrawRectangle(fadeOverlay, Renderer::Color(0.0f, 0.0f, 0.0f, m_levelProgress.fadeAlpha)); + } + + if (m_levelProgress.transitionPhase == TransitionPhase::LOADING) { + Renderer::Rectangle fullScreen; + fullScreen.position = Renderer::Vector2(0.0f, 0.0f); + fullScreen.size = Renderer::Vector2(1280.0f, 720.0f); + m_renderer->DrawRectangle(fullScreen, Renderer::Color(0.0f, 0.0f, 0.0f, 1.0f)); + } + } + + void InGameState::renderBossWarning() { + if (!m_bossWarningActive) { + return; + } + + if (!m_bossWarningFlashState) { + return; + } + + if (m_gameOverFontLarge != Renderer::INVALID_FONT_ID) { + Renderer::TextParams textParams; + textParams.position = Renderer::Vector2(440.0f, 300.0f); + textParams.color = Renderer::Color(1.0f, 0.0f, 0.0f, 1.0f); + textParams.scale = 1.0f; + m_renderer->DrawText(m_gameOverFontLarge, "WARNING !!", textParams); + } else { + Core::Logger::Error("[GameState] Cannot render boss warning - Invalid Font ID"); + } + } + + } +} diff --git a/games/rtype/client/src/game/GameStateUpdate.cpp b/games/rtype/client/src/game/GameStateUpdate.cpp new file mode 100644 index 0000000..b432b55 --- /dev/null +++ b/games/rtype/client/src/game/GameStateUpdate.cpp @@ -0,0 +1,800 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** GameState - Update and input handling functions +*/ + +#include "../../include/GameState.hpp" +#include "Core/InputMapping.hpp" +#include "ECS/Components/TextLabel.hpp" +#include "ECS/Component.hpp" +#include "Core/Logger.hpp" +#include "ResultsState.hpp" +#include "RoomListState.hpp" +#include "MenuState.hpp" +#include + +using namespace RType::ECS; + +namespace RType { + namespace Client { + + void InGameState::HandleInput() { + if (m_levelProgress.allLevelsComplete) { + if (m_renderer->IsKeyPressed(Renderer::Key::Enter) && !m_gameOverEnterPressed) { + m_gameOverEnterPressed = true; + enterResultsScreen(); + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Enter)) { + m_gameOverEnterPressed = false; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Escape) && !m_gameOverEscapePressed) { + m_gameOverEscapePressed = true; + if (m_context.networkClient) { + m_context.networkClient->Stop(); + m_context.networkClient.reset(); + } + m_machine.ChangeState(std::make_unique(m_machine, m_context)); + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Escape)) { + m_gameOverEscapePressed = false; + } + return; + } + + if (m_isGameOver) { + if (m_renderer->IsKeyPressed(Renderer::Key::Enter) && !m_gameOverEnterPressed) { + m_gameOverEnterPressed = true; + enterResultsScreen(); + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Enter)) { + m_gameOverEnterPressed = false; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Escape) && !m_gameOverEscapePressed) { + m_gameOverEscapePressed = true; + if (m_context.networkClient) { + m_context.networkClient->Stop(); + m_context.networkClient.reset(); + } + m_machine.ChangeState(std::make_unique(m_machine, m_context)); + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Escape)) { + m_gameOverEscapePressed = false; + } + return; + } + + m_currentInputs = 0; + + Renderer::Key moveUpKey = Core::InputMapping::GetKey("MOVE_UP"); + Renderer::Key moveDownKey = Core::InputMapping::GetKey("MOVE_DOWN"); + Renderer::Key moveLeftKey = Core::InputMapping::GetKey("MOVE_LEFT"); + Renderer::Key moveRightKey = Core::InputMapping::GetKey("MOVE_RIGHT"); + Renderer::Key shootKey = Core::InputMapping::GetKey("SHOOT"); + + if (moveUpKey == Renderer::Key::Unknown) moveUpKey = Renderer::Key::Up; + if (moveDownKey == Renderer::Key::Unknown) moveDownKey = Renderer::Key::Down; + if (moveLeftKey == Renderer::Key::Unknown) moveLeftKey = Renderer::Key::Left; + if (moveRightKey == Renderer::Key::Unknown) moveRightKey = Renderer::Key::Right; + if (shootKey == Renderer::Key::Unknown) shootKey = Renderer::Key::Space; + + if (m_renderer->IsKeyPressed(moveUpKey)) { + m_currentInputs |= network::InputFlags::UP; + } + if (m_renderer->IsKeyPressed(moveDownKey)) { + m_currentInputs |= network::InputFlags::DOWN; + } + if (m_renderer->IsKeyPressed(moveLeftKey)) { + m_currentInputs |= network::InputFlags::LEFT; + } + if (m_renderer->IsKeyPressed(moveRightKey)) { + m_currentInputs |= network::InputFlags::RIGHT; + } + static bool shootPressedLastFrame = false; + bool shootPressed = m_renderer->IsKeyPressed(shootKey); + + if (shootPressed) { + m_isCharging = true; + m_chargeTime += 0.016f; + if (m_chargeTime > 2.0f) { + m_chargeTime = 2.0f; + } + } else { + if (shootPressedLastFrame) { + float savedChargeTime = m_chargeTime; + + if (savedChargeTime >= 2.0f) { + if (m_localPlayerEntity != ECS::NULL_ENTITY && + m_registry.IsEntityAlive(m_localPlayerEntity) && + m_registry.HasComponent(m_localPlayerEntity) && + m_registry.HasComponent(m_localPlayerEntity)) { + createBeamEntity(); + m_beamDuration = 2.0f; + } + } else { + m_currentInputs |= network::InputFlags::SHOOT; + if (m_isNetworkSession) { + if (m_shootMusic != Audio::INVALID_MUSIC_ID) { + Audio::PlaybackOptions opts; + opts.volume = 1.0f; + opts.loop = false; + m_context.audio->StopMusic(m_shootMusic); + m_context.audio->PlayMusic(m_shootMusic, opts); + } else if (m_playerShootSound != Audio::INVALID_SOUND_ID) { + Audio::PlaybackOptions opts; + opts.volume = 1.0f; + m_context.audio->PlaySound(m_playerShootSound, opts); + } + } + } + } + m_isCharging = false; + m_chargeTime = 0.0f; + } + shootPressedLastFrame = shootPressed; + + + + if (!m_isNetworkSession && + m_localPlayerEntity != ECS::NULL_ENTITY && + m_registry.HasComponent(m_localPlayerEntity)) { + auto& shootCmd = m_registry.GetComponent(m_localPlayerEntity); + shootCmd.wantsToShoot = (m_currentInputs & network::InputFlags::SHOOT); + } + if (m_context.networkClient && m_currentInputs != m_previousInputs) { + auto now = std::chrono::steady_clock::now(); + auto ms = std::chrono::duration_cast(now - m_lastInputTime).count(); + + if (ms >= 30) { + m_context.networkClient->SendInput(static_cast(m_currentInputs)); + m_lastInputTime = now; + + } + m_previousInputs = m_currentInputs; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Escape) && !m_escapeKeyPressed) { + m_escapeKeyPressed = true; + m_machine.PopState(); + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Escape)) { + m_escapeKeyPressed = false; + } + } + + void InGameState::Update(float dt) { + UpdateLevelTransition(dt); + + if (m_context.networkClient) { + m_context.networkClient->ReceivePackets(); + } else { + m_scoreAccumulator += dt; + if (m_scoreAccumulator >= 10.0f) { + uint32_t intervals = static_cast(m_scoreAccumulator / 10.0f); + m_playerScore += intervals * 10; + m_scoreAccumulator -= static_cast(intervals) * 10.0f; + } + } + + if (m_shootSfxCooldown > 0.0f) { + m_shootSfxCooldown -= dt; + } + + updateBeam(dt); + + triggerGameOverIfNeeded(); + if (m_isGameOver) { + m_gameOverElapsed += dt; + updateHUD(); + return; + } + + triggerVictoryIfNeeded(); + if (m_levelProgress.allLevelsComplete) { + m_levelProgress.victoryElapsed += dt; + updateHUD(); + return; + } + if (m_inputSystem) { + m_inputSystem->Update(m_registry, dt); + } + if (m_movementSystem) { + m_movementSystem->Update(m_registry, dt); + } + + + if (m_isNetworkSession) { + // Scroll backgrounds + for (auto& bg : m_backgroundEntities) { + if (!m_registry.IsEntityAlive(bg)) + continue; + if (!m_registry.HasComponent(bg) || + !m_registry.HasComponent(bg)) + continue; + auto& pos = m_registry.GetComponent(bg); + const auto& scrollable = m_registry.GetComponent(bg); + pos.x += scrollable.speed * dt; + } + auto obstacleColliders = m_registry.GetEntitiesWithComponent(); + for (auto collider : obstacleColliders) { + if (!m_registry.HasComponent(collider)) { + continue; + } + const auto& metadata = m_registry.GetComponent(collider); + auto visual = metadata.visualEntity; + + if (visual == ECS::NULL_ENTITY || + !m_registry.IsEntityAlive(visual) || + !m_registry.HasComponent(visual) || + !m_registry.HasComponent(visual)) { + continue; + } + + auto& pos = m_registry.GetComponent(visual); + const auto& scrollable = m_registry.GetComponent(visual); + pos.x += scrollable.speed * dt; + } + auto obstacleCollidersForSync = m_registry.GetEntitiesWithComponent(); + for (auto collider : obstacleCollidersForSync) { + if (!m_registry.IsEntityAlive(collider) || + !m_registry.HasComponent(collider) || + !m_registry.HasComponent(collider)) + continue; + + if (!m_registry.HasComponent(collider)) + continue; + + const auto& metadata = m_registry.GetComponent(collider); + + if (metadata.visualEntity != ECS::NULL_ENTITY && + m_registry.IsEntityAlive(metadata.visualEntity) && + m_registry.HasComponent(metadata.visualEntity)) { + + const auto& visualPos = m_registry.GetComponent(metadata.visualEntity); + auto& colliderPos = m_registry.GetComponent(collider); + colliderPos.x = visualPos.x + metadata.offsetX; + colliderPos.y = visualPos.y + metadata.offsetY; + } else if (m_registry.HasComponent(collider)) { + auto& pos = m_registry.GetComponent(collider); + const auto& scrollable = m_registry.GetComponent(collider); + pos.x += scrollable.speed * dt; + } + } + } else if (m_scrollingSystem) { + m_scrollingSystem->Update(m_registry, dt); + } + + if (m_isNetworkSession && m_localPlayerEntity != ECS::NULL_ENTITY && + m_registry.IsEntityAlive(m_localPlayerEntity) && + m_registry.HasComponent(m_localPlayerEntity)) { + auto& pos = m_registry.GetComponent(m_localPlayerEntity); + + float newX = pos.x; + float newY = pos.y; + if (m_currentInputs & network::InputFlags::UP) { + newY -= PREDICTION_SPEED * dt; + } + if (m_currentInputs & network::InputFlags::DOWN) { + newY += PREDICTION_SPEED * dt; + } + if (m_currentInputs & network::InputFlags::LEFT) { + newX -= PREDICTION_SPEED * dt; + } + if (m_currentInputs & network::InputFlags::RIGHT) { + newX += PREDICTION_SPEED * dt; + } + + newX = std::max(0.0f, std::min(newX, 1280.0f - 66.0f)); + newY = std::max(0.0f, std::min(newY, 720.0f - 32.0f)); + + float playerW = 25.0f, playerH = 25.0f; + if (m_registry.HasComponent(m_localPlayerEntity)) { + const auto& box = m_registry.GetComponent(m_localPlayerEntity); + playerW = box.width; + playerH = box.height; + } + + bool blocked = false; + for (auto& collider : m_obstacleColliderEntities) { + if (!m_registry.IsEntityAlive(collider) || + !m_registry.HasComponent(collider) || + !m_registry.HasComponent(collider)) + continue; + const auto& obstPos = m_registry.GetComponent(collider); + const auto& obstBox = m_registry.GetComponent(collider); + + bool wouldCollide = + newX < obstPos.x + obstBox.width && + newX + playerW > obstPos.x && + newY < obstPos.y + obstBox.height && + newY + playerH > obstPos.y; + + if (wouldCollide) { + blocked = true; + float overlapLeft = (newX + playerW) - obstPos.x; + float overlapRight = (obstPos.x + obstBox.width) - newX; + float overlapTop = (newY + playerH) - obstPos.y; + float overlapBottom = (obstPos.y + obstBox.height) - newY; + + float minOverlapX = std::min(overlapLeft, overlapRight); + float minOverlapY = std::min(overlapTop, overlapBottom); + + if (minOverlapX < minOverlapY) { + if (overlapLeft < overlapRight) { + newX = obstPos.x - playerW - 0.5f; + } else { + newX = obstPos.x + obstBox.width + 0.5f; + } + } else { + if (overlapTop < overlapBottom) { + newY = obstPos.y - playerH - 0.5f; + } else { + newY = obstPos.y + obstBox.height + 0.5f; + } + } + break; + } + } + + newX = std::max(0.0f, std::min(newX, 1280.0f - 66.0f)); + newY = std::max(0.0f, std::min(newY, 720.0f - 32.0f)); + + pos.x = newX; + pos.y = newY; + + PredictedInput prediction; + prediction.sequence = m_inputSequence; + prediction.inputs = m_currentInputs; + prediction.predictedX = newX; + prediction.predictedY = newY; + prediction.deltaTime = dt; + m_inputHistory.push_back(prediction); + + while (m_inputHistory.size() > MAX_INPUT_HISTORY) { + m_inputHistory.pop_front(); + } + + m_predictedX = newX; + m_predictedY = newY; + + (void)blocked; + } + + auto entities = m_registry.GetEntitiesWithComponent(); + for (auto entity : entities) { + if (m_registry.HasComponent(entity)) { + auto& pos = m_registry.GetComponent(entity); + if (entity == m_localPlayerEntity && !m_isNetworkSession) { + pos.x = std::max(0.0f, std::min(pos.x, 1280.0f - 66.0f)); + pos.y = std::max(0.0f, std::min(pos.y, 720.0f - 32.0f)); + } + UpdatePlayerNameLabelPosition(entity, pos.x, pos.y); + } + } + + if (m_collisionDetectionSystem) { + m_collisionDetectionSystem->Update(m_registry, dt); + } + if (m_powerUpCollisionSystem) { + m_powerUpCollisionSystem->Update(m_registry, dt); + } + if (m_bulletResponseSystem) { + m_bulletResponseSystem->Update(m_registry, dt); + } + if (m_playerResponseSystem) { + m_playerResponseSystem->Update(m_registry, dt); + } + if (m_obstacleResponseSystem) { + m_obstacleResponseSystem->Update(m_registry, dt); + } + if (m_scoreSystem) { + m_scoreSystem->Update(m_registry, dt); + } + if (m_healthSystem) { + m_healthSystem->Update(m_registry, dt); + } + + m_localScrollOffset += -150.0f * dt; + + if (m_bossWarningActive) { + m_bossWarningTimer += dt; + if (m_bossWarningTimer >= BOSS_WARNING_DURATION) { + m_bossWarningActive = false; + } else { + int interval = static_cast(m_bossWarningTimer / 0.25f); + m_bossWarningFlashState = (interval % 2 == 0); + } + } + + if (m_shieldSystem) { + m_shieldSystem->Update(m_registry, dt); + } + if (m_forcePodSystem) { + m_forcePodSystem->Update(m_registry, dt); + } + + if (m_shootingSystem) { + m_shootingSystem->Update(m_registry, dt); + } + + if (m_bossHealthBar.active && !m_bossMusicPlaying && !m_isGameOver) { + auto it = m_networkEntityMap.find(m_bossHealthBar.bossNetworkId); + if (it != m_networkEntityMap.end()) { + auto bossEntity = it->second; + if (m_registry.IsEntityAlive(bossEntity) && m_registry.HasComponent(bossEntity)) { + auto& pos = m_registry.GetComponent(bossEntity); + if (pos.x < 1300.0f) { + if (m_context.audio) { + if (m_gameMusicPlaying) { + m_context.audio->StopMusic(m_gameMusic); + m_gameMusicPlaying = false; + } + if (m_bossMusic != Audio::INVALID_MUSIC_ID) { + Audio::PlaybackOptions opts; + opts.loop = true; + opts.volume = 0.6f; + m_context.audio->PlayMusic(m_bossMusic, opts); + m_bossMusicPlaying = true; + } + } + } + } + } + } + + if (m_audioSystem) { + if (!m_isGameOver && !m_bossMusicPlaying && m_gameMusic != Audio::INVALID_MUSIC_ID && !m_gameMusicPlaying) { + if (m_context.audio) { + Audio::PlaybackOptions opts; + opts.loop = true; + opts.volume = 0.35f; + m_context.audio->PlayMusic(m_gameMusic, opts); + m_gameMusicPlaying = true; + } + } + m_audioSystem->Update(m_registry, dt); + } + + if (m_animationSystem) { + m_animationSystem->Update(m_registry, dt); + } + + for (auto& bg : m_backgroundEntities) { + if (!m_registry.HasComponent(bg)) + continue; + auto& pos = m_registry.GetComponent(bg); + if (pos.x <= -1280.0f) { + pos.x = pos.x + 3 * 1280.0f; + } + } + + if (m_isNetworkSession) { + std::vector toRemove; + for (auto& [networkId, interpState] : m_interpolationStates) { + auto entityIt = m_networkEntityMap.find(networkId); + if (entityIt == m_networkEntityMap.end()) { + toRemove.push_back(networkId); + continue; + } + + ECS::Entity entity = entityIt->second; + if (!m_registry.IsEntityAlive(entity)) { + toRemove.push_back(networkId); + continue; + } + + if (!m_registry.HasComponent(entity)) { + continue; + } + + interpState.interpTime += dt; + float t = interpState.interpTime / interpState.interpDuration; + if (t > 1.0f) { + t = 1.0f; + } + + auto& pos = m_registry.GetComponent(entity); + pos.x = interpState.prevX + (interpState.targetX - interpState.prevX) * t; + pos.y = interpState.prevY + (interpState.targetY - interpState.prevY) * t; + + if (m_registry.HasComponent(entity)) { + UpdatePlayerNameLabelPosition(entity, pos.x, pos.y); + } + } + for (uint32_t id : toRemove) { + m_interpolationStates.erase(id); + } + + for (auto& [networkId, ecsEntity] : m_networkEntityMap) { + if (!m_registry.IsEntityAlive(ecsEntity)) continue; + if (!m_registry.HasComponent(ecsEntity)) continue; + if (!m_registry.HasComponent(ecsEntity)) continue; + if (m_interpolationStates.count(networkId) > 0) continue; + if (ecsEntity == m_localPlayerEntity) continue; + + auto& pos = m_registry.GetComponent(ecsEntity); + const auto& vel = m_registry.GetComponent(ecsEntity); + pos.x += vel.dx * dt; + pos.y += vel.dy * dt; + } + } + updateHUD(); + } + + void InGameState::triggerGameOverIfNeeded() { + if (m_isGameOver) { + return; + } + + bool dead = false; + if (m_isNetworkSession) { + if (m_context.playerNumber >= 1 && m_context.playerNumber <= MAX_PLAYERS) { + size_t idx = static_cast(m_context.playerNumber - 1); + dead = m_playersHUD[idx].active && m_playersHUD[idx].isDead; + } + } else { + if (m_localPlayerEntity == ECS::NULL_ENTITY || !m_registry.IsEntityAlive(m_localPlayerEntity)) { + dead = true; + } else if (m_registry.HasComponent(m_localPlayerEntity)) { + const auto& h = m_registry.GetComponent(m_localPlayerEntity); + dead = (h.current <= 0); + } + } + + if (!dead) { + return; + } + + m_isGameOver = true; + m_gameOverElapsed = 0.0f; + m_isCharging = false; + m_chargeTime = 0.0f; + + if (m_context.audio) { + if (m_gameMusic != Audio::INVALID_MUSIC_ID && m_gameMusicPlaying) { + m_context.audio->StopMusic(m_gameMusic); + m_gameMusicPlaying = false; + } + + if (m_bossMusic != Audio::INVALID_MUSIC_ID && m_bossMusicPlaying) { + m_context.audio->StopMusic(m_bossMusic); + m_bossMusicPlaying = false; + } + + if (m_gameOverMusic != Audio::INVALID_MUSIC_ID && !m_gameOverMusicPlaying) { + Audio::PlaybackOptions opts; + opts.loop = true; + opts.volume = 0.5f; + m_context.audio->PlayMusic(m_gameOverMusic, opts); + m_gameOverMusicPlaying = true; + } + } + + if (m_gameOverTitleEntity == NULL_ENTITY && m_gameOverFontLarge != Renderer::INVALID_FONT_ID) { + m_gameOverTitleEntity = m_registry.CreateEntity(); + m_registry.AddComponent(m_gameOverTitleEntity, Position{640.0f, 260.0f}); + TextLabel title("GAME OVER", m_gameOverFontLarge, 56); + title.centered = true; + title.color = {1.0f, 0.08f, 0.58f, 1.0f}; + m_registry.AddComponent(m_gameOverTitleEntity, std::move(title)); + } + + if (m_gameOverScoreEntity == NULL_ENTITY && m_gameOverFontMedium != Renderer::INVALID_FONT_ID) { + m_gameOverScoreEntity = m_registry.CreateEntity(); + m_registry.AddComponent(m_gameOverScoreEntity, Position{640.0f, 360.0f}); + TextLabel score("", m_gameOverFontMedium, 22); + score.centered = true; + score.color = {0.5f, 0.86f, 1.0f, 0.95f}; + m_registry.AddComponent(m_gameOverScoreEntity, std::move(score)); + } + + if (m_gameOverHintEntity == NULL_ENTITY && m_hudFontSmall != Renderer::INVALID_FONT_ID) { + m_gameOverHintEntity = m_registry.CreateEntity(); + m_registry.AddComponent(m_gameOverHintEntity, Position{640.0f, 430.0f}); + TextLabel hint("Press ENTER to view results | ESC to return to lobby", m_hudFontSmall, 14); + hint.centered = true; + hint.color = {0.5f, 0.86f, 1.0f, 0.85f}; + m_registry.AddComponent(m_gameOverHintEntity, std::move(hint)); + } + + if (m_gameOverScoreEntity != NULL_ENTITY && m_registry.IsEntityAlive(m_gameOverScoreEntity)) { + auto& label = m_registry.GetComponent(m_gameOverScoreEntity); + label.text = "SCORE " + std::to_string(m_playerScore); + } + } + + void InGameState::triggerVictoryIfNeeded() { + if (!m_levelProgress.allLevelsComplete) { + return; + } + + if (m_context.audio) { + if (m_gameMusic != Audio::INVALID_MUSIC_ID && m_gameMusicPlaying) { + m_context.audio->StopMusic(m_gameMusic); + m_gameMusicPlaying = false; + } + + if (m_bossMusic != Audio::INVALID_MUSIC_ID && m_bossMusicPlaying) { + m_context.audio->StopMusic(m_bossMusic); + m_bossMusicPlaying = false; + } + + if (m_victoryMusic != Audio::INVALID_MUSIC_ID && !m_victoryMusicPlaying) { + Audio::PlaybackOptions opts; + opts.loop = false; // Do not loop victory music + opts.volume = 0.5f; + m_context.audio->PlayMusic(m_victoryMusic, opts); + m_victoryMusicPlaying = true; + } + } + + if (m_victoryTitleEntity == NULL_ENTITY && m_gameOverFontLarge != Renderer::INVALID_FONT_ID) { + m_victoryTitleEntity = m_registry.CreateEntity(); + m_registry.AddComponent(m_victoryTitleEntity, Position{640.0f, 260.0f}); + TextLabel title("VICTORY!", m_gameOverFontLarge, 56); + title.centered = true; + title.color = {0.2f, 1.0f, 0.2f, 1.0f}; + m_registry.AddComponent(m_victoryTitleEntity, std::move(title)); + } + + if (m_victoryScoreEntity == NULL_ENTITY && m_gameOverFontMedium != Renderer::INVALID_FONT_ID) { + m_victoryScoreEntity = m_registry.CreateEntity(); + m_registry.AddComponent(m_victoryScoreEntity, Position{640.0f, 360.0f}); + TextLabel score("", m_gameOverFontMedium, 22); + score.centered = true; + score.color = {0.5f, 1.0f, 0.5f, 0.95f}; + m_registry.AddComponent(m_victoryScoreEntity, std::move(score)); + } + + if (m_victoryHintEntity == NULL_ENTITY && m_hudFontSmall != Renderer::INVALID_FONT_ID) { + m_victoryHintEntity = m_registry.CreateEntity(); + m_registry.AddComponent(m_victoryHintEntity, Position{640.0f, 430.0f}); + TextLabel hint("Press ENTER to view results | ESC to return to menu", m_hudFontSmall, 14); + hint.centered = true; + hint.color = {0.5f, 1.0f, 0.5f, 0.85f}; + m_registry.AddComponent(m_victoryHintEntity, std::move(hint)); + } + + if (m_victoryScoreEntity != NULL_ENTITY && m_registry.IsEntityAlive(m_victoryScoreEntity)) { + auto& label = m_registry.GetComponent(m_victoryScoreEntity); + label.text = "SCORE " + std::to_string(m_playerScore); + } + } + + void InGameState::Cleanup() { + + if (m_context.audio && m_shootMusic != Audio::INVALID_MUSIC_ID) { + m_context.audio->StopMusic(m_shootMusic); + m_context.audio->UnloadMusic(m_shootMusic); + } + + if (m_context.audio && m_powerUpMusic != Audio::INVALID_MUSIC_ID) { + m_context.audio->StopMusic(m_powerUpMusic); + m_context.audio->UnloadMusic(m_powerUpMusic); + } + + if (m_context.audio && m_gameMusic != Audio::INVALID_MUSIC_ID) { + m_context.audio->StopMusic(m_gameMusic); + m_context.audio->UnloadMusic(m_gameMusic); + m_gameMusic = Audio::INVALID_MUSIC_ID; + m_gameMusicPlaying = false; + } + + if (m_context.audio && m_bossMusic != Audio::INVALID_MUSIC_ID) { + m_context.audio->StopMusic(m_bossMusic); + m_context.audio->UnloadMusic(m_bossMusic); + m_bossMusic = Audio::INVALID_MUSIC_ID; + m_bossMusicPlaying = false; + } + + if (m_context.audio && m_gameOverMusic != Audio::INVALID_MUSIC_ID) { + m_context.audio->StopMusic(m_gameOverMusic); + m_context.audio->UnloadMusic(m_gameOverMusic); + m_gameOverMusic = Audio::INVALID_MUSIC_ID; + m_gameOverMusicPlaying = false; + } + + if (m_context.audio && m_victoryMusic != Audio::INVALID_MUSIC_ID) { + m_context.audio->StopMusic(m_victoryMusic); + m_context.audio->UnloadMusic(m_victoryMusic); + m_victoryMusic = Audio::INVALID_MUSIC_ID; + m_victoryMusicPlaying = false; + } + + for (auto& bg : m_backgroundEntities) { + if (m_registry.IsEntityAlive(bg)) { + m_registry.DestroyEntity(bg); + } + } + m_backgroundEntities.clear(); + + for (auto& obstacle : m_obstacleSpriteEntities) { + if (m_registry.IsEntityAlive(obstacle)) { + m_registry.DestroyEntity(obstacle); + } + } + m_obstacleSpriteEntities.clear(); + + for (auto& obstacle : m_obstacleColliderEntities) { + if (m_registry.IsEntityAlive(obstacle)) { + m_registry.DestroyEntity(obstacle); + } + } + m_obstacleColliderEntities.clear(); + m_obstacleIdToCollider.clear(); + + if (m_hudPlayerEntity != NULL_ENTITY && m_registry.IsEntityAlive(m_hudPlayerEntity)) { + m_registry.DestroyEntity(m_hudPlayerEntity); + } + if (m_hudScoreEntity != NULL_ENTITY && m_registry.IsEntityAlive(m_hudScoreEntity)) { + m_registry.DestroyEntity(m_hudScoreEntity); + } + if (m_hudLivesEntity != NULL_ENTITY && m_registry.IsEntityAlive(m_hudLivesEntity)) { + m_registry.DestroyEntity(m_hudLivesEntity); + } + if (m_hudScoreboardTitle != NULL_ENTITY && m_registry.IsEntityAlive(m_hudScoreboardTitle)) { + m_registry.DestroyEntity(m_hudScoreboardTitle); + } + for (auto& playerHUD : m_playersHUD) { + if (playerHUD.scoreEntity != NULL_ENTITY && m_registry.IsEntityAlive(playerHUD.scoreEntity)) { + m_registry.DestroyEntity(playerHUD.scoreEntity); + } + if (playerHUD.powerupSpreadEntity != ECS::NULL_ENTITY && m_registry.IsEntityAlive(playerHUD.powerupSpreadEntity)) { + m_registry.DestroyEntity(playerHUD.powerupSpreadEntity); + } + if (playerHUD.powerupLaserEntity != ECS::NULL_ENTITY && m_registry.IsEntityAlive(playerHUD.powerupLaserEntity)) { + m_registry.DestroyEntity(playerHUD.powerupLaserEntity); + } + if (playerHUD.powerupSpeedEntity != ECS::NULL_ENTITY && m_registry.IsEntityAlive(playerHUD.powerupSpeedEntity)) { + m_registry.DestroyEntity(playerHUD.powerupSpeedEntity); + } + if (playerHUD.powerupShieldEntity != ECS::NULL_ENTITY && m_registry.IsEntityAlive(playerHUD.powerupShieldEntity)) { + m_registry.DestroyEntity(playerHUD.powerupShieldEntity); + } + } + + for (auto& [playerEntity, labelEntity] : m_playerNameLabels) { + if (m_registry.IsEntityAlive(labelEntity)) { + m_registry.DestroyEntity(labelEntity); + } + } + m_playerNameLabels.clear(); + + if (m_beamEntity != ECS::NULL_ENTITY && m_registry.IsEntityAlive(m_beamEntity)) { + m_registry.DestroyEntity(m_beamEntity); + m_beamEntity = ECS::NULL_ENTITY; + } + } + + void InGameState::enterResultsScreen() { + std::vector> scores; + scores.reserve(MAX_PLAYERS); + + for (size_t i = 0; i < MAX_PLAYERS; i++) { + if (!m_playersHUD[i].active) { + continue; + } + uint8_t playerNum = static_cast(i + 1); + std::string name = "P" + std::to_string(playerNum); + for (const auto& p : m_context.allPlayers) { + if (p.number == playerNum && p.name[0] != '\0') { + name = std::string(p.name); + break; + } + } + scores.push_back({name, m_playersHUD[i].score}); + } + + if (!m_isNetworkSession && scores.empty()) { + std::string name = m_context.playerName.empty() ? "P1" : m_context.playerName; + scores.push_back({name, m_playerScore}); + } + + if (m_context.networkClient) { + m_context.networkClient->Stop(); + m_context.networkClient.reset(); + } + + m_machine.ChangeState(std::make_unique(m_machine, m_context, std::move(scores))); + } + + } +} diff --git a/games/rtype/client/src/lobby/LobbyStateInit.cpp b/games/rtype/client/src/lobby/LobbyStateInit.cpp new file mode 100644 index 0000000..2245d51 --- /dev/null +++ b/games/rtype/client/src/lobby/LobbyStateInit.cpp @@ -0,0 +1,203 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** LobbyState - Initialization and cleanup functions +*/ + +#include "../../include/LobbyState.hpp" +#include "ECS/Components/TextLabel.hpp" +#include "ECS/Component.hpp" +#include "ECS/AudioSystem.hpp" +#include + +using namespace RType::ECS; + +namespace RType { + namespace Client { + + LobbyState::LobbyState(GameStateMachine& machine, GameContext& context) + : m_machine(machine), m_context(context), m_playerName(context.playerName) { + m_renderer = context.renderer; + m_renderingSystem = std::make_unique(m_renderer.get()); + m_textSystem = std::make_unique(m_renderer.get()); + } + + LobbyState::LobbyState(GameStateMachine& machine, GameContext& context, network::NetworkTcpSocket&& socket) + : m_machine(machine), m_context(context), m_playerName(context.playerName) { + m_renderer = context.renderer; + m_renderingSystem = std::make_unique(m_renderer.get()); + m_textSystem = std::make_unique(m_renderer.get()); + m_client = std::make_unique(std::move(socket)); + } + + void LobbyState::Init() { + std::cout << "[LobbyState] Connecting to " << m_context.serverIp << ":" << m_context.serverPort << " as " << m_playerName << std::endl; + + if (m_context.audio) { + m_audioSystem = std::make_unique(m_context.audio.get()); + m_lobbyMusic = m_context.audio->LoadMusic("assets/sounds/lobby.flac"); + if (m_lobbyMusic == Audio::INVALID_MUSIC_ID) { + m_lobbyMusic = m_context.audio->LoadMusic("../assets/sounds/lobby.flac"); + } + + if (m_lobbyMusic != Audio::INVALID_MUSIC_ID) { + auto cmd = m_registry.CreateEntity(); + auto& me = m_registry.AddComponent(cmd, MusicEffect(m_lobbyMusic)); + me.play = true; + me.stop = false; + me.loop = true; + me.volume = 0.35f; + me.pitch = 1.0f; + m_lobbyMusicPlaying = true; + } + } + + if (!m_client) { + if (!m_context.networkModule) { + std::cout << "[LobbyState] ERROR: networkModule is null (cannot create LobbyClient)" << std::endl; + m_errorMessage = "Internal error: network module missing"; + m_hasError = true; + return; + } + m_client = std::make_unique( + m_context.networkModule.get(), m_context.serverIp, m_context.serverPort); + } + + m_fontLarge = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 32); + if (m_fontLarge == Renderer::INVALID_FONT_ID) { + m_fontLarge = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 32); + } + + m_fontMedium = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 20); + if (m_fontMedium == Renderer::INVALID_FONT_ID) { + m_fontMedium = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 20); + } + + m_fontSmall = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 14); + if (m_fontSmall == Renderer::INVALID_FONT_ID) { + m_fontSmall = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 14); + } + + m_playerBlueTexture = m_renderer->LoadTexture("assets/spaceships/player_blue.png"); + if (m_playerBlueTexture == Renderer::INVALID_TEXTURE_ID) { + m_playerBlueTexture = m_renderer->LoadTexture("../assets/spaceships/player_blue.png"); + } + + m_playerGreenTexture = m_renderer->LoadTexture("assets/spaceships/player_green.png"); + if (m_playerGreenTexture == Renderer::INVALID_TEXTURE_ID) { + m_playerGreenTexture = m_renderer->LoadTexture("../assets/spaceships/player_green.png"); + } + + m_nave2BlueTexture = m_renderer->LoadTexture("assets/spaceships/nave2_blue.png"); + if (m_nave2BlueTexture == Renderer::INVALID_TEXTURE_ID) { + m_nave2BlueTexture = m_renderer->LoadTexture("../assets/spaceships/nave2_blue.png"); + } + + m_nave2Texture = m_renderer->LoadTexture("assets/spaceships/nave2.png"); + if (m_nave2Texture == Renderer::INVALID_TEXTURE_ID) { + m_nave2Texture = m_renderer->LoadTexture("../assets/spaceships/nave2.png"); + } + + m_bgTexture = m_renderer->LoadTexture("assets/backgrounds/1.jpg"); + if (m_bgTexture == Renderer::INVALID_TEXTURE_ID) { + m_bgTexture = m_renderer->LoadTexture("../assets/backgrounds/1.jpg"); + } + + m_client->onPlayerLeft([this](uint8_t playerNum) { + removePlayer(playerNum); + }); + + m_client->onConnectionError([this](const std::string& error) { + m_errorMessage = error; + m_hasError = true; + }); + + m_client->onCountdown([this](uint8_t seconds) { + m_countdownSeconds = seconds; + }); + + createUI(); + m_client->connect(m_playerName); + } + + void LobbyState::Cleanup() { + std::cout << "[LobbyState] Cleaning up..." << std::endl; + + if (m_context.audio && m_lobbyMusic != Audio::INVALID_MUSIC_ID) { + m_context.audio->StopMusic(m_lobbyMusic); + m_context.audio->UnloadMusic(m_lobbyMusic); + m_lobbyMusic = Audio::INVALID_MUSIC_ID; + m_lobbyMusicPlaying = false; + } + + if (m_client) { + m_client->disconnect(); + m_client.reset(); + } + + for (auto& [playerNum, entity] : m_playerEntities) { + if (m_registry.IsEntityAlive(entity)) { + m_registry.DestroyEntity(entity); + } + } + m_playerEntities.clear(); + + for (auto& [playerNum, sprite] : m_playerSprites) { + if (m_registry.IsEntityAlive(sprite)) { + m_registry.DestroyEntity(sprite); + } + } + m_playerSprites.clear(); + } + + void LobbyState::createUI() { + if (m_bgTexture != Renderer::INVALID_TEXTURE_ID) { + m_bgEntity = m_registry.CreateEntity(); + m_registry.AddComponent(m_bgEntity, Position{0.0f, 0.0f}); + + Renderer::SpriteId spriteId = m_renderer->CreateSprite(m_bgTexture, {}); + Drawable drawable(spriteId, -10); + + Renderer::Vector2 texSize = m_renderer->GetTextureSize(m_bgTexture); + if (texSize.x > 0 && texSize.y > 0) { + drawable.scale = {1280.0f / texSize.x, 720.0f / texSize.y}; + } + + m_registry.AddComponent(m_bgEntity, std::move(drawable)); + } + + if (m_fontLarge == Renderer::INVALID_FONT_ID) + return; + m_titleEntity = m_registry.CreateEntity(); + m_registry.AddComponent(m_titleEntity, Position{640.0f, 50.0f}); + TextLabel titleLabel("MULTIPLAYER LOBBY", m_fontLarge, 36); + titleLabel.color = {0.0f, 1.0f, 1.0f, 1.0f}; + titleLabel.centered = true; + m_registry.AddComponent(m_titleEntity, std::move(titleLabel)); + + m_instructionsEntity = m_registry.CreateEntity(); + m_registry.AddComponent(m_instructionsEntity, Position{640.0f, 650.0f}); + TextLabel instrLabel("[R] Toggle Ready | [ESC] Change Room", m_fontSmall, 14); + instrLabel.color = {0.5f, 0.86f, 1.0f, 0.9f}; + instrLabel.centered = true; + m_registry.AddComponent(m_instructionsEntity, std::move(instrLabel)); + } + + Renderer::TextureId LobbyState::getPlayerTexture(uint8_t playerNum) { + switch (playerNum) { + case 0: + return m_playerBlueTexture; + case 1: + return m_playerGreenTexture; + case 2: + return m_nave2BlueTexture; + case 3: + return m_nave2Texture; + default: + return m_playerBlueTexture; + } + } + + } +} diff --git a/games/rtype/client/src/lobby/LobbyStateUpdate.cpp b/games/rtype/client/src/lobby/LobbyStateUpdate.cpp new file mode 100644 index 0000000..b7a2bc4 --- /dev/null +++ b/games/rtype/client/src/lobby/LobbyStateUpdate.cpp @@ -0,0 +1,253 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** LobbyState - Update, input handling, and player management functions +*/ + +#include "../../include/LobbyState.hpp" +#include "../../include/RoomListState.hpp" +#include "ECS/Components/TextLabel.hpp" +#include "ECS/Component.hpp" +#include +#include +#include + +using namespace RType::ECS; + +namespace RType { + namespace Client { + + void LobbyState::HandleInput() { + if (m_renderer->IsKeyPressed(Renderer::Key::R) && !m_rKeyPressed) { + m_rKeyPressed = true; + if (m_client && m_client->isConnected()) { + m_client->ready(); + std::cout << "[LobbyState] Toggled ready!" << std::endl; + } + } else if (!m_renderer->IsKeyPressed(Renderer::Key::R)) { + m_rKeyPressed = false; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Escape) && !m_escapeKeyPressed) { + m_escapeKeyPressed = true; + std::cout << "[LobbyState] Returning to room selection..." << std::endl; + m_machine.ChangeState(std::make_unique(m_machine, m_context)); + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Escape)) { + m_escapeKeyPressed = false; + } + } + + void LobbyState::Update(float dt) { + if (m_hasError) { + if (m_errorEntity == NULL_ENTITY && m_fontLarge != Renderer::INVALID_FONT_ID) { + m_errorEntity = m_registry.CreateEntity(); + m_registry.AddComponent(m_errorEntity, Position{640.0f, 300.0f}); + TextLabel errorLabel(m_errorMessage, m_fontMedium, 24); + errorLabel.color = {1.0f, 0.08f, 0.58f, 1.0f}; + errorLabel.centered = true; + m_registry.AddComponent(m_errorEntity, std::move(errorLabel)); + + Entity backMsg = m_registry.CreateEntity(); + m_registry.AddComponent(backMsg, Position{640.0f, 380.0f}); + TextLabel backLabel("Press ESC to return to room selection", m_fontSmall, 14); + backLabel.color = {0.5f, 0.86f, 1.0f, 0.9f}; + backLabel.centered = true; + m_registry.AddComponent(backMsg, std::move(backLabel)); + } + m_errorTimer += dt; + if (m_errorTimer >= 3.0f) { + std::cout << "[LobbyState] Auto-returning to room selection after error..." << std::endl; + m_machine.ChangeState(std::make_unique(m_machine, m_context)); + } + return; + } + + if (m_client) { + m_client->update(); + } + updateLobbyState(); + + if (m_audioSystem && m_lobbyMusic != Audio::INVALID_MUSIC_ID && !m_lobbyMusicPlaying) { + auto cmd = m_registry.CreateEntity(); + auto& me = m_registry.AddComponent(cmd, MusicEffect(m_lobbyMusic)); + me.play = true; + me.stop = false; + me.loop = true; + me.volume = 0.35f; + me.pitch = 1.0f; + m_lobbyMusicPlaying = true; + } + + if (m_audioSystem) { + m_audioSystem->Update(m_registry, dt); + } + + if (m_countdownSeconds > 0) { + if (m_countdownEntity == NULL_ENTITY && m_fontLarge != Renderer::INVALID_FONT_ID) { + m_countdownEntity = m_registry.CreateEntity(); + m_registry.AddComponent(m_countdownEntity, Position{640.0f, 500.0f}); + TextLabel countdownLabel("", m_fontLarge, 48); + countdownLabel.color = {1.0f, 0.08f, 0.58f, 1.0f}; + countdownLabel.centered = true; + m_registry.AddComponent(m_countdownEntity, std::move(countdownLabel)); + } + + if (m_countdownEntity != NULL_ENTITY && m_registry.IsEntityAlive(m_countdownEntity)) { + auto& label = m_registry.GetComponent(m_countdownEntity); + label.text = "Starting in " + std::to_string((int)m_countdownSeconds) + "..."; + } + } else { + if (m_countdownEntity != NULL_ENTITY && m_registry.IsEntityAlive(m_countdownEntity)) { + m_registry.DestroyEntity(m_countdownEntity); + m_countdownEntity = NULL_ENTITY; + } + } + + if (m_client && m_client->isGameStarted()) { + m_countdownSeconds = 0; + + uint32_t seed = m_client->getGameSeed(); + std::string serverIp = m_context.serverIp; + uint16_t udpPort = m_context.serverPort; + + std::cout << "[LobbyState] Game started! Seed: " << seed << std::endl; + std::cout << "[LobbyState] Waiting for server to start UDP..." << std::endl; + std::this_thread::sleep_for(std::chrono::milliseconds(1500)); + + std::cout << "[LobbyState] Transitioning to GameState..." << std::endl; + + network::PlayerInfo localPlayer = m_client->getMyInfo(); + + m_context.playerHash = localPlayer.hash; + m_context.playerNumber = localPlayer.number; + m_context.allPlayers = m_client->getPlayers(); + + auto gameClient = std::make_shared( + m_context.networkModule.get(), serverIp, udpPort, localPlayer); + if (gameClient->ConnectToServer()) { + m_context.networkClient = gameClient; + m_machine.ChangeState(std::make_unique(m_machine, m_context, seed)); + } + return; + } + } + + void LobbyState::Draw() { + m_renderer->Clear({0.05f, 0.05f, 0.12f, 1.0f}); + + m_renderingSystem->Update(m_registry, 0.0f); + m_textSystem->Update(m_registry, 0.0f); + } + + void LobbyState::updateLobbyState() { + if (!m_client || !m_client->isConnected()) { + return; + } + + const auto& players = m_client->getPlayers(); + for (const auto& player : players) { + updateOrCreatePlayerEntity(player); + } + } + + void LobbyState::updateOrCreatePlayerEntity(const network::PlayerInfo& player) { + Entity entity; + auto it = m_playerEntities.find(player.number); + + if (it == m_playerEntities.end()) { + entity = m_registry.CreateEntity(); + m_playerEntities[player.number] = entity; + + float baseX = 240.0f; + float baseY = 180.0f; + float spacingX = 420.0f; + float spacingY = 220.0f; + + uint8_t index = player.number - 1; + float cardX = baseX + (index % 2) * spacingX; + float cardY = baseY + (index / 2) * spacingY; + + Entity spriteEntity = m_registry.CreateEntity(); + m_playerSprites[player.number] = spriteEntity; + + Renderer::TextureId texture = getPlayerTexture(index); + if (texture != Renderer::INVALID_TEXTURE_ID) { + Renderer::SpriteId spriteId = m_renderer->CreateSprite(texture, {}); + Drawable drawable(spriteId, 0); + drawable.scale = {1.0f, 1.0f}; + m_registry.AddComponent(spriteEntity, Position{cardX, cardY}); + m_registry.AddComponent(spriteEntity, std::move(drawable)); + } + + m_registry.AddComponent(entity, Position{cardX + 10.0f, cardY + 50.0f}); + NetworkPlayer netPlayer{player.number, player.hash, player.name, player.ready}; + m_registry.AddComponent(entity, std::move(netPlayer)); + + if (m_fontMedium != Renderer::INVALID_FONT_ID) { + TextLabel label("", m_fontMedium, 16); + m_registry.AddComponent(entity, std::move(label)); + } + + updatePlayerCardVisuals(player.number, player); + } else { + entity = it->second; + auto& netPlayer = m_registry.GetComponent(entity); + if (netPlayer.ready != player.ready) { + netPlayer.ready = player.ready; + updatePlayerCardVisuals(player.number, player); + } + } + } + + void LobbyState::updatePlayerCardVisuals(uint8_t playerNum, const network::PlayerInfo& player) { + auto it = m_playerEntities.find(playerNum); + if (it == m_playerEntities.end()) + return; + + Entity entity = it->second; + const auto& netPlayer = m_registry.GetComponent(entity); + + std::string playerNameStr = std::string(player.name); + if (playerNameStr.length() > 16) { + playerNameStr = playerNameStr.substr(0, 16); + } + std::string displayText = "P" + std::to_string(netPlayer.playerNumber) + " - " + playerNameStr; + if (netPlayer.ready) { + displayText += "\n[READY]"; + } else { + displayText += "\n[WAITING]"; + } + + if (m_fontMedium != Renderer::INVALID_FONT_ID) { + auto& label = m_registry.GetComponent(entity); + label.text = displayText; + + if (netPlayer.ready) { + label.color = {0.4f, 1.0f, 0.5f, 1.0f}; + } else { + label.color = {1.0f, 0.85f, 0.3f, 1.0f}; + } + } + } + + void LobbyState::removePlayer(uint8_t playerNum) { + auto it = m_playerEntities.find(playerNum); + if (it != m_playerEntities.end()) { + if (m_registry.IsEntityAlive(it->second)) { + m_registry.DestroyEntity(it->second); + } + m_playerEntities.erase(it); + } + + auto spriteIt = m_playerSprites.find(playerNum); + if (spriteIt != m_playerSprites.end()) { + if (m_registry.IsEntityAlive(spriteIt->second)) { + m_registry.DestroyEntity(spriteIt->second); + } + m_playerSprites.erase(spriteIt); + } + } + + } +} diff --git a/games/rtype/client/src/room/RoomListState.cpp b/games/rtype/client/src/room/RoomListState.cpp new file mode 100644 index 0000000..d92bf7e --- /dev/null +++ b/games/rtype/client/src/room/RoomListState.cpp @@ -0,0 +1,358 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** RoomListState - Room selection screen implementation +*/ + +#include "../../include/RoomListState.hpp" +#include "../../include/LobbyState.hpp" +#include "ECS/Components/TextLabel.hpp" +#include +#include + +using namespace RType::ECS; + +namespace RType { + namespace Client { + + RoomListState::RoomListState(GameStateMachine& machine, GameContext& context) + : m_machine(machine), m_context(context) { + m_renderer = context.renderer; + m_renderingSystem = std::make_unique(m_renderer.get()); + m_textSystem = std::make_unique(m_renderer.get()); + } + + void RoomListState::Init() { + std::cout << "[RoomListState] Initializing room selection..." << std::endl; + + m_fontLarge = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 32); + if (m_fontLarge == Renderer::INVALID_FONT_ID) { + m_fontLarge = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 32); + } + + m_fontMedium = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 18); + if (m_fontMedium == Renderer::INVALID_FONT_ID) { + m_fontMedium = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 18); + } + + m_fontSmall = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 14); + if (m_fontSmall == Renderer::INVALID_FONT_ID) { + m_fontSmall = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 14); + } + + m_bgTexture = m_renderer->LoadTexture("assets/backgrounds/1.jpg"); + if (m_bgTexture == Renderer::INVALID_TEXTURE_ID) { + m_bgTexture = m_renderer->LoadTexture("../assets/backgrounds/1.jpg"); + } + + if (!m_context.networkModule) { + m_hasError = true; + m_errorMessage = "Internal error: network module missing"; + m_errorTimer = 3.0f; + createUI(); + return; + } + + m_roomClient = std::make_unique( + m_context.networkModule.get(), m_context.serverIp, m_context.serverPort); + + if (!m_roomClient->isConnected()) { + m_hasError = true; + m_errorMessage = "Failed to connect to server"; + } else { + m_roomClient->onRoomList([this](const std::vector& rooms) { + m_rooms = rooms; + createRoomEntities(); + }); + + m_roomClient->onRoomCreated([this](uint32_t roomId) { + std::cout << "[RoomListState] Room created! Joining room " << roomId << std::endl; + m_roomClient->joinRoom(roomId); + }); + + m_roomClient->onRoomJoined([this](network::JoinRoomStatus status) { + if (status == network::JoinRoomStatus::SUCCESS) { + std::cout << "[RoomListState] Joined room! Transitioning to lobby..." << std::endl; + network::NetworkTcpSocket socket = m_roomClient->releaseSocket(); + m_machine.ChangeState(std::make_unique(m_machine, m_context, std::move(socket))); + } else { + m_hasError = true; + switch (status) { + case network::JoinRoomStatus::ROOM_FULL: + m_errorMessage = "Room is full"; + break; + case network::JoinRoomStatus::ROOM_NOT_FOUND: + m_errorMessage = "Room not found"; + break; + case network::JoinRoomStatus::ROOM_IN_GAME: + m_errorMessage = "Game already in progress"; + break; + default: + m_errorMessage = "Failed to join room"; + break; + } + m_errorTimer = 3.0f; + } + }); + + m_roomClient->onRoomUpdate([this](const network::RoomInfo& room) { + bool found = false; + for (auto& r : m_rooms) { + if (r.id == room.id) { + r = room; + found = true; + break; + } + } + if (!found) { + m_rooms.push_back(room); + } + createRoomEntities(); + }); + + m_roomClient->onConnectionError([this](const std::string& error) { + m_hasError = true; + m_errorMessage = error; + m_errorTimer = 3.0f; + }); + + m_roomClient->requestRooms(); + } + + createUI(); + } + + void RoomListState::Cleanup() { + std::cout << "[RoomListState] Cleaning up..." << std::endl; + clearRoomEntities(); + m_roomClient.reset(); + } + + void RoomListState::createUI() { + if (m_bgTexture != Renderer::INVALID_TEXTURE_ID) { + m_bgEntity = m_registry.CreateEntity(); + m_registry.AddComponent(m_bgEntity, Position{0.0f, 0.0f}); + + Renderer::SpriteId spriteId = m_renderer->CreateSprite(m_bgTexture, {}); + Drawable drawable(spriteId, -10); + + Renderer::Vector2 texSize = m_renderer->GetTextureSize(m_bgTexture); + if (texSize.x > 0 && texSize.y > 0) { + drawable.scale = {1280.0f / texSize.x, 720.0f / texSize.y}; + } + + m_registry.AddComponent(m_bgEntity, std::move(drawable)); + } + + if (m_fontLarge == Renderer::INVALID_FONT_ID) + return; + + m_titleEntity = m_registry.CreateEntity(); + m_registry.AddComponent(m_titleEntity, Position{640.0f, 50.0f}); + TextLabel titleLabel("SELECT A ROOM", m_fontLarge, 32); + titleLabel.color = {0.0f, 1.0f, 1.0f, 1.0f}; + titleLabel.centered = true; + m_registry.AddComponent(m_titleEntity, std::move(titleLabel)); + + m_instructionsEntity = m_registry.CreateEntity(); + m_registry.AddComponent(m_instructionsEntity, Position{640.0f, 670.0f}); + TextLabel instrLabel("[ENTER] Join | [C] Create Room | [ESC] Back", m_fontSmall, 14); + instrLabel.color = {0.5f, 0.86f, 1.0f, 0.9f}; + instrLabel.centered = true; + m_registry.AddComponent(m_instructionsEntity, std::move(instrLabel)); + } + + void RoomListState::clearRoomEntities() { + for (Entity e : m_roomEntities) { + if (m_registry.IsEntityAlive(e)) { + m_registry.DestroyEntity(e); + } + } + m_roomEntities.clear(); + + if (m_noRoomsEntity != NULL_ENTITY && m_registry.IsEntityAlive(m_noRoomsEntity)) { + m_registry.DestroyEntity(m_noRoomsEntity); + m_noRoomsEntity = NULL_ENTITY; + } + } + + void RoomListState::createRoomEntities() { + clearRoomEntities(); + + if (m_rooms.empty()) { + m_noRoomsEntity = m_registry.CreateEntity(); + m_registry.AddComponent(m_noRoomsEntity, Position{640.0f, 300.0f}); + TextLabel noRoomsLabel("No rooms available\nPress [C] to create one!", m_fontMedium, 18); + noRoomsLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; + noRoomsLabel.centered = true; + m_registry.AddComponent(m_noRoomsEntity, std::move(noRoomsLabel)); + return; + } + + float startY = 140.0f; + float spacing = 60.0f; + + for (size_t i = 0; i < m_rooms.size(); ++i) { + const auto& room = m_rooms[i]; + + Entity roomEntity = m_registry.CreateEntity(); + m_roomEntities.push_back(roomEntity); + + float y = startY + i * spacing; + m_registry.AddComponent(roomEntity, Position{640.0f, y}); + + std::string prefix = (static_cast(i) == m_selectedIndex) ? "> " : " "; + std::string status = room.inGame ? "[IN GAME]" : "[WAITING]"; + std::string playerCount = std::to_string(room.playerCount) + "/" + std::to_string(room.maxPlayers); + + std::string roomName(room.name); + if (roomName.length() > 16) { + roomName = roomName.substr(0, 16); + } + while (roomName.length() < 16) { + roomName += " "; + } + + std::string text = prefix + roomName + " " + playerCount + " " + status; + + TextLabel roomLabel(text, m_fontMedium, 18); + + if (static_cast(i) == m_selectedIndex) { + if (room.inGame) { + roomLabel.color = {0.5f, 0.5f, 0.5f, 1.0f}; + } else { + roomLabel.color = {1.0f, 0.08f, 0.58f, 1.0f}; + } + } else { + if (room.inGame) { + roomLabel.color = {0.4f, 0.4f, 0.4f, 0.8f}; + } else { + roomLabel.color = {0.8f, 0.8f, 0.8f, 1.0f}; + } + } + + roomLabel.centered = true; + m_registry.AddComponent(roomEntity, std::move(roomLabel)); + } + } + + void RoomListState::handleCreateRoom() { + std::string roomName = m_context.playerName + "'s Room"; + std::cout << "[RoomListState] Creating room: " << roomName << std::endl; + m_roomClient->createRoom(roomName); + } + + void RoomListState::handleJoinRoom() { + if (m_rooms.empty() || m_selectedIndex < 0 || m_selectedIndex >= static_cast(m_rooms.size())) { + return; + } + + const auto& room = m_rooms[m_selectedIndex]; + + if (room.inGame) { + m_hasError = true; + m_errorMessage = "Cannot join - game in progress"; + m_errorTimer = 2.0f; + return; + } + + if (room.playerCount >= room.maxPlayers) { + m_hasError = true; + m_errorMessage = "Cannot join - room is full"; + m_errorTimer = 2.0f; + return; + } + + std::cout << "[RoomListState] Joining room " << room.id << " (" << room.name << ")" << std::endl; + m_roomClient->joinRoom(room.id); + } + + void RoomListState::HandleInput() { + if (m_renderer->IsKeyPressed(Renderer::Key::Up) && !m_upKeyPressed) { + m_upKeyPressed = true; + if (!m_rooms.empty()) { + m_selectedIndex = (m_selectedIndex - 1 + static_cast(m_rooms.size())) % static_cast(m_rooms.size()); + createRoomEntities(); + } + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Up)) { + m_upKeyPressed = false; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Down) && !m_downKeyPressed) { + m_downKeyPressed = true; + if (!m_rooms.empty()) { + m_selectedIndex = (m_selectedIndex + 1) % static_cast(m_rooms.size()); + createRoomEntities(); + } + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Down)) { + m_downKeyPressed = false; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Enter) && !m_enterKeyPressed) { + m_enterKeyPressed = true; + handleJoinRoom(); + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Enter)) { + m_enterKeyPressed = false; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::C) && !m_cKeyPressed) { + m_cKeyPressed = true; + handleCreateRoom(); + } else if (!m_renderer->IsKeyPressed(Renderer::Key::C)) { + m_cKeyPressed = false; + } + + if (m_renderer->IsKeyPressed(Renderer::Key::Escape) && !m_escapeKeyPressed) { + m_escapeKeyPressed = true; + std::cout << "[RoomListState] Returning to menu..." << std::endl; + m_machine.PopState(); + } else if (!m_renderer->IsKeyPressed(Renderer::Key::Escape)) { + m_escapeKeyPressed = false; + } + } + + void RoomListState::Update(float dt) { + if (m_roomClient) { + m_roomClient->update(); + } + + m_refreshTimer += dt; + if (m_refreshTimer >= REFRESH_INTERVAL) { + m_refreshTimer = 0.0f; + if (m_roomClient && m_roomClient->isConnected()) { + m_roomClient->requestRooms(); + } + } + + if (m_hasError) { + m_errorTimer -= dt; + if (m_errorTimer <= 0.0f) { + m_hasError = false; + m_errorMessage.clear(); + + if (m_errorEntity != NULL_ENTITY && m_registry.IsEntityAlive(m_errorEntity)) { + m_registry.DestroyEntity(m_errorEntity); + m_errorEntity = NULL_ENTITY; + } + } + } + + if (m_hasError && m_errorEntity == NULL_ENTITY) { + m_errorEntity = m_registry.CreateEntity(); + m_registry.AddComponent(m_errorEntity, Position{640.0f, 620.0f}); + TextLabel errorLabel(m_errorMessage, m_fontSmall, 14); + errorLabel.color = {1.0f, 0.3f, 0.3f, 1.0f}; // Red + errorLabel.centered = true; + m_registry.AddComponent(m_errorEntity, std::move(errorLabel)); + } + } + + void RoomListState::Draw() { + m_renderer->Clear({0.05f, 0.05f, 0.1f, 1.0f}); + m_renderingSystem->Update(m_registry, 0.0f); + m_textSystem->Update(m_registry, 0.0f); + } + + } +} diff --git a/games/rtype/server/main.cpp b/games/rtype/server/main.cpp new file mode 100644 index 0000000..b8d1418 --- /dev/null +++ b/games/rtype/server/main.cpp @@ -0,0 +1,81 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** Server main entry point +*/ + +#include "RoomManager.hpp" +#include "GameServer.hpp" +#include "AsioNetworkModule.hpp" +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char* argv[]) { + uint16_t port = 4242; + size_t minPlayers = 2; + std::string levelPath = "assets/levels/level1.json"; + + if (argc > 1) + port = std::stoi(argv[1]); + if (argc > 2) + minPlayers = std::stoi(argv[2]); + if (argc > 3) + levelPath = argv[3]; + + std::cout << "\n=== Starting R-Type server with room support on port " << port << " ===" << std::endl; + + auto networkModule = std::make_shared(); + networkModule->Initialize(nullptr); + + network::RoomManager roomManager(networkModule.get(), port, MAX_ROOMS, minPlayers); + + std::atomic gameStarted{false}; + std::optional startedRoomId; + std::vector gamePlayers; + + roomManager.onGameStart([&](uint32_t roomId, const network::RoomManager::Room& room) { + gamePlayers.clear(); + for (const auto& maybePlayer : room.players) { + if (maybePlayer) { + gamePlayers.push_back(*maybePlayer); + } + } + startedRoomId = roomId; + gameStarted = true; + }); + + while (true) { + roomManager.update(); + + if (gameStarted) { + std::cout << "Game started in room " << *startedRoomId << " with " << gamePlayers.size() << " players!" << std::endl; + + std::cout << "Waiting 2 seconds for clients to transition..." << std::endl; + std::this_thread::sleep_for(std::chrono::seconds(2)); + + std::cout << "Starting UDP GameServer on port " << port << " with " << gamePlayers.size() << " players..." << std::endl; + std::cout << "Level: " << levelPath << std::endl; + + network::GameServer gameServer(networkModule.get(), port, gamePlayers, levelPath); + gameServer.Run(); + + std::cout << "\n=== Game ended in room " << *startedRoomId << ". Room available again. ===" << std::endl; + + gameStarted = false; + startedRoomId.reset(); + gamePlayers.clear(); + + std::this_thread::sleep_for(std::chrono::seconds(2)); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(16)); + } + + return 0; +} From 7837bff292c4af0bcf2d553f7e5efc76038789b0 Mon Sep 17 00:00:00 2001 From: erwan Date: Mon, 2 Feb 2026 00:01:33 +0100 Subject: [PATCH 2/7] chore: separated the components into different files --- engine/include/ECS/Components/Animation.hpp | 156 +++++++++++++++++++ engine/include/ECS/Components/Audio.hpp | 46 ++++++ engine/include/ECS/Components/Gameplay.hpp | 142 +++++++++++++++++ engine/include/ECS/Components/IComponent.hpp | 23 +++ engine/include/ECS/Components/Physics.hpp | 121 ++++++++++++++ engine/include/ECS/Components/Rendering.hpp | 70 +++++++++ engine/include/ECS/Components/Transform.hpp | 89 +++++++++++ 7 files changed, 647 insertions(+) create mode 100644 engine/include/ECS/Components/Animation.hpp create mode 100644 engine/include/ECS/Components/Audio.hpp create mode 100644 engine/include/ECS/Components/Gameplay.hpp create mode 100644 engine/include/ECS/Components/IComponent.hpp create mode 100644 engine/include/ECS/Components/Physics.hpp create mode 100644 engine/include/ECS/Components/Rendering.hpp create mode 100644 engine/include/ECS/Components/Transform.hpp diff --git a/engine/include/ECS/Components/Animation.hpp b/engine/include/ECS/Components/Animation.hpp new file mode 100644 index 0000000..239c590 --- /dev/null +++ b/engine/include/ECS/Components/Animation.hpp @@ -0,0 +1,156 @@ +#pragma once +/** + * @file Animation.hpp + * @brief Generic animation components for any game engine. + */ + +#include +#include +#include "Animation/AnimationTypes.hpp" +#include "Math/Types.hpp" +#include "ECS/Entity.hpp" +#include "ECS/Components/IComponent.hpp" + +namespace RType { +namespace ECS { + + /** + * @brief Sprite animation playback component. + */ + struct SpriteAnimation : public IComponent { + Animation::AnimationClipId clipId = Animation::INVALID_CLIP_ID; + float currentTime = 0.0f; + float playbackSpeed = 1.0f; + bool playing = true; + bool looping = false; + bool destroyOnComplete = false; + + std::size_t currentFrameIndex = 0; + Math::Rectangle currentRegion{}; + + SpriteAnimation() = default; + SpriteAnimation(Animation::AnimationClipId clip, bool loop = false, float speed = 1.0f) + : clipId(clip), playbackSpeed(speed), looping(loop) {} + }; + + /** + * @brief Animation state machine for complex animations. + */ + struct AnimationStateMachine : public IComponent { + Animation::AnimationGraphId graphId = Animation::INVALID_GRAPH_ID; + Animation::AnimationStateId currentState = Animation::INVALID_STATE_ID; + Animation::AnimationStateId previousState = Animation::INVALID_STATE_ID; + float stateTime = 0.0f; + float blendFactor = 0.0f; + float blendDuration = 0.0f; + bool isTransitioning = false; + + static constexpr std::size_t MAX_PARAMS = 8; + std::array parameters{}; + std::array, MAX_PARAMS> parameterNames{}; + std::size_t parameterCount = 0; + + AnimationStateMachine() = default; + explicit AnimationStateMachine(Animation::AnimationGraphId graph) + : graphId(graph) {} + + void SetParameter(const char* name, float value) { + for (std::size_t i = 0; i < parameterCount; ++i) { + if (std::strncmp(parameterNames[i].data(), name, 31) == 0) { + parameters[i] = value; + return; + } + } + if (parameterCount < MAX_PARAMS) { + std::strncpy(parameterNames[parameterCount].data(), name, 31); + parameterNames[parameterCount][31] = '\0'; + parameters[parameterCount] = value; + parameterCount++; + } + } + + float GetParameter(const char* name) const { + for (std::size_t i = 0; i < parameterCount; ++i) { + if (std::strncmp(parameterNames[i].data(), name, 31) == 0) { + return parameters[i]; + } + } + return 0.0f; + } + }; + + /** + * @brief Marker for animated sprites that need frame updates. + */ + struct AnimatedSprite : public IComponent { + bool needsUpdate = true; + + AnimatedSprite() = default; + }; + + /** + * @brief Visual effect component (explosions, particles, etc). + */ + struct VisualEffect : public IComponent { + Animation::EffectType type = Animation::EffectType::EXPLOSION_SMALL; + float lifetime = 0.0f; + float maxLifetime = 1.0f; + Entity owner = NULL_ENTITY; + float offsetX = 0.0f; + float offsetY = 0.0f; + + VisualEffect() = default; + VisualEffect(Animation::EffectType t, float duration) + : type(t), maxLifetime(duration) {} + VisualEffect(Animation::EffectType t, float duration, Entity ownerEntity, float offX, float offY) + : type(t), maxLifetime(duration), owner(ownerEntity), offsetX(offX), offsetY(offY) {} + }; + + /** + * @brief Animation events for triggering actions at specific frames. + */ + struct AnimationEvents : public IComponent { + static constexpr std::size_t MAX_EVENTS = 4; + std::array, MAX_EVENTS> eventNames{}; + std::size_t eventCount = 0; + + AnimationEvents() = default; + + void PushEvent(const char* name) { + if (eventCount < MAX_EVENTS && name) { + std::strncpy(eventNames[eventCount].data(), name, 31); + eventNames[eventCount][31] = '\0'; + eventCount++; + } + } + + void Clear() { eventCount = 0; } + + bool HasEvent(const char* name) const { + for (std::size_t i = 0; i < eventCount; ++i) { + if (std::strncmp(eventNames[i].data(), name, 31) == 0) { + return true; + } + } + return false; + } + }; + + /** + * @brief Animation layer for blending multiple animations. + */ + struct AnimationLayer : public IComponent { + Animation::AnimationClipId clipId = Animation::INVALID_CLIP_ID; + float currentTime = 0.0f; + float weight = 1.0f; + float playbackSpeed = 1.0f; + bool additive = false; + int layerIndex = 0; + + AnimationLayer() = default; + AnimationLayer(Animation::AnimationClipId clip, int layer, float w = 1.0f) + : clipId(clip), weight(w), layerIndex(layer) {} + }; + +} // namespace ECS +} // namespace RType diff --git a/engine/include/ECS/Components/Audio.hpp b/engine/include/ECS/Components/Audio.hpp new file mode 100644 index 0000000..b51ec0c --- /dev/null +++ b/engine/include/ECS/Components/Audio.hpp @@ -0,0 +1,46 @@ +#pragma once +/** + * @file Audio.hpp + * @brief Generic audio components for any game engine. + */ + +#include "Audio/IAudio.hpp" +#include "ECS/Components/IComponent.hpp" + +namespace RType { +namespace ECS { + + /** + * @brief Sound effect component for one-shot or looping sounds. + */ + struct SoundEffect : public IComponent { + Audio::SoundId soundId = Audio::INVALID_SOUND_ID; + float volume = 1.0f; + float pitch = 1.0f; + float pan = 0.0f; + bool loop = false; + bool positional = false; + + SoundEffect() = default; + SoundEffect(Audio::SoundId id, float vol = 1.0f) + : soundId(id), volume(vol) {} + }; + + /** + * @brief Music/stream component for background music. + */ + struct MusicEffect : public IComponent { + Audio::MusicId musicId = Audio::INVALID_MUSIC_ID; + bool play = true; + bool stop = false; + float volume = 1.0f; + float pitch = 1.0f; + bool loop = true; + + MusicEffect() = default; + explicit MusicEffect(Audio::MusicId id) + : musicId(id) {} + }; + +} // namespace ECS +} // namespace RType diff --git a/engine/include/ECS/Components/Gameplay.hpp b/engine/include/ECS/Components/Gameplay.hpp new file mode 100644 index 0000000..96b7257 --- /dev/null +++ b/engine/include/ECS/Components/Gameplay.hpp @@ -0,0 +1,142 @@ +#pragma once +/** + * @file Gameplay.hpp + * @brief Generic gameplay components for any game engine. + * + * These include health, damage, scoring, and combat-related components. + */ + +#include +#include +#include "ECS/Entity.hpp" +#include "ECS/Components/IComponent.hpp" + +namespace RType { +namespace ECS { + + /** + * @brief Health component for damageable entities. + */ + struct Health : public IComponent { + int current = 100; + int max = 100; + + Health() = default; + Health(int maxHealth) + : current(maxHealth), max(maxHealth) {} + Health(int currentHealth, int maxHealth) + : current(currentHealth), max(maxHealth) {} + + bool IsDead() const { return current <= 0; } + float GetPercentage() const { return max > 0 ? static_cast(current) / max : 0.0f; } + }; + + /** + * @brief Damage component for entities that deal damage. + */ + struct Damage : public IComponent { + int amount = 10; + + Damage() = default; + Damage(int damageAmount) + : amount(damageAmount) {} + }; + + /** + * @brief Score value for collectibles/enemies. + */ + struct ScoreValue : public IComponent { + uint32_t points = 100; + + ScoreValue() = default; + ScoreValue(uint32_t scorePoints) + : points(scorePoints) {} + }; + + /** + * @brief Score timer for time-based scoring. + */ + struct ScoreTimer : public IComponent { + float elapsed = 0.0f; + + ScoreTimer() = default; + ScoreTimer(float startElapsed) + : elapsed(startElapsed) {} + }; + + /** + * @brief Bullet/projectile component. + */ + struct Bullet : public IComponent { + Entity owner = NULL_ENTITY; + + Bullet() = default; + Bullet(Entity shooter) + : owner(shooter) {} + }; + + /** + * @brief Component for entities that can shoot. + */ + struct Shooter : public IComponent { + float fireRate = 0.2f; + float cooldown = 0.0f; + float offsetX = 50.0f; + float offsetY = 20.0f; + + Shooter() = default; + Shooter(float rate, float oX = 50.0f, float oY = 20.0f) + : fireRate(rate), offsetX(oX), offsetY(oY) {} + }; + + /** + * @brief Shoot command input state. + */ + struct ShootCommand : public IComponent { + bool wantsToShoot = false; + + ShootCommand() = default; + ShootCommand(bool shoot) : wantsToShoot(shoot) {} + }; + + /** + * @brief Proximity damage for AOE effects. + */ + struct ProximityDamage : public IComponent { + float damageRadius = 120.0f; + float damageAmount = 1.0f; + float tickRate = 0.5f; + float timeSinceDamage = 0.0f; + + ProximityDamage() = default; + ProximityDamage(float radius, float damage, float rate) + : damageRadius(radius), damageAmount(damage), tickRate(rate) {} + }; + + /** + * @brief Lifetime component for auto-destruction. + */ + struct Lifetime : public IComponent { + float remaining = 1.0f; + + Lifetime() = default; + Lifetime(float duration) : remaining(duration) {} + }; + + /** + * @brief Tag component for entity categorization. + */ + struct Tag : public IComponent { + char name[32] = {}; + + Tag() = default; + Tag(const char* tagName) { + if (tagName) { + std::strncpy(name, tagName, 31); + name[31] = '\0'; + } + } + }; + +} // namespace ECS +} // namespace RType diff --git a/engine/include/ECS/Components/IComponent.hpp b/engine/include/ECS/Components/IComponent.hpp new file mode 100644 index 0000000..6697e1f --- /dev/null +++ b/engine/include/ECS/Components/IComponent.hpp @@ -0,0 +1,23 @@ +#pragma once +/** + * @file IComponent.hpp + * @brief Base interface for all ECS components. + */ + +#include + +namespace RType { +namespace ECS { + + using ComponentID = std::type_index; + + /** + * @brief Base interface for all components. + * All components should inherit from this. + */ + struct IComponent { + virtual ~IComponent() = default; + }; + +} // namespace ECS +} // namespace RType diff --git a/engine/include/ECS/Components/Physics.hpp b/engine/include/ECS/Components/Physics.hpp new file mode 100644 index 0000000..a97c743 --- /dev/null +++ b/engine/include/ECS/Components/Physics.hpp @@ -0,0 +1,121 @@ +#pragma once +/** + * @file Physics.hpp + * @brief Generic physics and collision components for any 2D game engine. + */ + +#include +#include "ECS/Entity.hpp" +#include "ECS/Components/IComponent.hpp" + +namespace RType { +namespace ECS { + + /** + * @brief Axis-aligned box collider. + */ + struct BoxCollider : public IComponent { + float width = 0.0f; + float height = 0.0f; + + BoxCollider() = default; + BoxCollider(float width, float height) + : width(width), height(height) {} + }; + + /** + * @brief Circle collider. + */ + struct CircleCollider : public IComponent { + float radius = 0.0f; + + CircleCollider() = default; + CircleCollider(float r) : radius(r) {} + }; + + /** + * @brief Collision layer for filtering collisions. + */ + struct CollisionLayer : public IComponent { + uint16_t layer = 0; // What layer this entity is on + uint16_t mask = 0xFFFF; // Which layers this entity collides with + + CollisionLayer() = default; + CollisionLayer(uint16_t l, uint16_t m) : layer(l), mask(m) {} + }; + + /** + * @brief Standard collision layer masks. + */ + namespace CollisionLayers { + constexpr uint16_t NONE = 0; + constexpr uint16_t PLAYER = 1 << 0; // 0x0001 + constexpr uint16_t ENEMY = 1 << 1; // 0x0002 + constexpr uint16_t PLAYER_BULLET = 1 << 2; // 0x0004 + constexpr uint16_t ENEMY_BULLET = 1 << 3; // 0x0008 + constexpr uint16_t OBSTACLE = 1 << 4; // 0x0010 + constexpr uint16_t POWERUP = 1 << 5; // 0x0020 + constexpr uint16_t GROUND = 1 << 6; // 0x0040 (for platformers) + constexpr uint16_t TRIGGER = 1 << 7; // 0x0080 (for trigger zones) + constexpr uint16_t ALL = 0xFFFF; + } + + /** + * @brief Collision event marker component. + */ + struct CollisionEvent : public IComponent { + Entity other = NULL_ENTITY; + + CollisionEvent() = default; + CollisionEvent(Entity e) : other(e) {} + }; + + /** + * @brief Static obstacle that blocks movement. + */ + struct Obstacle : public IComponent { + bool blocking = true; + + Obstacle() = default; + Obstacle(bool isBlocking) : blocking(isBlocking) {} + }; + + /** + * @brief Invincibility frames/state. + */ + struct Invincibility : public IComponent { + float remainingTime = 0.0f; + + Invincibility() = default; + Invincibility(float duration) : remainingTime(duration) {} + }; + + /** + * @brief 2D Rigidbody with physics properties (NEW for metroidvania). + */ + struct Rigidbody2D : public IComponent { + float mass = 1.0f; + float gravityScale = 1.0f; + float drag = 0.0f; + bool isKinematic = false; + bool isGrounded = false; + bool useGravity = true; + + Rigidbody2D() = default; + Rigidbody2D(float m, float gravity = 1.0f) + : mass(m), gravityScale(gravity) {} + }; + + /** + * @brief Physics material for collision response. + */ + struct PhysicsMaterial : public IComponent { + float friction = 0.5f; + float bounciness = 0.0f; + + PhysicsMaterial() = default; + PhysicsMaterial(float f, float b) : friction(f), bounciness(b) {} + }; + +} // namespace ECS +} // namespace RType diff --git a/engine/include/ECS/Components/Rendering.hpp b/engine/include/ECS/Components/Rendering.hpp new file mode 100644 index 0000000..bbd0fc1 --- /dev/null +++ b/engine/include/ECS/Components/Rendering.hpp @@ -0,0 +1,70 @@ +#pragma once +/** + * @file Rendering.hpp + * @brief Generic rendering components for any 2D game engine. + */ + +#include +#include "Renderer/IRenderer.hpp" +#include "Math/Types.hpp" +#include "ECS/Components/IComponent.hpp" + +namespace RType { +namespace ECS { + + /** + * @brief Sprite rendering component. + */ + struct Drawable : public IComponent { + Renderer::SpriteId spriteId = Renderer::INVALID_SPRITE_ID; + Math::Vector2 scale{1.0f, 1.0f}; + float rotation = 0.0f; + Math::Vector2 origin{0.0f, 0.0f}; + Math::Color tint{1.0f, 1.0f, 1.0f, 1.0f}; + int layer = 0; + + Drawable() = default; + Drawable(Renderer::SpriteId sprite, int renderLayer = 0) + : spriteId(sprite), layer(renderLayer) {} + }; + + /** + * @brief Visual damage flash effect. + */ + struct DamageFlash : public IComponent { + float duration = 0.1f; + float timeRemaining = 0.0f; + bool isActive = false; + + DamageFlash() = default; + DamageFlash(float flashDuration) : duration(flashDuration) {} + + void Trigger() { + isActive = true; + timeRemaining = duration; + } + }; + + /** + * @brief Floating text that animates upward and fades. + */ + struct FloatingText : public IComponent { + char text[32] = {}; + float lifetime = 0.0f; + float maxLifetime = 1.5f; + float velocityY = -50.0f; + float fadeStartTime = 0.5f; + Math::Color color{1.0f, 1.0f, 1.0f, 1.0f}; + + FloatingText() = default; + FloatingText(const char* txt, float duration, const Math::Color& col) + : maxLifetime(duration), color(col) { + if (txt) { + std::strncpy(text, txt, 31); + text[31] = '\0'; + } + } + }; + +} // namespace ECS +} // namespace RType diff --git a/engine/include/ECS/Components/Transform.hpp b/engine/include/ECS/Components/Transform.hpp new file mode 100644 index 0000000..0d198c0 --- /dev/null +++ b/engine/include/ECS/Components/Transform.hpp @@ -0,0 +1,89 @@ +#pragma once +/** + * @file Transform.hpp + * @brief Generic transform-related components for any 2D/3D game engine. + * + * These components are game-agnostic and can be used by any project. + */ + +#include +#include "Math/Types.hpp" +#include "ECS/Entity.hpp" +#include "ECS/Components/IComponent.hpp" + +namespace RType { +namespace ECS { + + /** + * @brief 2D position component. + */ + struct Position : public IComponent { + float x = 0.0f; + float y = 0.0f; + + Position() = default; + Position(float x, float y) : x(x), y(y) {} + }; + + /** + * @brief 2D velocity component. + */ + struct Velocity : public IComponent { + float dx = 0.0f; + float dy = 0.0f; + + Velocity() = default; + Velocity(float dx, float dy) : dx(dx), dy(dy) {} + }; + + /** + * @brief Full 2D transform (position + rotation + scale). + * Use this when you need full transform capabilities. + */ + struct Transform2D : public IComponent { + Math::Vector2 position{0.0f, 0.0f}; + float rotation = 0.0f; + Math::Vector2 scale{1.0f, 1.0f}; + + Transform2D() = default; + Transform2D(float x, float y, float rot = 0.0f, float scaleX = 1.0f, float scaleY = 1.0f) + : position{x, y}, rotation(rot), scale{scaleX, scaleY} {} + }; + + /** + * @brief Component for entities that can be controlled by input. + */ + struct Controllable : public IComponent { + float speed = 200.0f; + + Controllable() = default; + Controllable(float moveSpeed) : speed(moveSpeed) {} + }; + + /** + * @brief Indicates scrolling behavior for backgrounds/parallax. + */ + struct Scrollable : public IComponent { + float speed = -100.0f; + + Scrollable() = default; + Scrollable(float scrollSpeed) : speed(scrollSpeed) {} + }; + + /** + * @brief Stable network identifier for server->client entity mirroring. + * + * IMPORTANT: This must live on the entity as a component, because raw ECS + * entity IDs are recycled. If we map "ECS entity id -> network id" in a + * hash map, then destroying and reusing an ECS id in the same tick can + * cause a new entity to inherit the old network id (type confusion). + */ + struct NetworkId : public IComponent { + uint32_t id = 0; + + NetworkId() = default; + explicit NetworkId(uint32_t networkId) : id(networkId) {} + }; + +} // namespace ECS +} // namespace RType From 92f5c0da36b7393cf4c4b772910a52234d65bf79 Mon Sep 17 00:00:00 2001 From: erwan Date: Mon, 2 Feb 2026 00:08:28 +0100 Subject: [PATCH 3/7] fix: fixed the compilation error --- engine/include/ECS/Component.hpp | 914 ++++++++++--------------------- 1 file changed, 284 insertions(+), 630 deletions(-) diff --git a/engine/include/ECS/Component.hpp b/engine/include/ECS/Component.hpp index 7920583..9032814 100644 --- a/engine/include/ECS/Component.hpp +++ b/engine/include/ECS/Component.hpp @@ -1,644 +1,298 @@ #pragma once - +/** + * @file Component.hpp + * @brief Master include file for all ECS components. + * + * This file includes all generic engine components from modular files, + * and contains R-Type specific components inline for backwards compatibility. + * + * For new games, use only the generic component headers from ECS/Components/. + * R-Type specific components will be moved to games/rtype/components/ in a future phase. + */ + +// ============================================================================= +// Generic Engine Components (game-agnostic) +// ============================================================================= +#include "ECS/Components/IComponent.hpp" // Base component interface +#include "ECS/Components/Transform.hpp" // Position, Velocity, Transform2D, Controllable, Scrollable, NetworkId +#include "ECS/Components/Rendering.hpp" // Drawable, DamageFlash, FloatingText +#include "ECS/Components/Physics.hpp" // BoxCollider, CircleCollider, CollisionLayer, Obstacle, Invincibility, Rigidbody2D, PhysicsMaterial +#include "ECS/Components/Audio.hpp" // SoundEffect, MusicEffect +#include "ECS/Components/Animation.hpp" // SpriteAnimation, AnimationStateMachine, VisualEffect, AnimationEvents, AnimationLayer +#include "ECS/Components/Gameplay.hpp" // Health, Damage, ScoreValue, ScoreTimer, Bullet, Shooter, ShootCommand, ProximityDamage, Lifetime, Tag + +// Standard library includes for R-Type components #include #include #include -#include -#include #include #include #include "Renderer/IRenderer.hpp" #include "Math/Types.hpp" #include "Entity.hpp" -#include "Audio/IAudio.hpp" #include "Animation/AnimationTypes.hpp" namespace RType { - - namespace ECS { - using ComponentID = std::type_index; - - struct IComponent { - virtual ~IComponent() = default; - }; - - struct Position : public IComponent { - float x = 0.0f; - float y = 0.0f; - - Position() = default; - Position(float x, float y) - : x(x), y(y) {} - }; - - struct Velocity : public IComponent { - float dx = 0.0f; - float dy = 0.0f; - - Velocity() = default; - Velocity(float dx, float dy) - : dx(dx), dy(dy) {} - }; - - struct Drawable : public IComponent { - Renderer::SpriteId spriteId = Renderer::INVALID_SPRITE_ID; - Math::Vector2 scale{1.0f, 1.0f}; - float rotation = 0.0f; - Math::Vector2 origin{0.0f, 0.0f}; - Math::Color tint{1.0f, 1.0f, 1.0f, 1.0f}; - int layer = 0; - - Drawable() = default; - Drawable(Renderer::SpriteId sprite, int renderLayer = 0) - : spriteId(sprite), layer(renderLayer) {} - }; - - struct NetworkPlayer : public IComponent { - uint8_t playerNumber = 0; - uint64_t playerHash = 0; - char name[32] = {}; - bool ready = false; - - NetworkPlayer() = default; - NetworkPlayer(uint8_t num, uint64_t hash, const char* playerName, bool isReady = false) - : playerNumber(num), playerHash(hash), ready(isReady) { - if (playerName) { - std::strncpy(name, playerName, 31); - name[31] = '\0'; - } +namespace ECS { + +// ============================================================================= +// R-Type Specific Components (will be moved to games/rtype/components/ later) +// ============================================================================= + + struct NetworkPlayer : public IComponent { + uint8_t playerNumber = 0; + uint64_t playerHash = 0; + char name[32] = {}; + bool ready = false; + + NetworkPlayer() = default; + NetworkPlayer(uint8_t num, uint64_t hash, const char* playerName, bool isReady = false) + : playerNumber(num), playerHash(hash), ready(isReady) { + if (playerName) { + std::strncpy(name, playerName, 31); + name[31] = '\0'; } - }; - - struct BoxCollider : public IComponent { - float width = 0.0f; - float height = 0.0f; - - BoxCollider() = default; - BoxCollider(float width, float height) - : width(width), height(height) {} - }; - - struct Controllable : public IComponent { - float speed = 200.0f; - - Controllable() = default; - Controllable(float moveSpeed) - : speed(moveSpeed) {} - }; - - struct Player : public IComponent { - uint8_t playerNumber = 0; - uint64_t playerHash = 0; - bool isLocalPlayer = false; - uint8_t lives = 3; - - Player() = default; - Player(uint8_t number, uint64_t hash, bool local = false, uint8_t startLives = 3) - : playerNumber(number), playerHash(hash), isLocalPlayer(local), lives(startLives) {} - }; - - enum class EnemyType : uint8_t { - BASIC = 0, - FAST = 1, - TANK = 2, - BOSS = 3, - FORMATION = 4 - }; - - struct Enemy : public IComponent { - EnemyType type = EnemyType::BASIC; - uint32_t id = 0; - - Enemy() = default; - Enemy(EnemyType enemyType, uint32_t enemyId = 0) - : type(enemyType), id(enemyId) {} - }; - - struct Boss : public IComponent { - uint8_t bossId = 1; - - Boss() = default; - Boss(uint8_t id) : bossId(id) {} - }; - - struct BossKilled : public IComponent { - Entity bossEntity; - int levelNumber; - float timeSinceDeath = 0.0f; - - BossKilled() = default; - BossKilled(Entity boss, int level) - : bossEntity(boss), levelNumber(level) {} - }; - - enum class BossAttackPattern { - IDLE = 0, - // Boss 1 - FAN_SPRAY = 1, - DIRECT_SHOT = 2, - CIRCLE = 3, - BLACK_ORB = 4, - THIRD_BULLET = 5, - // Boss 2 - SPIRAL_WAVE = 6, - ANIMATED_ORB = 7, - LASER_BEAM = 8 - }; - - struct BossAttack : public IComponent { - float attackCooldown = 3.0f; - float timeSinceLastAttack = 0.0f; - BossAttackPattern currentPattern = BossAttackPattern::FAN_SPRAY; - - BossAttack() = default; - BossAttack(float cooldown) : attackCooldown(cooldown) {} - }; - - struct BossBullet : public IComponent { - BossBullet() = default; - }; - - struct WaveAttack : public IComponent { - WaveAttack() = default; - }; - - struct SecondAttack : public IComponent { - SecondAttack() = default; - }; - - struct FireBullet : public IComponent { - FireBullet() = default; - }; - - struct Mine : public IComponent { - float proximityRadius = 80.0f; - float explosionRadius = 100.0f; - float lifeTime = 10.0f; - float timer = 0.0f; - bool isExploding = false; - float explosionTimer = 0.0f; - - Mine() = default; - Mine(float proximity, float explosion, float life) - : proximityRadius(proximity), explosionRadius(explosion), lifeTime(life) {} - }; - - struct BossMovementPattern : public IComponent { - float timer = 0.0f; - float amplitudeY = 200.0f; - float amplitudeX = 80.0f; - float frequencyY = 0.5f; - float frequencyX = 0.3f; - float centerY = 0.0f; - float centerX = 0.0f; - - BossMovementPattern() = default; - BossMovementPattern(float ampY, float ampX, float freqY, float freqX, float centerYPos, float centerXPos) - : amplitudeY(ampY), amplitudeX(ampX), frequencyY(freqY), frequencyX(freqX), centerY(centerYPos), centerX(centerXPos) {} - }; - - struct BlackOrb : public IComponent { - float attractionRadius = 200.0f; - float absorptionRadius = 30.0f; - float attractionForce = 500.0f; - bool isActive = true; - - BlackOrb() = default; - BlackOrb(float attraction, float absorption, float force) - : attractionRadius(attraction), absorptionRadius(absorption), attractionForce(force) {} - }; - - struct ProximityDamage : public IComponent { - float damageRadius = 120.0f; - float damageAmount = 1.0f; - float tickRate = 0.5f; - float timeSinceDamage = 0.0f; - - ProximityDamage() = default; - ProximityDamage(float radius, float damage, float rate) - : damageRadius(radius), damageAmount(damage), tickRate(rate) {} - }; - - struct DamageFlash : public IComponent { - float duration = 0.1f; - float timeRemaining = 0.0f; - bool isActive = false; - - DamageFlash() = default; - DamageFlash(float flashDuration) : duration(flashDuration) {} - - void Trigger() { - isActive = true; - timeRemaining = duration; - } - }; - - struct ThirdBullet : public IComponent { - float spawnInterval = 0.3f; - float timeSinceSpawn = 0.0f; - int damage = 50; - bool isActive = true; - - ThirdBullet() = default; - ThirdBullet(float interval, int dmg) - : spawnInterval(interval), damage(dmg) {} - }; - - struct Health : public IComponent { - int current = 100; - int max = 100; - - Health() = default; - Health(int maxHealth) - : current(maxHealth), max(maxHealth) {} - Health(int currentHealth, int maxHealth) - : current(currentHealth), max(maxHealth) {} - }; - - struct ScoreValue : public IComponent { - uint32_t points = 100; - - ScoreValue() = default; - ScoreValue(uint32_t scorePoints) - : points(scorePoints) {} - }; - - struct ScoreTimer : public IComponent { - float elapsed = 0.0f; - - ScoreTimer() = default; - ScoreTimer(float startElapsed) - : elapsed(startElapsed) {} - }; - - struct Damage : public IComponent { - int amount = 10; - - Damage() = default; - Damage(int damageAmount) - : amount(damageAmount) {} - }; - - struct EnemyKilled : public IComponent { - uint32_t enemyId = 0; - Entity killedBy = NULL_ENTITY; - - EnemyKilled() = default; - EnemyKilled(uint32_t id, Entity killer = NULL_ENTITY) - : enemyId(id), killedBy(killer) {} - }; - - struct Bullet : public IComponent { - Entity owner = NULL_ENTITY; - - Bullet() = default; - Bullet(Entity shooter) - : owner(shooter) {} - }; - - struct Shooter : public IComponent { - float fireRate = 0.2f; - float cooldown = 0.0f; - float offsetX = 50.0f; - float offsetY = 20.0f; - - Shooter() = default; - Shooter(float rate, float oX = 50.0f, float oY = 20.0f) : fireRate(rate), offsetX(oX), offsetY(oY) {} - }; - - struct ShootCommand : public IComponent { - bool wantsToShoot = false; - - ShootCommand() = default; - ShootCommand(bool shoot) : wantsToShoot(shoot) {} - }; - - struct Scrollable : public IComponent { - float speed = -100.0f; - - Scrollable() = default; - Scrollable(float scrollSpeed) : speed(scrollSpeed) {} - }; - - struct Obstacle : public IComponent { - bool blocking = true; - - Obstacle() = default; - Obstacle(bool isBlocking) : blocking(isBlocking) {} - }; - - struct ObstacleVisual : public IComponent { - }; - - // Stable network identifier for server->client entity mirroring. - // IMPORTANT: This must live on the entity as a component, because raw ECS entity IDs are recycled. - // If we instead map "ECS entity id -> network id" in a hash map, then destroying and reusing an - // ECS id in the same tick can cause a new entity to inherit the old network id (type confusion). - struct NetworkId : public IComponent { - uint32_t id = 0; - - NetworkId() = default; - explicit NetworkId(uint32_t networkId) : id(networkId) {} - }; - - struct ObstacleMetadata : public IComponent { - uint32_t uniqueId = 0; - Entity visualEntity = NULL_ENTITY; - float offsetX = 0.0f; - float offsetY = 0.0f; - - ObstacleMetadata() = default; - ObstacleMetadata(uint32_t id, - Entity visual = NULL_ENTITY, - float offsetX = 0.0f, - float offsetY = 0.0f) - : uniqueId(id), - visualEntity(visual), - offsetX(offsetX), - offsetY(offsetY) {} - }; - - struct Invincibility : public IComponent { - float remainingTime = 0.0f; - - Invincibility() = default; - Invincibility(float duration) : remainingTime(duration) {} - }; - - struct CollisionLayer : public IComponent { - uint16_t layer = 0; // What layer this entity is on - uint16_t mask = 0xFFFF; // Which layers this entity collides with - - CollisionLayer() = default; - CollisionLayer(uint16_t l, uint16_t m) : layer(l), mask(m) {} - }; - - // masks for collision layers - namespace CollisionLayers { - constexpr uint16_t NONE = 0; - constexpr uint16_t PLAYER = 1 << 0; // 0x0001 - constexpr uint16_t ENEMY = 1 << 1; // 0x0002 - constexpr uint16_t PLAYER_BULLET = 1 << 2; // 0x0004 - constexpr uint16_t ENEMY_BULLET = 1 << 3; // 0x0008 - constexpr uint16_t OBSTACLE = 1 << 4; // 0x0010 - constexpr uint16_t POWERUP = 1 << 5; // 0x0020 - constexpr uint16_t ALL = 0xFFFF; } - - struct CircleCollider : public IComponent { - float radius = 0.0f; - - CircleCollider() = default; - CircleCollider(float r) : radius(r) {} - }; - - struct CollisionEvent : public IComponent { - Entity other = NULL_ENTITY; - - CollisionEvent() = default; - CollisionEvent(Entity e) : other(e) {} - }; - - // Powerup system components - enum class PowerUpType : uint8_t { - FIRE_RATE_BOOST = 0, - SPREAD_SHOT = 1, - LASER_BEAM = 2, - FORCE_POD = 3, - SPEED_BOOST = 4, - SHIELD = 5 - }; - - struct PowerUp : public IComponent { - PowerUpType type = PowerUpType::FIRE_RATE_BOOST; - uint32_t id = 0; - - PowerUp() = default; - PowerUp(PowerUpType powerupType, uint32_t powerupId = 0) - : type(powerupType), id(powerupId) {} - }; - - struct ActivePowerUps : public IComponent { - bool hasFireRateBoost = false; - bool hasSpreadShot = false; - bool hasLaserBeam = false; - bool hasShield = false; - float speedMultiplier = 1.0f; - - ActivePowerUps() = default; - }; - - enum class WeaponType : uint8_t { - STANDARD = 0, - SPREAD = 1, - LASER = 2 - }; - - struct WeaponSlot : public IComponent { - WeaponType type = WeaponType::STANDARD; - float fireRate = 0.2f; - float cooldown = 0.0f; - int damage = 25; - bool enabled = true; - - WeaponSlot() = default; - WeaponSlot(WeaponType weaponType, float rate, int dmg) - : type(weaponType), fireRate(rate), damage(dmg) {} - }; - - struct ForcePod : public IComponent { - Entity owner = NULL_ENTITY; - float offsetX = -60.0f; - float offsetY = 0.0f; - bool isAttached = true; - - ForcePod() = default; - ForcePod(Entity ownerEntity, float oX = -60.0f, float oY = 0.0f) - : owner(ownerEntity), offsetX(oX), offsetY(oY) {} - }; - - struct Shield : public IComponent { - float duration = 0.0f; // 0 = permanent (until death) - float timeRemaining = 0.0f; - - Shield() = default; - Shield(float dur = 0.0f) : duration(dur), timeRemaining(dur) {} - }; - - struct SoundEffect : public IComponent { - Audio::SoundId soundId = Audio::INVALID_SOUND_ID; - float volume = 1.0f; - float pitch = 1.0f; - float pan = 0.0f; - bool loop = false; - bool positional = false; - - SoundEffect() = default; - SoundEffect(Audio::SoundId id, float vol = 1.0f) - : soundId(id), volume(vol) {} - }; - - struct MusicEffect : public IComponent { - Audio::MusicId musicId = Audio::INVALID_MUSIC_ID; - bool play = true; - bool stop = false; - float volume = 1.0f; - float pitch = 1.0f; - bool loop = true; - - MusicEffect() = default; - explicit MusicEffect(Audio::MusicId id) - : musicId(id) {} - }; - - struct SpriteAnimation : public IComponent { - Animation::AnimationClipId clipId = Animation::INVALID_CLIP_ID; - float currentTime = 0.0f; - float playbackSpeed = 1.0f; - bool playing = true; - bool looping = false; - bool destroyOnComplete = false; - - std::size_t currentFrameIndex = 0; - Math::Rectangle currentRegion{}; - - SpriteAnimation() = default; - SpriteAnimation(Animation::AnimationClipId clip, bool loop = false, float speed = 1.0f) - : clipId(clip), playbackSpeed(speed), looping(loop) {} - }; - - struct AnimationStateMachine : public IComponent { - Animation::AnimationGraphId graphId = Animation::INVALID_GRAPH_ID; - Animation::AnimationStateId currentState = Animation::INVALID_STATE_ID; - Animation::AnimationStateId previousState = Animation::INVALID_STATE_ID; - float stateTime = 0.0f; - float blendFactor = 0.0f; - float blendDuration = 0.0f; - bool isTransitioning = false; - - static constexpr std::size_t MAX_PARAMS = 8; - std::array parameters{}; - std::array, MAX_PARAMS> parameterNames{}; - std::size_t parameterCount = 0; - - AnimationStateMachine() = default; - explicit AnimationStateMachine(Animation::AnimationGraphId graph) - : graphId(graph) {} - - void SetParameter(const char* name, float value) { - for (std::size_t i = 0; i < parameterCount; ++i) { - if (std::strncmp(parameterNames[i].data(), name, 31) == 0) { - parameters[i] = value; - return; - } - } - if (parameterCount < MAX_PARAMS) { - std::strncpy(parameterNames[parameterCount].data(), name, 31); - parameterNames[parameterCount][31] = '\0'; - parameters[parameterCount] = value; - parameterCount++; - } - } - - float GetParameter(const char* name) const { - for (std::size_t i = 0; i < parameterCount; ++i) { - if (std::strncmp(parameterNames[i].data(), name, 31) == 0) { - return parameters[i]; - } - } - return 0.0f; - } - }; - - struct AnimatedSprite : public IComponent { - bool needsUpdate = true; - - AnimatedSprite() = default; - }; - - struct VisualEffect : public IComponent { - Animation::EffectType type = Animation::EffectType::EXPLOSION_SMALL; - float lifetime = 0.0f; - float maxLifetime = 1.0f; - Entity owner = NULL_ENTITY; - float offsetX = 0.0f; - float offsetY = 0.0f; - - VisualEffect() = default; - VisualEffect(Animation::EffectType t, float duration) - : type(t), maxLifetime(duration) {} - VisualEffect(Animation::EffectType t, float duration, Entity ownerEntity, float offX, float offY) - : type(t), maxLifetime(duration), owner(ownerEntity), offsetX(offX), offsetY(offY) {} - }; - - struct FloatingText : public IComponent { - char text[32] = {}; - float lifetime = 0.0f; - float maxLifetime = 1.5f; - float velocityY = -50.0f; - float fadeStartTime = 0.5f; - Math::Color color{1.0f, 1.0f, 1.0f, 1.0f}; - - FloatingText() = default; - FloatingText(const char* txt, float duration, const Math::Color& col) - : maxLifetime(duration), color(col) { - if (txt) { - std::strncpy(text, txt, 31); - text[31] = '\0'; - } - } - }; - - struct AnimationEvents : public IComponent { - static constexpr std::size_t MAX_EVENTS = 4; - std::array, MAX_EVENTS> eventNames{}; - std::size_t eventCount = 0; - - AnimationEvents() = default; - - void PushEvent(const char* name) { - if (eventCount < MAX_EVENTS && name) { - std::strncpy(eventNames[eventCount].data(), name, 31); - eventNames[eventCount][31] = '\0'; - eventCount++; - } - } - - void Clear() { eventCount = 0; } - - bool HasEvent(const char* name) const { - for (std::size_t i = 0; i < eventCount; ++i) { - if (std::strncmp(eventNames[i].data(), name, 31) == 0) { - return true; - } - } - return false; - } - }; - - struct AnimationLayer : public IComponent { - Animation::AnimationClipId clipId = Animation::INVALID_CLIP_ID; - float currentTime = 0.0f; - float weight = 1.0f; - float playbackSpeed = 1.0f; - bool additive = false; - int layerIndex = 0; - - AnimationLayer() = default; - AnimationLayer(Animation::AnimationClipId clip, int layer, float w = 1.0f) - : clipId(clip), weight(w), layerIndex(layer) {} - }; - - struct PowerUpGlow : public IComponent { - float time = 0.0f; - float pulseSpeed = 2.0f; - float minAlpha = 0.7f; - float maxAlpha = 1.0f; - float baseScale = 2.5f; - float scalePulse = 0.08f; - - PowerUpGlow() = default; - }; - } - -} + }; + + struct Player : public IComponent { + uint8_t playerNumber = 0; + uint64_t playerHash = 0; + bool isLocalPlayer = false; + uint8_t lives = 3; + + Player() = default; + Player(uint8_t number, uint64_t hash, bool local = false, uint8_t startLives = 3) + : playerNumber(number), playerHash(hash), isLocalPlayer(local), lives(startLives) {} + }; + + enum class EnemyType : uint8_t { + BASIC = 0, + FAST = 1, + TANK = 2, + BOSS = 3, + FORMATION = 4 + }; + + struct Enemy : public IComponent { + EnemyType type = EnemyType::BASIC; + uint32_t id = 0; + + Enemy() = default; + Enemy(EnemyType enemyType, uint32_t enemyId = 0) + : type(enemyType), id(enemyId) {} + }; + + struct Boss : public IComponent { + uint8_t bossId = 1; + + Boss() = default; + Boss(uint8_t id) : bossId(id) {} + }; + + struct BossKilled : public IComponent { + Entity bossEntity; + int levelNumber; + float timeSinceDeath = 0.0f; + + BossKilled() = default; + BossKilled(Entity boss, int level) + : bossEntity(boss), levelNumber(level) {} + }; + + enum class BossAttackPattern { + IDLE = 0, + // Boss 1 + FAN_SPRAY = 1, + DIRECT_SHOT = 2, + CIRCLE = 3, + BLACK_ORB = 4, + THIRD_BULLET = 5, + // Boss 2 + SPIRAL_WAVE = 6, + ANIMATED_ORB = 7, + LASER_BEAM = 8 + }; + + struct BossAttack : public IComponent { + float attackCooldown = 3.0f; + float timeSinceLastAttack = 0.0f; + BossAttackPattern currentPattern = BossAttackPattern::FAN_SPRAY; + + BossAttack() = default; + BossAttack(float cooldown) : attackCooldown(cooldown) {} + }; + + struct BossBullet : public IComponent { + BossBullet() = default; + }; + + struct WaveAttack : public IComponent { + WaveAttack() = default; + }; + + struct SecondAttack : public IComponent { + SecondAttack() = default; + }; + + struct FireBullet : public IComponent { + FireBullet() = default; + }; + + struct Mine : public IComponent { + float proximityRadius = 80.0f; + float explosionRadius = 100.0f; + float lifeTime = 10.0f; + float timer = 0.0f; + bool isExploding = false; + float explosionTimer = 0.0f; + + Mine() = default; + Mine(float proximity, float explosion, float life) + : proximityRadius(proximity), explosionRadius(explosion), lifeTime(life) {} + }; + + struct BossMovementPattern : public IComponent { + float timer = 0.0f; + float amplitudeY = 200.0f; + float amplitudeX = 80.0f; + float frequencyY = 0.5f; + float frequencyX = 0.3f; + float centerY = 0.0f; + float centerX = 0.0f; + + BossMovementPattern() = default; + BossMovementPattern(float ampY, float ampX, float freqY, float freqX, float centerYPos, float centerXPos) + : amplitudeY(ampY), amplitudeX(ampX), frequencyY(freqY), frequencyX(freqX), centerY(centerYPos), centerX(centerXPos) {} + }; + + struct BlackOrb : public IComponent { + float attractionRadius = 200.0f; + float absorptionRadius = 30.0f; + float attractionForce = 500.0f; + bool isActive = true; + + BlackOrb() = default; + BlackOrb(float attraction, float absorption, float force) + : attractionRadius(attraction), absorptionRadius(absorption), attractionForce(force) {} + }; + + struct ThirdBullet : public IComponent { + float spawnInterval = 0.3f; + float timeSinceSpawn = 0.0f; + int damage = 50; + bool isActive = true; + + ThirdBullet() = default; + ThirdBullet(float interval, int dmg) + : spawnInterval(interval), damage(dmg) {} + }; + + struct EnemyKilled : public IComponent { + uint32_t enemyId = 0; + Entity killedBy = NULL_ENTITY; + + EnemyKilled() = default; + EnemyKilled(uint32_t id, Entity killer = NULL_ENTITY) + : enemyId(id), killedBy(killer) {} + }; + + struct ObstacleVisual : public IComponent { + }; + + struct ObstacleMetadata : public IComponent { + uint32_t uniqueId = 0; + Entity visualEntity = NULL_ENTITY; + float offsetX = 0.0f; + float offsetY = 0.0f; + + ObstacleMetadata() = default; + ObstacleMetadata(uint32_t id, + Entity visual = NULL_ENTITY, + float offsetX = 0.0f, + float offsetY = 0.0f) + : uniqueId(id), + visualEntity(visual), + offsetX(offsetX), + offsetY(offsetY) {} + }; + + // Powerup system components + enum class PowerUpType : uint8_t { + FIRE_RATE_BOOST = 0, + SPREAD_SHOT = 1, + LASER_BEAM = 2, + FORCE_POD = 3, + SPEED_BOOST = 4, + SHIELD = 5 + }; + + struct PowerUp : public IComponent { + PowerUpType type = PowerUpType::FIRE_RATE_BOOST; + uint32_t id = 0; + + PowerUp() = default; + PowerUp(PowerUpType powerupType, uint32_t powerupId = 0) + : type(powerupType), id(powerupId) {} + }; + + struct ActivePowerUps : public IComponent { + bool hasFireRateBoost = false; + bool hasSpreadShot = false; + bool hasLaserBeam = false; + bool hasShield = false; + float speedMultiplier = 1.0f; + + ActivePowerUps() = default; + }; + + enum class WeaponType : uint8_t { + STANDARD = 0, + SPREAD = 1, + LASER = 2 + }; + + struct WeaponSlot : public IComponent { + WeaponType type = WeaponType::STANDARD; + float fireRate = 0.2f; + float cooldown = 0.0f; + int damage = 25; + bool enabled = true; + + WeaponSlot() = default; + WeaponSlot(WeaponType weaponType, float rate, int dmg) + : type(weaponType), fireRate(rate), damage(dmg) {} + }; + + struct ForcePod : public IComponent { + Entity owner = NULL_ENTITY; + float offsetX = -60.0f; + float offsetY = 0.0f; + bool isAttached = true; + + ForcePod() = default; + ForcePod(Entity ownerEntity, float oX = -60.0f, float oY = 0.0f) + : owner(ownerEntity), offsetX(oX), offsetY(oY) {} + }; + + struct Shield : public IComponent { + float duration = 0.0f; // 0 = permanent (until death) + float timeRemaining = 0.0f; + + Shield() = default; + Shield(float dur = 0.0f) : duration(dur), timeRemaining(dur) {} + }; + + struct PowerUpGlow : public IComponent { + float time = 0.0f; + float pulseSpeed = 2.0f; + float minAlpha = 0.7f; + float maxAlpha = 1.0f; + float baseScale = 2.5f; + float scalePulse = 0.08f; + + PowerUpGlow() = default; + }; + +} // namespace ECS +} // namespace RType From bb33b47d046150785eae33aec284dec37c8da8b4 Mon Sep 17 00:00:00 2001 From: erwan Date: Mon, 2 Feb 2026 00:22:03 +0100 Subject: [PATCH 4/7] chore: removed all the old file architeture --- CMakeLists.txt | 29 +- engine/include/ECS/BossAttackSystem.hpp | 39 - engine/include/ECS/BossSystem.hpp | 27 - .../ECS/BulletCollisionResponseSystem.hpp | 36 - engine/include/ECS/EnemySystem.hpp | 34 - engine/include/ECS/ForcePodSystem.hpp | 25 - engine/include/ECS/MineSystem.hpp | 31 - .../ECS/ObstacleCollisionResponseSystem.hpp | 27 - engine/include/ECS/PlayerFactory.hpp | 24 - engine/include/ECS/PlayerSystem.hpp | 28 - engine/include/ECS/PowerUpCollisionSystem.hpp | 34 - engine/include/ECS/PowerUpFactory.hpp | 59 -- engine/include/ECS/PowerUpSpawnSystem.hpp | 48 -- engine/include/ECS/ThirdBulletSystem.hpp | 28 - engine/src/ECS/BossSystem.cpp | 104 --- .../src/ECS/BulletCollisionResponseSystem.cpp | 189 ---- engine/src/ECS/CMakeLists.txt | 81 +- engine/src/ECS/EffectFactory.cpp | 410 --------- engine/src/ECS/ForcePodSystem.cpp | 62 -- engine/src/ECS/LevelLoader.cpp | 578 ------------- engine/src/ECS/MineSystem.cpp | 101 --- .../ECS/ObstacleCollisionResponseSystem.cpp | 149 ---- .../src/ECS/PlayerCollisionResponseSystem.cpp | 324 ------- engine/src/ECS/PlayerFactory.cpp | 95 --- engine/src/ECS/PlayerSystem.cpp | 56 -- engine/src/ECS/ShootingSystem.cpp | 276 ------ games/rtype/CMakeLists.txt | 65 ++ games/rtype/client/include/GameState.hpp | 22 +- .../include/editor/EditorColliderManager.hpp | 2 +- .../client/include/editor/EditorDrawing.hpp | 2 +- .../include/editor/EditorFileManager.hpp | 2 +- .../client/include/editor/EditorTypes.hpp | 2 +- games/rtype/client/src/game/GameStateInit.cpp | 2 +- .../client/src/game/GameStateNetwork.cpp | 2 +- .../rtype/include}/BlackOrbSystem.hpp | 4 +- .../rtype/include}/BossAttackSystem.hpp | 4 +- .../rtype/include}/BossSystem.hpp | 4 +- .../BulletCollisionResponseSystem.hpp | 6 +- .../rtype/include}/EffectFactory.hpp | 2 +- .../rtype/include}/EnemyFactory.hpp | 4 +- .../rtype/include}/EnemySystem.hpp | 2 +- .../rtype/include}/ForcePodSystem.hpp | 2 +- .../rtype/include}/LevelLoader.hpp | 4 +- .../rtype/include}/MineSystem.hpp | 4 +- .../ObstacleCollisionResponseSystem.hpp | 6 +- .../PlayerCollisionResponseSystem.hpp | 6 +- .../rtype/include}/PlayerFactory.hpp | 4 +- .../rtype/include}/PlayerSystem.hpp | 2 +- .../rtype/include}/PowerUpCollisionSystem.hpp | 6 +- .../rtype/include}/PowerUpFactory.hpp | 4 +- .../rtype/include}/PowerUpSpawnSystem.hpp | 2 +- .../rtype/include}/ShieldSystem.hpp | 2 +- .../rtype/include}/ShootingSystem.hpp | 6 +- .../rtype/include}/ThirdBulletSystem.hpp | 4 +- .../rtype/systems}/BlackOrbSystem.cpp | 2 +- .../rtype/systems}/BossAttackSystem.cpp | 2 +- .../rtype/systems}/BossSystem.cpp | 2 +- .../BulletCollisionResponseSystem.cpp | 4 +- .../rtype/systems}/EffectFactory.cpp | 2 +- .../rtype/systems}/EnemyFactory.cpp | 2 +- .../rtype/systems}/EnemySystem.cpp | 4 +- .../rtype/systems}/ForcePodSystem.cpp | 2 +- .../rtype/systems}/LevelLoader.cpp | 4 +- .../rtype/systems}/MineSystem.cpp | 2 +- .../ObstacleCollisionResponseSystem.cpp | 2 +- .../PlayerCollisionResponseSystem.cpp | 2 +- .../rtype/systems}/PlayerFactory.cpp | 2 +- .../rtype/systems}/PlayerSystem.cpp | 4 +- .../rtype/systems}/PowerUpCollisionSystem.cpp | 4 +- .../rtype/systems}/PowerUpFactory.cpp | 4 +- .../rtype/systems}/PowerUpSpawnSystem.cpp | 6 +- .../rtype/systems}/ShieldSystem.cpp | 2 +- .../rtype/systems}/ShootingSystem.cpp | 10 +- .../rtype/systems}/ThirdBulletSystem.cpp | 2 +- libs/engine/CMakeLists.txt | 5 - libs/engine/README.md | 805 ------------------ .../include/Animation/AnimationModule.hpp | 67 -- .../include/Animation/AnimationTypes.hpp | 115 --- libs/engine/include/Animation/IAnimation.hpp | 68 -- libs/engine/include/Audio/IAudio.hpp | 72 -- libs/engine/include/Audio/SFMLAudio.hpp | 63 -- libs/engine/include/Core/ColorFilter.hpp | 20 - libs/engine/include/Core/Engine.hpp | 117 --- libs/engine/include/Core/InputMapping.hpp | 22 - libs/engine/include/Core/Logger.hpp | 111 --- libs/engine/include/Core/Module.hpp | 39 - libs/engine/include/Core/ModuleLoader.hpp | 62 -- libs/engine/include/Core/Platform.hpp | 95 --- libs/engine/include/ECS/AnimationSystem.hpp | 33 - libs/engine/include/ECS/AudioSystem.hpp | 28 - libs/engine/include/ECS/BlackOrbSystem.hpp | 28 - .../include/ECS/CollisionDetectionSystem.hpp | 42 - libs/engine/include/ECS/Component.hpp | 644 -------------- .../include/ECS/Components/Clickable.hpp | 29 - .../include/ECS/Components/TextLabel.hpp | 25 - libs/engine/include/ECS/EffectFactory.hpp | 115 --- libs/engine/include/ECS/EnemyFactory.hpp | 26 - libs/engine/include/ECS/Entity.hpp | 11 - libs/engine/include/ECS/HealthSystem.hpp | 22 - libs/engine/include/ECS/ISystem.hpp | 22 - libs/engine/include/ECS/InputSystem.hpp | 20 - libs/engine/include/ECS/LevelLoader.hpp | 189 ---- libs/engine/include/ECS/MenuSystem.hpp | 32 - libs/engine/include/ECS/MovementSystem.hpp | 20 - .../ECS/PlayerCollisionResponseSystem.hpp | 38 - libs/engine/include/ECS/Registry.hpp | 232 ----- libs/engine/include/ECS/RenderingSystem.hpp | 23 - libs/engine/include/ECS/ScoreSystem.hpp | 18 - libs/engine/include/ECS/ScrollingSystem.hpp | 20 - libs/engine/include/ECS/ShieldSystem.hpp | 25 - libs/engine/include/ECS/ShootingSystem.hpp | 32 - libs/engine/include/ECS/SparseArray.hpp | 184 ---- .../include/ECS/TextRenderingSystem.hpp | 21 - libs/engine/include/Math/Types.hpp | 30 - libs/engine/include/Physics/IPhysics.hpp | 52 -- libs/engine/include/Renderer/IRenderer.hpp | 178 ---- libs/engine/include/Renderer/SFMLRenderer.hpp | 105 --- libs/engine/src/Animation/AnimationModule.cpp | 254 ------ libs/engine/src/Animation/CMakeLists.txt | 31 - libs/engine/src/Audio/CMakeLists.txt | 89 -- libs/engine/src/Audio/SFMLAudio.cpp | 219 ----- libs/engine/src/Core/CMakeLists.txt | 44 - libs/engine/src/Core/ColorFilter.cpp | 39 - libs/engine/src/Core/Engine.cpp | 168 ---- libs/engine/src/Core/InputMapping.cpp | 38 - libs/engine/src/Core/ModuleLoader.cpp | 200 ----- libs/engine/src/ECS/AnimationSystem.cpp | 195 ----- libs/engine/src/ECS/AudioSystem.cpp | 66 -- libs/engine/src/ECS/BlackOrbSystem.cpp | 110 --- libs/engine/src/ECS/BossAttackSystem.cpp | 398 --------- libs/engine/src/ECS/CMakeLists.txt | 91 -- .../src/ECS/CollisionDetectionSystem.cpp | 155 ---- libs/engine/src/ECS/EnemyFactory.cpp | 111 --- libs/engine/src/ECS/EnemySystem.cpp | 119 --- libs/engine/src/ECS/HealthSystem.cpp | 45 - libs/engine/src/ECS/InputSystem.cpp | 61 -- libs/engine/src/ECS/MenuSystem.cpp | 73 -- libs/engine/src/ECS/MovementSystem.cpp | 38 - .../engine/src/ECS/PowerUpCollisionSystem.cpp | 76 -- libs/engine/src/ECS/PowerUpFactory.cpp | 265 ------ libs/engine/src/ECS/PowerUpSpawnSystem.cpp | 83 -- libs/engine/src/ECS/Registry.cpp | 80 -- libs/engine/src/ECS/RenderingSystem.cpp | 73 -- libs/engine/src/ECS/ScoreSystem.cpp | 61 -- libs/engine/src/ECS/ScrollingSystem.cpp | 115 --- libs/engine/src/ECS/ShieldSystem.cpp | 75 -- libs/engine/src/ECS/TextRenderingSystem.cpp | 43 - libs/engine/src/ECS/ThirdBulletSystem.cpp | 73 -- libs/engine/src/Renderer/CMakeLists.txt | 114 --- libs/engine/src/Renderer/SFMLRenderer.cpp | 541 ------------ libs/engine/src/main.cpp | 27 - libs/network/CMakeLists.txt | 5 +- libs/network/include/GameServer.hpp | 34 +- libs/network/src/GameServer.cpp | 4 +- tests/test_health_system.cpp | 8 +- tests/test_player_system.cpp | 4 +- 156 files changed, 213 insertions(+), 11052 deletions(-) delete mode 100644 engine/include/ECS/BossAttackSystem.hpp delete mode 100644 engine/include/ECS/BossSystem.hpp delete mode 100644 engine/include/ECS/BulletCollisionResponseSystem.hpp delete mode 100644 engine/include/ECS/EnemySystem.hpp delete mode 100644 engine/include/ECS/ForcePodSystem.hpp delete mode 100644 engine/include/ECS/MineSystem.hpp delete mode 100644 engine/include/ECS/ObstacleCollisionResponseSystem.hpp delete mode 100644 engine/include/ECS/PlayerFactory.hpp delete mode 100644 engine/include/ECS/PlayerSystem.hpp delete mode 100644 engine/include/ECS/PowerUpCollisionSystem.hpp delete mode 100644 engine/include/ECS/PowerUpFactory.hpp delete mode 100644 engine/include/ECS/PowerUpSpawnSystem.hpp delete mode 100644 engine/include/ECS/ThirdBulletSystem.hpp delete mode 100644 engine/src/ECS/BossSystem.cpp delete mode 100644 engine/src/ECS/BulletCollisionResponseSystem.cpp delete mode 100644 engine/src/ECS/EffectFactory.cpp delete mode 100644 engine/src/ECS/ForcePodSystem.cpp delete mode 100644 engine/src/ECS/LevelLoader.cpp delete mode 100644 engine/src/ECS/MineSystem.cpp delete mode 100644 engine/src/ECS/ObstacleCollisionResponseSystem.cpp delete mode 100644 engine/src/ECS/PlayerCollisionResponseSystem.cpp delete mode 100644 engine/src/ECS/PlayerFactory.cpp delete mode 100644 engine/src/ECS/PlayerSystem.cpp delete mode 100644 engine/src/ECS/ShootingSystem.cpp create mode 100644 games/rtype/CMakeLists.txt rename {engine/include/ECS => games/rtype/include}/BlackOrbSystem.hpp (90%) rename {libs/engine/include/ECS => games/rtype/include}/BossAttackSystem.hpp (96%) rename {libs/engine/include/ECS => games/rtype/include}/BossSystem.hpp (89%) rename {libs/engine/include/ECS => games/rtype/include}/BulletCollisionResponseSystem.hpp (91%) rename {engine/include/ECS => games/rtype/include}/EffectFactory.hpp (99%) rename {engine/include/ECS => games/rtype/include}/EnemyFactory.hpp (91%) rename {libs/engine/include/ECS => games/rtype/include}/EnemySystem.hpp (97%) rename {libs/engine/include/ECS => games/rtype/include}/ForcePodSystem.hpp (94%) rename {engine/include/ECS => games/rtype/include}/LevelLoader.hpp (98%) rename {libs/engine/include/ECS => games/rtype/include}/MineSystem.hpp (91%) rename {libs/engine/include/ECS => games/rtype/include}/ObstacleCollisionResponseSystem.hpp (87%) rename {engine/include/ECS => games/rtype/include}/PlayerCollisionResponseSystem.hpp (92%) rename {libs/engine/include/ECS => games/rtype/include}/PlayerFactory.hpp (90%) rename {libs/engine/include/ECS => games/rtype/include}/PlayerSystem.hpp (97%) rename {libs/engine/include/ECS => games/rtype/include}/PowerUpCollisionSystem.hpp (88%) rename {libs/engine/include/ECS => games/rtype/include}/PowerUpFactory.hpp (96%) rename {libs/engine/include/ECS => games/rtype/include}/PowerUpSpawnSystem.hpp (98%) rename {engine/include/ECS => games/rtype/include}/ShieldSystem.hpp (94%) rename {engine/include/ECS => games/rtype/include}/ShootingSystem.hpp (91%) rename {libs/engine/include/ECS => games/rtype/include}/ThirdBulletSystem.hpp (91%) rename {engine/src/ECS => games/rtype/systems}/BlackOrbSystem.cpp (99%) rename {engine/src/ECS => games/rtype/systems}/BossAttackSystem.cpp (99%) rename {libs/engine/src/ECS => games/rtype/systems}/BossSystem.cpp (99%) rename {libs/engine/src/ECS => games/rtype/systems}/BulletCollisionResponseSystem.cpp (98%) rename {libs/engine/src/ECS => games/rtype/systems}/EffectFactory.cpp (99%) rename {engine/src/ECS => games/rtype/systems}/EnemyFactory.cpp (99%) rename {engine/src/ECS => games/rtype/systems}/EnemySystem.cpp (98%) rename {libs/engine/src/ECS => games/rtype/systems}/ForcePodSystem.cpp (98%) rename {libs/engine/src/ECS => games/rtype/systems}/LevelLoader.cpp (99%) rename {libs/engine/src/ECS => games/rtype/systems}/MineSystem.cpp (99%) rename {libs/engine/src/ECS => games/rtype/systems}/ObstacleCollisionResponseSystem.cpp (99%) rename {libs/engine/src/ECS => games/rtype/systems}/PlayerCollisionResponseSystem.cpp (99%) rename {libs/engine/src/ECS => games/rtype/systems}/PlayerFactory.cpp (99%) rename {libs/engine/src/ECS => games/rtype/systems}/PlayerSystem.cpp (96%) rename {engine/src/ECS => games/rtype/systems}/PowerUpCollisionSystem.cpp (96%) rename {engine/src/ECS => games/rtype/systems}/PowerUpFactory.cpp (99%) rename {engine/src/ECS => games/rtype/systems}/PowerUpSpawnSystem.cpp (95%) rename {engine/src/ECS => games/rtype/systems}/ShieldSystem.cpp (98%) rename {libs/engine/src/ECS => games/rtype/systems}/ShootingSystem.cpp (98%) rename {engine/src/ECS => games/rtype/systems}/ThirdBulletSystem.cpp (98%) delete mode 100644 libs/engine/CMakeLists.txt delete mode 100644 libs/engine/README.md delete mode 100644 libs/engine/include/Animation/AnimationModule.hpp delete mode 100644 libs/engine/include/Animation/AnimationTypes.hpp delete mode 100644 libs/engine/include/Animation/IAnimation.hpp delete mode 100644 libs/engine/include/Audio/IAudio.hpp delete mode 100644 libs/engine/include/Audio/SFMLAudio.hpp delete mode 100644 libs/engine/include/Core/ColorFilter.hpp delete mode 100644 libs/engine/include/Core/Engine.hpp delete mode 100644 libs/engine/include/Core/InputMapping.hpp delete mode 100644 libs/engine/include/Core/Logger.hpp delete mode 100644 libs/engine/include/Core/Module.hpp delete mode 100644 libs/engine/include/Core/ModuleLoader.hpp delete mode 100644 libs/engine/include/Core/Platform.hpp delete mode 100644 libs/engine/include/ECS/AnimationSystem.hpp delete mode 100644 libs/engine/include/ECS/AudioSystem.hpp delete mode 100644 libs/engine/include/ECS/BlackOrbSystem.hpp delete mode 100644 libs/engine/include/ECS/CollisionDetectionSystem.hpp delete mode 100644 libs/engine/include/ECS/Component.hpp delete mode 100644 libs/engine/include/ECS/Components/Clickable.hpp delete mode 100644 libs/engine/include/ECS/Components/TextLabel.hpp delete mode 100644 libs/engine/include/ECS/EffectFactory.hpp delete mode 100644 libs/engine/include/ECS/EnemyFactory.hpp delete mode 100644 libs/engine/include/ECS/Entity.hpp delete mode 100644 libs/engine/include/ECS/HealthSystem.hpp delete mode 100644 libs/engine/include/ECS/ISystem.hpp delete mode 100644 libs/engine/include/ECS/InputSystem.hpp delete mode 100644 libs/engine/include/ECS/LevelLoader.hpp delete mode 100644 libs/engine/include/ECS/MenuSystem.hpp delete mode 100644 libs/engine/include/ECS/MovementSystem.hpp delete mode 100644 libs/engine/include/ECS/PlayerCollisionResponseSystem.hpp delete mode 100644 libs/engine/include/ECS/Registry.hpp delete mode 100644 libs/engine/include/ECS/RenderingSystem.hpp delete mode 100644 libs/engine/include/ECS/ScoreSystem.hpp delete mode 100644 libs/engine/include/ECS/ScrollingSystem.hpp delete mode 100644 libs/engine/include/ECS/ShieldSystem.hpp delete mode 100644 libs/engine/include/ECS/ShootingSystem.hpp delete mode 100644 libs/engine/include/ECS/SparseArray.hpp delete mode 100644 libs/engine/include/ECS/TextRenderingSystem.hpp delete mode 100644 libs/engine/include/Math/Types.hpp delete mode 100644 libs/engine/include/Physics/IPhysics.hpp delete mode 100644 libs/engine/include/Renderer/IRenderer.hpp delete mode 100644 libs/engine/include/Renderer/SFMLRenderer.hpp delete mode 100644 libs/engine/src/Animation/AnimationModule.cpp delete mode 100644 libs/engine/src/Animation/CMakeLists.txt delete mode 100644 libs/engine/src/Audio/CMakeLists.txt delete mode 100644 libs/engine/src/Audio/SFMLAudio.cpp delete mode 100644 libs/engine/src/Core/CMakeLists.txt delete mode 100644 libs/engine/src/Core/ColorFilter.cpp delete mode 100644 libs/engine/src/Core/Engine.cpp delete mode 100644 libs/engine/src/Core/InputMapping.cpp delete mode 100644 libs/engine/src/Core/ModuleLoader.cpp delete mode 100644 libs/engine/src/ECS/AnimationSystem.cpp delete mode 100644 libs/engine/src/ECS/AudioSystem.cpp delete mode 100644 libs/engine/src/ECS/BlackOrbSystem.cpp delete mode 100644 libs/engine/src/ECS/BossAttackSystem.cpp delete mode 100644 libs/engine/src/ECS/CMakeLists.txt delete mode 100644 libs/engine/src/ECS/CollisionDetectionSystem.cpp delete mode 100644 libs/engine/src/ECS/EnemyFactory.cpp delete mode 100644 libs/engine/src/ECS/EnemySystem.cpp delete mode 100644 libs/engine/src/ECS/HealthSystem.cpp delete mode 100644 libs/engine/src/ECS/InputSystem.cpp delete mode 100644 libs/engine/src/ECS/MenuSystem.cpp delete mode 100644 libs/engine/src/ECS/MovementSystem.cpp delete mode 100644 libs/engine/src/ECS/PowerUpCollisionSystem.cpp delete mode 100644 libs/engine/src/ECS/PowerUpFactory.cpp delete mode 100644 libs/engine/src/ECS/PowerUpSpawnSystem.cpp delete mode 100644 libs/engine/src/ECS/Registry.cpp delete mode 100644 libs/engine/src/ECS/RenderingSystem.cpp delete mode 100644 libs/engine/src/ECS/ScoreSystem.cpp delete mode 100644 libs/engine/src/ECS/ScrollingSystem.cpp delete mode 100644 libs/engine/src/ECS/ShieldSystem.cpp delete mode 100644 libs/engine/src/ECS/TextRenderingSystem.cpp delete mode 100644 libs/engine/src/ECS/ThirdBulletSystem.cpp delete mode 100644 libs/engine/src/Renderer/CMakeLists.txt delete mode 100644 libs/engine/src/Renderer/SFMLRenderer.cpp delete mode 100644 libs/engine/src/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 06bb9dd..2414740 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,15 +44,18 @@ find_package(Threads REQUIRED) add_subdirectory(engine) # ============================================================================= -# Network Libraries (also generic) +# Games (R-Type game library must be built before network due to dependencies) # ============================================================================= -add_subdirectory(libs/network) + +# R-Type Game (systems library - used by network library) +add_subdirectory(games/rtype) # ============================================================================= -# Games +# Network Libraries (depends on rtype_game for GameServer/GameClient) # ============================================================================= +add_subdirectory(libs/network) -# R-Type Game +# R-Type Server add_executable(r-type_server games/rtype/server/main.cpp) target_link_libraries(r-type_server PRIVATE rtype_network rtype_asio_network) @@ -77,11 +80,13 @@ add_executable(r-type_client ) target_include_directories(r-type_client PRIVATE ${CMAKE_SOURCE_DIR}/games/rtype/client/include + ${CMAKE_SOURCE_DIR}/games/rtype/include ) target_link_libraries(r-type_client PRIVATE rtype_network rtype_asio_network - rtype_ecs + rtype_game + rtype_ecs_core rtype_core rtype_sfml_renderer rtype_sfml_audio @@ -129,28 +134,34 @@ if(TARGET rtype_sfml_renderer) add_executable(test_renderer tests/test_renderer.cpp) target_link_libraries(test_renderer PRIVATE rtype_core - rtype_ecs + rtype_ecs_core ) message(STATUS "test_renderer will be built (loads renderer plugin dynamically)") add_executable(test_player_system tests/test_player_system.cpp) + target_include_directories(test_player_system PRIVATE + ${CMAKE_SOURCE_DIR}/games/rtype/include + ) target_link_libraries(test_player_system PRIVATE rtype_core - rtype_ecs + rtype_game ) message(STATUS "test_player_system will be built (loads renderer plugin dynamically)") add_executable(test_health_system tests/test_health_system.cpp) + target_include_directories(test_health_system PRIVATE + ${CMAKE_SOURCE_DIR}/games/rtype/include + ) target_link_libraries(test_health_system PRIVATE rtype_core - rtype_ecs + rtype_game ) message(STATUS "test_health_system will be built (loads renderer plugin dynamically)") add_executable(test_background tests/test_background.cpp) target_link_libraries(test_background PRIVATE rtype_core - rtype_ecs + rtype_ecs_core rtype_sfml_renderer ) message(STATUS "test_background will be built") diff --git a/engine/include/ECS/BossAttackSystem.hpp b/engine/include/ECS/BossAttackSystem.hpp deleted file mode 100644 index 8cef9ee..0000000 --- a/engine/include/ECS/BossAttackSystem.hpp +++ /dev/null @@ -1,39 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** BossAttackSystem - Handles boss attack patterns -*/ - -#pragma once - -#include "ISystem.hpp" -#include "Registry.hpp" -#include - -namespace RType { - namespace ECS { - - class BossAttackSystem : public ISystem { - public: - BossAttackSystem() = default; - ~BossAttackSystem() override = default; - - const char* GetName() const override { return "BossAttackSystem"; } - - void Update(Registry& registry, float deltaTime) override; - - private: - void CreateFanSpray(Registry& registry, Entity bossEntity, float bossX, float bossY); - void CreateBossBullet(Registry& registry, float x, float y, float angle, float speed); - void CreateBlackOrb(Registry& registry, Entity bossEntity, float bossX, float bossY); - void CreateThirdBullet(Registry& registry, Entity bossEntity, float bossX, float bossY); - - void CreateAnimatedOrb(Registry& registry, Entity bossEntity, float bossX, float bossY); - void CreateSecondAttackSpray(Registry& registry, Entity bossEntity, float bossX, float bossY); - void CreateContinuousFire(Registry& registry, Entity bossEntity, float bossX, float bossY); - void CreateMine(Registry& registry, Entity bossEntity, float bossX, float bossY); - }; - - } -} diff --git a/engine/include/ECS/BossSystem.hpp b/engine/include/ECS/BossSystem.hpp deleted file mode 100644 index ba492a7..0000000 --- a/engine/include/ECS/BossSystem.hpp +++ /dev/null @@ -1,27 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** BossSystem - Handles boss behavior -*/ - -#pragma once - -#include "ISystem.hpp" -#include "Registry.hpp" - -namespace RType { - namespace ECS { - - class BossSystem : public ISystem { - public: - BossSystem() = default; - ~BossSystem() override = default; - - const char* GetName() const override { return "BossSystem"; } - - void Update(Registry& registry, float deltaTime) override; - }; - - } -} diff --git a/engine/include/ECS/BulletCollisionResponseSystem.hpp b/engine/include/ECS/BulletCollisionResponseSystem.hpp deleted file mode 100644 index 0a09137..0000000 --- a/engine/include/ECS/BulletCollisionResponseSystem.hpp +++ /dev/null @@ -1,36 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** BulletCollisionResponseSystem - Handles bullet collision responses -*/ - -#pragma once - -#include "ISystem.hpp" -#include "Registry.hpp" -#include "Component.hpp" - -namespace RType { - namespace ECS { - - class EffectFactory; - - class BulletCollisionResponseSystem : public ISystem { - public: - BulletCollisionResponseSystem() = default; - explicit BulletCollisionResponseSystem(EffectFactory* effectFactory) - : m_effectFactory(effectFactory) {} - ~BulletCollisionResponseSystem() override = default; - - const char* GetName() const override { return "BulletCollisionResponseSystem"; } - void Update(Registry& registry, float deltaTime) override; - - void SetEffectFactory(EffectFactory* effectFactory) { m_effectFactory = effectFactory; } - - private: - EffectFactory* m_effectFactory = nullptr; - }; - - } -} diff --git a/engine/include/ECS/EnemySystem.hpp b/engine/include/ECS/EnemySystem.hpp deleted file mode 100644 index f392589..0000000 --- a/engine/include/ECS/EnemySystem.hpp +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include "ISystem.hpp" -#include "Renderer/IRenderer.hpp" -#include - -namespace RType { - - namespace ECS { - - class EnemySystem : public ISystem { - public: - explicit EnemySystem(Renderer::IRenderer* renderer = nullptr, float screenWidth = 1280.0f, - float screenHeight = 720.0f); - ~EnemySystem() override = default; - - void Update(Registry& registry, float deltaTime) override; - const char* GetName() const override { return "EnemySystem"; } - - void SpawnRandomEnemy(Registry& registry); - static void DestroyEnemiesOffScreen(Registry& registry, float screenWidth); - static void ApplyMovementPattern(Registry& registry, Entity enemy, float /* deltaTime */); - private: - Renderer::IRenderer* m_renderer; - float m_screenWidth; - float m_screenHeight; - float m_spawnTimer = 0.0f; - float m_spawnInterval = 3.0f; - std::mt19937 m_rng; - }; - - } - -} diff --git a/engine/include/ECS/ForcePodSystem.hpp b/engine/include/ECS/ForcePodSystem.hpp deleted file mode 100644 index c827392..0000000 --- a/engine/include/ECS/ForcePodSystem.hpp +++ /dev/null @@ -1,25 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** ForcePodSystem -*/ - -#pragma once - -#include "ISystem.hpp" - -namespace RType { - namespace ECS { - - class ForcePodSystem : public ISystem { - public: - ForcePodSystem() = default; - ~ForcePodSystem() override = default; - - void Update(Registry& registry, float deltaTime) override; - const char* GetName() const override { return "ForcePodSystem"; } - }; - - } -} diff --git a/engine/include/ECS/MineSystem.hpp b/engine/include/ECS/MineSystem.hpp deleted file mode 100644 index 5873b85..0000000 --- a/engine/include/ECS/MineSystem.hpp +++ /dev/null @@ -1,31 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** MineSystem - Handles mine proximity detection and explosions -*/ - -#pragma once - -#include "ISystem.hpp" -#include "Registry.hpp" -#include - -namespace RType { - namespace ECS { - - class MineSystem : public ISystem { - public: - MineSystem() = default; - ~MineSystem() override = default; - - const char* GetName() const override { return "MineSystem"; } - - void Update(Registry& registry, float deltaTime) override; - - private: - float Distance(float x1, float y1, float x2, float y2); - }; - - } -} diff --git a/engine/include/ECS/ObstacleCollisionResponseSystem.hpp b/engine/include/ECS/ObstacleCollisionResponseSystem.hpp deleted file mode 100644 index 080f588..0000000 --- a/engine/include/ECS/ObstacleCollisionResponseSystem.hpp +++ /dev/null @@ -1,27 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** ObstacleCollisionResponseSystem - Handles obstacle collision responses -*/ - -#pragma once - -#include "ISystem.hpp" -#include "Registry.hpp" -#include "Component.hpp" - -namespace RType { - namespace ECS { - - class ObstacleCollisionResponseSystem : public ISystem { - public: - ObstacleCollisionResponseSystem() = default; - ~ObstacleCollisionResponseSystem() override = default; - - const char* GetName() const override { return "ObstacleCollisionResponseSystem"; } - void Update(Registry& registry, float deltaTime) override; - }; - - } -} diff --git a/engine/include/ECS/PlayerFactory.hpp b/engine/include/ECS/PlayerFactory.hpp deleted file mode 100644 index 6ada9f0..0000000 --- a/engine/include/ECS/PlayerFactory.hpp +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include "Registry.hpp" -#include "Component.hpp" -#include "Renderer/IRenderer.hpp" -#include "Math/Types.hpp" -#include - -namespace RType { - - namespace ECS { - - class PlayerFactory { - public: - static Entity CreatePlayer(Registry& registry, uint8_t playerNumber, uint64_t playerHash, - float startX, float startY, Renderer::IRenderer* renderer); - private: - static Math::Color GetPlayerColor(uint8_t playerNumber); - static std::string GetPlayerSpritePath(uint8_t playerNumber); - }; - - } - -} diff --git a/engine/include/ECS/PlayerSystem.hpp b/engine/include/ECS/PlayerSystem.hpp deleted file mode 100644 index d7e3d36..0000000 --- a/engine/include/ECS/PlayerSystem.hpp +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include "ISystem.hpp" -#include "Renderer/IRenderer.hpp" - -namespace RType { - - namespace ECS { - - class PlayerSystem : public ISystem { - public: - explicit PlayerSystem(Renderer::IRenderer* renderer = nullptr); - ~PlayerSystem() override = default; - - void Update(Registry& registry, float deltaTime) override; - const char* GetName() const override { return "PlayerSystem"; } - - static Entity CreatePlayer(Registry& registry, uint8_t playerNumber, uint64_t playerHash, - float startX, float startY, Renderer::IRenderer* renderer); - static void ClampPlayerToScreen(Registry& registry, Entity player, float screenWidth = 1280.0f, - float screenHeight = 720.0f); - private: - Renderer::IRenderer* m_renderer; - }; - - } - -} diff --git a/engine/include/ECS/PowerUpCollisionSystem.hpp b/engine/include/ECS/PowerUpCollisionSystem.hpp deleted file mode 100644 index f6dbc1e..0000000 --- a/engine/include/ECS/PowerUpCollisionSystem.hpp +++ /dev/null @@ -1,34 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** PowerUpCollisionSystem -*/ - -#pragma once - -#include "ISystem.hpp" -#include "CollisionDetectionSystem.hpp" -#include "Renderer/IRenderer.hpp" -#include "../Audio/IAudio.hpp" - -namespace RType { - namespace ECS { - - class PowerUpCollisionSystem : public ISystem { - public: - explicit PowerUpCollisionSystem(Renderer::IRenderer* renderer = nullptr); - ~PowerUpCollisionSystem() override = default; - - void Update(Registry& registry, float deltaTime) override; - const char* GetName() const override { return "PowerUpCollisionSystem"; } - - void SetPowerUpSound(Audio::SoundId id) { m_powerUpSound = id; } - - private: - Renderer::IRenderer* m_renderer; - Audio::SoundId m_powerUpSound = Audio::INVALID_SOUND_ID; - }; - - } -} diff --git a/engine/include/ECS/PowerUpFactory.hpp b/engine/include/ECS/PowerUpFactory.hpp deleted file mode 100644 index 4bced5f..0000000 --- a/engine/include/ECS/PowerUpFactory.hpp +++ /dev/null @@ -1,59 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** PowerUpFactory -*/ - -#pragma once - -#include "Registry.hpp" -#include "Component.hpp" -#include "Renderer/IRenderer.hpp" -#include "Math/Types.hpp" -#include - -namespace RType { - namespace ECS { - class EffectFactory; - } -} - -namespace RType { - namespace ECS { - - class PowerUpFactory { - public: - // Create a powerup entity in the world - static Entity CreatePowerUp( - Registry& registry, - PowerUpType type, - float startX, - float startY, - Renderer::IRenderer* renderer, - const EffectFactory* effectFactory = nullptr - ); - - // Apply powerup effect to player - static void ApplyPowerUpToPlayer( - Registry& registry, - Entity player, - PowerUpType type, - Renderer::IRenderer* renderer - ); - - static Entity CreateForcePod( - Registry& registry, - Entity owner, - Renderer::IRenderer* renderer - ); - - // Get powerup properties - static Math::Color GetPowerUpColor(PowerUpType type); - static const char* GetPowerUpSpritePath(PowerUpType type); - static const char* GetPowerUpName(PowerUpType type); - static float GetPowerUpScale(PowerUpType type); - }; - - } -} diff --git a/engine/include/ECS/PowerUpSpawnSystem.hpp b/engine/include/ECS/PowerUpSpawnSystem.hpp deleted file mode 100644 index 2abeb35..0000000 --- a/engine/include/ECS/PowerUpSpawnSystem.hpp +++ /dev/null @@ -1,48 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** PowerUpSpawnSystem -*/ - -#pragma once - -#include "ISystem.hpp" -#include "Renderer/IRenderer.hpp" -#include - -namespace RType { - namespace ECS { - - class EffectFactory; - - class PowerUpSpawnSystem : public ISystem { - public: - explicit PowerUpSpawnSystem( - Renderer::IRenderer* renderer = nullptr, - float screenWidth = 1280.0f, - float screenHeight = 720.0f - ); - ~PowerUpSpawnSystem() override = default; - - void Update(Registry& registry, float deltaTime) override; - const char* GetName() const override { return "PowerUpSpawnSystem"; } - - void SpawnRandomPowerUp(Registry& registry); - static void DestroyPowerUpsOffScreen(Registry& registry); - - void SetSpawnInterval(float interval) { m_spawnInterval = interval; } - void SetEffectFactory(const EffectFactory* effectFactory) { m_effectFactory = effectFactory; } - - private: - Renderer::IRenderer* m_renderer; - float m_screenWidth; - float m_screenHeight; - float m_spawnTimer = 0.0f; - float m_spawnInterval = 5.0f; - const EffectFactory* m_effectFactory = nullptr; - std::mt19937 m_rng; - }; - - } -} diff --git a/engine/include/ECS/ThirdBulletSystem.hpp b/engine/include/ECS/ThirdBulletSystem.hpp deleted file mode 100644 index 10d8f6a..0000000 --- a/engine/include/ECS/ThirdBulletSystem.hpp +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include "ISystem.hpp" -#include "Registry.hpp" - -namespace RType { - namespace ECS { - - class ThirdBulletSystem : public ISystem { - public: - struct Direction { - float vx; - float vy; - }; - - ThirdBulletSystem() = default; - ~ThirdBulletSystem() override = default; - - const char* GetName() const override { return "ThirdBulletSystem"; } - - void Update(Registry& registry, float deltaTime) override; - - private: - void SpawnSmallProjectile(Registry& registry, float x, float y); - }; - - } -} diff --git a/engine/src/ECS/BossSystem.cpp b/engine/src/ECS/BossSystem.cpp deleted file mode 100644 index 16e0826..0000000 --- a/engine/src/ECS/BossSystem.cpp +++ /dev/null @@ -1,104 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** BossSystem - Handles boss behavior -*/ - -#include "ECS/BossSystem.hpp" -#include "ECS/Component.hpp" -#include "Core/Logger.hpp" -#include - -#ifndef M_PI -#define M_PI 3.14159265358979323846 -#endif - -namespace RType { - namespace ECS { - - - void StopScrollingAndFixPosition(Registry& registry, Entity entity, float fixedX) { - if (!registry.HasComponent(entity)) { - return; - } - - // Remove scrollable component to stop movement - registry.RemoveComponent(entity); - - // Fix position - if (registry.HasComponent(entity)) { - auto& pos = registry.GetComponent(entity); - pos.x = fixedX; - Core::Logger::Info("[BossSystem] Entity {} stopped scrolling, fixed at x={}", - static_cast(entity), fixedX); - } - } - - bool HasEnteredScreen(Registry& registry, Entity entity, float screenWidth) { - if (!registry.HasComponent(entity)) { - return false; - } - - const auto& pos = registry.GetComponent(entity); - return pos.x < screenWidth; - } - - void BossSystem::Update(Registry& registry, float deltaTime) { - // Process all boss entities - auto bosses = registry.GetEntitiesWithComponent(); - - for (auto bossEntity : bosses) { - if (!registry.IsEntityAlive(bossEntity)) { - continue; - } - - // If boss has entered screen and is still scrolling, stop it - if (registry.HasComponent(bossEntity) && - HasEnteredScreen(registry, bossEntity, 1920.0f)) { - StopScrollingAndFixPosition(registry, bossEntity, 900.0f); - } - - if (registry.HasComponent(bossEntity) && - !registry.HasComponent(bossEntity) && - registry.HasComponent(bossEntity)) { - - auto& movement = registry.GetComponent(bossEntity); - auto& pos = registry.GetComponent(bossEntity); - - movement.timer += deltaTime; - - const float pi = 3.14159265358979323846f; - - // Vertical movement - float timeY = movement.timer * movement.frequencyY * 2.0f * pi; - float newY = movement.centerY + movement.amplitudeY * std::sin(static_cast(timeY)); - - // Horizontal oscillation - float timeX = movement.timer * movement.frequencyX * 2.0f * pi; - float newX = movement.centerX + movement.amplitudeX * std::sin(static_cast(timeX * 2.0)); - - const float minY = 100.0f; - const float maxY = 620.0f; - const float minX = 800.0f; - const float maxX = 1000.0f; - - pos.y = std::max(minY, std::min(maxY, newY)); - pos.x = std::max(minX, std::min(maxX, newX)); - } - - if (registry.HasComponent(bossEntity)) { - auto& flash = registry.GetComponent(bossEntity); - if (flash.isActive) { - flash.timeRemaining -= deltaTime; - if (flash.timeRemaining <= 0.0f) { - flash.isActive = false; - flash.timeRemaining = 0.0f; - } - } - } - } - } - - } -} diff --git a/engine/src/ECS/BulletCollisionResponseSystem.cpp b/engine/src/ECS/BulletCollisionResponseSystem.cpp deleted file mode 100644 index f56c3e1..0000000 --- a/engine/src/ECS/BulletCollisionResponseSystem.cpp +++ /dev/null @@ -1,189 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** BulletCollisionResponseSystem implementation -*/ - -#include "ECS/BulletCollisionResponseSystem.hpp" -#include "ECS/EffectFactory.hpp" -#include "Core/Logger.hpp" -#include -#include - -namespace RType { - namespace ECS { - - void BulletCollisionResponseSystem::Update(Registry& registry, float deltaTime) { - auto bullets = registry.GetEntitiesWithComponent(); - - static std::unordered_map lastBeamDamageTime; - static float totalTime = 0.0f; - totalTime += deltaTime; - - auto it = lastBeamDamageTime.begin(); - while (it != lastBeamDamageTime.end()) { - uint64_t key = it->first; - Entity bulletEntity = static_cast(key & 0xFFFFFFFF); - Entity otherEntity = static_cast((key >> 32) & 0xFFFFFFFF); - - if (!registry.IsEntityAlive(bulletEntity) || !registry.IsEntityAlive(otherEntity)) { - it = lastBeamDamageTime.erase(it); - } else { - ++it; - } - } - - for (auto bullet : bullets) { - if (!registry.IsEntityAlive(bullet)) { - continue; - } - - if (!registry.HasComponent(bullet)) { - continue; - } - - auto& event = registry.GetComponent(bullet); - Entity other = event.other; - - if (!registry.IsEntityAlive(other)) { - continue; - } - - bool isBeam = false; - if (registry.HasComponent(bullet)) { - const auto& collider = registry.GetComponent(bullet); - isBeam = (collider.width > 500.0f); - } - - bool hitEnemy = registry.HasComponent(other); - bool hitBoss = registry.HasComponent(other); - bool hitPlayer = registry.HasComponent(other); - bool hitObstacle = registry.HasComponent(other); - - bool isEnemyBullet = false; - if (registry.HasComponent(bullet)) { - const auto& bulletLayer = registry.GetComponent(bullet); - isEnemyBullet = (bulletLayer.layer == CollisionLayers::ENEMY_BULLET); - } - - if (isEnemyBullet && (hitEnemy || hitBoss)) { - continue; - } - - bool shouldApplyDamage = true; - constexpr float BEAM_DAMAGE_TICK_INTERVAL = 0.1f; - - if (isBeam && (hitEnemy || hitBoss)) { - uint64_t damageKey = (static_cast(other) << 32) | static_cast(bullet); - - auto damageIt = lastBeamDamageTime.find(damageKey); - if (damageIt != lastBeamDamageTime.end()) { - float timeSinceLastDamage = totalTime - damageIt->second; - if (timeSinceLastDamage < BEAM_DAMAGE_TICK_INTERVAL) { - shouldApplyDamage = false; - } - } - - if (shouldApplyDamage) { - lastBeamDamageTime[damageKey] = totalTime; - } - } - - // Handle enemy damage - if (hitEnemy && registry.HasComponent(other) && shouldApplyDamage) { - auto& health = registry.GetComponent(other); - const auto& damage = registry.GetComponent(bullet); - - int actualDamage = damage.amount; - if (isBeam) { - actualDamage = damage.amount / 10; - } - - health.current -= actualDamage; - - if (m_effectFactory && registry.HasComponent(bullet) && !isBeam) { - const auto& bulletPos = registry.GetComponent(bullet); - m_effectFactory->CreateHitEffect(registry, bulletPos.x, bulletPos.y); - } - - if (health.current <= 0) { - const auto& enemyComp = registry.GetComponent(other); - const auto& bulletComp = registry.GetComponent(bullet); - registry.AddComponent(other, - EnemyKilled(enemyComp.id, bulletComp.owner)); - if (isBeam) { - uint64_t damageKey = (static_cast(other) << 32) | static_cast(bullet); - lastBeamDamageTime.erase(damageKey); - } - } - } - - // Handle boss damage - if (hitBoss && registry.HasComponent(other) && shouldApplyDamage) { - auto& health = registry.GetComponent(other); - const auto& damage = registry.GetComponent(bullet); - - int actualDamage = damage.amount; - if (isBeam) { - actualDamage = damage.amount / 10; - } - - health.current -= actualDamage; - - if (registry.HasComponent(other)) { - auto& flash = registry.GetComponent(other); - flash.Trigger(); - } - - const auto& bulletComp = registry.GetComponent(bullet); - if (bulletComp.owner != NULL_ENTITY && registry.IsEntityAlive(bulletComp.owner)) { - if (registry.HasComponent(bulletComp.owner)) { - auto& score = registry.GetComponent(bulletComp.owner); - score.points += actualDamage * 5; - } - } - - if (health.current <= 0) { - health.current = 0; - } - } - - if (hitPlayer && registry.HasComponent(other)) { - if (registry.HasComponent(other)) { - continue; - } else { - auto& health = registry.GetComponent(other); - const auto& damage = registry.GetComponent(bullet); - health.current -= damage.amount; - - if (health.current < 0) { - health.current = 0; - } - } - } - - bool shouldDestroy = false; - if (hitObstacle) { - const auto& obstacle = registry.GetComponent(other); - if (obstacle.blocking) { - shouldDestroy = true; - - if (m_effectFactory && registry.HasComponent(bullet) && !isBeam) { - const auto& bulletPos = registry.GetComponent(bullet); - m_effectFactory->CreateHitEffect(registry, bulletPos.x, bulletPos.y); - } - } - } - - if (isBeam) { - } else { - if (hitEnemy || hitBoss || hitPlayer || shouldDestroy) { - registry.DestroyEntity(bullet); - } - } - } - } - - } -} diff --git a/engine/src/ECS/CMakeLists.txt b/engine/src/ECS/CMakeLists.txt index 5aca64c..4cff039 100644 --- a/engine/src/ECS/CMakeLists.txt +++ b/engine/src/ECS/CMakeLists.txt @@ -1,8 +1,7 @@ # ECS CMakeLists.txt # -# This builds two libraries: -# 1. rtype_ecs_core - Generic ECS components and systems (reusable by any game) -# 2. rtype_ecs - Full ECS including R-Type specific code (for backwards compatibility) +# This builds the core ECS library (game-agnostic). +# R-Type specific systems have been moved to games/rtype/ # ============================================================================= # Generic ECS Core Library (game-agnostic) @@ -28,6 +27,13 @@ set(ECS_CORE_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/ISystem.hpp ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/SparseArray.hpp ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/Component.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/Components/IComponent.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/Components/Transform.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/Components/Rendering.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/Components/Physics.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/Components/Audio.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/Components/Animation.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/Components/Gameplay.hpp ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/Components/TextLabel.hpp ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/Components/Clickable.hpp ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/MovementSystem.hpp @@ -60,70 +66,7 @@ set_target_properties(rtype_ecs_core PROPERTIES ) # ============================================================================= -# R-Type Specific ECS Extensions (for R-Type game only) +# Legacy rtype_ecs library (alias to rtype_ecs_core for backwards compatibility) # ============================================================================= -set(RTYPE_ECS_SOURCES - PlayerSystem.cpp - PlayerFactory.cpp - BulletCollisionResponseSystem.cpp - PlayerCollisionResponseSystem.cpp - ObstacleCollisionResponseSystem.cpp - EnemySystem.cpp - EnemyFactory.cpp - ShootingSystem.cpp - LevelLoader.cpp - PowerUpFactory.cpp - PowerUpSpawnSystem.cpp - PowerUpCollisionSystem.cpp - ForcePodSystem.cpp - ShieldSystem.cpp - BossSystem.cpp - BossAttackSystem.cpp - MineSystem.cpp - BlackOrbSystem.cpp - ThirdBulletSystem.cpp - EffectFactory.cpp -) - -set(RTYPE_ECS_HEADERS - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/PlayerSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/PlayerFactory.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/BulletCollisionResponseSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/PlayerCollisionResponseSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/ObstacleCollisionResponseSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/EnemySystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/EnemyFactory.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/ShootingSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/LevelLoader.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/PowerUpFactory.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/PowerUpSpawnSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/PowerUpCollisionSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/ForcePodSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/ShieldSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/BossSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/BossAttackSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/MineSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/BlackOrbSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/ThirdBulletSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/EffectFactory.hpp -) - -# Full rtype_ecs library (includes both core and R-Type specific) -# This maintains backwards compatibility with existing code -add_library(rtype_ecs STATIC - ${ECS_CORE_SOURCES} - ${RTYPE_ECS_SOURCES} - ${ECS_CORE_HEADERS} - ${RTYPE_ECS_HEADERS} -) - -target_include_directories(rtype_ecs PUBLIC - $ - $ -) - -target_link_libraries(rtype_ecs PUBLIC nlohmann_json::nlohmann_json rtype_animation) - -set_target_properties(rtype_ecs PROPERTIES - POSITION_INDEPENDENT_CODE ON -) +# Note: R-Type specific systems are now in games/rtype/systems/ and built as rtype_game +add_library(rtype_ecs ALIAS rtype_ecs_core) diff --git a/engine/src/ECS/EffectFactory.cpp b/engine/src/ECS/EffectFactory.cpp deleted file mode 100644 index 159af8f..0000000 --- a/engine/src/ECS/EffectFactory.cpp +++ /dev/null @@ -1,410 +0,0 @@ -#include "ECS/EffectFactory.hpp" -#include "ECS/Component.hpp" -#include "ECS/Components/TextLabel.hpp" -#include "ECS/Registry.hpp" -#include "Core/Logger.hpp" -#include -#include - -namespace RType { -namespace ECS { - - EffectFactory::EffectFactory(const EffectConfig& config) - : m_config(config) {} - - Entity EffectFactory::CreateBaseEffect(Registry& registry, - float x, float y, - Animation::EffectType type, - float duration, - Entity owner, - float offsetX, - float offsetY) { - Entity entity = registry.CreateEntity(); - - registry.AddComponent(entity, Position(x, y)); - registry.AddComponent(entity, VisualEffect(type, duration, owner, offsetX, offsetY)); - - return entity; - } - - Entity EffectFactory::CreateExplosionSmall(Registry& registry, float x, float y) { - return CreateExplosion(registry, x, y, m_config.explosionSmall, 2.0f, 100); - } - - Entity EffectFactory::CreateExplosionLarge(Registry& registry, float x, float y) { - return CreateExplosion(registry, x, y, m_config.explosionLarge, 3.0f, 100); - } - - Entity EffectFactory::CreateExplosion(Registry& registry, - float x, float y, - Animation::AnimationClipId clipId, - float scale, - int layer) { - Entity entity = CreateBaseEffect(registry, x, y, - Animation::EffectType::EXPLOSION_SMALL, 1.0f); - - if (clipId != Animation::INVALID_CLIP_ID) { - auto& anim = registry.AddComponent(entity, - SpriteAnimation(clipId, false, 1.5f)); - anim.destroyOnComplete = true; - - if (clipId == m_config.explosionSmall && - m_config.explosionFirstFrameRegion.size.x > 0.0f && - m_config.explosionFirstFrameRegion.size.y > 0.0f) { - anim.currentRegion = m_config.explosionFirstFrameRegion; - anim.currentFrameIndex = 0; - } - - auto& animatedSprite = registry.AddComponent(entity); - animatedSprite.needsUpdate = true; - } - - auto& drawable = registry.AddComponent(entity, Drawable()); - drawable.layer = layer; - drawable.scale = Math::Vector2(scale, scale); - if (m_config.effectsSprite != Renderer::INVALID_SPRITE_ID) { - drawable.spriteId = m_config.effectsSprite; - } - - return entity; - } - - Entity EffectFactory::CreateBulletImpact(Registry& registry, float x, float y) { - Entity entity = CreateBaseEffect(registry, x, y, - Animation::EffectType::BULLET_IMPACT, 0.3f); - - if (m_config.bulletImpact != Animation::INVALID_CLIP_ID) { - auto& anim = registry.AddComponent(entity, - SpriteAnimation(m_config.bulletImpact, false, 2.0f)); - anim.destroyOnComplete = true; - registry.AddComponent(entity); - } - - auto& drawable = registry.AddComponent(entity, Drawable()); - drawable.layer = 99; - drawable.scale = Math::Vector2(0.5f, 0.5f); - if (m_config.effectsSprite != Renderer::INVALID_SPRITE_ID) { - drawable.spriteId = m_config.effectsSprite; - } - - return entity; - } - - Entity EffectFactory::CreateDamageNumber(Registry& registry, - float x, float y, - int damage, - const Math::Color& color) { - char buffer[32]; - std::snprintf(buffer, sizeof(buffer), "-%d", damage); - return CreateFloatingText(registry, x, y - 20.0f, buffer, color, 1.5f); - } - - Entity EffectFactory::CreateScorePopup(Registry& registry, - float x, float y, - int score, - const Math::Color& color) { - char buffer[32]; - std::snprintf(buffer, sizeof(buffer), "+%d", score); - return CreateFloatingText(registry, x, y - 30.0f, buffer, color, 2.0f); - } - - Entity EffectFactory::CreateFloatingText(Registry& registry, - float x, float y, - const char* text, - const Math::Color& color, - float duration) { - Entity entity = registry.CreateEntity(); - - registry.AddComponent(entity, Position(x, y)); - registry.AddComponent(entity, FloatingText(text, duration, color)); - - auto& label = registry.AddComponent(entity); - label.text = text ? text : ""; - label.fontId = m_config.damageFont; - label.color = color; - label.centered = true; - - return entity; - } - - Entity EffectFactory::CreatePowerUpEffect(Registry& registry, float x, float y) { - Entity entity = CreateBaseEffect(registry, x, y, - Animation::EffectType::POWER_UP_COLLECT, 0.5f); - - if (m_config.powerUpCollect != Animation::INVALID_CLIP_ID) { - auto& anim = registry.AddComponent(entity, - SpriteAnimation(m_config.powerUpCollect, false, 1.5f)); - anim.destroyOnComplete = true; - registry.AddComponent(entity); - } - - auto& drawable = registry.AddComponent(entity, Drawable()); - drawable.layer = 101; - if (m_config.effectsSprite != Renderer::INVALID_SPRITE_ID) { - drawable.spriteId = m_config.effectsSprite; - } - - return entity; - } - - Entity EffectFactory::CreateBossTransitionEffect(Registry& registry, float x, float y) { - Entity entity = CreateBaseEffect(registry, x, y, - Animation::EffectType::BOSS_PHASE_TRANSITION, 2.0f); - - if (m_config.bossPhaseTransition != Animation::INVALID_CLIP_ID) { - auto& anim = registry.AddComponent(entity, - SpriteAnimation(m_config.bossPhaseTransition, false, 1.0f)); - anim.destroyOnComplete = true; - registry.AddComponent(entity); - } - - auto& drawable = registry.AddComponent(entity, Drawable()); - drawable.layer = 102; - if (m_config.effectsSprite != Renderer::INVALID_SPRITE_ID) { - drawable.spriteId = m_config.effectsSprite; - } - - return entity; - } - - Entity EffectFactory::CreateSpawnEffect(Registry& registry, float x, float y) { - Entity entity = CreateBaseEffect(registry, x, y, - Animation::EffectType::SPAWN_EFFECT, 0.5f); - - if (m_config.spawnEffect != Animation::INVALID_CLIP_ID) { - auto& anim = registry.AddComponent(entity, - SpriteAnimation(m_config.spawnEffect, false, 1.0f)); - anim.destroyOnComplete = true; - registry.AddComponent(entity); - } - - auto& drawable = registry.AddComponent(entity, Drawable()); - drawable.layer = 98; - if (m_config.effectsSprite != Renderer::INVALID_SPRITE_ID) { - drawable.spriteId = m_config.effectsSprite; - } - - return entity; - } - - Entity EffectFactory::CreateDeathEffect(Registry& registry, float x, float y) { - Entity entity = CreateBaseEffect(registry, x, y, - Animation::EffectType::DEATH_EFFECT, 0.8f); - - if (m_config.deathEffect != Animation::INVALID_CLIP_ID) { - auto& anim = registry.AddComponent(entity, - SpriteAnimation(m_config.deathEffect, false, 1.0f)); - anim.destroyOnComplete = true; - registry.AddComponent(entity); - } - - auto& drawable = registry.AddComponent(entity, Drawable()); - drawable.layer = 100; - if (m_config.effectsSprite != Renderer::INVALID_SPRITE_ID) { - drawable.spriteId = m_config.effectsSprite; - } - - return entity; - } - - void EffectFactory::CreateHitMarker(Registry& registry, Entity target, int damage) { - if (!registry.IsEntityAlive(target)) { - return; - } - - if (registry.HasComponent(target)) { - registry.GetComponent(target).Trigger(); - } - - if (registry.HasComponent(target)) { - const auto& pos = registry.GetComponent(target); - CreateDamageNumber(registry, pos.x, pos.y, damage); - } - } - - void EffectFactory::CreateEnemyDeathEffect(Registry& registry, - float x, float y, - int scoreValue) { - CreateExplosionSmall(registry, x, y); - - if (scoreValue > 0) { - CreateScorePopup(registry, x, y, scoreValue); - } - } - - Entity EffectFactory::CreateShootingEffect(Registry& registry, float x, float y, Entity owner) { - if (m_config.shootingAnimation == Animation::INVALID_CLIP_ID) { - return registry.CreateEntity(); - } - - constexpr float SHOOTING_EFFECT_OFFSET_X = 27.0f; - constexpr float SHOOTING_EFFECT_OFFSET_Y = -10.0f; - - float offsetX = SHOOTING_EFFECT_OFFSET_X; - float offsetY = SHOOTING_EFFECT_OFFSET_Y; - - if (owner != NULL_ENTITY && registry.IsEntityAlive(owner) && - registry.HasComponent(owner)) { - const auto& shooterComp = registry.GetComponent(owner); - offsetX = shooterComp.offsetX + SHOOTING_EFFECT_OFFSET_X; - offsetY = shooterComp.offsetY + SHOOTING_EFFECT_OFFSET_Y; - } - - Entity entity = CreateBaseEffect(registry, x + offsetX, y + offsetY, - Animation::EffectType::CUSTOM, 0.4f, owner, offsetX, offsetY); - - Renderer::SpriteId spriteId = Renderer::INVALID_SPRITE_ID; - if (m_config.shootingSprite != Renderer::INVALID_SPRITE_ID) { - spriteId = m_config.shootingSprite; - } else if (m_config.effectsSprite != Renderer::INVALID_SPRITE_ID) { - spriteId = m_config.effectsSprite; - } - - if (spriteId == Renderer::INVALID_SPRITE_ID) { - return entity; - } - - auto& drawable = registry.AddComponent(entity, Drawable()); - drawable.layer = 11; - drawable.scale = Math::Vector2(2.0f, 2.0f); - drawable.origin = Math::Vector2(12.5f, 12.5f); - drawable.spriteId = spriteId; - - auto& anim = registry.AddComponent(entity, - SpriteAnimation(m_config.shootingAnimation, false, 1.5f)); - anim.destroyOnComplete = true; - - if (m_config.shootingFirstFrameRegion.size.x > 0.0f && - m_config.shootingFirstFrameRegion.size.y > 0.0f) { - anim.currentRegion = m_config.shootingFirstFrameRegion; - anim.currentFrameIndex = 0; - } - - auto& animatedSprite = registry.AddComponent(entity); - animatedSprite.needsUpdate = true; - - return entity; - } - - Entity EffectFactory::CreateBeam(Registry& registry, float x, float y, Entity owner, float chargeTime, float screenWidth, float beamHeight) { - if (m_config.beamAnimation == Animation::INVALID_CLIP_ID) { - return registry.CreateEntity(); - } - - Entity entity = registry.CreateEntity(); - - registry.AddComponent(entity, Position(x, y)); - registry.AddComponent(entity, Velocity(0.0f, 0.0f)); - - // Calculate offsets from owner's shooter component - float offsetX = 0.0f; - float offsetY = 0.0f; - if (owner != NULL_ENTITY && registry.IsEntityAlive(owner) && - registry.HasComponent(owner)) { - const auto& shooterComp = registry.GetComponent(owner); - offsetX = shooterComp.offsetX; - offsetY = shooterComp.offsetY; - } - - // Add VisualEffect component to make the beam follow the player - if (owner != NULL_ENTITY) { - registry.AddComponent(entity, - VisualEffect(Animation::EffectType::CUSTOM, 10.0f, owner, offsetX, offsetY)); - } - - float frameWidth = 200.0f; - float frameHeight = beamHeight; - if (m_config.beamFirstFrameRegion.size.x > 0.0f && m_config.beamFirstFrameRegion.size.y > 0.0f) { - frameWidth = m_config.beamFirstFrameRegion.size.x; - frameHeight = m_config.beamFirstFrameRegion.size.y; - } - - float beamWidth = screenWidth - x; - if (beamWidth < frameWidth) { - beamWidth = frameWidth; - } - - float scaleX = beamWidth / frameWidth; - float scaleY = beamHeight / frameHeight; - - if (m_config.beamSprite != Renderer::INVALID_SPRITE_ID) { - auto& drawable = registry.AddComponent(entity, Drawable(m_config.beamSprite, 12)); - drawable.scale = Math::Vector2(scaleX, scaleY); - drawable.origin = Math::Vector2(0.0f, frameHeight * 0.5f); - } - - if (m_config.beamAnimation != Animation::INVALID_CLIP_ID) { - auto& anim = registry.AddComponent(entity, - SpriteAnimation(m_config.beamAnimation, true, 1.0f)); - anim.looping = true; - - if (m_config.beamFirstFrameRegion.size.x > 0.0f && - m_config.beamFirstFrameRegion.size.y > 0.0f) { - anim.currentRegion = m_config.beamFirstFrameRegion; - anim.currentFrameIndex = 0; - } - - auto& animatedSprite = registry.AddComponent(entity); - animatedSprite.needsUpdate = true; - } - - int beamDamage = static_cast(50 + (chargeTime / 2.0f) * 100); - registry.AddComponent(entity, Damage(beamDamage)); - registry.AddComponent(entity, Bullet(owner)); - registry.AddComponent(entity, BoxCollider(beamWidth, frameHeight * scaleY)); - registry.AddComponent(entity, - CollisionLayer(CollisionLayers::PLAYER_BULLET, - CollisionLayers::ENEMY | CollisionLayers::OBSTACLE)); - - return entity; - } - - Entity EffectFactory::CreateHitEffect(Registry& registry, float x, float y) { - if (m_config.hitAnimation == Animation::INVALID_CLIP_ID) { - return registry.CreateEntity(); - } - - Entity entity = CreateBaseEffect(registry, x, y, - Animation::EffectType::BULLET_IMPACT, 0.5f); - - Renderer::SpriteId spriteId = Renderer::INVALID_SPRITE_ID; - if (m_config.hitSprite != Renderer::INVALID_SPRITE_ID) { - spriteId = m_config.hitSprite; - } else if (m_config.effectsSprite != Renderer::INVALID_SPRITE_ID) { - spriteId = m_config.effectsSprite; - } - - if (spriteId == Renderer::INVALID_SPRITE_ID) { - return entity; - } - - auto& drawable = registry.AddComponent(entity, Drawable()); - drawable.layer = 99; - drawable.scale = Math::Vector2(2.0f, 2.0f); - if (m_config.hitFirstFrameRegion.size.x > 0.0f && m_config.hitFirstFrameRegion.size.y > 0.0f) { - drawable.origin = Math::Vector2(m_config.hitFirstFrameRegion.size.x * 0.5f, m_config.hitFirstFrameRegion.size.y * 0.5f); - } else { - drawable.origin = Math::Vector2(0.0f, 0.0f); - } - drawable.spriteId = spriteId; - - if (m_config.hitAnimation != Animation::INVALID_CLIP_ID) { - auto& anim = registry.AddComponent(entity, - SpriteAnimation(m_config.hitAnimation, false, 1.0f)); - anim.destroyOnComplete = true; - - if (m_config.hitFirstFrameRegion.size.x > 0.0f && - m_config.hitFirstFrameRegion.size.y > 0.0f) { - anim.currentRegion = m_config.hitFirstFrameRegion; - anim.currentFrameIndex = 0; - } - - auto& animatedSprite = registry.AddComponent(entity); - animatedSprite.needsUpdate = true; - } - - return entity; - } - -} -} diff --git a/engine/src/ECS/ForcePodSystem.cpp b/engine/src/ECS/ForcePodSystem.cpp deleted file mode 100644 index e730551..0000000 --- a/engine/src/ECS/ForcePodSystem.cpp +++ /dev/null @@ -1,62 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** ForcePodSystem -*/ - -#include "ECS/ForcePodSystem.hpp" -#include "ECS/Component.hpp" - -namespace RType { - namespace ECS { - - void ForcePodSystem::Update(Registry& registry, float deltaTime) { - (void)deltaTime; - - auto forcePods = registry.GetEntitiesWithComponent(); - - for (Entity pod : forcePods) { - if (!registry.IsEntityAlive(pod)) { - continue; - } - - auto& forcePodComp = registry.GetComponent(pod); - - // Check if owner still exists - if (!registry.IsEntityAlive(forcePodComp.owner)) { - // Owner died, destroy force pod - registry.DestroyEntity(pod); - continue; - } - - // Update force pod position to follow owner - if (registry.HasComponent(forcePodComp.owner) && - registry.HasComponent(pod)) { - - const auto& ownerPos = registry.GetComponent(forcePodComp.owner); - auto& podPos = registry.GetComponent(pod); - - // Smooth follow (lerp for smooth movement) - float targetX = ownerPos.x + forcePodComp.offsetX; - float targetY = ownerPos.y + forcePodComp.offsetY; - - float lerpFactor = 0.15f; // Adjust for smoothness - podPos.x += (targetX - podPos.x) * lerpFactor; - podPos.y += (targetY - podPos.y) * lerpFactor; - } - - // Sync shoot command with owner - if (registry.HasComponent(forcePodComp.owner) && - registry.HasComponent(pod)) { - - const auto& ownerShoot = registry.GetComponent(forcePodComp.owner); - auto& podShoot = registry.GetComponent(pod); - - podShoot.wantsToShoot = ownerShoot.wantsToShoot; - } - } - } - - } -} diff --git a/engine/src/ECS/LevelLoader.cpp b/engine/src/ECS/LevelLoader.cpp deleted file mode 100644 index e8fbc57..0000000 --- a/engine/src/ECS/LevelLoader.cpp +++ /dev/null @@ -1,578 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** LevelLoader - JSON-based level loading implementation -*/ - -#include "ECS/LevelLoader.hpp" -#include "ECS/EnemyFactory.hpp" -#include "Core/Logger.hpp" -#include -#include -#include - -using json = nlohmann::json; - -namespace RType { - - namespace ECS { - - LevelData LevelLoader::LoadFromFile(const std::string& path) { - std::string sourcePath = "../" + path; - std::ifstream file(sourcePath); - - if (!file.is_open()) { - file.open(path); - if (!file.is_open()) { - throw std::runtime_error("Failed to open level file: " + path + " (tried: " + sourcePath + " and " + path + ")"); - } - } - - std::string content((std::istreambuf_iterator(file)), - std::istreambuf_iterator()); - return LoadFromString(content); - } - - LevelData LevelLoader::LoadFromString(const std::string& jsonString) { - LevelData level; - - try { - json j = json::parse(jsonString); - - if (j.contains("name")) { - level.name = j["name"].get(); - } - - if (j.contains("assets")) { - const auto& assets = j["assets"]; - - if (assets.contains("textures")) { - for (auto& [key, value] : assets["textures"].items()) { - level.textures[key] = value.get(); - } - } - - if (assets.contains("fonts")) { - for (auto& [key, value] : assets["fonts"].items()) { - FontDef font; - if (value.is_object()) { - font.path = value.value("path", ""); - font.size = value.value("size", 16u); - } else { - font.path = value.get(); - } - level.fonts[key] = font; - } - } - } - - if (j.contains("background")) { - const auto& bg = j["background"]; - level.background.texture = bg.value("texture", ""); - level.background.scrollSpeed = bg.value("scrollSpeed", -150.0f); - level.background.copies = bg.value("copies", 3); - level.background.layer = bg.value("layer", -100); - } - - if (j.contains("obstacles")) { - for (const auto& obs : j["obstacles"]) { - ObstacleDef obstacle; - obstacle.texture = obs.value("texture", ""); - - if (obs.contains("position")) { - obstacle.x = obs["position"].value("x", 0.0f); - obstacle.y = obs["position"].value("y", 0.0f); - } else { - obstacle.x = obs.value("x", 0.0f); - obstacle.y = obs.value("y", 0.0f); - } - - obstacle.scaleWidth = obs.value("scaleWidth", 1200.0f); - obstacle.scaleHeight = obs.value("scaleHeight", 720.0f); - obstacle.scrollSpeed = obs.value("scrollSpeed", -150.0f); - obstacle.layer = obs.value("layer", 1); - - if (obs.contains("colliders")) { - for (const auto& col : obs["colliders"]) { - ColliderDef collider; - collider.x = col.value("x", 0.0f); - collider.y = col.value("y", 0.0f); - collider.width = col.value("width", 0.0f); - collider.height = col.value("height", 0.0f); - obstacle.colliders.push_back(collider); - } - } - - level.obstacles.push_back(obstacle); - } - } - - if (j.contains("enemies")) { - for (const auto& en : j["enemies"]) { - EnemyDef enemy; - enemy.type = en.value("type", "BASIC"); - - if (en.contains("position")) { - enemy.x = en["position"].value("x", 0.0f); - enemy.y = en["position"].value("y", 0.0f); - } else { - enemy.x = en.value("x", 0.0f); - enemy.y = en.value("y", 0.0f); - } - - level.enemies.push_back(enemy); - } - } - - if (j.contains("playerSpawns")) { - for (const auto& spawn : j["playerSpawns"]) { - PlayerSpawnDef ps; - ps.x = spawn.value("x", 100.0f); - ps.y = spawn.value("y", 360.0f); - level.playerSpawns.push_back(ps); - } - } - - if (level.playerSpawns.empty()) { - level.playerSpawns.push_back({100.0f, 200.0f}); - level.playerSpawns.push_back({100.0f, 360.0f}); - level.playerSpawns.push_back({100.0f, 520.0f}); - level.playerSpawns.push_back({100.0f, 680.0f}); - } - - if (j.contains("boss")) { - const auto& bossJson = j["boss"]; - BossDef boss; - boss.texture = bossJson.value("texture", ""); - - if (bossJson.contains("position")) { - boss.x = bossJson["position"].value("x", 0.0f); - boss.y = bossJson["position"].value("y", 0.0f); - } else { - boss.x = bossJson.value("x", 0.0f); - boss.y = bossJson.value("y", 0.0f); - } - - boss.width = bossJson.value("width", 200.0f); - boss.height = bossJson.value("height", 200.0f); - boss.health = bossJson.value("health", 1000); - boss.scrollSpeed = bossJson.value("scrollSpeed", -300.0f); - boss.attackPattern = bossJson.value("attackPattern", 1); - boss.bossId = bossJson.value("bossId", static_cast(1)); - - level.boss = boss; - } - - Core::Logger::Info("Loaded level '{}' with {} obstacles, {} enemies, {} player spawns", - level.name.empty() ? "unnamed" : level.name, - level.obstacles.size(), - level.enemies.size(), - level.playerSpawns.size()); - - } catch (const json::parse_error& e) { - throw std::runtime_error("JSON parse error: " + std::string(e.what())); - } catch (const json::type_error& e) { - throw std::runtime_error("JSON type error: " + std::string(e.what())); - } - - return level; - } - - LoadedAssets LevelLoader::LoadAssets( - const LevelData& level, - Renderer::IRenderer* renderer) { - LoadedAssets assets; - - if (!renderer) { - return assets; - } - - for (const auto& [key, path] : level.textures) { - Renderer::TextureId texId = renderer->LoadTexture("../" + path); - if (texId == Renderer::INVALID_TEXTURE_ID) { - texId = renderer->LoadTexture(path); - } - - if (texId != Renderer::INVALID_TEXTURE_ID) { - assets.textures[key] = texId; - assets.sprites[key] = renderer->CreateSprite(texId, {}); - Core::Logger::Debug("Loaded texture '{}' from '{}'", key, path); - } else { - Core::Logger::Warning("Failed to load texture '{}' from '{}'", key, path); - } - } - - for (const auto& [key, fontDef] : level.fonts) { - Renderer::FontId fontId = renderer->LoadFont("../" + fontDef.path, fontDef.size); - if (fontId == Renderer::INVALID_FONT_ID) { - fontId = renderer->LoadFont(fontDef.path, fontDef.size); - } - - if (fontId != Renderer::INVALID_FONT_ID) { - assets.fonts[key] = fontId; - Core::Logger::Debug("Loaded font '{}' from '{}'", key, fontDef.path); - } else { - Core::Logger::Warning("Failed to load font '{}' from '{}'", key, fontDef.path); - } - } - - return assets; - } - - CreatedEntities LevelLoader::CreateEntities( - Registry& registry, - const LevelData& level, - const LoadedAssets& assets, - Renderer::IRenderer* renderer) { - CreatedEntities entities; - uint32_t obstacleIdCounter = 1; - - CreateBackgrounds(registry, level.background, assets, renderer, entities); - CreateObstacles(registry, level.obstacles, assets, renderer, entities, obstacleIdCounter); - CreateEnemies(registry, level.enemies, assets, renderer, entities); - - Core::Logger::Info("Created {} backgrounds, {} obstacle visuals, {} obstacle colliders, {} enemy entities", - entities.backgrounds.size(), - entities.obstacleVisuals.size(), - entities.obstacleColliders.size(), - entities.enemies.size()); - - return entities; - } - - CreatedEntities LevelLoader::CreateServerEntities( - Registry& registry, - const LevelData& level) { - CreatedEntities entities; - uint32_t obstacleIdCounter = 1; - - CreateServerObstacles(registry, level.obstacles, entities, obstacleIdCounter); - CreateServerEnemies(registry, level.enemies, entities); - CreateServerBoss(registry, level.boss, entities); - - Core::Logger::Info("Created server entities: {} obstacle visuals, {} obstacle colliders, {} enemies, boss: {}", - entities.obstacleVisuals.size(), - entities.obstacleColliders.size(), - entities.enemies.size(), - entities.boss != NULL_ENTITY ? "yes" : "no"); - - return entities; - } - - const std::vector& LevelLoader::GetPlayerSpawns(const LevelData& level) { - return level.playerSpawns; - } - - EnemyType LevelLoader::ParseEnemyType(const std::string& typeStr) { - if (typeStr == "BASIC") - return EnemyType::BASIC; - if (typeStr == "FAST") - return EnemyType::FAST; - if (typeStr == "TANK") - return EnemyType::TANK; - if (typeStr == "BOSS") - return EnemyType::BOSS; - if (typeStr == "FORMATION") - return EnemyType::FORMATION; - - Core::Logger::Warning("Unknown enemy type '{}', defaulting to BASIC", typeStr); - return EnemyType::BASIC; - } - - void LevelLoader::CreateBackgrounds( - Registry& registry, - const BackgroundDef& background, - const LoadedAssets& assets, - Renderer::IRenderer* renderer, - CreatedEntities& entities) { - if (background.texture.empty()) { - return; - } - - auto spriteIt = assets.sprites.find(background.texture); - auto texIt = assets.textures.find(background.texture); - - if (spriteIt == assets.sprites.end() || texIt == assets.textures.end()) { - Core::Logger::Warning("Background texture '{}' not found in loaded assets", background.texture); - return; - } - - Renderer::SpriteId bgSprite = spriteIt->second; - Renderer::TextureId bgTexture = texIt->second; - - Math::Vector2 bgSize = renderer->GetTextureSize(bgTexture); - float scaleX = 1280.0f / bgSize.x; - float scaleY = 720.0f / bgSize.y; - - for (int i = 0; i < background.copies; i++) { - Entity bgEntity = registry.CreateEntity(); - - registry.AddComponent(bgEntity, Position{i * 1280.0f, 0.0f}); - - auto& drawable = registry.AddComponent(bgEntity, Drawable(bgSprite, background.layer)); - drawable.scale = {scaleX, scaleY}; - - registry.AddComponent(bgEntity, Scrollable(background.scrollSpeed)); - - entities.backgrounds.push_back(bgEntity); - } - } - - void LevelLoader::CreateObstacles( - Registry& registry, - const std::vector& obstacles, - const LoadedAssets& assets, - Renderer::IRenderer* renderer, - CreatedEntities& entities, - uint32_t& obstacleIdCounter) { - for (const auto& obs : obstacles) { - auto spriteIt = assets.sprites.find(obs.texture); - auto texIt = assets.textures.find(obs.texture); - - bool hasTexture = (spriteIt != assets.sprites.end() && texIt != assets.textures.end()); - - Entity obsEntity = NULL_ENTITY; - if (hasTexture) { - obsEntity = registry.CreateEntity(); - - registry.AddComponent(obsEntity, Position{obs.x, obs.y}); - - Math::Vector2 obsSize = renderer->GetTextureSize(texIt->second); - auto& drawable = registry.AddComponent(obsEntity, Drawable(spriteIt->second, obs.layer)); - drawable.scale = {obs.scaleWidth / obsSize.x, obs.scaleHeight / obsSize.y}; - drawable.origin = {0.0f, 0.0f}; - - registry.AddComponent(obsEntity, Scrollable(obs.scrollSpeed)); - registry.AddComponent(obsEntity, ObstacleVisual{}); - entities.obstacleVisuals.push_back(obsEntity); - } else { - Core::Logger::Warning("Obstacle texture '{}' not found in loaded assets", obs.texture); - } - - for (const auto& col : obs.colliders) { - Entity colliderEntity = registry.CreateEntity(); - // Store collider position as offset from visual entity (not absolute) - registry.AddComponent(colliderEntity, Position{col.x - obs.x, col.y - obs.y}); - registry.AddComponent(colliderEntity, BoxCollider{col.width, col.height}); - registry.AddComponent(colliderEntity, Scrollable(obs.scrollSpeed)); - registry.AddComponent(colliderEntity, Obstacle(true)); - registry.AddComponent(colliderEntity, - ObstacleMetadata(obstacleIdCounter++, obsEntity, col.x - obs.x, col.y - obs.y)); - registry.AddComponent(colliderEntity, - CollisionLayer(CollisionLayers::OBSTACLE, CollisionLayers::ALL)); - - entities.obstacleColliders.push_back(colliderEntity); - } - } - } - - void LevelLoader::CreateEnemies( - Registry& registry, - const std::vector& enemies, - const LoadedAssets& assets, - Renderer::IRenderer* renderer, - CreatedEntities& entities) { - for (const auto& en : enemies) { - EnemyType type = ParseEnemyType(en.type); - Entity enemy = EnemyFactory::CreateEnemy(registry, type, en.x, en.y, renderer); - entities.enemies.push_back(enemy); - } - } - - void LevelLoader::CreateServerObstacles( - Registry& registry, - const std::vector& obstacles, - CreatedEntities& entities, - uint32_t& obstacleIdCounter) { - for (const auto& obs : obstacles) { - Entity obsEntity = registry.CreateEntity(); - - registry.AddComponent(obsEntity, Position{obs.x, obs.y}); - registry.AddComponent(obsEntity, Scrollable(obs.scrollSpeed)); - registry.AddComponent(obsEntity, ObstacleVisual{}); - entities.obstacleVisuals.push_back(obsEntity); - - for (const auto& col : obs.colliders) { - Entity colliderEntity = registry.CreateEntity(); - // Store collider position as offset from visual entity (not absolute) - registry.AddComponent(colliderEntity, Position{col.x - obs.x, col.y - obs.y}); - registry.AddComponent(colliderEntity, BoxCollider{col.width, col.height}); - registry.AddComponent(colliderEntity, Scrollable(obs.scrollSpeed)); - registry.AddComponent(colliderEntity, Obstacle(true)); - registry.AddComponent(colliderEntity, - ObstacleMetadata(obstacleIdCounter++, obsEntity, col.x - obs.x, col.y - obs.y)); - registry.AddComponent(colliderEntity, - CollisionLayer(CollisionLayers::OBSTACLE, CollisionLayers::ALL)); - - entities.obstacleColliders.push_back(colliderEntity); - } - } - } - - void LevelLoader::CreateServerEnemies( - Registry& registry, - const std::vector& enemies, - CreatedEntities& entities) { - for (const auto& en : enemies) { - EnemyType type = ParseEnemyType(en.type); - Entity enemy = EnemyFactory::CreateEnemy(registry, type, en.x, en.y, nullptr); - entities.enemies.push_back(enemy); - } - } - - void LevelLoader::CreateServerBoss( - Registry& registry, - const std::optional& bossOpt, - CreatedEntities& entities) { - if (!bossOpt.has_value()) { - return; - } - - const BossDef& boss = bossOpt.value(); - - Entity bossEntity = registry.CreateEntity(); - - registry.AddComponent(bossEntity, Boss{boss.bossId}); - - registry.AddComponent(bossEntity, Position{boss.x, boss.y}); - - registry.AddComponent(bossEntity, Velocity{0.0f, 0.0f}); - - registry.AddComponent(bossEntity, Health{boss.health}); - - registry.AddComponent(bossEntity, BoxCollider{boss.width, boss.height}); - - registry.AddComponent(bossEntity, Scrollable{boss.scrollSpeed}); - - auto& bossAttack = registry.AddComponent(bossEntity, BossAttack{0.40f}); - if (boss.bossId == 2) { - bossAttack = registry.AddComponent(bossEntity, BossAttack{1.3f}); - bossAttack.currentPattern = BossAttackPattern::ANIMATED_ORB; - - registry.AddComponent(bossEntity, - BossMovementPattern{150.0f, 60.0f, 0.3f, 0.2f, boss.y, boss.x}); - } else if (boss.bossId == 3) { - bossAttack = registry.AddComponent(bossEntity, BossAttack{3.0f}); - bossAttack.currentPattern = static_cast(boss.attackPattern); - } else { - bossAttack.currentPattern = BossAttackPattern::FAN_SPRAY; - - registry.AddComponent(bossEntity, - BossMovementPattern{180.0f, 120.0f, 0.4f, 0.4f, boss.y, boss.x}); - } - - registry.AddComponent(bossEntity, DamageFlash{0.1f}); - - registry.AddComponent(bossEntity, - CollisionLayer(CollisionLayers::ENEMY, CollisionLayers::PLAYER | CollisionLayers::PLAYER_BULLET)); - - entities.boss = bossEntity; - - Core::Logger::Info("Created boss entity at position ({}, {}) with {} health", - boss.x, boss.y, boss.health); - } - - std::string LevelLoader::SerializeToString(const LevelData& level) { - json j; - - j["name"] = level.name; - - if (!level.textures.empty() || !level.fonts.empty()) { - j["assets"] = json::object(); - - if (!level.textures.empty()) { - j["assets"]["textures"] = json::object(); - for (const auto& [key, path] : level.textures) { - j["assets"]["textures"][key] = path; - } - } - - if (!level.fonts.empty()) { - j["assets"]["fonts"] = json::object(); - for (const auto& [key, font] : level.fonts) { - j["assets"]["fonts"][key] = { - {"path", font.path}, - {"size", font.size} - }; - } - } - } - - j["background"] = { - {"texture", level.background.texture}, - {"scrollSpeed", level.background.scrollSpeed}, - {"copies", level.background.copies}, - {"layer", level.background.layer} - }; - - j["obstacles"] = json::array(); - for (const auto& obs : level.obstacles) { - json obsJson = { - {"texture", obs.texture}, - {"position", {{"x", obs.x}, {"y", obs.y}}}, - {"scaleWidth", obs.scaleWidth}, - {"scaleHeight", obs.scaleHeight}, - {"scrollSpeed", obs.scrollSpeed}, - {"layer", obs.layer} - }; - - if (!obs.colliders.empty()) { - obsJson["colliders"] = json::array(); - for (const auto& col : obs.colliders) { - obsJson["colliders"].push_back({ - {"x", col.x}, - {"y", col.y}, - {"width", col.width}, - {"height", col.height} - }); - } - } - - j["obstacles"].push_back(obsJson); - } - - j["enemies"] = json::array(); - for (const auto& enemy : level.enemies) { - j["enemies"].push_back({ - {"type", enemy.type}, - {"position", {{"x", enemy.x}, {"y", enemy.y}}} - }); - } - - j["playerSpawns"] = json::array(); - for (const auto& spawn : level.playerSpawns) { - j["playerSpawns"].push_back({ - {"x", spawn.x}, - {"y", spawn.y} - }); - } - - return j.dump(4); - } - - void LevelLoader::SaveToFile(const LevelData& level, const std::string& path) { - try { - std::string jsonString = SerializeToString(level); - - std::ofstream file(path); - if (!file.is_open()) { - throw std::runtime_error("Failed to open file for writing: " + path); - } - - file << jsonString; - file.close(); - - Core::Logger::Info("Successfully saved level '{}' to {}", - level.name.empty() ? "unnamed" : level.name, - path); - - } catch (const std::exception& e) { - Core::Logger::Error("Failed to save level: {}", e.what()); - throw; - } - } - - } - -} diff --git a/engine/src/ECS/MineSystem.cpp b/engine/src/ECS/MineSystem.cpp deleted file mode 100644 index ec0cd4c..0000000 --- a/engine/src/ECS/MineSystem.cpp +++ /dev/null @@ -1,101 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** MineSystem - Handles mine proximity detection and explosions -*/ - -#include "ECS/MineSystem.hpp" -#include "ECS/Component.hpp" -#include "Core/Logger.hpp" -#include - -namespace RType { - namespace ECS { - - float MineSystem::Distance(float x1, float y1, float x2, float y2) { - float dx = x2 - x1; - float dy = y2 - y1; - return std::sqrt(dx * dx + dy * dy); - } - - void MineSystem::Update(Registry& registry, float deltaTime) { - auto mines = registry.GetEntitiesWithComponent(); - - for (auto mineEntity : mines) { - if (!registry.IsEntityAlive(mineEntity)) { - continue; - } - - auto& mine = registry.GetComponent(mineEntity); - const auto& minePos = registry.GetComponent(mineEntity); - - mine.timer += deltaTime; - - if (mine.timer >= mine.lifeTime) { - Core::Logger::Debug("[MineSystem] Mine {} expired", static_cast(mineEntity)); - registry.DestroyEntity(mineEntity); - continue; - } - - if (mine.isExploding) { - mine.explosionTimer += deltaTime; - if (mine.explosionTimer >= 0.6f) { - Core::Logger::Debug("[MineSystem] Mine {} explosion finished", static_cast(mineEntity)); - registry.DestroyEntity(mineEntity); - } - continue; - } - - auto players = registry.GetEntitiesWithComponent(); - for (auto playerEntity : players) { - if (!registry.IsEntityAlive(playerEntity)) { - continue; - } - - if (!registry.HasComponent(playerEntity)) { - continue; - } - - const auto& playerPos = registry.GetComponent(playerEntity); - float distance = Distance(minePos.x, minePos.y, playerPos.x, playerPos.y); - - if (distance <= mine.proximityRadius) { - mine.isExploding = true; - mine.explosionTimer = 0.0f; - - Core::Logger::Info("[MineSystem] Mine {} triggered by player at distance {}", - static_cast(mineEntity), distance); - - for (auto targetPlayer : players) { - if (!registry.IsEntityAlive(targetPlayer)) { - continue; - } - - if (!registry.HasComponent(targetPlayer)) { - continue; - } - - const auto& targetPos = registry.GetComponent(targetPlayer); - float explosionDist = Distance(minePos.x, minePos.y, targetPos.x, targetPos.y); - - if (explosionDist <= mine.explosionRadius) { - if (registry.HasComponent(targetPlayer)) { - auto& health = registry.GetComponent(targetPlayer); - int damage = registry.HasComponent(mineEntity) ? - registry.GetComponent(mineEntity).amount : 30; - health.current -= damage; - - Core::Logger::Info("[MineSystem] Player {} took {} damage from mine explosion", - static_cast(targetPlayer), damage); - } - } - } - break; - } - } - } - } - - } -} diff --git a/engine/src/ECS/ObstacleCollisionResponseSystem.cpp b/engine/src/ECS/ObstacleCollisionResponseSystem.cpp deleted file mode 100644 index be288e0..0000000 --- a/engine/src/ECS/ObstacleCollisionResponseSystem.cpp +++ /dev/null @@ -1,149 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** ObstacleCollisionResponseSystem implementation -*/ - -#include "ECS/ObstacleCollisionResponseSystem.hpp" -#include -#include - -namespace RType { - namespace ECS { - - void ObstacleCollisionResponseSystem::Update(Registry& registry, float deltaTime) { - auto obstacles = registry.GetEntitiesWithComponent(); - - for (auto obstacle : obstacles) { - if (!registry.IsEntityAlive(obstacle)) { - continue; - } - - if (!registry.HasComponent(obstacle)) { - continue; - } - - const auto& obstacleComp = registry.GetComponent(obstacle); - if (!obstacleComp.blocking) { - continue; - } - - auto& event = registry.GetComponent(obstacle); - Entity other = event.other; - - if (!registry.IsEntityAlive(other)) { - continue; - } - - // Handle enemy-obstacle collisions (enemies take damage and are blocked) - if (registry.HasComponent(other)) { - // Apply damage to enemy - if (registry.HasComponent(other)) { - auto& enemyHealth = registry.GetComponent(other); - // Obstacles deal damage to enemies - constexpr int OBSTACLE_DAMAGE = 50; - enemyHealth.current -= OBSTACLE_DAMAGE; - - if (enemyHealth.current <= 0) { - enemyHealth.current = 0; - // Enemy will be destroyed by HealthSystem - } - } - - if (registry.HasComponent(other) && - registry.HasComponent(obstacle)) { - - bool resolved = false; - const bool hasEnemyBox = registry.HasComponent(other); - const bool hasObstacleBox = registry.HasComponent(obstacle); - - if (hasEnemyBox && hasObstacleBox) { - auto& enemyPosRef = registry.GetComponent(other); - const auto& obstaclePos = registry.GetComponent(obstacle); - const auto& enemyBox = registry.GetComponent(other); - const auto& obstacleBox = registry.GetComponent(obstacle); - - float enemyLeft = enemyPosRef.x; - float enemyRight = enemyPosRef.x + enemyBox.width; - float enemyTop = enemyPosRef.y; - float enemyBottom = enemyPosRef.y + enemyBox.height; - - float obstacleLeft = obstaclePos.x; - float obstacleRight = obstaclePos.x + obstacleBox.width; - float obstacleTop = obstaclePos.y; - float obstacleBottom = obstaclePos.y + obstacleBox.height; - - float penRight = obstacleRight - enemyLeft; - float penLeft = enemyRight - obstacleLeft; - float penBottom = obstacleBottom - enemyTop; - float penTop = enemyBottom - obstacleTop; - - if (penLeft > 0.0f && penRight > 0.0f && - penTop > 0.0f && penBottom > 0.0f) { - const float separationBias = 0.5f; - - if (std::min(penLeft, penRight) < - std::min(penTop, penBottom)) { - if (penLeft < penRight) { - enemyPosRef.x -= penLeft + separationBias; - } else { - enemyPosRef.x += penRight + separationBias; - } - if (registry.HasComponent(other)) { - auto& enemyVel = registry.GetComponent(other); - enemyVel.dx = 0.0f; - } - } else { - if (penTop < penBottom) { - enemyPosRef.y -= penTop + separationBias; - } else { - enemyPosRef.y += penBottom + separationBias; - } - if (registry.HasComponent(other)) { - auto& enemyVel = registry.GetComponent(other); - enemyVel.dy = 0.0f; - } - } - resolved = true; - } - } - - if (!resolved) { - if (registry.HasComponent(other)) { - auto& enemyVel = registry.GetComponent(other); - enemyVel.dx = 0.0f; - enemyVel.dy = 0.0f; - } - - const auto& enemyPos = registry.GetComponent(other); - const auto& obstaclePos = registry.GetComponent(obstacle); - float dx = enemyPos.x - obstaclePos.x; - float dy = enemyPos.y - obstaclePos.y; - float distance = std::sqrt(dx * dx + dy * dy); - - if (distance > 0.0f) { - dx /= distance; - dy /= distance; - - float pushDistance = 5.0f; - if (registry.HasComponent(obstacle)) { - const auto& box = registry.GetComponent(obstacle); - pushDistance = std::max(box.width, box.height) * 0.5f + 10.0f; - } else if (registry.HasComponent(obstacle)) { - const auto& circle = registry.GetComponent(obstacle); - pushDistance = circle.radius + 10.0f; - } - - auto& enemyPosRef = registry.GetComponent(other); - enemyPosRef.x = obstaclePos.x + dx * pushDistance; - enemyPosRef.y = obstaclePos.y + dy * pushDistance; - } - } - } - } - } - } - - } -} diff --git a/engine/src/ECS/PlayerCollisionResponseSystem.cpp b/engine/src/ECS/PlayerCollisionResponseSystem.cpp deleted file mode 100644 index 2e75ab2..0000000 --- a/engine/src/ECS/PlayerCollisionResponseSystem.cpp +++ /dev/null @@ -1,324 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** PlayerCollisionResponseSystem implementation -*/ - -#include "ECS/PlayerCollisionResponseSystem.hpp" -#include -#include -#include - -namespace RType { - namespace ECS { - - void PlayerCollisionResponseSystem::Update(Registry& registry, float deltaTime) { - auto players = registry.GetEntitiesWithComponent(); - - for (auto player : players) { - if (!registry.IsEntityAlive(player)) { - continue; - } - - if (registry.HasComponent(player)) { - auto& invincibility = registry.GetComponent(player); - invincibility.remainingTime -= deltaTime; - if (invincibility.remainingTime <= 0.0f) { - invincibility.remainingTime = 0.0f; - } - } - - if (!registry.HasComponent(player)) { - continue; - } - - auto& event = registry.GetComponent(player); - Entity other = event.other; - - if (!registry.IsEntityAlive(other)) { - continue; - } - - bool hitEnemy = registry.HasComponent(other); - bool hitObstacle = registry.HasComponent(other); - bool hitBoss = registry.HasComponent(other); - - if (hitEnemy) { - if (registry.HasComponent(other) && registry.HasComponent(player)) { - if (!registry.HasComponent(player)) { - const auto& damageComp = registry.GetComponent(other); - auto& playerHealth = registry.GetComponent(player); - - playerHealth.current -= damageComp.amount; - if (playerHealth.current < 0) { - playerHealth.current = 0; - } - } - } - - registry.DestroyEntity(other); - } - - if (hitObstacle) { - const auto& obstacle = registry.GetComponent(other); - - if (obstacle.blocking) { - static int serverCollisionLog = 0; - if (serverCollisionLog < 5) { - if (registry.HasComponent(other) && registry.HasComponent(other)) { - const auto& obstPos = registry.GetComponent(other); - const auto& obstBox = registry.GetComponent(other); - std::cout << "[SERVER PLAYER-OBSTACLE COLLISION] Player entity=" << player - << " vs Obstacle entity=" << other - << " at (" << obstPos.x << "," << obstPos.y << ")" - << " size=(" << obstBox.width << "," << obstBox.height << ")" << std::endl; - serverCollisionLog++; - } - } - bool hasShield = registry.HasComponent(player); - - bool isInvincible = false; - if (registry.HasComponent(player)) { - auto& invincibility = registry.GetComponent(player); - if (invincibility.remainingTime > 0.0f) { - isInvincible = true; - } - } - - if (!hasShield && !isInvincible && registry.HasComponent(player)) { - auto& playerHealth = registry.GetComponent(player); - constexpr int OBSTACLE_DAMAGE = 10; - playerHealth.current -= OBSTACLE_DAMAGE; - - if (playerHealth.current < 0) { - playerHealth.current = 0; - } - - constexpr float INVINCIBILITY_DURATION = 1.0f; - if (registry.HasComponent(player)) { - auto& invincibility = registry.GetComponent(player); - invincibility.remainingTime = INVINCIBILITY_DURATION; - } else { - registry.AddComponent(player, Invincibility(INVINCIBILITY_DURATION)); - } - } - - if (registry.HasComponent(player) && - registry.HasComponent(other)) { - - bool resolved = false; - const bool hasPlayerBox = registry.HasComponent(player); - const bool hasObstacleBox = registry.HasComponent(other); - - if (hasPlayerBox && hasObstacleBox) { - auto& playerPosRef = registry.GetComponent(player); - const auto& obstaclePos = registry.GetComponent(other); - const auto& playerBox = registry.GetComponent(player); - const auto& obstacleBox = registry.GetComponent(other); - - float playerLeft = playerPosRef.x; - float playerRight = playerPosRef.x + playerBox.width; - float playerTop = playerPosRef.y; - float playerBottom = playerPosRef.y + playerBox.height; - - float obstacleLeft = obstaclePos.x; - float obstacleRight = obstaclePos.x + obstacleBox.width; - float obstacleTop = obstaclePos.y; - float obstacleBottom = obstaclePos.y + obstacleBox.height; - - float penetrationRight = obstacleRight - playerLeft; - float penetrationLeft = playerRight - obstacleLeft; - float penetrationBottom = obstacleBottom - playerTop; - float penetrationTop = playerBottom - obstacleTop; - - if (penetrationLeft > 0.0f && penetrationRight > 0.0f && - penetrationTop > 0.0f && penetrationBottom > 0.0f) { - const float separationBias = 0.5f; - - if (std::min(penetrationLeft, penetrationRight) < - std::min(penetrationTop, penetrationBottom)) { - if (penetrationLeft < penetrationRight) { - playerPosRef.x -= penetrationLeft + separationBias; - } else { - playerPosRef.x += penetrationRight + separationBias; - } - if (registry.HasComponent(player)) { - auto& playerVel = registry.GetComponent(player); - playerVel.dx = 0.0f; - } - } else { - if (penetrationTop < penetrationBottom) { - playerPosRef.y -= penetrationTop + separationBias; - } else { - playerPosRef.y += penetrationBottom + separationBias; - } - if (registry.HasComponent(player)) { - auto& playerVel = registry.GetComponent(player); - playerVel.dy = 0.0f; - } - } - resolved = true; - } - } - - if (!resolved) { - // Fallback to radial push when AABB data is missing - if (registry.HasComponent(player)) { - auto& playerVel = registry.GetComponent(player); - playerVel.dx = 0.0f; - playerVel.dy = 0.0f; - } - - const auto& playerPos = registry.GetComponent(player); - const auto& obstaclePos = registry.GetComponent(other); - - float dx = playerPos.x - obstaclePos.x; - float dy = playerPos.y - obstaclePos.y; - float distance = std::sqrt(dx * dx + dy * dy); - - if (distance > 0.0f) { - dx /= distance; - dy /= distance; - - float pushDistance = 5.0f; - if (registry.HasComponent(other)) { - const auto& box = registry.GetComponent(other); - pushDistance = std::max(box.width, box.height) * 0.5f + 10.0f; - } else if (registry.HasComponent(other)) { - const auto& circle = registry.GetComponent(other); - pushDistance = circle.radius + 10.0f; - } - - auto& playerPosRef = registry.GetComponent(player); - playerPosRef.x = obstaclePos.x + dx * pushDistance; - playerPosRef.y = obstaclePos.y + dy * pushDistance; - } - } - } - } - } - - if (hitBoss) { - bool hasShield = registry.HasComponent(player); - - bool isInvincible = false; - if (registry.HasComponent(player)) { - auto& invincibility = registry.GetComponent(player); - if (invincibility.remainingTime > 0.0f) { - isInvincible = true; - } - } - - if (!hasShield && !isInvincible && registry.HasComponent(player)) { - auto& playerHealth = registry.GetComponent(player); - constexpr int BOSS_COLLISION_DAMAGE = 10; - playerHealth.current -= BOSS_COLLISION_DAMAGE; - - if (playerHealth.current < 0) { - playerHealth.current = 0; - } - - constexpr float INVINCIBILITY_DURATION = 1.0f; - if (registry.HasComponent(player)) { - auto& invincibility = registry.GetComponent(player); - invincibility.remainingTime = INVINCIBILITY_DURATION; - } else { - registry.AddComponent(player, Invincibility(INVINCIBILITY_DURATION)); - } - } - - if (registry.HasComponent(player) && - registry.HasComponent(other)) { - - bool resolved = false; - const bool hasPlayerBox = registry.HasComponent(player); - const bool hasBossBox = registry.HasComponent(other); - - if (hasPlayerBox && hasBossBox) { - auto& playerPosRef = registry.GetComponent(player); - const auto& bossPos = registry.GetComponent(other); - const auto& playerBox = registry.GetComponent(player); - const auto& bossBox = registry.GetComponent(other); - - float playerLeft = playerPosRef.x; - float playerRight = playerPosRef.x + playerBox.width; - float playerTop = playerPosRef.y; - float playerBottom = playerPosRef.y + playerBox.height; - - float bossLeft = bossPos.x; - float bossRight = bossPos.x + bossBox.width; - float bossTop = bossPos.y; - float bossBottom = bossPos.y + bossBox.height; - - float penetrationRight = bossRight - playerLeft; - float penetrationLeft = playerRight - bossLeft; - float penetrationBottom = bossBottom - playerTop; - float penetrationTop = playerBottom - bossTop; - - if (penetrationLeft > 0.0f && penetrationRight > 0.0f && - penetrationTop > 0.0f && penetrationBottom > 0.0f) { - const float separationBias = 2.0f; - - if (std::min(penetrationLeft, penetrationRight) < - std::min(penetrationTop, penetrationBottom)) { - if (penetrationLeft < penetrationRight) { - playerPosRef.x -= penetrationLeft + separationBias; - } else { - playerPosRef.x += penetrationRight + separationBias; - } - if (registry.HasComponent(player)) { - auto& playerVel = registry.GetComponent(player); - playerVel.dx = 0.0f; - } - } else { - if (penetrationTop < penetrationBottom) { - playerPosRef.y -= penetrationTop + separationBias; - } else { - playerPosRef.y += penetrationBottom + separationBias; - } - if (registry.HasComponent(player)) { - auto& playerVel = registry.GetComponent(player); - playerVel.dy = 0.0f; - } - } - resolved = true; - } - } - - if (!resolved) { - if (registry.HasComponent(player)) { - auto& playerVel = registry.GetComponent(player); - playerVel.dx = 0.0f; - playerVel.dy = 0.0f; - } - - const auto& playerPos = registry.GetComponent(player); - const auto& bossPos = registry.GetComponent(other); - - float dx = playerPos.x - bossPos.x; - float dy = playerPos.y - bossPos.y; - float distance = std::sqrt(dx * dx + dy * dy); - - if (distance > 0.0f) { - dx /= distance; - dy /= distance; - - float pushDistance = 50.0f; - if (registry.HasComponent(other)) { - const auto& box = registry.GetComponent(other); - pushDistance = std::max(box.width, box.height) * 0.5f + 20.0f; - } - - auto& playerPosRef = registry.GetComponent(player); - playerPosRef.x = bossPos.x + dx * pushDistance; - playerPosRef.y = bossPos.y + dy * pushDistance; - } - } - } - } - } - } - - } -} diff --git a/engine/src/ECS/PlayerFactory.cpp b/engine/src/ECS/PlayerFactory.cpp deleted file mode 100644 index 113a091..0000000 --- a/engine/src/ECS/PlayerFactory.cpp +++ /dev/null @@ -1,95 +0,0 @@ -#include "ECS/PlayerFactory.hpp" -#include "Core/Logger.hpp" -#include -#include -#include - -namespace RType { - - namespace ECS { - - Entity PlayerFactory::CreatePlayer(Registry& registry, uint8_t playerNumber, uint64_t playerHash, - float startX, float startY, Renderer::IRenderer* renderer) { - Entity player = registry.CreateEntity(); - - float yPos = startY + (playerNumber - 1) * 150.0f; - - registry.AddComponent(player, Position(startX, yPos)); - - registry.AddComponent(player, Velocity(0.0f, 0.0f)); - - registry.AddComponent(player, Player(playerNumber, playerHash, false)); - - registry.AddComponent(player, Controllable(200.0f)); - - registry.AddComponent(player, Health(300, 300)); - registry.AddComponent(player, ScoreValue(0)); - registry.AddComponent(player, ScoreTimer(0.0f)); - registry.AddComponent(player, Shooter(0.2f, 50.0f, 0.0f)); - registry.AddComponent(player, ShootCommand()); - registry.AddComponent(player, BoxCollider(25.0f, 25.0f)); - registry.AddComponent(player, CircleCollider(12.5f)); - registry.AddComponent(player, - CollisionLayer(CollisionLayers::PLAYER, - CollisionLayers::ENEMY | CollisionLayers::ENEMY_BULLET | CollisionLayers::OBSTACLE)); - - if (renderer) { - std::string spritePath = GetPlayerSpritePath(playerNumber); - Renderer::TextureId textureId = renderer->LoadTexture(spritePath); - - if (textureId == Renderer::INVALID_TEXTURE_ID) { - Core::Logger::Warning("Failed to load player texture: {}, using default", spritePath); - textureId = renderer->LoadTexture("../assets/spaceships/nave2.png"); - } - - if (textureId != Renderer::INVALID_TEXTURE_ID) { - Renderer::SpriteId spriteId = renderer->CreateSprite( - textureId, Renderer::Rectangle{{0.0f, 0.0f}, {256.0f, 256.0f}}); - - auto& drawable = registry.AddComponent(player, Drawable(spriteId, 2)); - drawable.tint = GetPlayerColor(playerNumber); - drawable.scale = Math::Vector2(0.5f, 0.5f); - drawable.origin = Math::Vector2(128.0f, 128.0f); - } else { - Core::Logger::Error("Failed to load any player texture for player {}", playerNumber); - } - } - - Core::Logger::Info("Created player #{} at position ({}, {})", playerNumber, startX, yPos); - - return player; - } - - Math::Color PlayerFactory::GetPlayerColor(uint8_t playerNumber) { - switch (playerNumber) { - case 1: - return Math::Color(0.2f, 0.6f, 1.0f, 1.0f); - case 2: - return Math::Color(1.0f, 0.2f, 0.2f, 1.0f); - case 3: - return Math::Color(0.2f, 1.0f, 0.2f, 1.0f); - case 4: - return Math::Color(1.0f, 0.8f, 0.2f, 1.0f); - default: - return Math::Color(1.0f, 1.0f, 1.0f, 1.0f); - } - } - - std::string PlayerFactory::GetPlayerSpritePath(uint8_t playerNumber) { - switch (playerNumber) { - case 1: - return "../assets/spaceships/player_blue.png"; - case 2: - return "../assets/spaceships/player_red.png"; - case 3: - return "../assets/spaceships/player_green.png"; - case 4: - return "../assets/spaceships/nave2.png"; - default: - return "../assets/spaceships/nave2.png"; - } - } - - } - -} diff --git a/engine/src/ECS/PlayerSystem.cpp b/engine/src/ECS/PlayerSystem.cpp deleted file mode 100644 index 2e078b2..0000000 --- a/engine/src/ECS/PlayerSystem.cpp +++ /dev/null @@ -1,56 +0,0 @@ -#include "ECS/PlayerSystem.hpp" -#include "ECS/Component.hpp" -#include "ECS/PlayerFactory.hpp" -#include - -namespace RType { - - namespace ECS { - - PlayerSystem::PlayerSystem(Renderer::IRenderer* renderer) - : m_renderer(renderer) {} - - void PlayerSystem::Update(Registry& registry, float /* deltaTime */) { - auto players = registry.GetEntitiesWithComponent(); - - for (Entity player : players) { - if (!registry.HasComponent(player)) { - continue; - } - - ClampPlayerToScreen(registry, player, 1280.0f, 720.0f); - } - } - - Entity PlayerSystem::CreatePlayer(Registry& registry, uint8_t playerNumber, uint64_t playerHash, - float startX, float startY, Renderer::IRenderer* renderer) { - return PlayerFactory::CreatePlayer(registry, playerNumber, playerHash, startX, startY, renderer); - } - - void PlayerSystem::ClampPlayerToScreen(Registry& registry, Entity player, float screenWidth, - float screenHeight) { - if (!registry.HasComponent(player)) { - return; - } - - auto& pos = registry.GetComponent(player); - - float minX = 0.0f; - float maxX = screenWidth; - float minY = 0.0f; - float maxY = screenHeight; - - if (registry.HasComponent(player)) { - const auto& collider = registry.GetComponent(player); - maxX -= collider.width; - maxY -= collider.height; - } - - pos.x = std::clamp(pos.x, minX, maxX); - pos.y = std::clamp(pos.y, minY, maxY); - } - - - } - -} diff --git a/engine/src/ECS/ShootingSystem.cpp b/engine/src/ECS/ShootingSystem.cpp deleted file mode 100644 index f33f12c..0000000 --- a/engine/src/ECS/ShootingSystem.cpp +++ /dev/null @@ -1,276 +0,0 @@ -#include "../../include/ECS/ScoreSystem.hpp" -#include "../../include/ECS/Component.hpp" -#include "../../include/ECS/ShootingSystem.hpp" -#include "../../include/ECS/RenderingSystem.hpp" -#include "../../include/ECS/EffectFactory.hpp" -#include -#include - -namespace RType { - namespace ECS { - - ShootingSystem::ShootingSystem(Renderer::SpriteId bulletSprite) - : m_bulletSprite(bulletSprite) {} - - void ShootingSystem::Update(Registry& registry, float deltaTime) { - - auto bullets = registry.GetEntitiesWithComponent(); - std::vector bulletsToDestroy; - - for (auto bulletEntity : bullets) { - if (registry.IsEntityAlive(bulletEntity) && registry.HasComponent(bulletEntity)) { - auto& pos = registry.GetComponent(bulletEntity); - if (pos.x > 1380.0f || pos.x < -100.0f || pos.y > 820.0f || pos.y < -100.0f) { - bulletsToDestroy.push_back(bulletEntity); - } - } - } - - for (auto entity : bulletsToDestroy) { - registry.DestroyEntity(entity); - } - - auto shooters = registry.GetEntitiesWithComponent(); - struct BulletSpawn { - float x, y; - Entity shooter; - }; - std::vector spawns; - - for (auto shooterEntity : shooters) { - if (!registry.IsEntityAlive(shooterEntity) || !registry.HasComponent(shooterEntity)) { - continue; - } - - // Skip entities with WeaponSlot - they use special weapons instead of base Shooter - if (registry.HasComponent(shooterEntity)) { - continue; - } - - // CRITICAL FIX: Prevent obstacles from shooting due to entity ID reuse - // Actively clean up contaminated components - if (registry.HasComponent(shooterEntity) || - registry.HasComponent(shooterEntity)) { - std::cerr << "[SHOOTING] BLOCKED obstacle entity " << shooterEntity - << " from shooting (has Shooter=" << registry.HasComponent(shooterEntity) - << " ShootCommand=" << registry.HasComponent(shooterEntity) << ")" << std::endl; - if (registry.HasComponent(shooterEntity)) { - registry.RemoveComponent(shooterEntity); - } - if (registry.HasComponent(shooterEntity)) { - registry.RemoveComponent(shooterEntity); - } - continue; - } - - auto& shooterComp = registry.GetComponent(shooterEntity); - - shooterComp.cooldown = shooterComp.cooldown - deltaTime; - - if (shooterComp.cooldown < 0.0f) { - shooterComp.cooldown = 0.0f; - } - - if (registry.HasComponent(shooterEntity)) { - auto& shootCmd = registry.GetComponent(shooterEntity); - - if (shootCmd.wantsToShoot && shooterComp.cooldown <= 0.0f) { - if (registry.HasComponent(shooterEntity)) { - const auto& positionComp = registry.GetComponent(shooterEntity); - - spawns.push_back({positionComp.x + shooterComp.offsetX, - positionComp.y + shooterComp.offsetY, - shooterEntity}); - - if (m_effectFactory) { - m_effectFactory->CreateShootingEffect(registry, - positionComp.x + shooterComp.offsetX, - positionComp.y + shooterComp.offsetY, - shooterEntity); - } - - shooterComp.cooldown = shooterComp.fireRate; - shootCmd.wantsToShoot = false; - } - } - } - } - - for (const auto& spawn : spawns) { - auto bulletEntity = registry.CreateEntity(); - - // CRITICAL FIX: Clean up obstacle components from entity ID reuse - if (registry.HasComponent(bulletEntity)) { - std::cerr << "[SHOOTING CLEANUP] Removing Obstacle from bullet entity " << bulletEntity << std::endl; - registry.RemoveComponent(bulletEntity); - } - if (registry.HasComponent(bulletEntity)) { - std::cerr << "[SHOOTING CLEANUP] Removing ObstacleMetadata from bullet entity " << bulletEntity << std::endl; - registry.RemoveComponent(bulletEntity); - } - - registry.AddComponent(bulletEntity, Position(spawn.x, spawn.y)); - registry.AddComponent(bulletEntity, Velocity(600.0f, 0.0f)); - registry.AddComponent(bulletEntity, Bullet(spawn.shooter)); - - if (m_bulletSprite != 0) { - auto& d = registry.AddComponent(bulletEntity, Drawable(m_bulletSprite, 2)); - d.scale = {0.1f, 0.1f}; - d.origin = Math::Vector2(128.0f, 128.0f); - } - - registry.AddComponent(bulletEntity, Damage(25)); - registry.AddComponent(bulletEntity, BoxCollider(10.0f, 5.0f)); - - registry.AddComponent(bulletEntity, CircleCollider(5.0f)); - registry.AddComponent(bulletEntity, - CollisionLayer(CollisionLayers::PLAYER_BULLET, - CollisionLayers::ENEMY | CollisionLayers::OBSTACLE)); - - if (m_shootSound != Audio::INVALID_SOUND_ID) { - auto sfx = registry.CreateEntity(); - auto& se = registry.AddComponent(sfx, SoundEffect(m_shootSound, 1.0f)); - se.pitch = 1.0f; - } - } - - auto weaponSlots = registry.GetEntitiesWithComponent(); - - for (auto entity : weaponSlots) { - if (!registry.IsEntityAlive(entity)) continue; - - // CRITICAL FIX: Prevent obstacles from shooting with weapon slots - if (registry.HasComponent(entity) || - registry.HasComponent(entity)) { - std::cerr << "[SHOOTING] BLOCKED obstacle entity " << entity - << " from using weapon slot (has WeaponSlot=" << registry.HasComponent(entity) - << " ShootCommand=" << registry.HasComponent(entity) << ")" << std::endl; - if (registry.HasComponent(entity)) { - registry.RemoveComponent(entity); - } - if (registry.HasComponent(entity)) { - registry.RemoveComponent(entity); - } - continue; - } - - auto& weapon = registry.GetComponent(entity); - if (!weapon.enabled) continue; - - weapon.cooldown -= deltaTime; - if (weapon.cooldown < 0.0f) weapon.cooldown = 0.0f; - - if (registry.HasComponent(entity)) { - auto& shootCmd = registry.GetComponent(entity); - - if (shootCmd.wantsToShoot && weapon.cooldown <= 0.0f) { - if (registry.HasComponent(entity)) { - const auto& pos = registry.GetComponent(entity); - - switch (weapon.type) { - case WeaponType::SPREAD: - CreateSpreadShot(registry, entity, pos, weapon.damage); - break; - case WeaponType::LASER: - CreateLaserShot(registry, entity, pos, weapon.damage); - break; - default: - break; - } - - weapon.cooldown = weapon.fireRate; - shootCmd.wantsToShoot = false; - } - } - } - } - } - - void ShootingSystem::CreateSpreadShot(Registry& registry, Entity shooter, const Position& pos, int damage) { - float offsetX = 50.0f; - float offsetY = 20.0f; - - if (registry.HasComponent(shooter)) { - const auto& shooterComp = registry.GetComponent(shooter); - offsetX = shooterComp.offsetX; - offsetY = shooterComp.offsetY; - } - - if (m_effectFactory) { - m_effectFactory->CreateShootingEffect(registry, pos.x + offsetX, pos.y + offsetY, shooter); - } - - float angles[] = {-15.0f, 0.0f, 15.0f}; // degrees - - for (float angle : angles) { - float radians = angle * 3.14159f / 180.0f; - float vx = 600.0f * std::cos(radians); - float vy = 600.0f * std::sin(radians); - - auto bullet = registry.CreateEntity(); - - // CRITICAL FIX: Clean up obstacle components from entity ID reuse - if (registry.HasComponent(bullet)) { - registry.RemoveComponent(bullet); - } - if (registry.HasComponent(bullet)) { - registry.RemoveComponent(bullet); - } - - registry.AddComponent(bullet, Position(pos.x + offsetX, pos.y + offsetY)); - registry.AddComponent(bullet, Velocity(vx, vy)); - registry.AddComponent(bullet, Bullet(shooter)); - registry.AddComponent(bullet, Damage(damage)); - registry.AddComponent(bullet, BoxCollider(8.0f, 4.0f)); - registry.AddComponent(bullet, - CollisionLayer(CollisionLayers::PLAYER_BULLET, - CollisionLayers::ENEMY | CollisionLayers::OBSTACLE)); - - if (m_bulletSprite != 0) { - auto& d = registry.AddComponent(bullet, Drawable(m_bulletSprite, 2)); - d.scale = {0.08f, 0.08f}; - d.tint = Math::Color(1.0f, 1.0f, 0.0f, 1.0f); // Yellow for spread - } - } - } - - void ShootingSystem::CreateLaserShot(Registry& registry, Entity shooter, const Position& pos, int damage) { - float offsetX = 50.0f; - float offsetY = 20.0f; - - if (registry.HasComponent(shooter)) { - const auto& shooterComp = registry.GetComponent(shooter); - offsetX = shooterComp.offsetX; - offsetY = shooterComp.offsetY; - } - - if (m_effectFactory) { - m_effectFactory->CreateShootingEffect(registry, pos.x + offsetX, pos.y + offsetY, shooter); - } - - auto bullet = registry.CreateEntity(); - - if (registry.HasComponent(bullet)) { - registry.RemoveComponent(bullet); - } - if (registry.HasComponent(bullet)) { - registry.RemoveComponent(bullet); - } - - registry.AddComponent(bullet, Position(pos.x + offsetX, pos.y + offsetY)); - registry.AddComponent(bullet, Velocity(800.0f, 0.0f)); // Faster - registry.AddComponent(bullet, Bullet(shooter)); - registry.AddComponent(bullet, Damage(damage)); - registry.AddComponent(bullet, BoxCollider(30.0f, 3.0f)); // Longer - registry.AddComponent(bullet, - CollisionLayer(CollisionLayers::PLAYER_BULLET, - CollisionLayers::ENEMY | CollisionLayers::OBSTACLE)); - - if (m_bulletSprite != 0) { - auto& d = registry.AddComponent(bullet, Drawable(m_bulletSprite, 2)); - d.scale = {0.3f, 0.05f}; - d.tint = Math::Color(0.0f, 1.0f, 1.0f, 1.0f); - } - } - } -} diff --git a/games/rtype/CMakeLists.txt b/games/rtype/CMakeLists.txt new file mode 100644 index 0000000..b49b644 --- /dev/null +++ b/games/rtype/CMakeLists.txt @@ -0,0 +1,65 @@ +# R-Type Game - Systems and Factories +# +# This builds R-Type specific systems as a library that extends rtype_ecs_core. + +set(RTYPE_SYSTEMS_SOURCES + systems/PlayerSystem.cpp + systems/PlayerFactory.cpp + systems/BulletCollisionResponseSystem.cpp + systems/PlayerCollisionResponseSystem.cpp + systems/ObstacleCollisionResponseSystem.cpp + systems/EnemySystem.cpp + systems/EnemyFactory.cpp + systems/ShootingSystem.cpp + systems/LevelLoader.cpp + systems/PowerUpFactory.cpp + systems/PowerUpSpawnSystem.cpp + systems/PowerUpCollisionSystem.cpp + systems/ForcePodSystem.cpp + systems/ShieldSystem.cpp + systems/BossSystem.cpp + systems/BossAttackSystem.cpp + systems/MineSystem.cpp + systems/BlackOrbSystem.cpp + systems/ThirdBulletSystem.cpp + systems/EffectFactory.cpp +) + +set(RTYPE_SYSTEMS_HEADERS + include/PlayerSystem.hpp + include/PlayerFactory.hpp + include/BulletCollisionResponseSystem.hpp + include/PlayerCollisionResponseSystem.hpp + include/ObstacleCollisionResponseSystem.hpp + include/EnemySystem.hpp + include/EnemyFactory.hpp + include/ShootingSystem.hpp + include/LevelLoader.hpp + include/PowerUpFactory.hpp + include/PowerUpSpawnSystem.hpp + include/PowerUpCollisionSystem.hpp + include/ForcePodSystem.hpp + include/ShieldSystem.hpp + include/BossSystem.hpp + include/BossAttackSystem.hpp + include/MineSystem.hpp + include/BlackOrbSystem.hpp + include/ThirdBulletSystem.hpp + include/EffectFactory.hpp +) + +add_library(rtype_game STATIC + ${RTYPE_SYSTEMS_SOURCES} + ${RTYPE_SYSTEMS_HEADERS} +) + +target_include_directories(rtype_game PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR}/../../engine/include +) + +target_link_libraries(rtype_game PUBLIC rtype_ecs_core rtype_animation) + +set_target_properties(rtype_game PROPERTIES + POSITION_INDEPENDENT_CODE ON +) diff --git a/games/rtype/client/include/GameState.hpp b/games/rtype/client/include/GameState.hpp index b9575cf..72d8dbb 100644 --- a/games/rtype/client/include/GameState.hpp +++ b/games/rtype/client/include/GameState.hpp @@ -12,25 +12,25 @@ #include "ECS/RenderingSystem.hpp" #include "ECS/TextRenderingSystem.hpp" #include "ECS/ScrollingSystem.hpp" -#include "ECS/ShootingSystem.hpp" +#include "ShootingSystem.hpp" #include "ECS/MovementSystem.hpp" #include "ECS/InputSystem.hpp" #include "ECS/CollisionDetectionSystem.hpp" -#include "ECS/BulletCollisionResponseSystem.hpp" -#include "ECS/PlayerCollisionResponseSystem.hpp" -#include "ECS/ObstacleCollisionResponseSystem.hpp" +#include "BulletCollisionResponseSystem.hpp" +#include "PlayerCollisionResponseSystem.hpp" +#include "ObstacleCollisionResponseSystem.hpp" #include "ECS/HealthSystem.hpp" #include "ECS/ScoreSystem.hpp" -#include "ECS/ShieldSystem.hpp" -#include "ECS/ForcePodSystem.hpp" -#include "ECS/PowerUpSpawnSystem.hpp" -#include "ECS/PowerUpCollisionSystem.hpp" +#include "ShieldSystem.hpp" +#include "ForcePodSystem.hpp" +#include "PowerUpSpawnSystem.hpp" +#include "PowerUpCollisionSystem.hpp" #include "ECS/AudioSystem.hpp" #include "ECS/AnimationSystem.hpp" -#include "ECS/EffectFactory.hpp" +#include "EffectFactory.hpp" #include "ECS/Component.hpp" -#include "ECS/PowerUpFactory.hpp" -#include "ECS/LevelLoader.hpp" +#include "PowerUpFactory.hpp" +#include "LevelLoader.hpp" #include "Animation/AnimationModule.hpp" #include "Renderer/IRenderer.hpp" diff --git a/games/rtype/client/include/editor/EditorColliderManager.hpp b/games/rtype/client/include/editor/EditorColliderManager.hpp index 11fb700..01aa777 100644 --- a/games/rtype/client/include/editor/EditorColliderManager.hpp +++ b/games/rtype/client/include/editor/EditorColliderManager.hpp @@ -8,7 +8,7 @@ #pragma once #include "editor/EditorTypes.hpp" -#include "ECS/LevelLoader.hpp" +#include "LevelLoader.hpp" #include "Renderer/IRenderer.hpp" #include "Math/Types.hpp" #include diff --git a/games/rtype/client/include/editor/EditorDrawing.hpp b/games/rtype/client/include/editor/EditorDrawing.hpp index 4645baa..7497e1d 100644 --- a/games/rtype/client/include/editor/EditorDrawing.hpp +++ b/games/rtype/client/include/editor/EditorDrawing.hpp @@ -9,7 +9,7 @@ #include "Math/Types.hpp" #include "Renderer/IRenderer.hpp" -#include "ECS/LevelLoader.hpp" +#include "LevelLoader.hpp" namespace RType { namespace Client { diff --git a/games/rtype/client/include/editor/EditorFileManager.hpp b/games/rtype/client/include/editor/EditorFileManager.hpp index e0140fa..dc45077 100644 --- a/games/rtype/client/include/editor/EditorFileManager.hpp +++ b/games/rtype/client/include/editor/EditorFileManager.hpp @@ -9,7 +9,7 @@ #include "editor/EditorTypes.hpp" #include "editor/EditorAssetLibrary.hpp" -#include "ECS/LevelLoader.hpp" +#include "LevelLoader.hpp" #include "ECS/Registry.hpp" #include "Renderer/IRenderer.hpp" #include diff --git a/games/rtype/client/include/editor/EditorTypes.hpp b/games/rtype/client/include/editor/EditorTypes.hpp index 8b18fd4..66c81cc 100644 --- a/games/rtype/client/include/editor/EditorTypes.hpp +++ b/games/rtype/client/include/editor/EditorTypes.hpp @@ -8,7 +8,7 @@ #pragma once #include "ECS/Entity.hpp" -#include "ECS/LevelLoader.hpp" +#include "LevelLoader.hpp" #include "Math/Types.hpp" #include #include diff --git a/games/rtype/client/src/game/GameStateInit.cpp b/games/rtype/client/src/game/GameStateInit.cpp index 8f5cbb9..0efacea 100644 --- a/games/rtype/client/src/game/GameStateInit.cpp +++ b/games/rtype/client/src/game/GameStateInit.cpp @@ -10,7 +10,7 @@ #include "ECS/Components/TextLabel.hpp" #include "ECS/Component.hpp" #include "Core/Logger.hpp" -#include "ECS/PlayerFactory.hpp" +#include "PlayerFactory.hpp" #include "Animation/AnimationTypes.hpp" using namespace RType::ECS; diff --git a/games/rtype/client/src/game/GameStateNetwork.cpp b/games/rtype/client/src/game/GameStateNetwork.cpp index d443c3e..83ce971 100644 --- a/games/rtype/client/src/game/GameStateNetwork.cpp +++ b/games/rtype/client/src/game/GameStateNetwork.cpp @@ -10,7 +10,7 @@ #include "ECS/Components/TextLabel.hpp" #include "ECS/Component.hpp" #include "Core/Logger.hpp" -#include "ECS/PowerUpFactory.hpp" +#include "PowerUpFactory.hpp" #include "Animation/AnimationModule.hpp" #include diff --git a/engine/include/ECS/BlackOrbSystem.hpp b/games/rtype/include/BlackOrbSystem.hpp similarity index 90% rename from engine/include/ECS/BlackOrbSystem.hpp rename to games/rtype/include/BlackOrbSystem.hpp index c9f71c1..fd65a65 100644 --- a/engine/include/ECS/BlackOrbSystem.hpp +++ b/games/rtype/include/BlackOrbSystem.hpp @@ -7,8 +7,8 @@ #pragma once -#include "ISystem.hpp" -#include "Registry.hpp" +#include "ECS/ISystem.hpp" +#include "ECS/Registry.hpp" #include namespace RType { diff --git a/libs/engine/include/ECS/BossAttackSystem.hpp b/games/rtype/include/BossAttackSystem.hpp similarity index 96% rename from libs/engine/include/ECS/BossAttackSystem.hpp rename to games/rtype/include/BossAttackSystem.hpp index 8cef9ee..1feecb7 100644 --- a/libs/engine/include/ECS/BossAttackSystem.hpp +++ b/games/rtype/include/BossAttackSystem.hpp @@ -7,8 +7,8 @@ #pragma once -#include "ISystem.hpp" -#include "Registry.hpp" +#include "ECS/ISystem.hpp" +#include "ECS/Registry.hpp" #include namespace RType { diff --git a/libs/engine/include/ECS/BossSystem.hpp b/games/rtype/include/BossSystem.hpp similarity index 89% rename from libs/engine/include/ECS/BossSystem.hpp rename to games/rtype/include/BossSystem.hpp index ba492a7..d850165 100644 --- a/libs/engine/include/ECS/BossSystem.hpp +++ b/games/rtype/include/BossSystem.hpp @@ -7,8 +7,8 @@ #pragma once -#include "ISystem.hpp" -#include "Registry.hpp" +#include "ECS/ISystem.hpp" +#include "ECS/Registry.hpp" namespace RType { namespace ECS { diff --git a/libs/engine/include/ECS/BulletCollisionResponseSystem.hpp b/games/rtype/include/BulletCollisionResponseSystem.hpp similarity index 91% rename from libs/engine/include/ECS/BulletCollisionResponseSystem.hpp rename to games/rtype/include/BulletCollisionResponseSystem.hpp index 0a09137..ee8b219 100644 --- a/libs/engine/include/ECS/BulletCollisionResponseSystem.hpp +++ b/games/rtype/include/BulletCollisionResponseSystem.hpp @@ -7,9 +7,9 @@ #pragma once -#include "ISystem.hpp" -#include "Registry.hpp" -#include "Component.hpp" +#include "ECS/ISystem.hpp" +#include "ECS/Registry.hpp" +#include "ECS/Component.hpp" namespace RType { namespace ECS { diff --git a/engine/include/ECS/EffectFactory.hpp b/games/rtype/include/EffectFactory.hpp similarity index 99% rename from engine/include/ECS/EffectFactory.hpp rename to games/rtype/include/EffectFactory.hpp index e4a3b7d..a4ee50a 100644 --- a/engine/include/ECS/EffectFactory.hpp +++ b/games/rtype/include/EffectFactory.hpp @@ -1,6 +1,6 @@ #pragma once -#include "Entity.hpp" +#include "ECS/Entity.hpp" #include "Animation/AnimationTypes.hpp" #include "Renderer/IRenderer.hpp" #include "Math/Types.hpp" diff --git a/engine/include/ECS/EnemyFactory.hpp b/games/rtype/include/EnemyFactory.hpp similarity index 91% rename from engine/include/ECS/EnemyFactory.hpp rename to games/rtype/include/EnemyFactory.hpp index 8410294..c561a83 100644 --- a/engine/include/ECS/EnemyFactory.hpp +++ b/games/rtype/include/EnemyFactory.hpp @@ -1,7 +1,7 @@ #pragma once -#include "Registry.hpp" -#include "Component.hpp" +#include "ECS/Registry.hpp" +#include "ECS/Component.hpp" #include "Renderer/IRenderer.hpp" #include "Math/Types.hpp" #include diff --git a/libs/engine/include/ECS/EnemySystem.hpp b/games/rtype/include/EnemySystem.hpp similarity index 97% rename from libs/engine/include/ECS/EnemySystem.hpp rename to games/rtype/include/EnemySystem.hpp index f392589..3ad6f07 100644 --- a/libs/engine/include/ECS/EnemySystem.hpp +++ b/games/rtype/include/EnemySystem.hpp @@ -1,6 +1,6 @@ #pragma once -#include "ISystem.hpp" +#include "ECS/ISystem.hpp" #include "Renderer/IRenderer.hpp" #include diff --git a/libs/engine/include/ECS/ForcePodSystem.hpp b/games/rtype/include/ForcePodSystem.hpp similarity index 94% rename from libs/engine/include/ECS/ForcePodSystem.hpp rename to games/rtype/include/ForcePodSystem.hpp index c827392..5fbfdec 100644 --- a/libs/engine/include/ECS/ForcePodSystem.hpp +++ b/games/rtype/include/ForcePodSystem.hpp @@ -7,7 +7,7 @@ #pragma once -#include "ISystem.hpp" +#include "ECS/ISystem.hpp" namespace RType { namespace ECS { diff --git a/engine/include/ECS/LevelLoader.hpp b/games/rtype/include/LevelLoader.hpp similarity index 98% rename from engine/include/ECS/LevelLoader.hpp rename to games/rtype/include/LevelLoader.hpp index cb4c96d..6de695b 100644 --- a/engine/include/ECS/LevelLoader.hpp +++ b/games/rtype/include/LevelLoader.hpp @@ -7,8 +7,8 @@ #pragma once -#include "Registry.hpp" -#include "Component.hpp" +#include "ECS/Registry.hpp" +#include "ECS/Component.hpp" #include "Renderer/IRenderer.hpp" #include #include diff --git a/libs/engine/include/ECS/MineSystem.hpp b/games/rtype/include/MineSystem.hpp similarity index 91% rename from libs/engine/include/ECS/MineSystem.hpp rename to games/rtype/include/MineSystem.hpp index 5873b85..f12920e 100644 --- a/libs/engine/include/ECS/MineSystem.hpp +++ b/games/rtype/include/MineSystem.hpp @@ -7,8 +7,8 @@ #pragma once -#include "ISystem.hpp" -#include "Registry.hpp" +#include "ECS/ISystem.hpp" +#include "ECS/Registry.hpp" #include namespace RType { diff --git a/libs/engine/include/ECS/ObstacleCollisionResponseSystem.hpp b/games/rtype/include/ObstacleCollisionResponseSystem.hpp similarity index 87% rename from libs/engine/include/ECS/ObstacleCollisionResponseSystem.hpp rename to games/rtype/include/ObstacleCollisionResponseSystem.hpp index 080f588..93224ad 100644 --- a/libs/engine/include/ECS/ObstacleCollisionResponseSystem.hpp +++ b/games/rtype/include/ObstacleCollisionResponseSystem.hpp @@ -7,9 +7,9 @@ #pragma once -#include "ISystem.hpp" -#include "Registry.hpp" -#include "Component.hpp" +#include "ECS/ISystem.hpp" +#include "ECS/Registry.hpp" +#include "ECS/Component.hpp" namespace RType { namespace ECS { diff --git a/engine/include/ECS/PlayerCollisionResponseSystem.hpp b/games/rtype/include/PlayerCollisionResponseSystem.hpp similarity index 92% rename from engine/include/ECS/PlayerCollisionResponseSystem.hpp rename to games/rtype/include/PlayerCollisionResponseSystem.hpp index 0acf686..31e08f6 100644 --- a/engine/include/ECS/PlayerCollisionResponseSystem.hpp +++ b/games/rtype/include/PlayerCollisionResponseSystem.hpp @@ -7,9 +7,9 @@ #pragma once -#include "ISystem.hpp" -#include "Registry.hpp" -#include "Component.hpp" +#include "ECS/ISystem.hpp" +#include "ECS/Registry.hpp" +#include "ECS/Component.hpp" namespace RType { namespace ECS { diff --git a/libs/engine/include/ECS/PlayerFactory.hpp b/games/rtype/include/PlayerFactory.hpp similarity index 90% rename from libs/engine/include/ECS/PlayerFactory.hpp rename to games/rtype/include/PlayerFactory.hpp index 6ada9f0..401891b 100644 --- a/libs/engine/include/ECS/PlayerFactory.hpp +++ b/games/rtype/include/PlayerFactory.hpp @@ -1,7 +1,7 @@ #pragma once -#include "Registry.hpp" -#include "Component.hpp" +#include "ECS/Registry.hpp" +#include "ECS/Component.hpp" #include "Renderer/IRenderer.hpp" #include "Math/Types.hpp" #include diff --git a/libs/engine/include/ECS/PlayerSystem.hpp b/games/rtype/include/PlayerSystem.hpp similarity index 97% rename from libs/engine/include/ECS/PlayerSystem.hpp rename to games/rtype/include/PlayerSystem.hpp index d7e3d36..9602e44 100644 --- a/libs/engine/include/ECS/PlayerSystem.hpp +++ b/games/rtype/include/PlayerSystem.hpp @@ -1,6 +1,6 @@ #pragma once -#include "ISystem.hpp" +#include "ECS/ISystem.hpp" #include "Renderer/IRenderer.hpp" namespace RType { diff --git a/libs/engine/include/ECS/PowerUpCollisionSystem.hpp b/games/rtype/include/PowerUpCollisionSystem.hpp similarity index 88% rename from libs/engine/include/ECS/PowerUpCollisionSystem.hpp rename to games/rtype/include/PowerUpCollisionSystem.hpp index f6dbc1e..efc3345 100644 --- a/libs/engine/include/ECS/PowerUpCollisionSystem.hpp +++ b/games/rtype/include/PowerUpCollisionSystem.hpp @@ -7,10 +7,10 @@ #pragma once -#include "ISystem.hpp" -#include "CollisionDetectionSystem.hpp" +#include "ECS/ISystem.hpp" +#include "ECS/CollisionDetectionSystem.hpp" #include "Renderer/IRenderer.hpp" -#include "../Audio/IAudio.hpp" +#include "Audio/IAudio.hpp" namespace RType { namespace ECS { diff --git a/libs/engine/include/ECS/PowerUpFactory.hpp b/games/rtype/include/PowerUpFactory.hpp similarity index 96% rename from libs/engine/include/ECS/PowerUpFactory.hpp rename to games/rtype/include/PowerUpFactory.hpp index 4bced5f..38c5ca3 100644 --- a/libs/engine/include/ECS/PowerUpFactory.hpp +++ b/games/rtype/include/PowerUpFactory.hpp @@ -7,8 +7,8 @@ #pragma once -#include "Registry.hpp" -#include "Component.hpp" +#include "ECS/Registry.hpp" +#include "ECS/Component.hpp" #include "Renderer/IRenderer.hpp" #include "Math/Types.hpp" #include diff --git a/libs/engine/include/ECS/PowerUpSpawnSystem.hpp b/games/rtype/include/PowerUpSpawnSystem.hpp similarity index 98% rename from libs/engine/include/ECS/PowerUpSpawnSystem.hpp rename to games/rtype/include/PowerUpSpawnSystem.hpp index 2abeb35..b3094d8 100644 --- a/libs/engine/include/ECS/PowerUpSpawnSystem.hpp +++ b/games/rtype/include/PowerUpSpawnSystem.hpp @@ -7,7 +7,7 @@ #pragma once -#include "ISystem.hpp" +#include "ECS/ISystem.hpp" #include "Renderer/IRenderer.hpp" #include diff --git a/engine/include/ECS/ShieldSystem.hpp b/games/rtype/include/ShieldSystem.hpp similarity index 94% rename from engine/include/ECS/ShieldSystem.hpp rename to games/rtype/include/ShieldSystem.hpp index f4a77d8..a5811cc 100644 --- a/engine/include/ECS/ShieldSystem.hpp +++ b/games/rtype/include/ShieldSystem.hpp @@ -7,7 +7,7 @@ #pragma once -#include "ISystem.hpp" +#include "ECS/ISystem.hpp" namespace RType { namespace ECS { diff --git a/engine/include/ECS/ShootingSystem.hpp b/games/rtype/include/ShootingSystem.hpp similarity index 91% rename from engine/include/ECS/ShootingSystem.hpp rename to games/rtype/include/ShootingSystem.hpp index fa309b1..a06121f 100644 --- a/engine/include/ECS/ShootingSystem.hpp +++ b/games/rtype/include/ShootingSystem.hpp @@ -1,8 +1,8 @@ #pragma once -#include "ISystem.hpp" -#include "../Renderer/IRenderer.hpp" -#include "../Audio/IAudio.hpp" +#include "ECS/ISystem.hpp" +#include "Renderer/IRenderer.hpp" +#include "Audio/IAudio.hpp" namespace RType { namespace ECS { diff --git a/libs/engine/include/ECS/ThirdBulletSystem.hpp b/games/rtype/include/ThirdBulletSystem.hpp similarity index 91% rename from libs/engine/include/ECS/ThirdBulletSystem.hpp rename to games/rtype/include/ThirdBulletSystem.hpp index 10d8f6a..ac03b4a 100644 --- a/libs/engine/include/ECS/ThirdBulletSystem.hpp +++ b/games/rtype/include/ThirdBulletSystem.hpp @@ -1,7 +1,7 @@ #pragma once -#include "ISystem.hpp" -#include "Registry.hpp" +#include "ECS/ISystem.hpp" +#include "ECS/Registry.hpp" namespace RType { namespace ECS { diff --git a/engine/src/ECS/BlackOrbSystem.cpp b/games/rtype/systems/BlackOrbSystem.cpp similarity index 99% rename from engine/src/ECS/BlackOrbSystem.cpp rename to games/rtype/systems/BlackOrbSystem.cpp index 3bff0a2..f6d9e2c 100644 --- a/engine/src/ECS/BlackOrbSystem.cpp +++ b/games/rtype/systems/BlackOrbSystem.cpp @@ -5,7 +5,7 @@ ** BlackOrbSystem implementation */ -#include "ECS/BlackOrbSystem.hpp" +#include "BlackOrbSystem.hpp" #include "ECS/Component.hpp" #include "Core/Logger.hpp" #include diff --git a/engine/src/ECS/BossAttackSystem.cpp b/games/rtype/systems/BossAttackSystem.cpp similarity index 99% rename from engine/src/ECS/BossAttackSystem.cpp rename to games/rtype/systems/BossAttackSystem.cpp index 7615dc3..46b4a14 100644 --- a/engine/src/ECS/BossAttackSystem.cpp +++ b/games/rtype/systems/BossAttackSystem.cpp @@ -5,7 +5,7 @@ ** BossAttackSystem - Handles boss attack patterns */ -#include "ECS/BossAttackSystem.hpp" +#include "BossAttackSystem.hpp" #include "ECS/Component.hpp" #include "Core/Logger.hpp" #include diff --git a/libs/engine/src/ECS/BossSystem.cpp b/games/rtype/systems/BossSystem.cpp similarity index 99% rename from libs/engine/src/ECS/BossSystem.cpp rename to games/rtype/systems/BossSystem.cpp index 16e0826..675d440 100644 --- a/libs/engine/src/ECS/BossSystem.cpp +++ b/games/rtype/systems/BossSystem.cpp @@ -5,7 +5,7 @@ ** BossSystem - Handles boss behavior */ -#include "ECS/BossSystem.hpp" +#include "BossSystem.hpp" #include "ECS/Component.hpp" #include "Core/Logger.hpp" #include diff --git a/libs/engine/src/ECS/BulletCollisionResponseSystem.cpp b/games/rtype/systems/BulletCollisionResponseSystem.cpp similarity index 98% rename from libs/engine/src/ECS/BulletCollisionResponseSystem.cpp rename to games/rtype/systems/BulletCollisionResponseSystem.cpp index f56c3e1..877ba7d 100644 --- a/libs/engine/src/ECS/BulletCollisionResponseSystem.cpp +++ b/games/rtype/systems/BulletCollisionResponseSystem.cpp @@ -5,8 +5,8 @@ ** BulletCollisionResponseSystem implementation */ -#include "ECS/BulletCollisionResponseSystem.hpp" -#include "ECS/EffectFactory.hpp" +#include "BulletCollisionResponseSystem.hpp" +#include "EffectFactory.hpp" #include "Core/Logger.hpp" #include #include diff --git a/libs/engine/src/ECS/EffectFactory.cpp b/games/rtype/systems/EffectFactory.cpp similarity index 99% rename from libs/engine/src/ECS/EffectFactory.cpp rename to games/rtype/systems/EffectFactory.cpp index 159af8f..b0f9e9d 100644 --- a/libs/engine/src/ECS/EffectFactory.cpp +++ b/games/rtype/systems/EffectFactory.cpp @@ -1,4 +1,4 @@ -#include "ECS/EffectFactory.hpp" +#include "EffectFactory.hpp" #include "ECS/Component.hpp" #include "ECS/Components/TextLabel.hpp" #include "ECS/Registry.hpp" diff --git a/engine/src/ECS/EnemyFactory.cpp b/games/rtype/systems/EnemyFactory.cpp similarity index 99% rename from engine/src/ECS/EnemyFactory.cpp rename to games/rtype/systems/EnemyFactory.cpp index 05acd74..c8d33af 100644 --- a/engine/src/ECS/EnemyFactory.cpp +++ b/games/rtype/systems/EnemyFactory.cpp @@ -1,4 +1,4 @@ -#include "ECS/EnemyFactory.hpp" +#include "EnemyFactory.hpp" #include "Core/Logger.hpp" #include #include diff --git a/engine/src/ECS/EnemySystem.cpp b/games/rtype/systems/EnemySystem.cpp similarity index 98% rename from engine/src/ECS/EnemySystem.cpp rename to games/rtype/systems/EnemySystem.cpp index 5eed3cc..112b909 100644 --- a/engine/src/ECS/EnemySystem.cpp +++ b/games/rtype/systems/EnemySystem.cpp @@ -1,5 +1,5 @@ -#include "ECS/EnemySystem.hpp" -#include "ECS/EnemyFactory.hpp" +#include "EnemySystem.hpp" +#include "EnemyFactory.hpp" #include "ECS/Component.hpp" #include "Core/Logger.hpp" #include diff --git a/libs/engine/src/ECS/ForcePodSystem.cpp b/games/rtype/systems/ForcePodSystem.cpp similarity index 98% rename from libs/engine/src/ECS/ForcePodSystem.cpp rename to games/rtype/systems/ForcePodSystem.cpp index e730551..a894603 100644 --- a/libs/engine/src/ECS/ForcePodSystem.cpp +++ b/games/rtype/systems/ForcePodSystem.cpp @@ -5,7 +5,7 @@ ** ForcePodSystem */ -#include "ECS/ForcePodSystem.hpp" +#include "ForcePodSystem.hpp" #include "ECS/Component.hpp" namespace RType { diff --git a/libs/engine/src/ECS/LevelLoader.cpp b/games/rtype/systems/LevelLoader.cpp similarity index 99% rename from libs/engine/src/ECS/LevelLoader.cpp rename to games/rtype/systems/LevelLoader.cpp index e8fbc57..157487a 100644 --- a/libs/engine/src/ECS/LevelLoader.cpp +++ b/games/rtype/systems/LevelLoader.cpp @@ -5,8 +5,8 @@ ** LevelLoader - JSON-based level loading implementation */ -#include "ECS/LevelLoader.hpp" -#include "ECS/EnemyFactory.hpp" +#include "LevelLoader.hpp" +#include "EnemyFactory.hpp" #include "Core/Logger.hpp" #include #include diff --git a/libs/engine/src/ECS/MineSystem.cpp b/games/rtype/systems/MineSystem.cpp similarity index 99% rename from libs/engine/src/ECS/MineSystem.cpp rename to games/rtype/systems/MineSystem.cpp index ec0cd4c..257dece 100644 --- a/libs/engine/src/ECS/MineSystem.cpp +++ b/games/rtype/systems/MineSystem.cpp @@ -5,7 +5,7 @@ ** MineSystem - Handles mine proximity detection and explosions */ -#include "ECS/MineSystem.hpp" +#include "MineSystem.hpp" #include "ECS/Component.hpp" #include "Core/Logger.hpp" #include diff --git a/libs/engine/src/ECS/ObstacleCollisionResponseSystem.cpp b/games/rtype/systems/ObstacleCollisionResponseSystem.cpp similarity index 99% rename from libs/engine/src/ECS/ObstacleCollisionResponseSystem.cpp rename to games/rtype/systems/ObstacleCollisionResponseSystem.cpp index be288e0..64b8568 100644 --- a/libs/engine/src/ECS/ObstacleCollisionResponseSystem.cpp +++ b/games/rtype/systems/ObstacleCollisionResponseSystem.cpp @@ -5,7 +5,7 @@ ** ObstacleCollisionResponseSystem implementation */ -#include "ECS/ObstacleCollisionResponseSystem.hpp" +#include "ObstacleCollisionResponseSystem.hpp" #include #include diff --git a/libs/engine/src/ECS/PlayerCollisionResponseSystem.cpp b/games/rtype/systems/PlayerCollisionResponseSystem.cpp similarity index 99% rename from libs/engine/src/ECS/PlayerCollisionResponseSystem.cpp rename to games/rtype/systems/PlayerCollisionResponseSystem.cpp index 2e75ab2..5530308 100644 --- a/libs/engine/src/ECS/PlayerCollisionResponseSystem.cpp +++ b/games/rtype/systems/PlayerCollisionResponseSystem.cpp @@ -5,7 +5,7 @@ ** PlayerCollisionResponseSystem implementation */ -#include "ECS/PlayerCollisionResponseSystem.hpp" +#include "PlayerCollisionResponseSystem.hpp" #include #include #include diff --git a/libs/engine/src/ECS/PlayerFactory.cpp b/games/rtype/systems/PlayerFactory.cpp similarity index 99% rename from libs/engine/src/ECS/PlayerFactory.cpp rename to games/rtype/systems/PlayerFactory.cpp index 113a091..259ecaa 100644 --- a/libs/engine/src/ECS/PlayerFactory.cpp +++ b/games/rtype/systems/PlayerFactory.cpp @@ -1,4 +1,4 @@ -#include "ECS/PlayerFactory.hpp" +#include "PlayerFactory.hpp" #include "Core/Logger.hpp" #include #include diff --git a/libs/engine/src/ECS/PlayerSystem.cpp b/games/rtype/systems/PlayerSystem.cpp similarity index 96% rename from libs/engine/src/ECS/PlayerSystem.cpp rename to games/rtype/systems/PlayerSystem.cpp index 2e078b2..967fed8 100644 --- a/libs/engine/src/ECS/PlayerSystem.cpp +++ b/games/rtype/systems/PlayerSystem.cpp @@ -1,6 +1,6 @@ -#include "ECS/PlayerSystem.hpp" +#include "PlayerSystem.hpp" #include "ECS/Component.hpp" -#include "ECS/PlayerFactory.hpp" +#include "PlayerFactory.hpp" #include namespace RType { diff --git a/engine/src/ECS/PowerUpCollisionSystem.cpp b/games/rtype/systems/PowerUpCollisionSystem.cpp similarity index 96% rename from engine/src/ECS/PowerUpCollisionSystem.cpp rename to games/rtype/systems/PowerUpCollisionSystem.cpp index 1dcdbad..9fdc989 100644 --- a/engine/src/ECS/PowerUpCollisionSystem.cpp +++ b/games/rtype/systems/PowerUpCollisionSystem.cpp @@ -5,8 +5,8 @@ ** PowerUpCollisionSystem */ -#include "ECS/PowerUpCollisionSystem.hpp" -#include "ECS/PowerUpFactory.hpp" +#include "PowerUpCollisionSystem.hpp" +#include "PowerUpFactory.hpp" #include "ECS/CollisionDetectionSystem.hpp" #include "ECS/Component.hpp" #include "Core/Logger.hpp" diff --git a/engine/src/ECS/PowerUpFactory.cpp b/games/rtype/systems/PowerUpFactory.cpp similarity index 99% rename from engine/src/ECS/PowerUpFactory.cpp rename to games/rtype/systems/PowerUpFactory.cpp index bf97a9c..b51841a 100644 --- a/engine/src/ECS/PowerUpFactory.cpp +++ b/games/rtype/systems/PowerUpFactory.cpp @@ -5,8 +5,8 @@ ** PowerUpFactory */ -#include "ECS/PowerUpFactory.hpp" -#include "ECS/EffectFactory.hpp" +#include "PowerUpFactory.hpp" +#include "EffectFactory.hpp" #include "Animation/AnimationTypes.hpp" #include "Core/Logger.hpp" #include diff --git a/engine/src/ECS/PowerUpSpawnSystem.cpp b/games/rtype/systems/PowerUpSpawnSystem.cpp similarity index 95% rename from engine/src/ECS/PowerUpSpawnSystem.cpp rename to games/rtype/systems/PowerUpSpawnSystem.cpp index a4016d7..1d9892b 100644 --- a/engine/src/ECS/PowerUpSpawnSystem.cpp +++ b/games/rtype/systems/PowerUpSpawnSystem.cpp @@ -5,9 +5,9 @@ ** PowerUpSpawnSystem */ -#include "ECS/PowerUpSpawnSystem.hpp" -#include "ECS/PowerUpFactory.hpp" -#include "ECS/EffectFactory.hpp" +#include "PowerUpSpawnSystem.hpp" +#include "PowerUpFactory.hpp" +#include "EffectFactory.hpp" #include "ECS/Component.hpp" #include "Core/Logger.hpp" #include diff --git a/engine/src/ECS/ShieldSystem.cpp b/games/rtype/systems/ShieldSystem.cpp similarity index 98% rename from engine/src/ECS/ShieldSystem.cpp rename to games/rtype/systems/ShieldSystem.cpp index 1ba20f4..538132a 100644 --- a/engine/src/ECS/ShieldSystem.cpp +++ b/games/rtype/systems/ShieldSystem.cpp @@ -5,7 +5,7 @@ ** ShieldSystem */ -#include "ECS/ShieldSystem.hpp" +#include "ShieldSystem.hpp" #include "ECS/Component.hpp" namespace RType { diff --git a/libs/engine/src/ECS/ShootingSystem.cpp b/games/rtype/systems/ShootingSystem.cpp similarity index 98% rename from libs/engine/src/ECS/ShootingSystem.cpp rename to games/rtype/systems/ShootingSystem.cpp index f33f12c..0643a4d 100644 --- a/libs/engine/src/ECS/ShootingSystem.cpp +++ b/games/rtype/systems/ShootingSystem.cpp @@ -1,8 +1,8 @@ -#include "../../include/ECS/ScoreSystem.hpp" -#include "../../include/ECS/Component.hpp" -#include "../../include/ECS/ShootingSystem.hpp" -#include "../../include/ECS/RenderingSystem.hpp" -#include "../../include/ECS/EffectFactory.hpp" +#include "ECS/ScoreSystem.hpp" +#include "ECS/Component.hpp" +#include "ShootingSystem.hpp" +#include "ECS/RenderingSystem.hpp" +#include "EffectFactory.hpp" #include #include diff --git a/engine/src/ECS/ThirdBulletSystem.cpp b/games/rtype/systems/ThirdBulletSystem.cpp similarity index 98% rename from engine/src/ECS/ThirdBulletSystem.cpp rename to games/rtype/systems/ThirdBulletSystem.cpp index 7ea3e87..405bd6c 100644 --- a/engine/src/ECS/ThirdBulletSystem.cpp +++ b/games/rtype/systems/ThirdBulletSystem.cpp @@ -1,4 +1,4 @@ -#include "ECS/ThirdBulletSystem.hpp" +#include "ThirdBulletSystem.hpp" #include "ECS/Component.hpp" #include "Core/Logger.hpp" #include diff --git a/libs/engine/CMakeLists.txt b/libs/engine/CMakeLists.txt deleted file mode 100644 index 05ba46a..0000000 --- a/libs/engine/CMakeLists.txt +++ /dev/null @@ -1,5 +0,0 @@ -add_subdirectory(src/ECS) -add_subdirectory(src/Core) -add_subdirectory(src/Renderer) -add_subdirectory(src/Audio) -add_subdirectory(src/Animation) diff --git a/libs/engine/README.md b/libs/engine/README.md deleted file mode 100644 index 0fd256e..0000000 --- a/libs/engine/README.md +++ /dev/null @@ -1,805 +0,0 @@ -# R-Type Engine & ECS - Developer Documentation - -## Table of Contents - -- [Overview](#overview) -- [Architecture](#architecture) -- [Core Engine](#core-engine) -- [Module System](#module-system) -- [ECS Fundamentals](#ecs-fundamentals) -- [Registry API](#registry-api) -- [Components](#components) -- [Systems](#systems) -- [SparseArray](#sparsearray) -- [Quick Start Guide](#quick-start-guide) -- [Best Practices](#best-practices) - ---- - -## Overview - -The R-Type Engine is a modular, data-oriented game engine built with C++17. It features: - -- **Plugin-based architecture**: Load modules dynamically at runtime -- **Entity Component System (ECS)**: Data-oriented design for efficient game object management -- **Cross-platform**: Works on Linux, macOS, and Windows -- **Type-safe**: Template-based component and system registration - -### Project Structure - -``` -libs/engine/ -├── include/ -│ ├── Core/ -│ │ ├── Engine.hpp # Main engine coordinator -│ │ ├── Module.hpp # IModule interface -│ │ ├── ModuleLoader.hpp # Dynamic plugin loader -│ │ └── Logger.hpp # Logging utility -│ └── ECS/ -│ ├── Entity.hpp # Entity type (uint32_t) -│ ├── Component.hpp # All component definitions -│ ├── SparseArray.hpp # Component storage container -│ ├── Registry.hpp # ECS registry -│ ├── ISystem.hpp # System interface -│ └── *System.hpp # Game logic systems -└── src/ - ├── Core/ - └── ECS/ -``` - ---- - -## Architecture - -The engine follows a layered architecture: - -``` -┌─────────────────────────────────────┐ -│ GAME APPLICATION │ -│ (Uses engine as static library) │ -└─────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────┐ -│ ENGINE CORE │ -│ ┌──────────┐ ┌─────────────────┐ │ -│ │ Engine │ │ Registry │ │ -│ │ │ │ (ECS Manager) │ │ -│ └──────────┘ └─────────────────┘ │ -│ ┌──────────┐ ┌─────────────────┐ │ -│ │ Module │ │ Systems │ │ -│ │ Loader │ │ (Game Logic) │ │ -│ └──────────┘ └─────────────────┘ │ -└─────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────┐ -│ MODULES / PLUGINS │ -│ ┌──────────┐ ┌─────────┐ ┌──────┐ │ -│ │ Renderer │ │ Physics │ │Audio │ │ -│ └──────────┘ └─────────┘ └──────┘ │ -└─────────────────────────────────────┘ -``` - ---- - -## Core Engine - -The `Engine` class is the central coordinator that manages modules, plugins, and the ECS registry. - -### Basic Usage - -```cpp -#include -#include - -int main() { - // Create engine with default config - RType::Core::EngineConfig config; - config.pluginPath = "./plugins"; - - auto engine = std::make_unique(config); - - // Load plugins (optional) - engine->LoadPlugin("plugins/libSFMLRenderer.so"); - - // Register built-in modules - engine->RegisterModule(std::make_unique()); - - // Initialize all modules (in priority order) - if (!engine->Initialize()) { - RType::Core::Logger::Error("Engine initialization failed"); - return 1; - } - - // Access the ECS registry - auto& registry = engine->GetRegistry(); - - // Register systems - engine->RegisterSystem(std::make_unique()); - - // Game loop - while (running) { - float deltaTime = getDeltaTime(); - engine->UpdateSystems(deltaTime); - // ... render, etc. - } - - // Shutdown (modules shutdown in reverse priority order) - engine->Shutdown(); - return 0; -} -``` - -### Engine API - -| Method | Description | -|--------|-------------| -| `Initialize()` | Initialize all registered modules (returns false on failure) | -| `Shutdown()` | Shutdown all modules in reverse priority order | -| `LoadPlugin(path)` | Load a plugin from shared library (.so/.dll/.dylib) | -| `UnloadPlugin(name)` | Unload a plugin by name | -| `RegisterModule(module)` | Register a built-in module | -| `GetModule()` | Get module by type (searches built-in and plugins) | -| `GetModuleByName(name)` | Get module by name string | -| `GetRegistry()` | Access the ECS registry | -| `RegisterSystem(system)` | Register an ECS system | -| `UpdateSystems(deltaTime)` | Update all registered systems | - ---- - -## Module System - -Modules are the building blocks of the engine. They can be built-in (compiled with engine) or plugins (loaded dynamically). - -### IModule Interface - -All modules must implement `IModule`: - -```cpp -class IModule { -public: - virtual ~IModule() = default; - virtual const char* GetName() const = 0; - virtual ModulePriority GetPriority() const = 0; - virtual bool Initialize(Engine* engine) = 0; - virtual void Shutdown() = 0; - virtual void Update(float deltaTime) = 0; - virtual bool ShouldUpdateInRenderThread() const { return false; } - virtual bool IsOverridable() const { return true; } -}; -``` - -### Module Priority - -Modules initialize in priority order (lowest value first): - -| Priority | Value | Use Case | -|----------|-------|----------| -| `Critical` | 0 | Core systems that others depend on | -| `High` | 1 | Rendering, Physics | -| `Normal` | 2 | Audio, Input | -| `Low` | 3 | Non-essential features | - -**Shutdown occurs in reverse priority order.** - -### Creating a Module - -```cpp -// MyModule.hpp -#pragma once -#include -#include - -class MyModule : public RType::Core::IModule { -public: - const char* GetName() const override { return "MyModule"; } - - RType::Core::ModulePriority GetPriority() const override { - return RType::Core::ModulePriority::Normal; - } - - bool Initialize(RType::Core::Engine* engine) override { - m_engine = engine; - RType::Core::Logger::Info("MyModule initialized"); - return true; - } - - void Shutdown() override { - RType::Core::Logger::Info("MyModule shutdown"); - } - - void Update(float deltaTime) override { - // Per-frame update - } - -private: - RType::Core::Engine* m_engine = nullptr; -}; -``` - -### Plugin Export Functions - -Plugins must export these C functions: - -```cpp -extern "C" { - RTYPE_MODULE_EXPORT RType::Core::IModule* CreateModule() { - return new MyModule(); - } - - RTYPE_MODULE_EXPORT void DestroyModule(RType::Core::IModule* module) { - delete module; - } -} -``` - -The `extern "C"` prevents C++ name mangling, allowing `dlsym()`/`GetProcAddress()` to find the functions. - ---- - -## ECS Fundamentals - -The Entity Component System provides a data-oriented approach to game object management. - -### Core Concepts - -- **Entity**: A unique identifier (`uint32_t`). Represents a game object. -- **Component**: Plain data struct attached to entities. Contains no logic. -- **System**: Logic that processes entities with specific component combinations. -- **Registry**: Central manager that stores entities and their components. - -### Entity - -```cpp -using Entity = uint32_t; -constexpr Entity NULL_ENTITY = 0; -``` - -Entities are just IDs. They have no data or behavior themselves. - ---- - -## Registry API - -The `Registry` manages all entities and components. - -### Entity Management - -```cpp -// Create entity -Entity player = registry.CreateEntity(); - -// Check if entity exists -if (registry.IsEntityAlive(player)) { - // Entity is valid -} - -// Destroy entity (removes all components) -registry.DestroyEntity(player); - -// Get total entity count -size_t count = registry.GetEntityCount(); -``` - -### Component Management - -```cpp -// Add component (by copy) -registry.AddComponent(player, RType::ECS::Position{100.0f, 200.0f}); - -// Add component (by move) -auto velocity = RType::ECS::Velocity{5.0f, 0.0f}; -registry.AddComponent(player, std::move(velocity)); - -// Add component (default constructed) -registry.AddComponent(player); - -// Get component (non-const) -auto& position = registry.GetComponent(player); -position.x = 150.0f; - -// Get component (const) -const auto& health = registry.GetComponent(player); - -// Check if entity has component -if (registry.HasComponent(player)) { - // Safe to access -} - -// Remove component -registry.RemoveComponent(player); - -// Get all entities with a component -auto entities = registry.GetEntitiesWithComponent(); -for (Entity e : entities) { - // Process entities with Position -} -``` - -### Error Handling - -- `GetComponent(entity)` throws `std::runtime_error` if component doesn't exist -- `AddComponent(entity)` throws if entity is `NULL_ENTITY` or doesn't exist -- Always check with `HasComponent()` before accessing - -**Best Practice:** -```cpp -if (registry.HasComponent(entity)) { - auto& pos = registry.GetComponent(entity); - // Safe to use -} -``` - ---- - -## Components - -Components are plain data structs that inherit from `IComponent`. They contain **no logic**. - -### Built-in Components - -The engine provides many built-in components: - -```cpp -// Transform -struct Position { float x, y; }; -struct Velocity { float dx, dy; }; - -// Rendering -struct Drawable { - Renderer::SpriteId spriteId; - Math::Vector2 scale{1.0f, 1.0f}; - float rotation = 0.0f; - int layer = 0; -}; - -// Gameplay -struct Health { int current, max; }; -struct Player { uint8_t playerNumber; uint64_t playerHash; }; -struct Enemy { EnemyType type; uint32_t id; }; -struct Bullet { Entity owner; }; -struct PowerUp { PowerUpType type; uint32_t id; }; - -// Physics -struct BoxCollider { float width, height; }; -struct CircleCollider { float radius; }; -struct CollisionLayer { uint16_t layer; uint16_t mask; }; - -// And many more... -``` - -### Creating Custom Components - -```cpp -// In Component.hpp or your own header -struct MyCustomComponent : public RType::ECS::IComponent { - int value = 0; - std::string name; - - MyCustomComponent() = default; - MyCustomComponent(int v, const std::string& n) - : value(v), name(n) {} -}; - -// Usage -registry.AddComponent(entity, MyCustomComponent{42, "test"}); -``` - -### Component Guidelines - -- ✅ Use plain structs with public members -- ✅ Provide default constructor -- ✅ Keep components small and focused -- ✅ No virtual functions (unless inheriting from IComponent) -- ❌ Don't put logic in components -- ❌ Don't store pointers to other entities (use `Entity` IDs instead) - ---- - -## Systems - -Systems contain the game logic. They process entities with specific component combinations. - -### ISystem Interface - -```cpp -class ISystem { -public: - virtual ~ISystem() = default; - virtual void Update(Registry& registry, float deltaTime) = 0; - virtual const char* GetName() const = 0; - virtual bool Initialize(Registry& registry) { return true; } - virtual void Shutdown() {} -}; -``` - -### Creating a System - -```cpp -// MovementSystem.hpp -#pragma once -#include "ECS/ISystem.hpp" - -namespace RType::ECS { - class MovementSystem : public ISystem { - public: - void Update(Registry& registry, float deltaTime) override; - const char* GetName() const override { return "MovementSystem"; } - }; -} - -// MovementSystem.cpp -#include "ECS/MovementSystem.hpp" -#include "ECS/Component.hpp" - -void MovementSystem::Update(Registry& registry, float deltaTime) { - // Get all entities with Velocity - auto entities = registry.GetEntitiesWithComponent(); - - for (Entity entity : entities) { - // Check if entity also has Position - if (!registry.HasComponent(entity)) { - continue; - } - - // Update position based on velocity - auto& position = registry.GetComponent(entity); - const auto& velocity = registry.GetComponent(entity); - - position.x += velocity.dx * deltaTime; - position.y += velocity.dy * deltaTime; - } -} -``` - -### Registering Systems - -```cpp -// In your game initialization -auto engine = std::make_unique(); - -engine->RegisterSystem(std::make_unique()); -engine->RegisterSystem(std::make_unique()); -engine->RegisterSystem(std::make_unique()); - -engine->Initialize(); - -// In game loop -while (running) { - float deltaTime = getDeltaTime(); - engine->UpdateSystems(deltaTime); // Updates all systems -} -``` - -### System Patterns - -**Query Multiple Components:** -```cpp -void MySystem::Update(Registry& registry, float deltaTime) { - // Get entities with first component - auto entities = registry.GetEntitiesWithComponent(); - - for (Entity e : entities) { - // Check for additional components - if (registry.HasComponent(e) && - registry.HasComponent(e)) { - - auto& a = registry.GetComponent(e); - auto& b = registry.GetComponent(e); - auto& c = registry.GetComponent(e); - - // Process... - } - } -} -``` - -**Create/Modify Entities:** -```cpp -void SpawnSystem::Update(Registry& registry, float deltaTime) { - if (shouldSpawn) { - Entity enemy = registry.CreateEntity(); - registry.AddComponent(enemy, Position{100.0f, 50.0f}); - registry.AddComponent(enemy, Velocity{0.0f, 50.0f}); - registry.AddComponent(enemy, Enemy{EnemyType::BASIC, nextId++}); - } -} -``` - ---- - -## SparseArray - -`SparseArray` is the internal container used by the Registry to store components efficiently. - -### Concept - -A sparse array is a vector where the **index directly corresponds to an Entity ID**. This provides: -- **O(1)** direct access by entity ID -- **Cache-friendly** sequential iteration -- **Holes allowed** (indices can be empty via `std::nullopt`) - -### Structure - -```cpp -template -class SparseArray { - std::vector> _data; - // Index = Entity ID -}; -``` - -### Visual Example - -``` -Entities: 0, 2, 5 have Position components - -Index: 0 1 2 3 4 5 - ┌────┬────┬────┬────┬────┬────┐ -Data: │{x,y}│null│{x,y}│null│null│{x,y}│ - └────┴────┴────┴────┴────┴────┘ -``` - -### Usage - -```cpp -SparseArray positions; - -// Insert component at entity ID 5 -positions.insert_at(5, Position{10.0f, 20.0f}); - -// Access directly -auto& pos_opt = positions[5]; -if (pos_opt.has_value()) { - Position& pos = pos_opt.value(); - pos.x = 100.0f; -} - -// Emplace (constructs in-place, more efficient) -positions.emplace_at(7, 30.0f, 40.0f); - -// Erase -positions.erase(5); - -// Iterate -for (size_t i = 0; i < positions.size(); ++i) { - if (positions[i].has_value()) { - Entity e = static_cast(i); - Position& pos = positions[i].value(); - // Process... - } -} -``` - ---- - -## Quick Start Guide - -### 1. Create an Entity with Components - -```cpp -#include -#include - -auto& registry = engine->GetRegistry(); - -// Create player entity -Entity player = registry.CreateEntity(); - -// Add components -registry.AddComponent(player, RType::ECS::Position{400.0f, 300.0f}); -registry.AddComponent(player, RType::ECS::Velocity{0.0f, 0.0f}); -registry.AddComponent(player, RType::ECS::Health{100, 100}); -registry.AddComponent(player, RType::ECS::Player{1, 0x1234, true}); -``` - -### 2. Create a System - -```cpp -// MySystem.hpp -class MySystem : public RType::ECS::ISystem { -public: - void Update(RType::ECS::Registry& registry, float deltaTime) override { - auto entities = registry.GetEntitiesWithComponent(); - for (Entity e : entities) { - auto& pos = registry.GetComponent(e); - // Update logic... - } - } - const char* GetName() const override { return "MySystem"; } -}; -``` - -### 3. Register and Run - -```cpp -engine->RegisterSystem(std::make_unique()); -engine->Initialize(); - -// Game loop -while (running) { - engine->UpdateSystems(deltaTime); -} -``` - -### 4. Complete Example - -```cpp -#include -#include -#include - -int main() { - auto engine = std::make_unique(); - - // Register systems - engine->RegisterSystem(std::make_unique()); - - // Initialize - engine->Initialize(); - auto& registry = engine->GetRegistry(); - - // Create entities - Entity player = registry.CreateEntity(); - registry.AddComponent(player, RType::ECS::Position{100.0f, 200.0f}); - registry.AddComponent(player, RType::ECS::Velocity{50.0f, 0.0f}); - - // Game loop - float deltaTime = 0.016f; // ~60 FPS - for (int i = 0; i < 100; ++i) { - engine->UpdateSystems(deltaTime); - - // Check updated position - const auto& pos = registry.GetComponent(player); - RType::Core::Logger::Info("Player position: ({}, {})", pos.x, pos.y); - } - - engine->Shutdown(); - return 0; -} -``` - ---- - -## Best Practices - -### Component Design - -1. **Keep components small and focused** - ```cpp - // ✅ Good: Single responsibility - struct Position { float x, y; }; - struct Velocity { float dx, dy; }; - - // ❌ Bad: Too much data in one component - struct Transform { float x, y, dx, dy, rotation, scale; }; - ``` - -2. **Use Entity IDs, not pointers** - ```cpp - // ✅ Good - struct Bullet { Entity owner; }; - - // ❌ Bad - struct Bullet { Entity* owner; }; - ``` - -3. **Provide sensible defaults** - ```cpp - struct Health { - int current = 100; - int max = 100; - }; - ``` - -### System Design - -1. **Process entities efficiently** - ```cpp - // ✅ Good: Get entities with most selective component first - auto entities = registry.GetEntitiesWithComponent(); - for (Entity e : entities) { - if (registry.HasComponent(e)) { - // Process... - } - } - ``` - -2. **Don't modify component pools during iteration** - ```cpp - // ❌ Bad: Modifying while iterating - for (Entity e : entities) { - registry.DestroyEntity(e); // Can cause issues - } - - // ✅ Good: Collect first, then modify - std::vector toDestroy; - for (Entity e : entities) { - if (shouldDestroy) { - toDestroy.push_back(e); - } - } - for (Entity e : toDestroy) { - registry.DestroyEntity(e); - } - ``` - -3. **Use const references when possible** - ```cpp - // ✅ Good - const auto& velocity = registry.GetComponent(entity); - - // Only use non-const when modifying - auto& position = registry.GetComponent(entity); - ``` - -### Performance Tips - -1. **Batch component access**: Get all entities once, then iterate -2. **Cache component references**: Don't call `GetComponent()` multiple times per entity -3. **Use `HasComponent()` before `GetComponent()`** to avoid exceptions -4. **Consider component layout**: Group frequently accessed components together - -### Error Handling - -```cpp -// Always check before accessing -if (registry.HasComponent(entity)) { - auto& health = registry.GetComponent(entity); - health.current -= damage; -} - -// Or use try-catch for critical paths -try { - auto& pos = registry.GetComponent(entity); - // Use pos... -} catch (const std::runtime_error& e) { - Logger::Error("Failed to get Position: {}", e.what()); -} -``` - ---- - -## API Reference Summary - -### RType::Core::Engine - -- `Initialize()` → `bool` -- `Shutdown()` → `void` -- `LoadPlugin(path)` → `IModule*` -- `RegisterModule(module)` → `void` -- `GetModule()` → `T*` -- `GetRegistry()` → `Registry&` -- `RegisterSystem(system)` → `void` -- `UpdateSystems(deltaTime)` → `void` - -### RType::ECS::Registry - -- `CreateEntity()` → `Entity` -- `DestroyEntity(entity)` → `void` -- `IsEntityAlive(entity)` → `bool` -- `AddComponent(entity, component)` → `T&` -- `GetComponent(entity)` → `T&` / `const T&` -- `HasComponent(entity)` → `bool` -- `RemoveComponent(entity)` → `void` -- `GetEntitiesWithComponent()` → `std::vector` -- `GetEntityCount()` → `size_t` - -### RType::Core::Logger - -- `Logger::Debug(format, ...)` -- `Logger::Info(format, ...)` -- `Logger::Warning(format, ...)` -- `Logger::Error(format, ...)` -- `Logger::Critical(format, ...)` - ---- - -## Additional Resources - -- Check `CODING_STYLE.md` for code style guidelines -- Review existing systems in `libs/engine/include/ECS/*System.hpp` for examples -- Examine component definitions in `libs/engine/include/ECS/Component.hpp` - ---- \ No newline at end of file diff --git a/libs/engine/include/Animation/AnimationModule.hpp b/libs/engine/include/Animation/AnimationModule.hpp deleted file mode 100644 index 7635af7..0000000 --- a/libs/engine/include/Animation/AnimationModule.hpp +++ /dev/null @@ -1,67 +0,0 @@ -#pragma once - -#include -#include -#include "Animation/IAnimation.hpp" - -namespace Animation { - - class AnimationModule : public IAnimation { - public: - AnimationModule(); - ~AnimationModule() override; - - const char* GetName() const override { return "AnimationModule"; } - RType::Core::ModulePriority GetPriority() const override { - return RType::Core::ModulePriority::Normal; - } - bool Initialize(RType::Core::Engine* engine) override; - void Shutdown() override; - void Update(float deltaTime) override; - - // Clip Management - AnimationClipId CreateClip(const AnimationClipConfig& config) override; - AnimationClipId CreateClipFromGrid(const std::string& name, - const std::string& texturePath, - const GridLayout& layout, - bool looping) override; - AnimationClipId LoadClipFromJson(const std::string& path) override; - void DestroyClip(AnimationClipId clipId) override; - const AnimationClipConfig* GetClipConfig(AnimationClipId clipId) const override; - - // Clip Introspection - float GetClipDuration(AnimationClipId clipId) const override; - std::size_t GetClipFrameCount(AnimationClipId clipId) const override; - bool IsClipValid(AnimationClipId clipId) const override; - - // Frame Calculation - FrameDef GetFrameAtTime(AnimationClipId clipId, - float time, - bool looping) const override; - std::size_t GetFrameIndexAtTime(AnimationClipId clipId, - float time, - bool looping) const override; - - // Bulk Loading - void LoadAnimationsFromManifest(const std::string& manifestPath) override; - void UnloadAll() override; - - // Utility - AnimationClipId GetClipByName(const std::string& name) const override; - - private: - // Internal storage - std::unordered_map m_clips; - std::unordered_map m_clipNameToId; - - // ID generation - AnimationClipId m_nextClipId = 1; - - // Engine reference - RType::Core::Engine* m_engine = nullptr; - - // Helper functions - float CalculateClipDuration(const AnimationClipConfig& config) const; - }; - -} diff --git a/libs/engine/include/Animation/AnimationTypes.hpp b/libs/engine/include/Animation/AnimationTypes.hpp deleted file mode 100644 index b2eecb0..0000000 --- a/libs/engine/include/Animation/AnimationTypes.hpp +++ /dev/null @@ -1,115 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include "Math/Types.hpp" - -namespace Animation { - - using AnimationClipId = std::uint32_t; - using AnimationGraphId = std::uint32_t; - using AnimationStateId = std::uint32_t; - - constexpr AnimationClipId INVALID_CLIP_ID = 0; - constexpr AnimationGraphId INVALID_GRAPH_ID = 0; - constexpr AnimationStateId INVALID_STATE_ID = 0; - - struct FrameDef { - Math::Rectangle region{}; - float duration = 0.1f; - std::string eventName; - - FrameDef() = default; - FrameDef(const Math::Rectangle& reg, float dur, const std::string& event = "") - : region(reg), duration(dur), eventName(event) {} - }; - - struct AnimationClipConfig { - std::string name; - std::string texturePath; - std::vector frames; - bool looping = false; - float playbackSpeed = 1.0f; - - AnimationClipConfig() = default; - }; - - struct GridLayout { - std::uint32_t columns = 1; - std::uint32_t rows = 1; - std::uint32_t startFrame = 0; - std::uint32_t frameCount = 0; - float frameWidth = 0.0f; - float frameHeight = 0.0f; - float defaultDuration = 0.1f; - - GridLayout() = default; - }; - - enum class CompareOp : std::uint8_t { - GREATER = 0, - GREATER_EQUAL = 1, - LESS = 2, - LESS_EQUAL = 3, - EQUAL = 4, - NOT_EQUAL = 5 - }; - - struct TransitionDef { - AnimationStateId targetState = INVALID_STATE_ID; - std::string conditionParam; - CompareOp compareOp = CompareOp::GREATER_EQUAL; - float conditionValue = 1.0f; - float blendDuration = 0.0f; - bool hasExitTime = false; - float exitTimeNormalized = 1.0f; - int priority = 0; - - TransitionDef() = default; - }; - - struct AnimationStateDef { - std::string name; - AnimationClipId clipId = INVALID_CLIP_ID; - std::vector transitions; - float speed = 1.0f; - bool looping = true; - - AnimationStateDef() = default; - }; - - struct AnimationGraphDef { - std::string name; - std::vector states; - AnimationStateId defaultStateId = INVALID_STATE_ID; - std::vector parameterNames; - - AnimationGraphDef() = default; - }; - - struct PlaybackOptions { - float speed = 1.0f; - bool reversed = false; - float startTimeNormalized = 0.0f; - bool destroyOnComplete = false; - - PlaybackOptions() = default; - }; - - using AnimationEventCallback = std::function; - - enum class EffectType : std::uint8_t { - EXPLOSION_SMALL = 0, - EXPLOSION_LARGE = 1, - BULLET_IMPACT = 2, - HIT_MARKER = 3, - POWER_UP_COLLECT = 4, - BOSS_PHASE_TRANSITION = 5, - SPAWN_EFFECT = 6, - DEATH_EFFECT = 7, - CUSTOM = 255 - }; - -} diff --git a/libs/engine/include/Animation/IAnimation.hpp b/libs/engine/include/Animation/IAnimation.hpp deleted file mode 100644 index a19e23e..0000000 --- a/libs/engine/include/Animation/IAnimation.hpp +++ /dev/null @@ -1,68 +0,0 @@ -#pragma once - -#include -#include -#include "Core/Module.hpp" -#include "Animation/AnimationTypes.hpp" - -namespace Animation { - - class IAnimation : public RType::Core::IModule { - public: - ~IAnimation() override = default; - - const char* GetName() const override = 0; - RType::Core::ModulePriority GetPriority() const override = 0; - bool Initialize(RType::Core::Engine* engine) override = 0; - void Shutdown() override = 0; - void Update(float deltaTime) override = 0; - - // Create a clip from explicit configuration - virtual AnimationClipId CreateClip(const AnimationClipConfig& config) = 0; - - // Create a clip from a grid layout (uniform sprite sheet) - virtual AnimationClipId CreateClipFromGrid(const std::string& name, - const std::string& texturePath, - const GridLayout& layout, - bool looping = false) = 0; - - // Load a clip definition from a JSON file - virtual AnimationClipId LoadClipFromJson(const std::string& path) = 0; - - // Destroy a clip and release its resources - virtual void DestroyClip(AnimationClipId clipId) = 0; - - // Get the configuration of a clip (nullptr if not found) - virtual const AnimationClipConfig* GetClipConfig(AnimationClipId clipId) const = 0; - - // Get total duration of a clip in seconds - virtual float GetClipDuration(AnimationClipId clipId) const = 0; - - // Get number of frames in a clip - virtual std::size_t GetClipFrameCount(AnimationClipId clipId) const = 0; - - // Check if a clip exists and is valid - virtual bool IsClipValid(AnimationClipId clipId) const = 0; - - // Get the frame definition at a given time - // Returns the frame data including texture region - virtual FrameDef GetFrameAtTime(AnimationClipId clipId, - float time, - bool looping) const = 0; - - // Get the frame index at a given time - virtual std::size_t GetFrameIndexAtTime(AnimationClipId clipId, - float time, - bool looping) const = 0; - - // Load multiple animations from a manifest JSON file - virtual void LoadAnimationsFromManifest(const std::string& manifestPath) = 0; - - // Unload all loaded animations - virtual void UnloadAll() = 0; - - // Get a clip ID by name (returns INVALID_CLIP_ID if not found) - virtual AnimationClipId GetClipByName(const std::string& name) const = 0; - }; - -} diff --git a/libs/engine/include/Audio/IAudio.hpp b/libs/engine/include/Audio/IAudio.hpp deleted file mode 100644 index 82fe93a..0000000 --- a/libs/engine/include/Audio/IAudio.hpp +++ /dev/null @@ -1,72 +0,0 @@ -#pragma once - -#include -#include -#include "Core/Module.hpp" -#include "Math/Types.hpp" - -namespace Audio { - - using SoundId = std::uint32_t; - using MusicId = std::uint32_t; - - constexpr SoundId INVALID_SOUND_ID = 0; - constexpr MusicId INVALID_MUSIC_ID = 0; - - struct AudioConfig { - std::uint32_t sampleRate = 44100; - std::uint32_t channelCount = 2; - float masterVolume = 1.0f; - std::uint32_t streamingBufferSize = 4096; - }; - - struct PlaybackOptions { - float volume = 1.0f; - float pitch = 1.0f; - float pan = 0.0f; - bool loop = false; - }; - - struct ListenerProperties { - Math::Vector2 position{0.0f, 0.0f}; - Math::Vector2 forward{0.0f, -1.0f}; - Math::Vector2 velocity{0.0f, 0.0f}; - }; - - class IAudio : public RType::Core::IModule { - public: - ~IAudio() override = default; - - const char* GetName() const override = 0; - RType::Core::ModulePriority GetPriority() const override = 0; - bool Initialize(RType::Core::Engine* engine) override = 0; - void Shutdown() override = 0; - void Update(float deltaTime) override = 0; - - virtual bool ConfigureDevice(const AudioConfig& config) = 0; - - virtual SoundId LoadSound(const std::string& path) = 0; - virtual void UnloadSound(SoundId soundId) = 0; - - virtual MusicId LoadMusic(const std::string& path) = 0; - virtual void UnloadMusic(MusicId musicId) = 0; - - virtual void PlaySound(SoundId soundId, - const PlaybackOptions& options = PlaybackOptions{}) = 0; - virtual void StopSound(SoundId soundId) = 0; - - virtual void PlayMusic(MusicId musicId, - const PlaybackOptions& options = PlaybackOptions{}) = 0; - virtual void StopMusic(MusicId musicId) = 0; - - virtual void PauseAll() = 0; - virtual void ResumeAll() = 0; - virtual void StopAll() = 0; - - virtual void SetMasterVolume(float volume) = 0; - virtual float GetMasterVolume() const = 0; - - virtual void SetListener(const ListenerProperties& listener) = 0; - }; - -} diff --git a/libs/engine/include/Audio/SFMLAudio.hpp b/libs/engine/include/Audio/SFMLAudio.hpp deleted file mode 100644 index bc77b1f..0000000 --- a/libs/engine/include/Audio/SFMLAudio.hpp +++ /dev/null @@ -1,63 +0,0 @@ -#pragma once - -#include "Audio/IAudio.hpp" - -#include -#include -#include -#include - -namespace Audio { - - class SFMLAudio : public IAudio { - public: - SFMLAudio() = default; - ~SFMLAudio() override = default; - - const char* GetName() const override { return "SFMLAudio"; } - RType::Core::ModulePriority GetPriority() const override { return RType::Core::ModulePriority::Normal; } - bool Initialize(RType::Core::Engine* /*engine*/) override { return true; } - void Shutdown() override; - void Update(float deltaTime) override; - - bool ConfigureDevice(const AudioConfig& config) override; - - SoundId LoadSound(const std::string& path) override; - void UnloadSound(SoundId soundId) override; - - MusicId LoadMusic(const std::string& path) override; - void UnloadMusic(MusicId musicId) override; - - void PlaySound(SoundId soundId, const PlaybackOptions& options = PlaybackOptions{}) override; - void StopSound(SoundId soundId) override; - - void PlayMusic(MusicId musicId, const PlaybackOptions& options = PlaybackOptions{}) override; - void StopMusic(MusicId musicId) override; - - void PauseAll() override; - void ResumeAll() override; - void StopAll() override; - - void SetMasterVolume(float volume) override; - float GetMasterVolume() const override; - - void SetListener(const ListenerProperties& listener) override; - - private: - void CleanupStoppedSounds(); - void UpdateAllVolumes(); - - float m_masterVolume = 1.0f; - SoundId m_nextSoundId = 1; - MusicId m_nextMusicId = 1; - - std::unordered_map m_soundBuffers; - std::list m_activeSounds; - std::unordered_map m_soundOriginalVolumes; - - std::unordered_map> m_music; - std::unordered_map m_musicOriginalVolumes; - }; - -} - diff --git a/libs/engine/include/Core/ColorFilter.hpp b/libs/engine/include/Core/ColorFilter.hpp deleted file mode 100644 index e04b6c3..0000000 --- a/libs/engine/include/Core/ColorFilter.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "Math/Types.hpp" - -namespace RType { - namespace Core { - - class ColorFilter { - public: - static bool IsColourBlindModeEnabled(); - static void SetColourBlindMode(bool enabled); - static Math::Color ApplyColourBlindFilter(const Math::Color& color); - - private: - static bool s_colourBlindMode; - }; - - } -} - diff --git a/libs/engine/include/Core/Engine.hpp b/libs/engine/include/Core/Engine.hpp deleted file mode 100644 index e01682f..0000000 --- a/libs/engine/include/Core/Engine.hpp +++ /dev/null @@ -1,117 +0,0 @@ -#pragma once - -#include "Module.hpp" -#include "ModuleLoader.hpp" -#include "Logger.hpp" -#include "../ECS/Registry.hpp" -#include "../ECS/ISystem.hpp" -#include -#include -#include -#include -#include - -namespace RType { - - namespace Core { - - struct EngineConfig { - std::string pluginPath = "./plugins"; - }; - - class Engine { - public: - explicit Engine(const EngineConfig& config = EngineConfig{}); - ~Engine(); - - Engine(const Engine&) = delete; - Engine& operator=(const Engine&) = delete; - - bool Initialize(); - void Shutdown(); - - IModule* LoadPlugin(const std::string& pluginPath); - bool UnloadPlugin(const std::string& pluginName); - - template - void RegisterModule(std::unique_ptr module); - - template - T* GetModule(); - - IModule* GetModuleByName(const std::string& name); - std::vector GetAllModules() const; - - ECS::Registry& GetRegistry() { return m_registry; } - const ECS::Registry& GetRegistry() const { return m_registry; } - - template - void RegisterSystem(std::unique_ptr system); - void UpdateSystems(float deltaTime); - private: - void SortModulesByPriority(); - bool InitializeModules(); - void ShutdownModules(); - void InitializeSystems(); - void ShutdownSystems(); - - EngineConfig m_config; - ModuleLoader m_moduleLoader; - std::unordered_map> m_builtinModules; - std::vector m_sortedModules; - ECS::Registry m_registry; - std::vector> m_systems; - bool m_initialized{false}; - }; - - template - void Engine::RegisterModule(std::unique_ptr module) { - static_assert(std::is_base_of::value, "T must derive from IModule"); - - if (!module) { - Logger::Error("Cannot register null module"); - return; - } - - std::type_index typeId = std::type_index(typeid(T)); - Logger::Info("Registering module '{}'", module->GetName()); - m_builtinModules[typeId] = std::move(module); - } - - template - T* Engine::GetModule() { - static_assert(std::is_base_of::value, "T must derive from IModule"); - - std::type_index typeId = std::type_index(typeid(T)); - - auto it = m_builtinModules.find(typeId); - if (it != m_builtinModules.end()) { - return static_cast(it->second.get()); - } - - for (IModule* module : m_moduleLoader.GetAllPlugins()) { - T* casted = dynamic_cast(module); - if (casted) { - return casted; - } - } - - return nullptr; - } - - template - void Engine::RegisterSystem(std::unique_ptr system) { - static_assert(std::is_base_of::value, "T must derive from ISystem"); - - if (!system) { - Logger::Error("Cannot register null system"); - return; - } - - Logger::Info("Registering system '{}'", system->GetName()); - m_systems.push_back(std::move(system)); - } - - } - -} diff --git a/libs/engine/include/Core/InputMapping.hpp b/libs/engine/include/Core/InputMapping.hpp deleted file mode 100644 index 6f32994..0000000 --- a/libs/engine/include/Core/InputMapping.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include "../Renderer/IRenderer.hpp" -#include -#include - -namespace RType { - namespace Core { - - class InputMapping { - public: - static Renderer::Key GetKey(const std::string& action); - static void SetKey(const std::string& action, Renderer::Key key); - static void LoadDefaults(); - - private: - static std::unordered_map s_mappings; - }; - - } -} - diff --git a/libs/engine/include/Core/Logger.hpp b/libs/engine/include/Core/Logger.hpp deleted file mode 100644 index 1c1f8ea..0000000 --- a/libs/engine/include/Core/Logger.hpp +++ /dev/null @@ -1,111 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -namespace RType { - - namespace Core { - - enum class LogLevel { Debug = 0, - Info = 1, - Warning = 2, - Error = 3, - Critical = 4 }; - - class Logger { - public: - static void SetLogLevel(LogLevel level) { s_logLevel = level; } - - template - static void Debug(const std::string& format, Args&&... args) { - Log(LogLevel::Debug, format, std::forward(args)...); - } - - template - static void Info(const std::string& format, Args&&... args) { - Log(LogLevel::Info, format, std::forward(args)...); - } - - template - static void Warning(const std::string& format, Args&&... args) { - Log(LogLevel::Warning, format, std::forward(args)...); - } - - template - static void Error(const std::string& format, Args&&... args) { - Log(LogLevel::Error, format, std::forward(args)...); - } - - template - static void Critical(const std::string& format, Args&&... args) { - Log(LogLevel::Critical, format, std::forward(args)...); - } - private: - static inline LogLevel s_logLevel = LogLevel::Debug; - - template - static void Log(LogLevel level, const std::string& format, Args&&... args) { - if (level < s_logLevel) { - return; - } - - std::string message = FormatString(format, std::forward(args)...); - std::string levelStr = GetLevelString(level); - std::string timestamp = GetTimestamp(); - - std::ostream& stream = (level >= LogLevel::Error) ? std::cerr : std::cout; - stream << "[" << timestamp << "] [" << levelStr << "] " << message << std::endl; - } - - static std::string GetLevelString(LogLevel level) { - switch (level) { - case LogLevel::Debug: - return "DEBUG"; - case LogLevel::Info: - return "INFO"; - case LogLevel::Warning: - return "WARN"; - case LogLevel::Error: - return "ERROR"; - case LogLevel::Critical: - return "CRITICAL"; - default: - return "UNKNOWN"; - } - } - - static std::string GetTimestamp() { - auto now = std::time(nullptr); - auto tm = *std::localtime(&now); - std::ostringstream oss; - oss << std::put_time(&tm, "%H:%M:%S"); - return oss.str(); - } - - template - static std::string FormatArg(const T& arg) { - std::ostringstream oss; - oss << arg; - return oss.str(); - } - - static std::string FormatString(const std::string& format) { return format; } - - template - static std::string FormatString(const std::string& format, T&& first, Args&&... rest) { - std::string result = format; - size_t pos = result.find("{}"); - if (pos != std::string::npos) { - result.replace(pos, 2, FormatArg(first)); - } - return FormatString(result, std::forward(rest)...); - } - }; - - } - -} diff --git a/libs/engine/include/Core/Module.hpp b/libs/engine/include/Core/Module.hpp deleted file mode 100644 index 4a7b733..0000000 --- a/libs/engine/include/Core/Module.hpp +++ /dev/null @@ -1,39 +0,0 @@ -#pragma once - -#include - -namespace RType { - - namespace Core { - - class Engine; - - enum class ModulePriority { Critical = 0, - High = 1, - Normal = 2, - Low = 3 }; - - class IModule { - public: - virtual ~IModule() = default; - virtual const char* GetName() const = 0; - virtual ModulePriority GetPriority() const = 0; - virtual bool Initialize(Engine* engine) = 0; - virtual void Shutdown() = 0; - virtual void Update(float deltaTime) = 0; - virtual bool ShouldUpdateInRenderThread() const { return false; } - virtual bool IsOverridable() const { return true; } - }; - - using CreateModuleFunc = IModule* (*)(); - using DestroyModuleFunc = void (*)(IModule*); - - } - -} - -#ifdef _WIN32 -#define RTYPE_MODULE_EXPORT __declspec(dllexport) -#else -#define RTYPE_MODULE_EXPORT __attribute__((visibility("default"))) -#endif diff --git a/libs/engine/include/Core/ModuleLoader.hpp b/libs/engine/include/Core/ModuleLoader.hpp deleted file mode 100644 index f8bd232..0000000 --- a/libs/engine/include/Core/ModuleLoader.hpp +++ /dev/null @@ -1,62 +0,0 @@ -#pragma once - -#define RTYPE_INCLUDE_WINDOWS_H -#include "Platform.hpp" -#include "Module.hpp" -#include "Logger.hpp" -#include -#include -#include -#include -#include - -#ifdef _WIN32 -using LibraryHandle = HMODULE; -#else -#include -using LibraryHandle = void*; -#endif - -namespace RType { - - namespace Core { - - struct PluginInfo { - std::string name; - std::string path; - LibraryHandle handle; - IModule* module; - DestroyModuleFunc destroyFunc; - }; - - class ModuleLoader { - public: - ModuleLoader() = default; - ~ModuleLoader(); - - ModuleLoader(const ModuleLoader&) = delete; - ModuleLoader& operator=(const ModuleLoader&) = delete; - - ModuleLoader(ModuleLoader&&) = default; - ModuleLoader& operator=(ModuleLoader&&) = default; - - IModule* LoadPlugin(const std::string& pluginPath); - bool UnloadPlugin(const std::string& pluginName); - void UnloadAllPlugins(); - IModule* GetPlugin(const std::string& pluginName); - std::vector GetAllPlugins() const; - bool IsPluginLoaded(const std::string& pluginName) const; - size_t GetPluginCount() const { return m_loadedPlugins.size(); } - private: - LibraryHandle LoadLibraryFromPath(const std::string& path); - void FreeLibraryHandle(LibraryHandle handle); - void* GetFunction(LibraryHandle handle, const std::string& name); - std::string GetLastErrorMessage(); - std::string ExtractPluginName(const std::string& path); - - std::unordered_map m_loadedPlugins; - }; - - } - -} diff --git a/libs/engine/include/Core/Platform.hpp b/libs/engine/include/Core/Platform.hpp deleted file mode 100644 index ff4c073..0000000 --- a/libs/engine/include/Core/Platform.hpp +++ /dev/null @@ -1,95 +0,0 @@ -#pragma once -#ifdef _WIN32 - - -#ifndef NOMINMAX -#define NOMINMAX -#endif - - -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif - - -#ifdef RTYPE_INCLUDE_WINDOWS_H -#include - - -#ifdef CreateWindow -#undef CreateWindow -#endif - -#ifdef DrawText -#undef DrawText -#endif - -#ifdef PlaySound -#undef PlaySound -#endif - -#ifdef LoadLibrary -#undef LoadLibrary -#endif - -#ifdef FreeLibrary -#undef FreeLibrary -#endif - -#ifdef GetMessage -#undef GetMessage -#endif - -#ifdef SendMessage -#undef SendMessage -#endif - -#ifdef PostMessage -#undef PostMessage -#endif - -#ifdef GetObject -#undef GetObject -#endif - -#ifdef RGB -#undef RGB -#endif - -#ifdef TRANSPARENT -#undef TRANSPARENT -#endif - -#ifdef max -#undef max -#endif - -#ifdef min -#undef min -#endif - -#endif // RTYPE_INCLUDE_WINDOWS_H - -#endif // _WIN32 - -#include - -namespace RType { - namespace Core { - namespace Platform { -#ifdef _WIN32 - constexpr const char* PLUGIN_EXTENSION = ".dll"; -#elif defined(__APPLE__) - constexpr const char* PLUGIN_EXTENSION = ".dylib"; -#else - constexpr const char* PLUGIN_EXTENSION = ".so"; -#endif - inline std::string GetPluginPath(const std::string& pluginName) { - return pluginName + PLUGIN_EXTENSION; - } - inline std::string GetPluginPathFromBin(const std::string& pluginName) { - return "../lib/" + pluginName + PLUGIN_EXTENSION; - } - } - } -} diff --git a/libs/engine/include/ECS/AnimationSystem.hpp b/libs/engine/include/ECS/AnimationSystem.hpp deleted file mode 100644 index f5129e6..0000000 --- a/libs/engine/include/ECS/AnimationSystem.hpp +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include "ISystem.hpp" -#include "Animation/IAnimation.hpp" -#include - -namespace RType { -namespace ECS { - - class AnimationSystem : public ISystem { - public: - explicit AnimationSystem(Animation::IAnimation* animation); - ~AnimationSystem() override = default; - - const char* GetName() const override { return "AnimationSystem"; } - void Update(Registry& registry, float deltaTime) override; - - void SetAnimationBackend(Animation::IAnimation* animation) { m_animation = animation; } - Animation::IAnimation* GetAnimationBackend() const { return m_animation; } - - private: - void UpdateSpriteAnimations(Registry& registry, float deltaTime); - void UpdateVisualEffects(Registry& registry, float deltaTime); - void UpdateFloatingTexts(Registry& registry, float deltaTime); - void UpdatePowerUpGlow(Registry& registry, float deltaTime); - void CleanupCompletedAnimations(Registry& registry); - - Animation::IAnimation* m_animation; - std::vector m_entitiesToDestroy; - }; - -} -} diff --git a/libs/engine/include/ECS/AudioSystem.hpp b/libs/engine/include/ECS/AudioSystem.hpp deleted file mode 100644 index aa080ad..0000000 --- a/libs/engine/include/ECS/AudioSystem.hpp +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include "ISystem.hpp" -#include "../Audio/IAudio.hpp" - -namespace RType { - namespace ECS { - - class AudioSystem : public ISystem { - public: - explicit AudioSystem(Audio::IAudio* audio) - : m_audio(audio) {} - - ~AudioSystem() override = default; - - const char* GetName() const override { return "AudioSystem"; } - - void Update(Registry& registry, float deltaTime) override; - - void SetAudioBackend(Audio::IAudio* audio) { m_audio = audio; } - - private: - Audio::IAudio* m_audio; - }; - - } -} - diff --git a/libs/engine/include/ECS/BlackOrbSystem.hpp b/libs/engine/include/ECS/BlackOrbSystem.hpp deleted file mode 100644 index c9f71c1..0000000 --- a/libs/engine/include/ECS/BlackOrbSystem.hpp +++ /dev/null @@ -1,28 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** BlackOrbSystem - Handles black orb attraction and proximity damage -*/ - -#pragma once - -#include "ISystem.hpp" -#include "Registry.hpp" -#include - -namespace RType { - namespace ECS { - - class BlackOrbSystem : public ISystem { - public: - BlackOrbSystem() = default; - ~BlackOrbSystem() override = default; - - const char* GetName() const override { return "BlackOrbSystem"; } - - void Update(Registry& registry, float deltaTime) override; - }; - - } -} diff --git a/libs/engine/include/ECS/CollisionDetectionSystem.hpp b/libs/engine/include/ECS/CollisionDetectionSystem.hpp deleted file mode 100644 index 0919416..0000000 --- a/libs/engine/include/ECS/CollisionDetectionSystem.hpp +++ /dev/null @@ -1,42 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** CollisionDetectionSystem - Unified collision detection system -*/ - -#pragma once - -#include "ISystem.hpp" -#include "Registry.hpp" -#include "Component.hpp" - -namespace RType { - namespace ECS { - - class CollisionDetectionSystem : public ISystem { - public: - CollisionDetectionSystem() = default; - ~CollisionDetectionSystem() override = default; - - const char* GetName() const override { return "CollisionDetectionSystem"; } - void Update(Registry& registry, float deltaTime) override; - - static bool CheckCollision(Registry& registry, Entity a, Entity b); - private: - void ClearCollisionEvents(Registry& registry); - std::vector GetCollidableEntities(Registry& registry); - bool ShouldCollide(Registry& registry, Entity a, Entity b); - - static bool CheckCircleCircle(float x1, float y1, float r1, - float x2, float y2, float r2); - - static bool CheckAABB(float x1, float y1, float w1, float h1, - float x2, float y2, float w2, float h2); - - static bool CheckCircleAABB(float cx, float cy, float radius, - float bx, float by, float bw, float bh); - }; - - } -} diff --git a/libs/engine/include/ECS/Component.hpp b/libs/engine/include/ECS/Component.hpp deleted file mode 100644 index 7920583..0000000 --- a/libs/engine/include/ECS/Component.hpp +++ /dev/null @@ -1,644 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include "Renderer/IRenderer.hpp" -#include "Math/Types.hpp" -#include "Entity.hpp" -#include "Audio/IAudio.hpp" -#include "Animation/AnimationTypes.hpp" - -namespace RType { - - namespace ECS { - using ComponentID = std::type_index; - - struct IComponent { - virtual ~IComponent() = default; - }; - - struct Position : public IComponent { - float x = 0.0f; - float y = 0.0f; - - Position() = default; - Position(float x, float y) - : x(x), y(y) {} - }; - - struct Velocity : public IComponent { - float dx = 0.0f; - float dy = 0.0f; - - Velocity() = default; - Velocity(float dx, float dy) - : dx(dx), dy(dy) {} - }; - - struct Drawable : public IComponent { - Renderer::SpriteId spriteId = Renderer::INVALID_SPRITE_ID; - Math::Vector2 scale{1.0f, 1.0f}; - float rotation = 0.0f; - Math::Vector2 origin{0.0f, 0.0f}; - Math::Color tint{1.0f, 1.0f, 1.0f, 1.0f}; - int layer = 0; - - Drawable() = default; - Drawable(Renderer::SpriteId sprite, int renderLayer = 0) - : spriteId(sprite), layer(renderLayer) {} - }; - - struct NetworkPlayer : public IComponent { - uint8_t playerNumber = 0; - uint64_t playerHash = 0; - char name[32] = {}; - bool ready = false; - - NetworkPlayer() = default; - NetworkPlayer(uint8_t num, uint64_t hash, const char* playerName, bool isReady = false) - : playerNumber(num), playerHash(hash), ready(isReady) { - if (playerName) { - std::strncpy(name, playerName, 31); - name[31] = '\0'; - } - } - }; - - struct BoxCollider : public IComponent { - float width = 0.0f; - float height = 0.0f; - - BoxCollider() = default; - BoxCollider(float width, float height) - : width(width), height(height) {} - }; - - struct Controllable : public IComponent { - float speed = 200.0f; - - Controllable() = default; - Controllable(float moveSpeed) - : speed(moveSpeed) {} - }; - - struct Player : public IComponent { - uint8_t playerNumber = 0; - uint64_t playerHash = 0; - bool isLocalPlayer = false; - uint8_t lives = 3; - - Player() = default; - Player(uint8_t number, uint64_t hash, bool local = false, uint8_t startLives = 3) - : playerNumber(number), playerHash(hash), isLocalPlayer(local), lives(startLives) {} - }; - - enum class EnemyType : uint8_t { - BASIC = 0, - FAST = 1, - TANK = 2, - BOSS = 3, - FORMATION = 4 - }; - - struct Enemy : public IComponent { - EnemyType type = EnemyType::BASIC; - uint32_t id = 0; - - Enemy() = default; - Enemy(EnemyType enemyType, uint32_t enemyId = 0) - : type(enemyType), id(enemyId) {} - }; - - struct Boss : public IComponent { - uint8_t bossId = 1; - - Boss() = default; - Boss(uint8_t id) : bossId(id) {} - }; - - struct BossKilled : public IComponent { - Entity bossEntity; - int levelNumber; - float timeSinceDeath = 0.0f; - - BossKilled() = default; - BossKilled(Entity boss, int level) - : bossEntity(boss), levelNumber(level) {} - }; - - enum class BossAttackPattern { - IDLE = 0, - // Boss 1 - FAN_SPRAY = 1, - DIRECT_SHOT = 2, - CIRCLE = 3, - BLACK_ORB = 4, - THIRD_BULLET = 5, - // Boss 2 - SPIRAL_WAVE = 6, - ANIMATED_ORB = 7, - LASER_BEAM = 8 - }; - - struct BossAttack : public IComponent { - float attackCooldown = 3.0f; - float timeSinceLastAttack = 0.0f; - BossAttackPattern currentPattern = BossAttackPattern::FAN_SPRAY; - - BossAttack() = default; - BossAttack(float cooldown) : attackCooldown(cooldown) {} - }; - - struct BossBullet : public IComponent { - BossBullet() = default; - }; - - struct WaveAttack : public IComponent { - WaveAttack() = default; - }; - - struct SecondAttack : public IComponent { - SecondAttack() = default; - }; - - struct FireBullet : public IComponent { - FireBullet() = default; - }; - - struct Mine : public IComponent { - float proximityRadius = 80.0f; - float explosionRadius = 100.0f; - float lifeTime = 10.0f; - float timer = 0.0f; - bool isExploding = false; - float explosionTimer = 0.0f; - - Mine() = default; - Mine(float proximity, float explosion, float life) - : proximityRadius(proximity), explosionRadius(explosion), lifeTime(life) {} - }; - - struct BossMovementPattern : public IComponent { - float timer = 0.0f; - float amplitudeY = 200.0f; - float amplitudeX = 80.0f; - float frequencyY = 0.5f; - float frequencyX = 0.3f; - float centerY = 0.0f; - float centerX = 0.0f; - - BossMovementPattern() = default; - BossMovementPattern(float ampY, float ampX, float freqY, float freqX, float centerYPos, float centerXPos) - : amplitudeY(ampY), amplitudeX(ampX), frequencyY(freqY), frequencyX(freqX), centerY(centerYPos), centerX(centerXPos) {} - }; - - struct BlackOrb : public IComponent { - float attractionRadius = 200.0f; - float absorptionRadius = 30.0f; - float attractionForce = 500.0f; - bool isActive = true; - - BlackOrb() = default; - BlackOrb(float attraction, float absorption, float force) - : attractionRadius(attraction), absorptionRadius(absorption), attractionForce(force) {} - }; - - struct ProximityDamage : public IComponent { - float damageRadius = 120.0f; - float damageAmount = 1.0f; - float tickRate = 0.5f; - float timeSinceDamage = 0.0f; - - ProximityDamage() = default; - ProximityDamage(float radius, float damage, float rate) - : damageRadius(radius), damageAmount(damage), tickRate(rate) {} - }; - - struct DamageFlash : public IComponent { - float duration = 0.1f; - float timeRemaining = 0.0f; - bool isActive = false; - - DamageFlash() = default; - DamageFlash(float flashDuration) : duration(flashDuration) {} - - void Trigger() { - isActive = true; - timeRemaining = duration; - } - }; - - struct ThirdBullet : public IComponent { - float spawnInterval = 0.3f; - float timeSinceSpawn = 0.0f; - int damage = 50; - bool isActive = true; - - ThirdBullet() = default; - ThirdBullet(float interval, int dmg) - : spawnInterval(interval), damage(dmg) {} - }; - - struct Health : public IComponent { - int current = 100; - int max = 100; - - Health() = default; - Health(int maxHealth) - : current(maxHealth), max(maxHealth) {} - Health(int currentHealth, int maxHealth) - : current(currentHealth), max(maxHealth) {} - }; - - struct ScoreValue : public IComponent { - uint32_t points = 100; - - ScoreValue() = default; - ScoreValue(uint32_t scorePoints) - : points(scorePoints) {} - }; - - struct ScoreTimer : public IComponent { - float elapsed = 0.0f; - - ScoreTimer() = default; - ScoreTimer(float startElapsed) - : elapsed(startElapsed) {} - }; - - struct Damage : public IComponent { - int amount = 10; - - Damage() = default; - Damage(int damageAmount) - : amount(damageAmount) {} - }; - - struct EnemyKilled : public IComponent { - uint32_t enemyId = 0; - Entity killedBy = NULL_ENTITY; - - EnemyKilled() = default; - EnemyKilled(uint32_t id, Entity killer = NULL_ENTITY) - : enemyId(id), killedBy(killer) {} - }; - - struct Bullet : public IComponent { - Entity owner = NULL_ENTITY; - - Bullet() = default; - Bullet(Entity shooter) - : owner(shooter) {} - }; - - struct Shooter : public IComponent { - float fireRate = 0.2f; - float cooldown = 0.0f; - float offsetX = 50.0f; - float offsetY = 20.0f; - - Shooter() = default; - Shooter(float rate, float oX = 50.0f, float oY = 20.0f) : fireRate(rate), offsetX(oX), offsetY(oY) {} - }; - - struct ShootCommand : public IComponent { - bool wantsToShoot = false; - - ShootCommand() = default; - ShootCommand(bool shoot) : wantsToShoot(shoot) {} - }; - - struct Scrollable : public IComponent { - float speed = -100.0f; - - Scrollable() = default; - Scrollable(float scrollSpeed) : speed(scrollSpeed) {} - }; - - struct Obstacle : public IComponent { - bool blocking = true; - - Obstacle() = default; - Obstacle(bool isBlocking) : blocking(isBlocking) {} - }; - - struct ObstacleVisual : public IComponent { - }; - - // Stable network identifier for server->client entity mirroring. - // IMPORTANT: This must live on the entity as a component, because raw ECS entity IDs are recycled. - // If we instead map "ECS entity id -> network id" in a hash map, then destroying and reusing an - // ECS id in the same tick can cause a new entity to inherit the old network id (type confusion). - struct NetworkId : public IComponent { - uint32_t id = 0; - - NetworkId() = default; - explicit NetworkId(uint32_t networkId) : id(networkId) {} - }; - - struct ObstacleMetadata : public IComponent { - uint32_t uniqueId = 0; - Entity visualEntity = NULL_ENTITY; - float offsetX = 0.0f; - float offsetY = 0.0f; - - ObstacleMetadata() = default; - ObstacleMetadata(uint32_t id, - Entity visual = NULL_ENTITY, - float offsetX = 0.0f, - float offsetY = 0.0f) - : uniqueId(id), - visualEntity(visual), - offsetX(offsetX), - offsetY(offsetY) {} - }; - - struct Invincibility : public IComponent { - float remainingTime = 0.0f; - - Invincibility() = default; - Invincibility(float duration) : remainingTime(duration) {} - }; - - struct CollisionLayer : public IComponent { - uint16_t layer = 0; // What layer this entity is on - uint16_t mask = 0xFFFF; // Which layers this entity collides with - - CollisionLayer() = default; - CollisionLayer(uint16_t l, uint16_t m) : layer(l), mask(m) {} - }; - - // masks for collision layers - namespace CollisionLayers { - constexpr uint16_t NONE = 0; - constexpr uint16_t PLAYER = 1 << 0; // 0x0001 - constexpr uint16_t ENEMY = 1 << 1; // 0x0002 - constexpr uint16_t PLAYER_BULLET = 1 << 2; // 0x0004 - constexpr uint16_t ENEMY_BULLET = 1 << 3; // 0x0008 - constexpr uint16_t OBSTACLE = 1 << 4; // 0x0010 - constexpr uint16_t POWERUP = 1 << 5; // 0x0020 - constexpr uint16_t ALL = 0xFFFF; - } - - struct CircleCollider : public IComponent { - float radius = 0.0f; - - CircleCollider() = default; - CircleCollider(float r) : radius(r) {} - }; - - struct CollisionEvent : public IComponent { - Entity other = NULL_ENTITY; - - CollisionEvent() = default; - CollisionEvent(Entity e) : other(e) {} - }; - - // Powerup system components - enum class PowerUpType : uint8_t { - FIRE_RATE_BOOST = 0, - SPREAD_SHOT = 1, - LASER_BEAM = 2, - FORCE_POD = 3, - SPEED_BOOST = 4, - SHIELD = 5 - }; - - struct PowerUp : public IComponent { - PowerUpType type = PowerUpType::FIRE_RATE_BOOST; - uint32_t id = 0; - - PowerUp() = default; - PowerUp(PowerUpType powerupType, uint32_t powerupId = 0) - : type(powerupType), id(powerupId) {} - }; - - struct ActivePowerUps : public IComponent { - bool hasFireRateBoost = false; - bool hasSpreadShot = false; - bool hasLaserBeam = false; - bool hasShield = false; - float speedMultiplier = 1.0f; - - ActivePowerUps() = default; - }; - - enum class WeaponType : uint8_t { - STANDARD = 0, - SPREAD = 1, - LASER = 2 - }; - - struct WeaponSlot : public IComponent { - WeaponType type = WeaponType::STANDARD; - float fireRate = 0.2f; - float cooldown = 0.0f; - int damage = 25; - bool enabled = true; - - WeaponSlot() = default; - WeaponSlot(WeaponType weaponType, float rate, int dmg) - : type(weaponType), fireRate(rate), damage(dmg) {} - }; - - struct ForcePod : public IComponent { - Entity owner = NULL_ENTITY; - float offsetX = -60.0f; - float offsetY = 0.0f; - bool isAttached = true; - - ForcePod() = default; - ForcePod(Entity ownerEntity, float oX = -60.0f, float oY = 0.0f) - : owner(ownerEntity), offsetX(oX), offsetY(oY) {} - }; - - struct Shield : public IComponent { - float duration = 0.0f; // 0 = permanent (until death) - float timeRemaining = 0.0f; - - Shield() = default; - Shield(float dur = 0.0f) : duration(dur), timeRemaining(dur) {} - }; - - struct SoundEffect : public IComponent { - Audio::SoundId soundId = Audio::INVALID_SOUND_ID; - float volume = 1.0f; - float pitch = 1.0f; - float pan = 0.0f; - bool loop = false; - bool positional = false; - - SoundEffect() = default; - SoundEffect(Audio::SoundId id, float vol = 1.0f) - : soundId(id), volume(vol) {} - }; - - struct MusicEffect : public IComponent { - Audio::MusicId musicId = Audio::INVALID_MUSIC_ID; - bool play = true; - bool stop = false; - float volume = 1.0f; - float pitch = 1.0f; - bool loop = true; - - MusicEffect() = default; - explicit MusicEffect(Audio::MusicId id) - : musicId(id) {} - }; - - struct SpriteAnimation : public IComponent { - Animation::AnimationClipId clipId = Animation::INVALID_CLIP_ID; - float currentTime = 0.0f; - float playbackSpeed = 1.0f; - bool playing = true; - bool looping = false; - bool destroyOnComplete = false; - - std::size_t currentFrameIndex = 0; - Math::Rectangle currentRegion{}; - - SpriteAnimation() = default; - SpriteAnimation(Animation::AnimationClipId clip, bool loop = false, float speed = 1.0f) - : clipId(clip), playbackSpeed(speed), looping(loop) {} - }; - - struct AnimationStateMachine : public IComponent { - Animation::AnimationGraphId graphId = Animation::INVALID_GRAPH_ID; - Animation::AnimationStateId currentState = Animation::INVALID_STATE_ID; - Animation::AnimationStateId previousState = Animation::INVALID_STATE_ID; - float stateTime = 0.0f; - float blendFactor = 0.0f; - float blendDuration = 0.0f; - bool isTransitioning = false; - - static constexpr std::size_t MAX_PARAMS = 8; - std::array parameters{}; - std::array, MAX_PARAMS> parameterNames{}; - std::size_t parameterCount = 0; - - AnimationStateMachine() = default; - explicit AnimationStateMachine(Animation::AnimationGraphId graph) - : graphId(graph) {} - - void SetParameter(const char* name, float value) { - for (std::size_t i = 0; i < parameterCount; ++i) { - if (std::strncmp(parameterNames[i].data(), name, 31) == 0) { - parameters[i] = value; - return; - } - } - if (parameterCount < MAX_PARAMS) { - std::strncpy(parameterNames[parameterCount].data(), name, 31); - parameterNames[parameterCount][31] = '\0'; - parameters[parameterCount] = value; - parameterCount++; - } - } - - float GetParameter(const char* name) const { - for (std::size_t i = 0; i < parameterCount; ++i) { - if (std::strncmp(parameterNames[i].data(), name, 31) == 0) { - return parameters[i]; - } - } - return 0.0f; - } - }; - - struct AnimatedSprite : public IComponent { - bool needsUpdate = true; - - AnimatedSprite() = default; - }; - - struct VisualEffect : public IComponent { - Animation::EffectType type = Animation::EffectType::EXPLOSION_SMALL; - float lifetime = 0.0f; - float maxLifetime = 1.0f; - Entity owner = NULL_ENTITY; - float offsetX = 0.0f; - float offsetY = 0.0f; - - VisualEffect() = default; - VisualEffect(Animation::EffectType t, float duration) - : type(t), maxLifetime(duration) {} - VisualEffect(Animation::EffectType t, float duration, Entity ownerEntity, float offX, float offY) - : type(t), maxLifetime(duration), owner(ownerEntity), offsetX(offX), offsetY(offY) {} - }; - - struct FloatingText : public IComponent { - char text[32] = {}; - float lifetime = 0.0f; - float maxLifetime = 1.5f; - float velocityY = -50.0f; - float fadeStartTime = 0.5f; - Math::Color color{1.0f, 1.0f, 1.0f, 1.0f}; - - FloatingText() = default; - FloatingText(const char* txt, float duration, const Math::Color& col) - : maxLifetime(duration), color(col) { - if (txt) { - std::strncpy(text, txt, 31); - text[31] = '\0'; - } - } - }; - - struct AnimationEvents : public IComponent { - static constexpr std::size_t MAX_EVENTS = 4; - std::array, MAX_EVENTS> eventNames{}; - std::size_t eventCount = 0; - - AnimationEvents() = default; - - void PushEvent(const char* name) { - if (eventCount < MAX_EVENTS && name) { - std::strncpy(eventNames[eventCount].data(), name, 31); - eventNames[eventCount][31] = '\0'; - eventCount++; - } - } - - void Clear() { eventCount = 0; } - - bool HasEvent(const char* name) const { - for (std::size_t i = 0; i < eventCount; ++i) { - if (std::strncmp(eventNames[i].data(), name, 31) == 0) { - return true; - } - } - return false; - } - }; - - struct AnimationLayer : public IComponent { - Animation::AnimationClipId clipId = Animation::INVALID_CLIP_ID; - float currentTime = 0.0f; - float weight = 1.0f; - float playbackSpeed = 1.0f; - bool additive = false; - int layerIndex = 0; - - AnimationLayer() = default; - AnimationLayer(Animation::AnimationClipId clip, int layer, float w = 1.0f) - : clipId(clip), weight(w), layerIndex(layer) {} - }; - - struct PowerUpGlow : public IComponent { - float time = 0.0f; - float pulseSpeed = 2.0f; - float minAlpha = 0.7f; - float maxAlpha = 1.0f; - float baseScale = 2.5f; - float scalePulse = 0.08f; - - PowerUpGlow() = default; - }; - } - -} diff --git a/libs/engine/include/ECS/Components/Clickable.hpp b/libs/engine/include/ECS/Components/Clickable.hpp deleted file mode 100644 index 1556bc3..0000000 --- a/libs/engine/include/ECS/Components/Clickable.hpp +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include "ECS/Component.hpp" -#include - -namespace RType { - namespace ECS { - - enum class ButtonState { - Idle, - Hover, - Active - }; - - struct Clickable : public IComponent { - float width = 0.0f; - float height = 0.0f; - - ButtonState state = ButtonState::Idle; - - int actionId = 0; - - Clickable() = default; - Clickable(float w, float h, int action) - : width(w), height(h), actionId(action) {} - }; - - } -} diff --git a/libs/engine/include/ECS/Components/TextLabel.hpp b/libs/engine/include/ECS/Components/TextLabel.hpp deleted file mode 100644 index 2f6af54..0000000 --- a/libs/engine/include/ECS/Components/TextLabel.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include "ECS/Component.hpp" -#include "Math/Types.hpp" -#include - -namespace RType { - namespace ECS { - - struct TextLabel : public IComponent { - std::string text; - Renderer::FontId fontId = 0; - unsigned int characterSize = 24; - Math::Color color{1.0f, 1.0f, 1.0f, 1.0f}; - - float offsetX = 0.0f; - float offsetY = 0.0f; - bool centered = false; - - TextLabel() = default; - TextLabel(const std::string& t, Renderer::FontId font, unsigned int size = 24) - : text(t), fontId(font), characterSize(size) {} - }; - } -} diff --git a/libs/engine/include/ECS/EffectFactory.hpp b/libs/engine/include/ECS/EffectFactory.hpp deleted file mode 100644 index e4a3b7d..0000000 --- a/libs/engine/include/ECS/EffectFactory.hpp +++ /dev/null @@ -1,115 +0,0 @@ -#pragma once - -#include "Entity.hpp" -#include "Animation/AnimationTypes.hpp" -#include "Renderer/IRenderer.hpp" -#include "Math/Types.hpp" - -namespace RType { -namespace ECS { - - class Registry; - - struct EffectConfig { - Animation::AnimationClipId explosionSmall = Animation::INVALID_CLIP_ID; - Animation::AnimationClipId explosionLarge = Animation::INVALID_CLIP_ID; - Animation::AnimationClipId bulletImpact = Animation::INVALID_CLIP_ID; - Animation::AnimationClipId powerUpCollect = Animation::INVALID_CLIP_ID; - Animation::AnimationClipId bossPhaseTransition = Animation::INVALID_CLIP_ID; - Animation::AnimationClipId spawnEffect = Animation::INVALID_CLIP_ID; - Animation::AnimationClipId deathEffect = Animation::INVALID_CLIP_ID; - Animation::AnimationClipId shootingAnimation = Animation::INVALID_CLIP_ID; - Animation::AnimationClipId forcePodAnimation = Animation::INVALID_CLIP_ID; - Animation::AnimationClipId beamAnimation = Animation::INVALID_CLIP_ID; - Animation::AnimationClipId hitAnimation = Animation::INVALID_CLIP_ID; - - Renderer::FontId damageFont = Renderer::INVALID_FONT_ID; - Renderer::TextureId effectsTexture = Renderer::INVALID_TEXTURE_ID; - Renderer::SpriteId effectsSprite = Renderer::INVALID_SPRITE_ID; - Renderer::TextureId shootingTexture = Renderer::INVALID_TEXTURE_ID; - Renderer::SpriteId shootingSprite = Renderer::INVALID_SPRITE_ID; - Renderer::TextureId forcePodTexture = Renderer::INVALID_TEXTURE_ID; - Renderer::SpriteId forcePodSprite = Renderer::INVALID_SPRITE_ID; - Renderer::TextureId beamTexture = Renderer::INVALID_TEXTURE_ID; - Renderer::SpriteId beamSprite = Renderer::INVALID_SPRITE_ID; - Renderer::TextureId hitTexture = Renderer::INVALID_TEXTURE_ID; - Renderer::SpriteId hitSprite = Renderer::INVALID_SPRITE_ID; - Math::Rectangle forcePodFirstFrameRegion{}; - Math::Rectangle shootingFirstFrameRegion{}; - Math::Rectangle explosionFirstFrameRegion{}; - Math::Rectangle beamFirstFrameRegion{}; - Math::Rectangle hitFirstFrameRegion{}; - - EffectConfig() = default; - }; - - class EffectFactory { - public: - explicit EffectFactory(const EffectConfig& config); - ~EffectFactory() = default; - - void SetConfig(const EffectConfig& config) { m_config = config; } - const EffectConfig& GetConfig() const { return m_config; } - - Entity CreateExplosionSmall(Registry& registry, float x, float y); - - Entity CreateExplosionLarge(Registry& registry, float x, float y); - - Entity CreateExplosion(Registry& registry, - float x, float y, - Animation::AnimationClipId clipId, - float scale = 1.0f, - int layer = 100); - - Entity CreateBulletImpact(Registry& registry, float x, float y); - - Entity CreateDamageNumber(Registry& registry, - float x, float y, - int damage, - const Math::Color& color = {1.0f, 0.3f, 0.3f, 1.0f}); - - Entity CreateScorePopup(Registry& registry, - float x, float y, - int score, - const Math::Color& color = {1.0f, 1.0f, 0.0f, 1.0f}); - - Entity CreateFloatingText(Registry& registry, - float x, float y, - const char* text, - const Math::Color& color, - float duration = 1.5f); - - Entity CreatePowerUpEffect(Registry& registry, float x, float y); - - Entity CreateBossTransitionEffect(Registry& registry, float x, float y); - - Entity CreateSpawnEffect(Registry& registry, float x, float y); - - Entity CreateDeathEffect(Registry& registry, float x, float y); - - Entity CreateShootingEffect(Registry& registry, float x, float y, Entity owner = NULL_ENTITY); - - Entity CreateBeam(Registry& registry, float x, float y, Entity owner, float chargeTime, float screenWidth = 1280.0f, float beamHeight = 64.0f); - - Entity CreateHitEffect(Registry& registry, float x, float y); - - void CreateHitMarker(Registry& registry, Entity target, int damage); - - void CreateEnemyDeathEffect(Registry& registry, - float x, float y, - int scoreValue); - - private: - EffectConfig m_config; - - Entity CreateBaseEffect(Registry& registry, - float x, float y, - Animation::EffectType type, - float duration, - Entity owner = NULL_ENTITY, - float offsetX = 0.0f, - float offsetY = 0.0f); - }; - -} -} diff --git a/libs/engine/include/ECS/EnemyFactory.hpp b/libs/engine/include/ECS/EnemyFactory.hpp deleted file mode 100644 index 8410294..0000000 --- a/libs/engine/include/ECS/EnemyFactory.hpp +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include "Registry.hpp" -#include "Component.hpp" -#include "Renderer/IRenderer.hpp" -#include "Math/Types.hpp" -#include - -namespace RType { - - namespace ECS { - - class EnemyFactory { - public: - static Entity CreateEnemy(Registry& registry, EnemyType type, float startX, float startY, - Renderer::IRenderer* renderer); - - static float GetEnemySpeed(EnemyType type); - static int GetEnemyHealth(EnemyType type); - static int GetEnemyDamage(EnemyType type); - static uint32_t GetEnemyScore(EnemyType type); - }; - - } - -} diff --git a/libs/engine/include/ECS/Entity.hpp b/libs/engine/include/ECS/Entity.hpp deleted file mode 100644 index c40d5ae..0000000 --- a/libs/engine/include/ECS/Entity.hpp +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once -#include - -namespace RType { - - namespace ECS { - using Entity = uint32_t; - constexpr Entity NULL_ENTITY = 0; - } - -} \ No newline at end of file diff --git a/libs/engine/include/ECS/HealthSystem.hpp b/libs/engine/include/ECS/HealthSystem.hpp deleted file mode 100644 index 1944983..0000000 --- a/libs/engine/include/ECS/HealthSystem.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include "ISystem.hpp" - -namespace RType { - - namespace ECS { - - class HealthSystem : public ISystem { - public: - HealthSystem() = default; - ~HealthSystem() override = default; - - void Update(Registry& registry, float deltaTime) override; - const char* GetName() const override { return "HealthSystem"; } - private: - void checkAndDestroyDeadEntities(Registry& registry); - }; - - } - -} diff --git a/libs/engine/include/ECS/ISystem.hpp b/libs/engine/include/ECS/ISystem.hpp deleted file mode 100644 index 9b722dd..0000000 --- a/libs/engine/include/ECS/ISystem.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include "Registry.hpp" -#include - -namespace RType { - - namespace ECS { - - class ISystem { - public: - virtual ~ISystem() = default; - - virtual void Update(Registry& registry, float deltaTime) = 0; - virtual const char* GetName() const = 0; - virtual bool Initialize(Registry& /* registry */) { return true; } - virtual void Shutdown() {} - }; - - } - -} diff --git a/libs/engine/include/ECS/InputSystem.hpp b/libs/engine/include/ECS/InputSystem.hpp deleted file mode 100644 index 28f46e4..0000000 --- a/libs/engine/include/ECS/InputSystem.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "ISystem.hpp" -#include "Renderer/IRenderer.hpp" - -namespace RType { - namespace ECS { - - class InputSystem : public ISystem { - public: - InputSystem(Renderer::IRenderer* renderer); - - const char* GetName() const override { return "InputSystem"; } - void Update(Registry& registry, float deltaTime) override; - private: - Renderer::IRenderer* m_renderer; - }; - - } -} \ No newline at end of file diff --git a/libs/engine/include/ECS/LevelLoader.hpp b/libs/engine/include/ECS/LevelLoader.hpp deleted file mode 100644 index cb4c96d..0000000 --- a/libs/engine/include/ECS/LevelLoader.hpp +++ /dev/null @@ -1,189 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** LevelLoader - JSON-based level loading system -*/ - -#pragma once - -#include "Registry.hpp" -#include "Component.hpp" -#include "Renderer/IRenderer.hpp" -#include -#include -#include -#include - -namespace RType { - - namespace ECS { - - struct FontDef { - std::string path; - uint32_t size = 16; - }; - - struct ColliderDef { - float x = 0.0f; - float y = 0.0f; - float width = 0.0f; - float height = 0.0f; - }; - - struct ObstacleDef { - std::string texture; - float x = 0.0f; - float y = 0.0f; - float scaleWidth = 1200.0f; - float scaleHeight = 720.0f; - float scrollSpeed = -150.0f; - int layer = 1; - std::vector colliders; - }; - - struct EnemyDef { - std::string type = "BASIC"; - float x = 0.0f; - float y = 0.0f; - }; - - struct BossDef { - std::string texture; - float x = 0.0f; - float y = 0.0f; - float width = 200.0f; - float height = 200.0f; - int health = 1000; - float scrollSpeed = -300.0f; - int attackPattern = 1; - uint8_t bossId = 1; - }; - - struct PlayerSpawnDef { - float x = 100.0f; - float y = 360.0f; - }; - - struct BackgroundDef { - std::string texture; - float scrollSpeed = -150.0f; - int copies = 3; - int layer = -100; - }; - - struct PlayerConfig { - float movementSpeed = 200.0f; - float fireRate = 0.2f; - float bulletOffsetX = 50.0f; - float bulletOffsetY = 25.0f; - int maxHealth = 100; - }; - - struct LevelConfig { - float screenWidth = 1280.0f; - float screenHeight = 720.0f; - float powerUpSpawnInterval = 5.0f; - PlayerConfig playerDefaults; - }; - - struct LevelData { - std::string name; - - // Asset paths - std::unordered_map textures; - std::unordered_map fonts; - - // Level configuration - LevelConfig config; - - // Level elements - BackgroundDef background; - std::vector obstacles; - std::vector enemies; - std::vector playerSpawns; - std::optional boss; - }; - - struct LoadedAssets { - std::unordered_map textures; - std::unordered_map sprites; - std::unordered_map fonts; - }; - - struct CreatedEntities { - std::vector backgrounds; - std::vector obstacleVisuals; - std::vector obstacleColliders; - std::vector enemies; - Entity boss = NULL_ENTITY; - }; - - class LevelLoader { - public: - static LevelData LoadFromFile(const std::string& path); - static LevelData LoadFromString(const std::string& jsonString); - - static std::string SerializeToString(const LevelData& level); - static void SaveToFile(const LevelData& level, const std::string& path); - - static LoadedAssets LoadAssets( - const LevelData& level, - Renderer::IRenderer* renderer); - - static CreatedEntities CreateEntities( - Registry& registry, - const LevelData& level, - const LoadedAssets& assets, - Renderer::IRenderer* renderer); - - static CreatedEntities CreateServerEntities( - Registry& registry, - const LevelData& level); - - static const std::vector& GetPlayerSpawns(const LevelData& level); - private: - static EnemyType ParseEnemyType(const std::string& typeStr); - - static void CreateBackgrounds( - Registry& registry, - const BackgroundDef& background, - const LoadedAssets& assets, - Renderer::IRenderer* renderer, - CreatedEntities& entities); - - static void CreateObstacles( - Registry& registry, - const std::vector& obstacles, - const LoadedAssets& assets, - Renderer::IRenderer* renderer, - CreatedEntities& entities, - uint32_t& obstacleIdCounter); - - static void CreateEnemies( - Registry& registry, - const std::vector& enemies, - const LoadedAssets& assets, - Renderer::IRenderer* renderer, - CreatedEntities& entities); - - static void CreateServerObstacles( - Registry& registry, - const std::vector& obstacles, - CreatedEntities& entities, - uint32_t& obstacleIdCounter); - - static void CreateServerEnemies( - Registry& registry, - const std::vector& enemies, - CreatedEntities& entities); - - static void CreateServerBoss( - Registry& registry, - const std::optional& boss, - CreatedEntities& entities); - }; - - } - -} diff --git a/libs/engine/include/ECS/MenuSystem.hpp b/libs/engine/include/ECS/MenuSystem.hpp deleted file mode 100644 index 93530d8..0000000 --- a/libs/engine/include/ECS/MenuSystem.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include "ISystem.hpp" -#include "Renderer/IRenderer.hpp" -#include - -namespace RType { - namespace ECS { - - class MenuSystem : public ISystem { - public: - using ActionCallback = std::function; - - explicit MenuSystem(Renderer::IRenderer* renderer); - ~MenuSystem() override = default; - - void Update(Registry& registry, float deltaTime) override; - const char* GetName() const override { - return "MenuSystem"; - } - - void SetActionCallback(ActionCallback callback) { - m_callback = callback; - } - private: - Renderer::IRenderer* m_renderer; - ActionCallback m_callback; - bool m_wasMouseDown = false; - }; - - } -} diff --git a/libs/engine/include/ECS/MovementSystem.hpp b/libs/engine/include/ECS/MovementSystem.hpp deleted file mode 100644 index e306d27..0000000 --- a/libs/engine/include/ECS/MovementSystem.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "ISystem.hpp" - -namespace RType { - - namespace ECS { - - class MovementSystem : public ISystem { - public: - MovementSystem() = default; - ~MovementSystem() override = default; - - void Update(Registry& registry, float deltaTime) override; - const char* GetName() const override { return "MovementSystem"; } - }; - - } - -} diff --git a/libs/engine/include/ECS/PlayerCollisionResponseSystem.hpp b/libs/engine/include/ECS/PlayerCollisionResponseSystem.hpp deleted file mode 100644 index 0acf686..0000000 --- a/libs/engine/include/ECS/PlayerCollisionResponseSystem.hpp +++ /dev/null @@ -1,38 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** PlayerCollisionResponseSystem - Handles player collision responses -*/ - -#pragma once - -#include "ISystem.hpp" -#include "Registry.hpp" -#include "Component.hpp" - -namespace RType { - namespace ECS { - - /** - * @brief Handles responses to player collisions - * - * Processes CollisionEvent components on players and: - * - Applies damage from enemy collisions - * - Destroys enemies on collision - * - Handles player-obstacle collisions (optional physics) - * - Applies powerup effects (future enhancement) - * - * This system should run after CollisionDetectionSystem - */ - class PlayerCollisionResponseSystem : public ISystem { - public: - PlayerCollisionResponseSystem() = default; - ~PlayerCollisionResponseSystem() override = default; - - const char* GetName() const override { return "PlayerCollisionResponseSystem"; } - void Update(Registry& registry, float deltaTime) override; - }; - - } // namespace ECS -} // namespace RType diff --git a/libs/engine/include/ECS/Registry.hpp b/libs/engine/include/ECS/Registry.hpp deleted file mode 100644 index 3f0e4e6..0000000 --- a/libs/engine/include/ECS/Registry.hpp +++ /dev/null @@ -1,232 +0,0 @@ -#pragma once - -#include "Entity.hpp" -#include "Component.hpp" -#include "SparseArray.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace RType { - - namespace ECS { - - class IComponentPool { - public: - virtual ~IComponentPool() = default; - virtual bool Has(Entity entity) const = 0; - virtual void Remove(Entity entity) = 0; - }; - - template - class ComponentPool : public IComponentPool { - public: - T& Add(Entity entity, T&& component = T{}); - T& Get(Entity entity); - const T& Get(Entity entity) const; - bool Has(Entity entity) const override; - void Remove(Entity entity) override; - std::vector GetEntities() const; - private: - SparseArray m_components; - }; - - class Registry { - public: - Registry(); - ~Registry() = default; - - Entity CreateEntity(); - void DestroyEntity(Entity entity); - bool IsEntityAlive(Entity entity) const; - - template - T& AddComponent(Entity entity, T&& component = T{}); - - template - T& GetComponent(Entity entity); - - template - const T& GetComponent(Entity entity) const; - - template - bool HasComponent(Entity entity) const; - - template - void RemoveComponent(Entity entity); - - template - std::vector GetEntitiesWithComponent() const; - size_t GetEntityCount() const { return m_entityCount; } - private: - template - ComponentPool* GetOrCreatePool(); - template - ComponentPool* GetPool(); - template - const ComponentPool* GetPool() const; - - Entity m_nextEntityID; - size_t m_entityCount; - std::unordered_set m_aliveEntities; - std::unordered_map> m_componentPools; - std::vector m_freeEntityIds; - }; - - template - T& ComponentPool::Add(Entity entity, T&& component) { - return m_components.insert_at(entity, std::move(component)).value(); - } - - template - T& ComponentPool::Get(Entity entity) { - if (entity >= m_components.size() || !m_components[entity].has_value()) { - std::ostringstream oss; - oss << "Component '" << typeid(T).name() << "' not found for entity " << entity; - throw std::runtime_error(oss.str()); - } - return m_components[entity].value(); - } - - template - const T& ComponentPool::Get(Entity entity) const { - if (entity >= m_components.size() || !m_components[entity].has_value()) { - std::ostringstream oss; - oss << "Component '" << typeid(T).name() << "' not found for entity " << entity; - throw std::runtime_error(oss.str()); - } - return m_components[entity].value(); - } - - template - bool ComponentPool::Has(Entity entity) const { - return entity < m_components.size() && m_components[entity].has_value(); - } - - template - void ComponentPool::Remove(Entity entity) { - if (entity >= m_components.size() || !m_components[entity].has_value()) { - std::ostringstream oss; - oss << "Component '" << typeid(T).name() << "' not found for entity " << entity; - throw std::runtime_error(oss.str()); - } - m_components.erase(entity); - } - - template - std::vector ComponentPool::GetEntities() const { - std::vector entities; - for (size_t i = 0; i < m_components.size(); ++i) { - if (m_components[i].has_value()) { - entities.push_back(static_cast(i)); - } - } - return entities; - } - - template - T& Registry::AddComponent(Entity entity, T&& component) { - if (entity == NULL_ENTITY) { - throw std::invalid_argument("Cannot add component to NULL_ENTITY"); - } - if (!IsEntityAlive(entity)) { - std::ostringstream oss; - oss << "Cannot add component '" << typeid(T).name() << "' to non-existent entity " - << entity; - throw std::runtime_error(oss.str()); - } - ComponentPool* pool = GetOrCreatePool(); - return pool->Add(entity, std::forward(component)); - } - - template - T& Registry::GetComponent(Entity entity) { - ComponentPool* pool = GetPool(); - if (!pool) { - std::ostringstream oss; - oss << "Component type '" << typeid(T).name() << "' not registered"; - throw std::runtime_error(oss.str()); - } - return pool->Get(entity); - } - - template - const T& Registry::GetComponent(Entity entity) const { - const ComponentPool* pool = GetPool(); - if (!pool) { - std::ostringstream oss; - oss << "Component type '" << typeid(T).name() << "' not registered"; - throw std::runtime_error(oss.str()); - } - return pool->Get(entity); - } - - template - bool Registry::HasComponent(Entity entity) const { - const ComponentPool* pool = GetPool(); - if (!pool) { - return false; - } - return pool->Has(entity); - } - - template - void Registry::RemoveComponent(Entity entity) { - ComponentPool* pool = GetPool(); - if (pool) { - pool->Remove(entity); - } - } - - template - std::vector Registry::GetEntitiesWithComponent() const { - const ComponentPool* pool = GetPool(); - if (!pool) { - return std::vector(); - } - return pool->GetEntities(); - } - - template - ComponentPool* Registry::GetOrCreatePool() { - ComponentID typeID = std::type_index(typeid(T)); - auto it = m_componentPools.find(typeID); - if (it == m_componentPools.end()) { - auto pool = std::make_unique>(); - ComponentPool* poolPtr = pool.get(); - m_componentPools[typeID] = std::move(pool); - return poolPtr; - } - return static_cast*>(it->second.get()); - } - - template - ComponentPool* Registry::GetPool() { - ComponentID typeID = std::type_index(typeid(T)); - auto it = m_componentPools.find(typeID); - if (it == m_componentPools.end()) { - return nullptr; - } - return static_cast*>(it->second.get()); - } - - template - const ComponentPool* Registry::GetPool() const { - ComponentID typeID = std::type_index(typeid(T)); - auto it = m_componentPools.find(typeID); - if (it == m_componentPools.end()) { - return nullptr; - } - return static_cast*>(it->second.get()); - } - - - } - -} \ No newline at end of file diff --git a/libs/engine/include/ECS/RenderingSystem.hpp b/libs/engine/include/ECS/RenderingSystem.hpp deleted file mode 100644 index bc01ad1..0000000 --- a/libs/engine/include/ECS/RenderingSystem.hpp +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include "ISystem.hpp" -#include "Renderer/IRenderer.hpp" - -namespace RType { - - namespace ECS { - - class RenderingSystem : public ISystem { - public: - explicit RenderingSystem(Renderer::IRenderer* renderer); - ~RenderingSystem() override = default; - - void Update(Registry& registry, float deltaTime) override; - const char* GetName() const override { return "RenderingSystem"; } - private: - Renderer::IRenderer* m_renderer; - }; - - } - -} diff --git a/libs/engine/include/ECS/ScoreSystem.hpp b/libs/engine/include/ECS/ScoreSystem.hpp deleted file mode 100644 index 80e1214..0000000 --- a/libs/engine/include/ECS/ScoreSystem.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include "ISystem.hpp" - -namespace RType { - namespace ECS { - - class ScoreSystem : public ISystem { - public: - ScoreSystem() = default; - ~ScoreSystem() override = default; - - const char* GetName() const override { return "ScoreSystem"; } - - void Update(Registry& registry, float deltaTime) override; - }; - } -} \ No newline at end of file diff --git a/libs/engine/include/ECS/ScrollingSystem.hpp b/libs/engine/include/ECS/ScrollingSystem.hpp deleted file mode 100644 index 6037b0c..0000000 --- a/libs/engine/include/ECS/ScrollingSystem.hpp +++ /dev/null @@ -1,20 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** ScrollingSystem -*/ - -#pragma once -#include "ISystem.hpp" - -namespace RType { - namespace ECS { - class ScrollingSystem : public ISystem { - public: - ScrollingSystem() = default; - const char* GetName() const override { return "ScrollingSystem"; } - void Update(Registry& registry, float deltaTime) override; - }; - } -} diff --git a/libs/engine/include/ECS/ShieldSystem.hpp b/libs/engine/include/ECS/ShieldSystem.hpp deleted file mode 100644 index f4a77d8..0000000 --- a/libs/engine/include/ECS/ShieldSystem.hpp +++ /dev/null @@ -1,25 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** ShieldSystem -*/ - -#pragma once - -#include "ISystem.hpp" - -namespace RType { - namespace ECS { - - class ShieldSystem : public ISystem { - public: - ShieldSystem() = default; - ~ShieldSystem() override = default; - - void Update(Registry& registry, float deltaTime) override; - const char* GetName() const override { return "ShieldSystem"; } - }; - - } -} diff --git a/libs/engine/include/ECS/ShootingSystem.hpp b/libs/engine/include/ECS/ShootingSystem.hpp deleted file mode 100644 index fa309b1..0000000 --- a/libs/engine/include/ECS/ShootingSystem.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include "ISystem.hpp" -#include "../Renderer/IRenderer.hpp" -#include "../Audio/IAudio.hpp" - -namespace RType { - namespace ECS { - - class EffectFactory; - - class ShootingSystem : public ISystem { - public: - ShootingSystem(Renderer::SpriteId bulletSprite); - ~ShootingSystem() override = default; - - const char* GetName() const override { return "ShootingSystem"; } - - void Update(Registry& registry, float deltaTime) override; - - void SetShootSound(Audio::SoundId soundId) { m_shootSound = soundId; } - void SetEffectFactory(EffectFactory* effectFactory) { m_effectFactory = effectFactory; } - private: - void CreateSpreadShot(Registry& registry, Entity shooter, const Position& pos, int damage); - void CreateLaserShot(Registry& registry, Entity shooter, const Position& pos, int damage); - - Renderer::SpriteId m_bulletSprite; - Audio::SoundId m_shootSound = Audio::INVALID_SOUND_ID; - EffectFactory* m_effectFactory = nullptr; - }; - } -} \ No newline at end of file diff --git a/libs/engine/include/ECS/SparseArray.hpp b/libs/engine/include/ECS/SparseArray.hpp deleted file mode 100644 index 8520f52..0000000 --- a/libs/engine/include/ECS/SparseArray.hpp +++ /dev/null @@ -1,184 +0,0 @@ -/** - * @file SparseArray.hpp - * @brief Implémentation d'un sparse array pour le système ECS - */ - -#pragma once - -#include -#include -#include -#include - -/** - * @class SparseArray - * @brief Container sparse array pour stocker des composants ECS - * - * Un sparse array est un tableau creux où l'index correspond directement à l'ID d'une entité. - * Les composants sont stockés dans un std::vector>, permettant - * d'avoir des "trous" (indices sans composant). - * - * @tparam Component Le type de composant à stocker - * - * @note Cette implémentation est optimisée pour les accès directs par index (O(1)) - * et les itérations séquentielles (cache-friendly). - * - * @example - * @code - * SparseArray positions; - * positions.insert_at(5, Position{10, 20}); // Entity 5 - * positions.insert_at(7, Position{30, 40}); // Entity 7 - * - * // Accès direct - * if (positions[5].has_value()) { - * Position& pos = positions[5].value(); - * } - * @endcode - */ -template -class SparseArray { - using value_type = std::optional; - - using reference_type = value_type&; - using const_reference_type = value_type const&; - - using sparse_array_t = std::vector; - - using size_type = typename sparse_array_t::size_type; - using iterator = typename sparse_array_t::iterator; - using const_iterator = typename sparse_array_t::const_iterator; -public: - SparseArray() = default; - - SparseArray(SparseArray const& other) : _data(other._data) { - } - - SparseArray(SparseArray&& other) noexcept : _data(std::move(other._data)) { - } - - ~SparseArray() = default; - - SparseArray& operator=(SparseArray const& other) { - if (this != &other) { - _data = other._data; - } - return *this; - } - - SparseArray& operator=(SparseArray&& other) noexcept { - if (this != &other) { - _data = std::move(other._data); - } - return *this; - } - - iterator begin() { - return _data.begin(); - } - - const_iterator begin() const { - return _data.begin(); - } - - const_iterator cbegin() const { - return _data.cbegin(); - } - - iterator end() { - return _data.end(); - } - - const_iterator end() const { - return _data.end(); - } - - const_iterator cend() const { - return _data.cend(); - } - - reference_type operator[](size_t idx) { - if (idx >= _data.size()) { - ensure_capacity(idx + 1); - } - return _data[idx]; - } - - const_reference_type operator[](size_t idx) const { - if (idx >= _data.size()) { - throw std::out_of_range("SparseArray::operator[] const: index out of range"); - } - return _data[idx]; - } - - size_type size() const { - return _data.size(); - } - - reference_type insert_at(size_type pos, Component const& component) { - if (pos >= _data.size()) { - ensure_capacity(pos + 1); - } - _data[pos] = component; - return _data[pos]; - } - - reference_type insert_at(size_type pos, Component&& component) { - if (pos >= _data.size()) { - ensure_capacity(pos + 1); - } - _data[pos] = std::move(component); - return _data[pos]; - } - - template - reference_type emplace_at(size_type pos, Params&&... params) { - if (pos >= _data.size()) { - ensure_capacity(pos + 1); - } - _data[pos].emplace(std::forward(params)...); - return _data[pos]; - } - - void erase(size_type pos) { - if (pos >= _data.size()) { - return; - } - _data[pos].reset(); - } - - size_type capacity() const { - return _data.capacity(); - } - - bool empty() const { - for (const auto& opt : _data) { - if (opt.has_value()) { - return false; - } - } - return true; - } - - void clear() { - _data.clear(); - } - - void reserve(size_type new_capacity) { - _data.reserve(new_capacity); - } -private: - void ensure_capacity(size_type required_size) { - if (required_size <= _data.size()) { - return; - } - - size_type new_capacity = required_size + (required_size / 2); - if (new_capacity < 8) { - new_capacity = 8; - } - _data.reserve(new_capacity); - _data.resize(required_size); - } - - sparse_array_t _data; -}; \ No newline at end of file diff --git a/libs/engine/include/ECS/TextRenderingSystem.hpp b/libs/engine/include/ECS/TextRenderingSystem.hpp deleted file mode 100644 index 2f31b89..0000000 --- a/libs/engine/include/ECS/TextRenderingSystem.hpp +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include "ISystem.hpp" -#include "Renderer/IRenderer.hpp" - -namespace RType { - namespace ECS { - - class TextRenderingSystem : public ISystem { - public: - explicit TextRenderingSystem(Renderer::IRenderer* renderer); - ~TextRenderingSystem() override = default; - - void Update(Registry& registry, float deltaTime) override; - const char* GetName() const override { return "TextRenderingSystem"; } - private: - Renderer::IRenderer* m_renderer; - }; - - } -} diff --git a/libs/engine/include/Math/Types.hpp b/libs/engine/include/Math/Types.hpp deleted file mode 100644 index e1f145c..0000000 --- a/libs/engine/include/Math/Types.hpp +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -namespace Math { - - struct Vector2 { - float x = 0.0f; - float y = 0.0f; - - Vector2() = default; - Vector2(float x, float y) - : x(x), y(y) {} - }; - - struct Color { - float r = 1.0f; - float g = 1.0f; - float b = 1.0f; - float a = 1.0f; - - Color() = default; - Color(float r, float g, float b, float a = 1.0f) - : r(r), g(g), b(b), a(a) {} - }; - - struct Rectangle { - Vector2 position{0.0f, 0.0f}; - Vector2 size{0.0f, 0.0f}; - }; - -} diff --git a/libs/engine/include/Physics/IPhysics.hpp b/libs/engine/include/Physics/IPhysics.hpp deleted file mode 100644 index e4ca536..0000000 --- a/libs/engine/include/Physics/IPhysics.hpp +++ /dev/null @@ -1,52 +0,0 @@ -#pragma once - -#include -#include -#include "Core/Module.hpp" -#include "Core/Logger.hpp" -#include "Core/Engine.hpp" - -namespace Physics { - - struct Vector2 { - float x = 0.0f; - float y = 0.0f; - - Vector2() = default; - Vector2(float x, float y) - : x(x), y(y) {} - }; - - struct BodyDef { - Vector2 position{0.0f, 0.0f}; - Vector2 velocity{0.0f, 0.0f}; - float mass = 1.0f; - bool isStatic = false; - }; - - class Body { - public: - virtual ~Body() = default; - - virtual Vector2 GetPosition() const = 0; - virtual void SetPosition(const Vector2& position) = 0; - virtual Vector2 GetVelocity() const = 0; - virtual void SetVelocity(const Vector2& velocity) = 0; - }; - - class IPhysics : public RType::Core::IModule { - public: - virtual ~IPhysics() = default; - - virtual const char* GetName() const override = 0; - virtual RType::Core::ModulePriority GetPriority() const override = 0; - virtual bool Initialize(RType::Core::Engine* engine) override = 0; - virtual void Shutdown() override = 0; - virtual void Update(float deltaTime) override = 0; - - virtual std::shared_ptr CreateBody(const BodyDef& def) = 0; - virtual void DestroyBody(Body* body) = 0; - virtual void SetGravity(const Vector2& gravity) = 0; - }; - -} diff --git a/libs/engine/include/Renderer/IRenderer.hpp b/libs/engine/include/Renderer/IRenderer.hpp deleted file mode 100644 index 6012dfb..0000000 --- a/libs/engine/include/Renderer/IRenderer.hpp +++ /dev/null @@ -1,178 +0,0 @@ -#pragma once - -#define RTYPE_INCLUDE_WINDOWS_H -#include "Core/Platform.hpp" -#include -#include -#include "Core/Module.hpp" -#include "Math/Types.hpp" - -namespace Renderer { - - using TextureId = std::uint32_t; - using SpriteId = std::uint32_t; - using FontId = std::uint32_t; - - constexpr TextureId INVALID_TEXTURE_ID = 0; - constexpr SpriteId INVALID_SPRITE_ID = 0; - constexpr FontId INVALID_FONT_ID = 0; - - using Math::Color; - using Math::Rectangle; - using Math::Vector2; - - struct WindowConfig { - std::string title{"R-Type"}; - std::uint32_t width = 1280; - std::uint32_t height = 720; - bool fullscreen = false; - bool resizable = false; - std::uint32_t targetFramerate = 60; - }; - - struct TextureConfig { - bool smooth = true; - bool repeated = false; - bool generateMipmaps = false; - }; - - struct Transform2D { - Vector2 position{0.0f, 0.0f}; - Vector2 scale{1.0f, 1.0f}; - float rotation = 0.0f; - Vector2 origin{0.0f, 0.0f}; - }; - - struct TextParams { - Vector2 position{0.0f, 0.0f}; - Color color{}; - float rotation = 0.0f; - float scale = 1.0f; - float letterSpacing = 0.0f; - float lineSpacing = 0.0f; - bool centered = false; - }; - - struct Camera2D { - Vector2 center{0.0f, 0.0f}; - Vector2 size{1280.0f, 720.0f}; - }; - - struct RenderStats { - std::uint32_t drawCalls = 0; - std::uint32_t textureSwitches = 0; - }; - - enum class Key { - Unknown = -1, - A, - B, - C, - D, - E, - F, - G, - H, - I, - J, - K, - L, - M, - N, - O, - P, - Q, - R, - S, - T, - U, - V, - W, - X, - Y, - Z, - Num0, - Num1, - Num2, - Num3, - Num4, - Num5, - Num6, - Num7, - Num8, - Num9, - Escape, - Space, - Enter, - Backspace, - Tab, - Left, - Right, - Up, - Down, - LShift, - RShift, - LControl, - RControl, - LAlt, - RAlt - }; - - class IRenderer : public RType::Core::IModule { - public: - ~IRenderer() override = default; - - const char* GetName() const override = 0; - RType::Core::ModulePriority GetPriority() const override = 0; - bool Initialize(RType::Core::Engine* engine) override = 0; - void Shutdown() override = 0; - void Update(float deltaTime) override = 0; - - virtual bool CreateWindow(const WindowConfig& config) = 0; - virtual void Destroy() = 0; - virtual bool IsWindowOpen() const = 0; - virtual void Resize(std::uint32_t width, std::uint32_t height) = 0; - virtual void SetWindowTitle(const std::string& title) = 0; - - virtual void BeginFrame() = 0; - virtual void EndFrame() = 0; - virtual void Clear(const Color& color) = 0; - - virtual TextureId LoadTexture(const std::string& path, - const TextureConfig& config = TextureConfig{}) = 0; - virtual void UnloadTexture(TextureId textureId) = 0; - virtual Vector2 GetTextureSize(TextureId textureId) const = 0; - - virtual SpriteId CreateSprite(TextureId textureId, const Rectangle& region) = 0; - virtual void DestroySprite(SpriteId spriteId) = 0; - - // Update the texture region of an existing sprite (for animations) - virtual void SetSpriteRegion(SpriteId spriteId, const Rectangle& region) = 0; - - virtual void DrawSprite(SpriteId spriteId, const Transform2D& transform, - const Color& tint = Color{}) = 0; - virtual void DrawRectangle(const Rectangle& rectangle, const Color& color) = 0; - - virtual FontId LoadFont(const std::string& path, std::uint32_t characterSize) = 0; - virtual void UnloadFont(FontId fontId) = 0; - virtual void DrawText(FontId fontId, const std::string& text, const TextParams& params) = 0; - - virtual void SetCamera(const Camera2D& camera) = 0; - virtual void ResetCamera() = 0; - - virtual RenderStats GetRenderStats() const = 0; - - virtual float GetDeltaTime() = 0; - - virtual bool IsKeyPressed(Key key) const = 0; - - enum class MouseButton { - Left, - Right, - Middle - }; - virtual bool IsMouseButtonPressed(MouseButton button) const = 0; - virtual Vector2 GetMousePosition() const = 0; - }; - -} diff --git a/libs/engine/include/Renderer/SFMLRenderer.hpp b/libs/engine/include/Renderer/SFMLRenderer.hpp deleted file mode 100644 index 3d8edd1..0000000 --- a/libs/engine/include/Renderer/SFMLRenderer.hpp +++ /dev/null @@ -1,105 +0,0 @@ -#pragma once - -#include "IRenderer.hpp" -#include -#include -#include - -namespace Renderer { - - class SFMLRenderer : public IRenderer { - public: - SFMLRenderer(); - ~SFMLRenderer() override; - - const char* GetName() const override; - RType::Core::ModulePriority GetPriority() const override; - bool Initialize(RType::Core::Engine* engine) override; - void Shutdown() override; - void Update(float deltaTime) override; - bool ShouldUpdateInRenderThread() const override; - - bool CreateWindow(const WindowConfig& config) override; - void Destroy() override; - bool IsWindowOpen() const override; - void Resize(std::uint32_t width, std::uint32_t height) override; - void SetWindowTitle(const std::string& title) override; - - void BeginFrame() override; - void EndFrame() override; - void Clear(const Color& color) override; - - TextureId LoadTexture(const std::string& path, - const TextureConfig& config = TextureConfig{}) override; - void UnloadTexture(TextureId textureId) override; - Vector2 GetTextureSize(TextureId textureId) const override; - - SpriteId CreateSprite(TextureId textureId, const Rectangle& region) override; - void DestroySprite(SpriteId spriteId) override; - void SetSpriteRegion(SpriteId spriteId, const Rectangle& region) override; - - void DrawSprite(SpriteId spriteId, const Transform2D& transform, - const Color& tint = Color{}) override; - void DrawRectangle(const Rectangle& rectangle, const Color& color) override; - - FontId LoadFont(const std::string& path, std::uint32_t characterSize) override; - void UnloadFont(FontId fontId) override; - void DrawText(FontId fontId, const std::string& text, - const TextParams& params) override; - - void SetCamera(const Camera2D& camera) override; - void ResetCamera() override; - - RenderStats GetRenderStats() const override; - - float GetDeltaTime() override; - - bool IsKeyPressed(Key key) const override; - bool IsMouseButtonPressed(MouseButton button) const override; - Vector2 GetMousePosition() const override; - - const sf::RenderWindow* GetWindow() const { return m_window.get(); } - void ProcessEvents(); - private: - static sf::Color ToSFMLColor(const Color& color); - static sf::Vector2f ToSFMLVector(const Vector2& vec); - static sf::IntRect ToSFMLRect(const Rectangle& rect); - static sf::Keyboard::Key ToSFMLKey(Key key); - - std::unique_ptr m_window; - WindowConfig m_windowConfig; - - struct TextureData { - std::shared_ptr texture; - TextureConfig config; - }; - std::unordered_map m_textures; - TextureId m_nextTextureId = 1; - - struct SpriteData { - sf::Sprite sprite; - TextureId textureId; - }; - std::unordered_map m_sprites; - SpriteId m_nextSpriteId = 1; - - struct FontData { - std::shared_ptr font; - std::uint32_t characterSize; - }; - std::unordered_map m_fonts; - FontId m_nextFontId = 1; - - sf::View m_defaultView; - sf::View m_currentView; - bool m_usingCustomCamera = false; - - RType::Core::Engine* m_engine = nullptr; - - RenderStats m_stats; - - sf::Clock m_clock; - float m_lastDeltaTime = 0.0f; - }; - -} diff --git a/libs/engine/src/Animation/AnimationModule.cpp b/libs/engine/src/Animation/AnimationModule.cpp deleted file mode 100644 index da57b1b..0000000 --- a/libs/engine/src/Animation/AnimationModule.cpp +++ /dev/null @@ -1,254 +0,0 @@ -#include "Animation/AnimationModule.hpp" -#include -#include -#include -#include - -namespace Animation { - - AnimationModule::AnimationModule() = default; - - AnimationModule::~AnimationModule() { - Shutdown(); - } - - bool AnimationModule::Initialize(RType::Core::Engine* engine) { - m_engine = engine; - return true; - } - - void AnimationModule::Shutdown() { - UnloadAll(); - m_engine = nullptr; - } - - void AnimationModule::Update(float /* deltaTime */) { - } - - AnimationClipId AnimationModule::CreateClip(const AnimationClipConfig& config) { - if (config.name.empty() || config.frames.empty()) { - return INVALID_CLIP_ID; - } - - AnimationClipId id = m_nextClipId++; - m_clips[id] = config; - m_clipNameToId[config.name] = id; - - return id; - } - - AnimationClipId AnimationModule::CreateClipFromGrid(const std::string& name, - const std::string& texturePath, - const GridLayout& layout, - bool looping) { - if (name.empty() || layout.columns == 0 || layout.rows == 0) { - return INVALID_CLIP_ID; - } - - AnimationClipConfig config; - config.name = name; - config.texturePath = texturePath; - config.looping = looping; - - std::uint32_t totalFrames = layout.columns * layout.rows; - std::uint32_t frameCount = (layout.frameCount > 0) ? - std::min(layout.frameCount, totalFrames - layout.startFrame) : - totalFrames - layout.startFrame; - - config.frames.reserve(frameCount); - - for (std::uint32_t i = 0; i < frameCount; ++i) { - std::uint32_t frameIndex = layout.startFrame + i; - std::uint32_t col = frameIndex % layout.columns; - std::uint32_t row = frameIndex / layout.columns; - - FrameDef frame; - frame.region.position.x = static_cast(col) * layout.frameWidth; - frame.region.position.y = static_cast(row) * layout.frameHeight; - frame.region.size.x = layout.frameWidth; - frame.region.size.y = layout.frameHeight; - frame.duration = layout.defaultDuration; - - config.frames.push_back(frame); - } - - return CreateClip(config); - } - - AnimationClipId AnimationModule::LoadClipFromJson(const std::string& path) { - std::ifstream file(path); - if (!file.is_open()) { - return INVALID_CLIP_ID; - } - - try { - nlohmann::json json; - file >> json; - - AnimationClipConfig config; - config.name = json.value("name", ""); - config.texturePath = json.value("texture", ""); - config.looping = json.value("looping", false); - config.playbackSpeed = json.value("playbackSpeed", 1.0f); - - if (json.contains("gridLayout")) { - const auto& grid = json["gridLayout"]; - GridLayout layout; - layout.columns = grid.value("columns", 1u); - layout.rows = grid.value("rows", 1u); - layout.startFrame = grid.value("startFrame", 0u); - layout.frameCount = grid.value("frameCount", 0u); - layout.frameWidth = grid.value("frameWidth", 0.0f); - layout.frameHeight = grid.value("frameHeight", 0.0f); - layout.defaultDuration = grid.value("defaultDuration", 0.1f); - - if (layout.frameWidth <= 0.0f && json.contains("frameWidth")) { - layout.frameWidth = json["frameWidth"].get(); - } - if (layout.frameHeight <= 0.0f && json.contains("frameHeight")) { - layout.frameHeight = json["frameHeight"].get(); - } - - return CreateClipFromGrid(config.name, config.texturePath, layout, config.looping); - } - - if (json.contains("frames")) { - for (const auto& frameJson : json["frames"]) { - FrameDef frame; - frame.region.position.x = frameJson.value("x", 0.0f); - frame.region.position.y = frameJson.value("y", 0.0f); - frame.region.size.x = frameJson.value("width", 0.0f); - frame.region.size.y = frameJson.value("height", 0.0f); - frame.duration = frameJson.value("duration", 0.1f); - frame.eventName = frameJson.value("event", ""); - config.frames.push_back(frame); - } - } - - return CreateClip(config); - - } catch (const std::exception&) { - return INVALID_CLIP_ID; - } - } - - void AnimationModule::DestroyClip(AnimationClipId clipId) { - auto it = m_clips.find(clipId); - if (it != m_clips.end()) { - m_clipNameToId.erase(it->second.name); - m_clips.erase(it); - } - } - - const AnimationClipConfig* AnimationModule::GetClipConfig(AnimationClipId clipId) const { - auto it = m_clips.find(clipId); - return (it != m_clips.end()) ? &it->second : nullptr; - } - - float AnimationModule::GetClipDuration(AnimationClipId clipId) const { - const auto* config = GetClipConfig(clipId); - if (!config) { - return 0.0f; - } - return CalculateClipDuration(*config); - } - - std::size_t AnimationModule::GetClipFrameCount(AnimationClipId clipId) const { - const auto* config = GetClipConfig(clipId); - return config ? config->frames.size() : 0; - } - - bool AnimationModule::IsClipValid(AnimationClipId clipId) const { - return m_clips.find(clipId) != m_clips.end(); - } - - FrameDef AnimationModule::GetFrameAtTime(AnimationClipId clipId, - float time, - bool looping) const { - const auto* config = GetClipConfig(clipId); - if (!config || config->frames.empty()) { - return FrameDef{}; - } - - std::size_t frameIndex = GetFrameIndexAtTime(clipId, time, looping); - if (frameIndex < config->frames.size()) { - return config->frames[frameIndex]; - } - - return config->frames.back(); - } - - std::size_t AnimationModule::GetFrameIndexAtTime(AnimationClipId clipId, - float time, - bool looping) const { - const auto* config = GetClipConfig(clipId); - if (!config || config->frames.empty()) { - return 0; - } - - float duration = CalculateClipDuration(*config); - if (duration <= 0.0f) { - return 0; - } - - float normalizedTime = time; - if (looping) { - normalizedTime = std::fmod(time, duration); - if (normalizedTime < 0.0f) { - normalizedTime += duration; - } - } else { - normalizedTime = std::clamp(time, 0.0f, duration); - } - - float accumulated = 0.0f; - for (std::size_t i = 0; i < config->frames.size(); ++i) { - accumulated += config->frames[i].duration; - if (normalizedTime < accumulated) { - return i; - } - } - - return config->frames.size() - 1; - } - - void AnimationModule::LoadAnimationsFromManifest(const std::string& manifestPath) { - std::ifstream file(manifestPath); - if (!file.is_open()) { - return; - } - - try { - nlohmann::json json; - file >> json; - - if (json.contains("clips")) { - for (const auto& clipPath : json["clips"]) { - LoadClipFromJson(clipPath.get()); - } - } - - } catch (const std::exception&) { - } - } - - void AnimationModule::UnloadAll() { - m_clips.clear(); - m_clipNameToId.clear(); - m_nextClipId = 1; - } - - AnimationClipId AnimationModule::GetClipByName(const std::string& name) const { - auto it = m_clipNameToId.find(name); - return (it != m_clipNameToId.end()) ? it->second : INVALID_CLIP_ID; - } - - float AnimationModule::CalculateClipDuration(const AnimationClipConfig& config) const { - float duration = 0.0f; - for (const auto& frame : config.frames) { - duration += frame.duration; - } - return duration; - } - -} diff --git a/libs/engine/src/Animation/CMakeLists.txt b/libs/engine/src/Animation/CMakeLists.txt deleted file mode 100644 index f174d06..0000000 --- a/libs/engine/src/Animation/CMakeLists.txt +++ /dev/null @@ -1,31 +0,0 @@ -set(ANIMATION_SOURCES - AnimationModule.cpp -) - -set(ANIMATION_HEADERS - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/Animation/AnimationTypes.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/Animation/IAnimation.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/Animation/AnimationModule.hpp -) - -add_library(rtype_animation STATIC - ${ANIMATION_SOURCES} - ${ANIMATION_HEADERS} -) - -target_include_directories(rtype_animation PUBLIC - $ - $ -) - -target_link_libraries(rtype_animation - PUBLIC - rtype_core - nlohmann_json::nlohmann_json -) - -target_compile_features(rtype_animation PUBLIC cxx_std_17) - -set_target_properties(rtype_animation PROPERTIES - POSITION_INDEPENDENT_CODE ON -) diff --git a/libs/engine/src/Audio/CMakeLists.txt b/libs/engine/src/Audio/CMakeLists.txt deleted file mode 100644 index d45f60a..0000000 --- a/libs/engine/src/Audio/CMakeLists.txt +++ /dev/null @@ -1,89 +0,0 @@ -# SFML Audio backend - -set(SFML_AUDIO_FOUND FALSE) - -if(DEFINED CMAKE_TOOLCHAIN_FILE AND CMAKE_TOOLCHAIN_FILE MATCHES "vcpkg") - message(STATUS "Searching for SFML Audio via vcpkg...") - find_package(SFML 2.6 COMPONENTS audio system QUIET) - if(SFML_FOUND) - set(SFML_AUDIO_FOUND TRUE) - message(STATUS "Found SFML ${SFML_VERSION} (audio) via vcpkg") - endif() -endif() - -if(NOT SFML_AUDIO_FOUND) - message(STATUS "Searching for system-installed SFML Audio 2.6+...") - find_package(SFML 2.6 COMPONENTS audio system QUIET) - if(SFML_FOUND) - set(SFML_AUDIO_FOUND TRUE) - message(STATUS "Found system-installed SFML ${SFML_VERSION} (audio)") - endif() -endif() - -if(NOT SFML_AUDIO_FOUND) - message(STATUS "SFML Audio not found, fetching SFML 2.6.2 from source...") - include(FetchContent) - - set(OLD_BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS}) - - FetchContent_Declare( - SFML - GIT_REPOSITORY https://github.com/SFML/SFML.git - GIT_TAG 2.6.2 - GIT_SHALLOW TRUE - ) - - set(SFML_BUILD_AUDIO ON CACHE BOOL "" FORCE) - set(SFML_BUILD_NETWORK OFF CACHE BOOL "" FORCE) - set(BUILD_SHARED_LIBS ON CACHE BOOL "" FORCE) - - FetchContent_MakeAvailable(SFML) - - set(BUILD_SHARED_LIBS ${OLD_BUILD_SHARED_LIBS} CACHE BOOL "" FORCE) - set(SFML_AUDIO_FOUND TRUE) - message(STATUS "SFML 2.6.2 fetched and configured (audio)") -endif() - -if(SFML_AUDIO_FOUND) - if(TARGET SFML::Audio) - set(SFML_AUDIO_LIBS SFML::Audio SFML::System) - message(STATUS "Using SFML Audio targets with SFML:: namespace") - elseif(TARGET sfml-audio) - set(SFML_AUDIO_LIBS sfml-audio sfml-system) - message(STATUS "Using SFML Audio targets with sfml- prefix") - else() - message(FATAL_ERROR "SFML Audio found but targets not available (expected SFML::Audio or sfml-audio).") - endif() -endif() - -add_library(rtype_sfml_audio STATIC - SFMLAudio.cpp -) - -target_include_directories(rtype_sfml_audio PUBLIC - $ - $ -) - -find_package(fmt CONFIG REQUIRED) - -target_link_libraries(rtype_sfml_audio - PUBLIC - rtype_core - ${SFML_AUDIO_LIBS} - fmt::fmt -) - -# Apple platforms need explicit framework linking for OpenAL -if(APPLE) - target_link_libraries(rtype_sfml_audio - PUBLIC - "-framework AudioToolbox" - "-framework AudioUnit" - "-framework CoreAudio" - "-framework ApplicationServices" - ) -endif() - -target_compile_features(rtype_sfml_audio PUBLIC cxx_std_17) - diff --git a/libs/engine/src/Audio/SFMLAudio.cpp b/libs/engine/src/Audio/SFMLAudio.cpp deleted file mode 100644 index ef4c23c..0000000 --- a/libs/engine/src/Audio/SFMLAudio.cpp +++ /dev/null @@ -1,219 +0,0 @@ -#include "Audio/SFMLAudio.hpp" -#include "Core/Logger.hpp" - -#include - -namespace Audio { - - bool SFMLAudio::ConfigureDevice(const AudioConfig& config) { - SetMasterVolume(config.masterVolume); - return true; - } - - void SFMLAudio::Shutdown() { - StopAll(); - m_activeSounds.clear(); - m_soundBuffers.clear(); - m_music.clear(); - } - - void SFMLAudio::Update(float /*deltaTime*/) { - CleanupStoppedSounds(); - } - - Audio::SoundId SFMLAudio::LoadSound(const std::string& path) { - sf::SoundBuffer buffer; - if (!buffer.loadFromFile(path)) { - RType::Core::Logger::Warning("[SFMLAudio] Failed to load sound '{}'", path); - return INVALID_SOUND_ID; - } - - SoundId id = m_nextSoundId++; - m_soundBuffers.emplace(id, std::move(buffer)); - RType::Core::Logger::Info("[SFMLAudio] Loaded sound '{}' (id={})", path, id); - return id; - } - - void SFMLAudio::UnloadSound(SoundId soundId) { - StopSound(soundId); - m_soundBuffers.erase(soundId); - } - - Audio::MusicId SFMLAudio::LoadMusic(const std::string& path) { - auto music = std::make_unique(); - if (!music->openFromFile(path)) { - RType::Core::Logger::Warning("[SFMLAudio] Failed to load music '{}'", path); - return INVALID_MUSIC_ID; - } - - MusicId id = m_nextMusicId++; - m_music.emplace(id, std::move(music)); - return id; - } - - void SFMLAudio::UnloadMusic(MusicId musicId) { - StopMusic(musicId); - m_music.erase(musicId); - m_musicOriginalVolumes.erase(musicId); - } - - void SFMLAudio::PlaySound(SoundId soundId, const PlaybackOptions& options) { - auto it = m_soundBuffers.find(soundId); - if (it == m_soundBuffers.end()) { - RType::Core::Logger::Warning("[SFMLAudio] PlaySound unknown id={}", soundId); - return; - } - - sf::Sound s; - s.setBuffer(it->second); - float finalVolume = std::clamp(options.volume * m_masterVolume, 0.0f, 1.0f) * 100.0f; - s.setVolume(finalVolume); - s.setPitch(std::max(0.01f, options.pitch)); - s.setLoop(options.loop); - - - s.setRelativeToListener(true); - s.setPosition(0.0f, 0.0f, 0.0f); - s.setMinDistance(1.0f); - s.setAttenuation(0.0f); // No attenuation - - - - s.play(); - m_activeSounds.push_back(std::move(s)); - m_soundOriginalVolumes[&it->second] = options.volume; - RType::Core::Logger::Debug("[SFMLAudio] PlaySound id={} vol={} pitch={} pan={} loop={}", - soundId, options.volume, options.pitch, options.pan, options.loop); - } - - void SFMLAudio::StopSound(SoundId soundId) { - auto it = m_soundBuffers.find(soundId); - if (it == m_soundBuffers.end()) { - return; - } - const sf::SoundBuffer* buffer = &it->second; - - for (auto& s : m_activeSounds) { - if (s.getBuffer() == buffer) { - s.stop(); - } - } - CleanupStoppedSounds(); - } - - void SFMLAudio::PlayMusic(MusicId musicId, const PlaybackOptions& options) { - auto it = m_music.find(musicId); - if (it == m_music.end() || !it->second) { - return; - } - auto& m = *it->second; - float finalVolume = std::clamp(options.volume * m_masterVolume, 0.0f, 1.0f) * 100.0f; - m.setVolume(finalVolume); - m.setPitch(std::max(0.01f, options.pitch)); - m.setLoop(options.loop); - m.play(); - m_musicOriginalVolumes[musicId] = options.volume; - } - - void SFMLAudio::StopMusic(MusicId musicId) { - auto it = m_music.find(musicId); - if (it == m_music.end() || !it->second) { - return; - } - it->second->stop(); - m_musicOriginalVolumes.erase(musicId); - } - - void SFMLAudio::PauseAll() { - for (auto& s : m_activeSounds) { - if (s.getStatus() == sf::Sound::Playing) { - s.pause(); - } - } - for (auto& [id, m] : m_music) { - (void)id; - if (m && m->getStatus() == sf::Music::Playing) { - m->pause(); - } - } - } - - void SFMLAudio::ResumeAll() { - for (auto& s : m_activeSounds) { - if (s.getStatus() == sf::Sound::Paused) { - s.play(); - } - } - for (auto& [id, m] : m_music) { - (void)id; - if (m && m->getStatus() == sf::Music::Paused) { - m->play(); - } - } - } - - void SFMLAudio::StopAll() { - for (auto& s : m_activeSounds) { - s.stop(); - } - m_soundOriginalVolumes.clear(); - for (auto& [id, m] : m_music) { - (void)id; - if (m) { - m->stop(); - } - } - m_musicOriginalVolumes.clear(); - CleanupStoppedSounds(); - } - - void SFMLAudio::SetMasterVolume(float volume) { - m_masterVolume = std::clamp(volume, 0.0f, 1.0f); - UpdateAllVolumes(); - } - - void SFMLAudio::UpdateAllVolumes() { - for (auto& s : m_activeSounds) { - if (s.getStatus() == sf::Sound::Playing || s.getStatus() == sf::Sound::Paused) { - const sf::SoundBuffer* buffer = s.getBuffer(); - auto volIt = m_soundOriginalVolumes.find(buffer); - if (volIt != m_soundOriginalVolumes.end()) { - float finalVolume = std::clamp(volIt->second * m_masterVolume, 0.0f, 1.0f) * 100.0f; - s.setVolume(finalVolume); - } - } - } - - for (auto& [id, m] : m_music) { - if (m && (m->getStatus() == sf::Music::Playing || m->getStatus() == sf::Music::Paused)) { - auto volIt = m_musicOriginalVolumes.find(id); - if (volIt != m_musicOriginalVolumes.end()) { - float finalVolume = std::clamp(volIt->second * m_masterVolume, 0.0f, 1.0f) * 100.0f; - m->setVolume(finalVolume); - } - } - } - } - - float SFMLAudio::GetMasterVolume() const { - return m_masterVolume; - } - - void SFMLAudio::SetListener(const ListenerProperties& listener) { - sf::Listener::setPosition(listener.position.x, listener.position.y, 0.0f); - sf::Listener::setDirection(listener.forward.x, listener.forward.y, 0.0f); - } - - void SFMLAudio::CleanupStoppedSounds() { - m_activeSounds.remove_if([this](const sf::Sound& s) { - if (s.getStatus() == sf::Sound::Stopped) { - const sf::SoundBuffer* buffer = s.getBuffer(); - m_soundOriginalVolumes.erase(buffer); - return true; - } - return false; - }); - } - -} - diff --git a/libs/engine/src/Core/CMakeLists.txt b/libs/engine/src/Core/CMakeLists.txt deleted file mode 100644 index da61b84..0000000 --- a/libs/engine/src/Core/CMakeLists.txt +++ /dev/null @@ -1,44 +0,0 @@ -set(CORE_SOURCES - Engine.cpp - ModuleLoader.cpp - ColorFilter.cpp - InputMapping.cpp -) - -set(CORE_HEADERS - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/Core/Engine.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/Core/Module.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/Core/ModuleLoader.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/Core/Logger.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/Core/ColorFilter.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/Core/InputMapping.hpp -) - -add_library(rtype_core STATIC - ${CORE_SOURCES} - ${CORE_HEADERS} -) - -target_include_directories(rtype_core PUBLIC - $ - $ -) - -target_link_libraries(rtype_core PUBLIC - rtype_ecs - ${CMAKE_DL_LIBS} -) - -target_compile_features(rtype_core PUBLIC cxx_std_17) - -set_target_properties(rtype_core PROPERTIES - POSITION_INDEPENDENT_CODE ON -) - -if(WIN32) - target_compile_definitions(rtype_core PUBLIC - NOMINMAX - WIN32_LEAN_AND_MEAN - _CRT_SECURE_NO_WARNINGS - ) -endif() diff --git a/libs/engine/src/Core/ColorFilter.cpp b/libs/engine/src/Core/ColorFilter.cpp deleted file mode 100644 index 3301ee8..0000000 --- a/libs/engine/src/Core/ColorFilter.cpp +++ /dev/null @@ -1,39 +0,0 @@ -#include "Core/ColorFilter.hpp" -#include - -namespace RType { - namespace Core { - - bool ColorFilter::s_colourBlindMode = false; - - bool ColorFilter::IsColourBlindModeEnabled() { - return s_colourBlindMode; - } - - void ColorFilter::SetColourBlindMode(bool enabled) { - s_colourBlindMode = enabled; - } - - Math::Color ColorFilter::ApplyColourBlindFilter(const Math::Color& color) { - if (!s_colourBlindMode) { - return color; - } - - float r = color.r; - float g = color.g; - float b = color.b; - - float newR = 0.567f * r + 0.433f * g + 0.0f * b; - float newG = 0.558f * r + 0.442f * g + 0.0f * b; - float newB = 0.0f * r + 0.242f * g + 0.758f * b; - - newR = std::clamp(newR, 0.0f, 1.0f); - newG = std::clamp(newG, 0.0f, 1.0f); - newB = std::clamp(newB, 0.0f, 1.0f); - - return Math::Color(newR, newG, newB, color.a); - } - - } -} - diff --git a/libs/engine/src/Core/Engine.cpp b/libs/engine/src/Core/Engine.cpp deleted file mode 100644 index 9c91b1d..0000000 --- a/libs/engine/src/Core/Engine.cpp +++ /dev/null @@ -1,168 +0,0 @@ -#include "../../include/Core/Engine.hpp" -#include - -namespace RType { - - namespace Core { - - Engine::Engine(const EngineConfig& config) - : m_config(config) { - Logger::Info("Creating R-Type Engine"); - } - - Engine::~Engine() { - if (m_initialized) { - Shutdown(); - } - } - - bool Engine::Initialize() { - if (m_initialized) { - Logger::Warning("Engine already initialized"); - return true; - } - - Logger::Info("Initializing R-Type Engine..."); - - SortModulesByPriority(); - - if (!InitializeModules()) { - Logger::Error("Failed to initialize modules"); - return false; - } - - InitializeSystems(); - - m_initialized = true; - Logger::Info("R-Type Engine initialized successfully"); - return true; - } - - void Engine::Shutdown() { - if (!m_initialized) { - return; - } - - Logger::Info("Shutting down R-Type Engine..."); - - ShutdownSystems(); - ShutdownModules(); - m_moduleLoader.UnloadAllPlugins(); - - m_initialized = false; - Logger::Info("R-Type Engine shutdown complete"); - } - - IModule* Engine::LoadPlugin(const std::string& pluginPath) { - IModule* module = m_moduleLoader.LoadPlugin(pluginPath); - - if (module && m_initialized) { - Logger::Info("Initializing late-loaded plugin '{}'", module->GetName()); - if (!module->Initialize(this)) { - Logger::Error("Failed to initialize plugin '{}'", module->GetName()); - m_moduleLoader.UnloadPlugin(module->GetName()); - return nullptr; - } - SortModulesByPriority(); - } - - return module; - } - - bool Engine::UnloadPlugin(const std::string& pluginName) { - IModule* module = m_moduleLoader.GetPlugin(pluginName); - - if (module && m_initialized) { - Logger::Info("Shutting down plugin '{}'", pluginName); - module->Shutdown(); - } - - bool result = m_moduleLoader.UnloadPlugin(pluginName); - - if (result) { - SortModulesByPriority(); - } - - return result; - } - - IModule* Engine::GetModuleByName(const std::string& name) { - for (const auto& [_, module] : m_builtinModules) { - if (std::string(module->GetName()) == name) { - return module.get(); - } - } - - return m_moduleLoader.GetPlugin(name); - } - - std::vector Engine::GetAllModules() const { return m_sortedModules; } - - void Engine::SortModulesByPriority() { - m_sortedModules.clear(); - - for (const auto& [_, module] : m_builtinModules) { - m_sortedModules.push_back(module.get()); - } - - for (IModule* plugin : m_moduleLoader.GetAllPlugins()) { - m_sortedModules.push_back(plugin); - } - - std::sort(m_sortedModules.begin(), m_sortedModules.end(), [](IModule* a, IModule* b) { - return static_cast(a->GetPriority()) < static_cast(b->GetPriority()); - }); - } - - bool Engine::InitializeModules() { - Logger::Info("Initializing {} modules...", m_sortedModules.size()); - - for (IModule* module : m_sortedModules) { - Logger::Info("Initializing module '{}'", module->GetName()); - - if (!module->Initialize(this)) { - Logger::Error("Failed to initialize module '{}'", module->GetName()); - return false; - } - } - - return true; - } - - void Engine::ShutdownModules() { - Logger::Info("Shutting down {} modules...", m_sortedModules.size()); - - for (auto it = m_sortedModules.rbegin(); it != m_sortedModules.rend(); ++it) { - IModule* module = *it; - Logger::Info("Shutting down module '{}'", module->GetName()); - module->Shutdown(); - } - } - - void Engine::UpdateSystems(float deltaTime) { - for (auto& system : m_systems) { - system->Update(m_registry, deltaTime); - } - } - - void Engine::InitializeSystems() { - Logger::Info("Initializing {} systems...", m_systems.size()); - - for (auto& system : m_systems) { - Logger::Info("Initializing system '{}'", system->GetName()); - system->Initialize(m_registry); - } - } - - void Engine::ShutdownSystems() { - Logger::Info("Shutting down {} systems...", m_systems.size()); - - for (auto it = m_systems.rbegin(); it != m_systems.rend(); ++it) { - Logger::Info("Shutting down system '{}'", (*it)->GetName()); - (*it)->Shutdown(); - } - } - - } - -} diff --git a/libs/engine/src/Core/InputMapping.cpp b/libs/engine/src/Core/InputMapping.cpp deleted file mode 100644 index 7f6c70e..0000000 --- a/libs/engine/src/Core/InputMapping.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include "../include/Core/InputMapping.hpp" - -namespace RType { - namespace Core { - - std::unordered_map InputMapping::s_mappings = { - {"MOVE_UP", Renderer::Key::Up}, - {"MOVE_DOWN", Renderer::Key::Down}, - {"MOVE_LEFT", Renderer::Key::Left}, - {"MOVE_RIGHT", Renderer::Key::Right}, - {"SHOOT", Renderer::Key::Space} - }; - - Renderer::Key InputMapping::GetKey(const std::string& action) { - auto it = s_mappings.find(action); - if (it != s_mappings.end()) { - return it->second; - } - return Renderer::Key::Unknown; - } - - void InputMapping::SetKey(const std::string& action, Renderer::Key key) { - s_mappings[action] = key; - } - - void InputMapping::LoadDefaults() { - s_mappings = { - {"MOVE_UP", Renderer::Key::Up}, - {"MOVE_DOWN", Renderer::Key::Down}, - {"MOVE_LEFT", Renderer::Key::Left}, - {"MOVE_RIGHT", Renderer::Key::Right}, - {"SHOOT", Renderer::Key::Space} - }; - } - - } -} - diff --git a/libs/engine/src/Core/ModuleLoader.cpp b/libs/engine/src/Core/ModuleLoader.cpp deleted file mode 100644 index 131fab2..0000000 --- a/libs/engine/src/Core/ModuleLoader.cpp +++ /dev/null @@ -1,200 +0,0 @@ -#include "../../include/Core/ModuleLoader.hpp" -#include -#include - -namespace RType { - - namespace Core { - - ModuleLoader::~ModuleLoader() { UnloadAllPlugins(); } - - IModule* ModuleLoader::LoadPlugin(const std::string& pluginPath) { - // Check if file exists - if (!std::filesystem::exists(pluginPath)) { - Logger::Error("Plugin file not found: {}", pluginPath); - return nullptr; - } - - // Extract plugin name from path - std::string pluginName = ExtractPluginName(pluginPath); - - // Check if already loaded - if (IsPluginLoaded(pluginName)) { - Logger::Warning("Plugin '{}' is already loaded", pluginName); - return m_loadedPlugins[pluginName].module; - } - - // Load the shared library - LibraryHandle handle = LoadLibraryFromPath(pluginPath); - if (!handle) { - Logger::Error("Failed to load library '{}': {}", pluginPath, GetLastErrorMessage()); - return nullptr; - } - - // Get the CreateModule function - auto createFunc = - reinterpret_cast(GetFunction(handle, "CreateModule")); - if (!createFunc) { - Logger::Error("Failed to find CreateModule function in '{}': {}", pluginPath, - GetLastErrorMessage()); - FreeLibraryHandle(handle); - return nullptr; - } - - // Get the DestroyModule function - auto destroyFunc = - reinterpret_cast(GetFunction(handle, "DestroyModule")); - if (!destroyFunc) { - Logger::Error("Failed to find DestroyModule function in '{}': {}", pluginPath, - GetLastErrorMessage()); - FreeLibraryHandle(handle); - return nullptr; - } - - // Create the module instance - IModule* module = createFunc(); - if (!module) { - Logger::Error("CreateModule returned nullptr for '{}'", pluginPath); - FreeLibraryHandle(handle); - return nullptr; - } - - // Store plugin info - PluginInfo info; - info.name = module->GetName(); - info.path = pluginPath; - info.handle = handle; - info.module = module; - info.destroyFunc = destroyFunc; - - m_loadedPlugins[info.name] = info; - - Logger::Info("Successfully loaded plugin '{}' from '{}'", info.name, pluginPath); - return module; - } - - bool ModuleLoader::UnloadPlugin(const std::string& pluginName) { - auto it = m_loadedPlugins.find(pluginName); - if (it == m_loadedPlugins.end()) { - Logger::Warning("Plugin '{}' not found for unloading", pluginName); - return false; - } - - PluginInfo& info = it->second; - - // Destroy the module - if (info.destroyFunc && info.module) { - info.destroyFunc(info.module); - info.module = nullptr; - } - - // Unload the library - if (info.handle) { - FreeLibraryHandle(info.handle); - info.handle = nullptr; - } - - // Remove from map - m_loadedPlugins.erase(it); - - Logger::Info("Successfully unloaded plugin '{}'", pluginName); - return true; - } - - void ModuleLoader::UnloadAllPlugins() { - // Get all plugin names first (to avoid modifying map while iterating) - std::vector pluginNames; - pluginNames.reserve(m_loadedPlugins.size()); - for (const auto& [name, _] : m_loadedPlugins) { - pluginNames.push_back(name); - } - - // Unload in reverse order (LIFO) - for (auto it = pluginNames.rbegin(); it != pluginNames.rend(); ++it) { - UnloadPlugin(*it); - } - } - - IModule* ModuleLoader::GetPlugin(const std::string& pluginName) { - auto it = m_loadedPlugins.find(pluginName); - if (it == m_loadedPlugins.end()) { - return nullptr; - } - return it->second.module; - } - - std::vector ModuleLoader::GetAllPlugins() const { - std::vector plugins; - plugins.reserve(m_loadedPlugins.size()); - for (const auto& [_, info] : m_loadedPlugins) { - plugins.push_back(info.module); - } - return plugins; - } - - bool ModuleLoader::IsPluginLoaded(const std::string& pluginName) const { - return m_loadedPlugins.find(pluginName) != m_loadedPlugins.end(); - } - - LibraryHandle ModuleLoader::LoadLibraryFromPath(const std::string& path) { -#ifdef _WIN32 - return ::LoadLibraryA(path.c_str()); -#else - return dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); -#endif - } - - void ModuleLoader::FreeLibraryHandle(LibraryHandle handle) { - if (!handle) - return; -#ifdef _WIN32 - ::FreeLibrary(handle); -#else - dlclose(handle); -#endif - } - - void* ModuleLoader::GetFunction(LibraryHandle handle, const std::string& name) { -#ifdef _WIN32 - return reinterpret_cast(::GetProcAddress(handle, name.c_str())); -#else - return dlsym(handle, name.c_str()); -#endif - } - - std::string ModuleLoader::GetLastErrorMessage() { -#ifdef _WIN32 - DWORD error = GetLastError(); - if (error == 0) { - return "No error"; - } - LPSTR messageBuffer = nullptr; - size_t size = - FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | - FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - (LPSTR)&messageBuffer, 0, NULL); - std::string message(messageBuffer, size); - LocalFree(messageBuffer); - return message; -#else - const char* error = dlerror(); - return error ? error : "No error"; -#endif - } - - std::string ModuleLoader::ExtractPluginName(const std::string& path) { - std::filesystem::path p(path); - std::string filename = p.stem().string(); - - // Remove common prefixes like "lib" - if (filename.substr(0, 3) == "lib") { - filename = filename.substr(3); - } - - return filename; - } - - } // namespace Core - -} // namespace RType diff --git a/libs/engine/src/ECS/AnimationSystem.cpp b/libs/engine/src/ECS/AnimationSystem.cpp deleted file mode 100644 index 7c7dfa8..0000000 --- a/libs/engine/src/ECS/AnimationSystem.cpp +++ /dev/null @@ -1,195 +0,0 @@ -#include "ECS/AnimationSystem.hpp" -#include "ECS/Component.hpp" -#include "ECS/Components/TextLabel.hpp" -#include "ECS/Registry.hpp" -#include -#include - -namespace RType { -namespace ECS { - - AnimationSystem::AnimationSystem(Animation::IAnimation* animation) - : m_animation(animation) {} - - void AnimationSystem::Update(Registry& registry, float deltaTime) { - if (!m_animation) { - return; - } - - m_entitiesToDestroy.clear(); - - UpdateSpriteAnimations(registry, deltaTime); - UpdateVisualEffects(registry, deltaTime); - UpdateFloatingTexts(registry, deltaTime); - UpdatePowerUpGlow(registry, deltaTime); - CleanupCompletedAnimations(registry); - } - - void AnimationSystem::UpdateSpriteAnimations(Registry& registry, float deltaTime) { - auto entities = registry.GetEntitiesWithComponent(); - - for (Entity entity : entities) { - if (!registry.IsEntityAlive(entity)) { - continue; - } - - auto& anim = registry.GetComponent(entity); - if (!anim.playing || anim.clipId == Animation::INVALID_CLIP_ID) { - continue; - } - - float clipDuration = m_animation->GetClipDuration(anim.clipId); - if (clipDuration <= 0.0f) { - continue; - } - - bool regionUninitialized = (anim.currentRegion.size.x <= 0.0f || anim.currentRegion.size.y <= 0.0f); - if (regionUninitialized && anim.currentTime == 0.0f) { - auto firstFrame = m_animation->GetFrameAtTime(anim.clipId, 0.0f, anim.looping); - std::size_t firstFrameIndex = m_animation->GetFrameIndexAtTime(anim.clipId, 0.0f, anim.looping); - anim.currentFrameIndex = firstFrameIndex; - anim.currentRegion = firstFrame.region; - if (registry.HasComponent(entity)) { - registry.GetComponent(entity).needsUpdate = true; - } - } - - anim.currentTime += deltaTime * anim.playbackSpeed; - if (anim.currentTime >= clipDuration) { - if (anim.looping) { - anim.currentTime = std::fmod(anim.currentTime, clipDuration); - } else { - anim.currentTime = clipDuration; - anim.playing = false; - - if (anim.destroyOnComplete) { - m_entitiesToDestroy.push_back(entity); - continue; - } - } - } - - auto frame = m_animation->GetFrameAtTime(anim.clipId, anim.currentTime, anim.looping); - std::size_t newFrameIndex = m_animation->GetFrameIndexAtTime( - anim.clipId, anim.currentTime, anim.looping); - - bool frameChanged = (newFrameIndex != anim.currentFrameIndex); - if (frameChanged || frame.region.size.x > 0.0f) { - anim.currentRegion = frame.region; - anim.currentFrameIndex = newFrameIndex; - - if (!frame.eventName.empty() && registry.HasComponent(entity)) { - auto& events = registry.GetComponent(entity); - events.PushEvent(frame.eventName.c_str()); - } - - if (registry.HasComponent(entity)) { - registry.GetComponent(entity).needsUpdate = true; - } - } - } - } - - void AnimationSystem::UpdateVisualEffects(Registry& registry, float deltaTime) { - auto entities = registry.GetEntitiesWithComponent(); - - for (Entity entity : entities) { - if (!registry.IsEntityAlive(entity)) { - continue; - } - - auto& effect = registry.GetComponent(entity); - effect.lifetime += deltaTime; - - if (effect.owner != NULL_ENTITY && registry.IsEntityAlive(effect.owner) && - registry.HasComponent(effect.owner) && - registry.HasComponent(entity)) { - const auto& ownerPos = registry.GetComponent(effect.owner); - auto& effectPos = registry.GetComponent(entity); - - float offsetX = effect.offsetX; - float offsetY = effect.offsetY; - - effectPos.x = ownerPos.x + offsetX; - effectPos.y = ownerPos.y + offsetY; - } - - if (effect.lifetime >= effect.maxLifetime) { - m_entitiesToDestroy.push_back(entity); - } - } - } - - void AnimationSystem::UpdateFloatingTexts(Registry& registry, float deltaTime) { - auto entities = registry.GetEntitiesWithComponent(); - - for (Entity entity : entities) { - if (!registry.IsEntityAlive(entity)) { - continue; - } - - auto& text = registry.GetComponent(entity); - text.lifetime += deltaTime; - - if (registry.HasComponent(entity)) { - auto& pos = registry.GetComponent(entity); - pos.y += text.velocityY * deltaTime; - } - if (text.lifetime >= text.fadeStartTime && text.maxLifetime > text.fadeStartTime) { - float fadeProgress = (text.lifetime - text.fadeStartTime) / - (text.maxLifetime - text.fadeStartTime); - fadeProgress = std::clamp(fadeProgress, 0.0f, 1.0f); - text.color.a = 1.0f - fadeProgress; - - if (registry.HasComponent(entity)) { - auto& label = registry.GetComponent(entity); - label.color.a = text.color.a; - } - } - - if (text.lifetime >= text.maxLifetime) { - m_entitiesToDestroy.push_back(entity); - } - } - } - - void AnimationSystem::UpdatePowerUpGlow(Registry& registry, float deltaTime) { - auto entities = registry.GetEntitiesWithComponent(); - - for (Entity entity : entities) { - if (!registry.IsEntityAlive(entity)) { - continue; - } - - auto& glow = registry.GetComponent(entity); - glow.time += deltaTime * glow.pulseSpeed; - - float pulse = (std::sin(glow.time) + 1.0f) * 0.5f; - float alpha = glow.minAlpha + (glow.maxAlpha - glow.minAlpha) * pulse; - float scaleMultiplier = 1.0f + glow.scalePulse * (pulse * 2.0f - 1.0f); - float scale = glow.baseScale * scaleMultiplier; - - if (registry.HasComponent(entity)) { - auto& drawable = registry.GetComponent(entity); - drawable.tint.a = alpha; - drawable.scale.x = scale; - drawable.scale.y = scale; - } - } - } - - void AnimationSystem::CleanupCompletedAnimations(Registry& registry) { - std::sort(m_entitiesToDestroy.begin(), m_entitiesToDestroy.end()); - m_entitiesToDestroy.erase( - std::unique(m_entitiesToDestroy.begin(), m_entitiesToDestroy.end()), - m_entitiesToDestroy.end()); - - for (Entity entity : m_entitiesToDestroy) { - if (registry.IsEntityAlive(entity)) { - registry.DestroyEntity(entity); - } - } - } - -} -} diff --git a/libs/engine/src/ECS/AudioSystem.cpp b/libs/engine/src/ECS/AudioSystem.cpp deleted file mode 100644 index 8ca9a0a..0000000 --- a/libs/engine/src/ECS/AudioSystem.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "../../include/ECS/AudioSystem.hpp" -#include "../../include/ECS/Component.hpp" - -namespace RType { - namespace ECS { - - void AudioSystem::Update(Registry& registry, float) { - if (!m_audio) { - return; - } - - auto musicEntities = registry.GetEntitiesWithComponent(); - for (auto entity : musicEntities) { - if (!registry.IsEntityAlive(entity) || - !registry.HasComponent(entity)) { - continue; - } - - auto& music = registry.GetComponent(entity); - if (music.musicId == Audio::INVALID_MUSIC_ID) { - registry.RemoveComponent(entity); - continue; - } - - if (music.stop) { - m_audio->StopMusic(music.musicId); - } else if (music.play) { - Audio::PlaybackOptions opts; - opts.volume = music.volume; - opts.pitch = music.pitch; - opts.loop = music.loop; - m_audio->PlayMusic(music.musicId, opts); - } - - registry.RemoveComponent(entity); - } - - auto entities = registry.GetEntitiesWithComponent(); - - for (auto entity : entities) { - if (!registry.IsEntityAlive(entity) || - !registry.HasComponent(entity)) { - continue; - } - - auto& sound = registry.GetComponent(entity); - if (sound.soundId == Audio::INVALID_SOUND_ID) { - registry.RemoveComponent(entity); - continue; - } - - Audio::PlaybackOptions opts; - opts.volume = sound.volume; - opts.pitch = sound.pitch; - opts.pan = sound.pan; - opts.loop = sound.loop; - - - m_audio->PlaySound(sound.soundId, opts); - registry.RemoveComponent(entity); - } - } - - } -} - diff --git a/libs/engine/src/ECS/BlackOrbSystem.cpp b/libs/engine/src/ECS/BlackOrbSystem.cpp deleted file mode 100644 index 3bff0a2..0000000 --- a/libs/engine/src/ECS/BlackOrbSystem.cpp +++ /dev/null @@ -1,110 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** BlackOrbSystem implementation -*/ - -#include "ECS/BlackOrbSystem.hpp" -#include "ECS/Component.hpp" -#include "Core/Logger.hpp" -#include - -namespace RType { - namespace ECS { - - void BlackOrbSystem::Update(Registry& registry, float deltaTime) { - auto orbs = registry.GetEntitiesWithComponent(); - - for (auto orb : orbs) { - if (!registry.IsEntityAlive(orb)) { - continue; - } - - auto& blackOrb = registry.GetComponent(orb); - if (!blackOrb.isActive) { - continue; - } - - const auto& orbPos = registry.GetComponent(orb); - - auto bullets = registry.GetEntitiesWithComponent(); - for (auto bullet : bullets) { - if (!registry.IsEntityAlive(bullet)) { - continue; - } - - if (registry.HasComponent(bullet) || registry.HasComponent(bullet)) { - continue; - } - - const auto& bulletPos = registry.GetComponent(bullet); - float dx = orbPos.x - bulletPos.x; - float dy = orbPos.y - bulletPos.y; - float distance = std::sqrt(dx * dx + dy * dy); - - // Destroy bullet if within absorption radius - if (distance < blackOrb.absorptionRadius) { - registry.DestroyEntity(bullet); - continue; - } - - // Apply attraction force if within attraction radius - if (distance < blackOrb.attractionRadius) { - if (registry.HasComponent(bullet)) { - auto& bulletVel = registry.GetComponent(bullet); - - float dirX = dx / distance; - float dirY = dy / distance; - - float attractionStrength = blackOrb.attractionForce * (1.0f - distance / blackOrb.attractionRadius); - bulletVel.dx += dirX * attractionStrength * deltaTime; - bulletVel.dy += dirY * attractionStrength * deltaTime; - } - } - } - - if (registry.HasComponent(orb)) { - auto& proxDamage = registry.GetComponent(orb); - proxDamage.timeSinceDamage += deltaTime; - - if (proxDamage.timeSinceDamage >= proxDamage.tickRate) { - proxDamage.timeSinceDamage = 0.0f; - - auto players = registry.GetEntitiesWithComponent(); - for (auto player : players) { - if (!registry.IsEntityAlive(player)) { - continue; - } - - const auto& playerPos = registry.GetComponent(player); - float dx = playerPos.x - orbPos.x; - float dy = playerPos.y - orbPos.y; - float distance = std::sqrt(dx * dx + dy * dy); - - if (distance < proxDamage.damageRadius) { - if (registry.HasComponent(player)) { - continue; - } - - // Apply proximity damage - if (registry.HasComponent(player)) { - auto& health = registry.GetComponent(player); - health.current -= static_cast(proxDamage.damageAmount); - - if (health.current < 0) { - health.current = 0; - } - - Core::Logger::Debug("[BlackOrbSystem] Proximity damage {} to player", - proxDamage.damageAmount); - } - } - } - } - } - } - } - - } -} diff --git a/libs/engine/src/ECS/BossAttackSystem.cpp b/libs/engine/src/ECS/BossAttackSystem.cpp deleted file mode 100644 index 7615dc3..0000000 --- a/libs/engine/src/ECS/BossAttackSystem.cpp +++ /dev/null @@ -1,398 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** BossAttackSystem - Handles boss attack patterns -*/ - -#include "ECS/BossAttackSystem.hpp" -#include "ECS/Component.hpp" -#include "Core/Logger.hpp" -#include -#include - -#ifndef M_PI -#define M_PI 3.14159265358979323846 -#endif - -namespace RType { - namespace ECS { - - void BossAttackSystem::Update(Registry& registry, float deltaTime) { - auto bosses = registry.GetEntitiesWithComponent(); - - for (auto bossEntity : bosses) { - if (!registry.IsEntityAlive(bossEntity)) { - continue; - } - - if (registry.HasComponent(bossEntity)) { - continue; - } - - if (registry.HasComponent(bossEntity)) { - continue; - } - - if (!registry.HasComponent(bossEntity) || - !registry.HasComponent(bossEntity) || - !registry.HasComponent(bossEntity)) { - continue; - } - - auto& attack = registry.GetComponent(bossEntity); - const auto& pos = registry.GetComponent(bossEntity); - const auto& boss = registry.GetComponent(bossEntity); - - attack.timeSinceLastAttack += deltaTime; - - if (attack.timeSinceLastAttack >= attack.attackCooldown) { - attack.timeSinceLastAttack = 0.0f; - - if (boss.bossId == 1) { - switch (attack.currentPattern) { - case BossAttackPattern::FAN_SPRAY: - CreateContinuousFire(registry, bossEntity, pos.x, pos.y); - attack.currentPattern = BossAttackPattern::SPIRAL_WAVE; - break; - case BossAttackPattern::SPIRAL_WAVE: - CreateMine(registry, bossEntity, pos.x, pos.y); - attack.currentPattern = BossAttackPattern::FAN_SPRAY; - break; - default: - CreateContinuousFire(registry, bossEntity, pos.x, pos.y); - attack.currentPattern = BossAttackPattern::SPIRAL_WAVE; - break; - } - } else if (boss.bossId == 2) { - switch (attack.currentPattern) { - case BossAttackPattern::ANIMATED_ORB: - CreateAnimatedOrb(registry, bossEntity, pos.x, pos.y); - attack.currentPattern = BossAttackPattern::SPIRAL_WAVE; // Switch to second attack - break; - case BossAttackPattern::SPIRAL_WAVE: - CreateSecondAttackSpray(registry, bossEntity, pos.x, pos.y); - attack.currentPattern = BossAttackPattern::ANIMATED_ORB; // Switch back to first attack - break; - default: - CreateAnimatedOrb(registry, bossEntity, pos.x, pos.y); - attack.currentPattern = BossAttackPattern::SPIRAL_WAVE; - break; - } - } else if (boss.bossId == 3) { - switch (attack.currentPattern) { - case BossAttackPattern::FAN_SPRAY: - CreateFanSpray(registry, bossEntity, pos.x, pos.y); - attack.currentPattern = BossAttackPattern::THIRD_BULLET; - break; - case BossAttackPattern::THIRD_BULLET: - CreateThirdBullet(registry, bossEntity, pos.x, pos.y); - attack.currentPattern = BossAttackPattern::BLACK_ORB; - break; - case BossAttackPattern::BLACK_ORB: - CreateBlackOrb(registry, bossEntity, pos.x, pos.y); - attack.currentPattern = BossAttackPattern::FAN_SPRAY; - break; - default: - break; - } - } - } - } - } - - void BossAttackSystem::CreateFanSpray(Registry& registry, Entity bossEntity, float bossX, float bossY) { - const int bulletCount = 10; - const float spreadAngle = M_PI / 3; - const float bulletSpeed = 300.0f; - - const float baseAngle = M_PI; - const float startAngle = baseAngle - spreadAngle / 2.0f; - - const float shootX = bossX + 64.0f; - const float shootY = bossY + 188.0f; - - Core::Logger::Info("[BossAttackSystem] Boss firing fan spray with {} bullets", bulletCount); - - for (int i = 0; i < bulletCount; i++) { - float angle = startAngle + (spreadAngle * i / (bulletCount - 1)); - CreateBossBullet(registry, shootX, shootY, angle, bulletSpeed); - } - } - - void BossAttackSystem::CreateBossBullet(Registry& registry, float x, float y, float angle, float speed) { - Entity bullet = registry.CreateEntity(); - - // CRITICAL FIX: Clean up obstacle components from entity ID reuse - if (registry.HasComponent(bullet)) { - std::cerr << "[BOSS CLEANUP] Removing Obstacle from bullet entity " << bullet << std::endl; - registry.RemoveComponent(bullet); - } - if (registry.HasComponent(bullet)) { - std::cerr << "[BOSS CLEANUP] Removing ObstacleMetadata from bullet entity " << bullet << std::endl; - registry.RemoveComponent(bullet); - } - - registry.AddComponent(bullet, Position{x, y}); - - float vx = std::cos(angle) * speed; - float vy = std::sin(angle) * speed; - registry.AddComponent(bullet, Velocity{vx, vy}); - - registry.AddComponent(bullet, Bullet{false}); - - registry.AddComponent(bullet, BossBullet{}); - - registry.AddComponent(bullet, Damage{5}); - - registry.AddComponent(bullet, CircleCollider{10.0f}); - - registry.AddComponent(bullet, - CollisionLayer(CollisionLayers::ENEMY_BULLET, - CollisionLayers::PLAYER)); - - Core::Logger::Debug("[BossAttackSystem] Created bullet at ({}, {}) angle={} speed={}", - x, y, angle, speed); - } - - void BossAttackSystem::CreateBlackOrb(Registry& registry, Entity bossEntity, float bossX, float bossY) { - Entity orb = registry.CreateEntity(); - - // CRITICAL FIX: Clean up obstacle components from entity ID reuse - if (registry.HasComponent(orb)) { - std::cerr << "[BOSS CLEANUP] Removing Obstacle from orb entity " << orb << std::endl; - registry.RemoveComponent(orb); - } - if (registry.HasComponent(orb)) { - std::cerr << "[BOSS CLEANUP] Removing ObstacleMetadata from orb entity " << orb << std::endl; - registry.RemoveComponent(orb); - } - - const float spawnX = bossX + 70.0f; - const float spawnY = bossY + 160.0f; - - registry.AddComponent(orb, Position{spawnX, spawnY}); - - - float vx, vy; - int trajectory = rand() % 3; - const float speed = 70.0f; - - switch(trajectory) { - case 0: - vx = -speed; - vy = 0.0f; - break; - case 1: - vx = -speed * 0.657f; - vy = speed * 0.657f; - break; - case 2: - vx = -speed; - vy = 30.0f; - break; - default: - vx = -speed * 0.707f; - vy = speed * 0.707f; - break; - } - - registry.AddComponent(orb, Velocity{vx, vy}); - - registry.AddComponent(orb, Bullet{bossEntity}); - - registry.AddComponent(orb, BlackOrb{900.0f, 110.0f, 1900.0f}); - registry.AddComponent(orb, ProximityDamage{150.0f, 2.0f, 0.2f}); - - Core::Logger::Info("[BossAttackSystem] Created Black Orb at ({}, {}) trajectory={}", - spawnX, spawnY, trajectory); - } - - void BossAttackSystem::CreateThirdBullet(Registry& registry, Entity bossEntity, float bossX, float bossY) { - Entity thirdBullet = registry.CreateEntity(); - - // CRITICAL FIX: Clean up obstacle components from entity ID reuse - if (registry.HasComponent(thirdBullet)) { - std::cerr << "[BOSS CLEANUP] Removing Obstacle from third bullet entity " << thirdBullet << std::endl; - registry.RemoveComponent(thirdBullet); - } - if (registry.HasComponent(thirdBullet)) { - std::cerr << "[BOSS CLEANUP] Removing ObstacleMetadata from third bullet entity " << thirdBullet << std::endl; - registry.RemoveComponent(thirdBullet); - } - - const float spawnX = bossX + 350.0f; - const float spawnY = bossY + -30.0f; - - registry.AddComponent(thirdBullet, Position{spawnX, spawnY}); - - const float speed = 150.0f; - registry.AddComponent(thirdBullet, Velocity{-speed, 0.0f}); - - registry.AddComponent(thirdBullet, Bullet{bossEntity}); - - registry.AddComponent(thirdBullet, ThirdBullet{0.4f, 50}); - - registry.AddComponent(thirdBullet, CircleCollider{30.0f}); - registry.AddComponent(thirdBullet, - CollisionLayer(CollisionLayers::OBSTACLE, CollisionLayers::PLAYER)); - registry.AddComponent(thirdBullet, Damage{50}); - - registry.AddComponent(thirdBullet, BossBullet{}); - - Core::Logger::Info("[BossAttackSystem] Created Third Bullet at ({}, {})", spawnX, spawnY); - } - - void BossAttackSystem::CreateAnimatedOrb(Registry& registry, Entity bossEntity, float bossX, float bossY) { - const int orbCount = 4; - const float orbSpacing = 100.0f; - const float baseSpeed = 350.0f; - - const float spawnX = bossX + 50.0f; - const float startY = bossY + 50.0f; - - for (int i = 0; i < orbCount; i++) { - Entity orb = registry.CreateEntity(); - - if (registry.HasComponent(orb)) { - registry.RemoveComponent(orb); - } - if (registry.HasComponent(orb)) { - registry.RemoveComponent(orb); - } - - float orbY = startY + (i * orbSpacing); - registry.AddComponent(orb, Position{spawnX, orbY}); - - float speedVariation = baseSpeed + (i * 10.0f); - float angle = -M_PI + (i * 0.1f); - float vx = std::cos(angle) * speedVariation; - float vy = std::sin(angle) * speedVariation * 0.3f; - - registry.AddComponent(orb, Velocity{vx, vy}); - - registry.AddComponent(orb, Bullet{bossEntity}); - registry.AddComponent(orb, BossBullet{}); - registry.AddComponent(orb, WaveAttack{}); - - registry.AddComponent(orb, CircleCollider{15.0f}); - registry.AddComponent(orb, - CollisionLayer(CollisionLayers::ENEMY_BULLET, CollisionLayers::PLAYER)); - registry.AddComponent(orb, Damage{20}); - } - - Core::Logger::Info("[BossAttackSystem] Boss 2 created wave attack burst with {} orbs", orbCount); - } - - void BossAttackSystem::CreateSecondAttackSpray(Registry& registry, Entity bossEntity, float bossX, float bossY) { - const int orbCount = 32; - const float baseSpeed = 300.0f; - - const float spawnX = bossX + 114.0f; - const float spawnY = bossY + 94.0f; - - const float fullCircle = 2.0f * M_PI; - const float angleStep = fullCircle / orbCount; - - for (int i = 0; i < orbCount; i++) { - Entity orb = registry.CreateEntity(); - - if (registry.HasComponent(orb)) { - registry.RemoveComponent(orb); - } - if (registry.HasComponent(orb)) { - registry.RemoveComponent(orb); - } - - registry.AddComponent(orb, Position{spawnX, spawnY}); - - float angle = i * angleStep; - - float speedVariation = baseSpeed + (std::sin(i * 0.5f) * 50.0f); - - float vx = std::cos(angle) * speedVariation; - float vy = std::sin(angle) * speedVariation; - - registry.AddComponent(orb, Velocity{vx, vy}); - - registry.AddComponent(orb, Bullet{bossEntity}); - registry.AddComponent(orb, BossBullet{}); - registry.AddComponent(orb, SecondAttack{}); - - registry.AddComponent(orb, CircleCollider{20.0f}); - registry.AddComponent(orb, - CollisionLayer(CollisionLayers::ENEMY_BULLET, CollisionLayers::PLAYER)); - registry.AddComponent(orb, Damage{10}); - } - - Core::Logger::Info("[BossAttackSystem] Boss 2 created massive 360° spray with {} orbs", orbCount); - } - - void BossAttackSystem::CreateContinuousFire(Registry& registry, Entity bossEntity, float bossX, float bossY) { - const float baseSpeed = 350.0f; - - const float spawnX = bossX + 20.0f; - const float spawnY = bossY + 100.0f; - - Entity bullet = registry.CreateEntity(); - - if (registry.HasComponent(bullet)) { - registry.RemoveComponent(bullet); - } - if (registry.HasComponent(bullet)) { - registry.RemoveComponent(bullet); - } - - registry.AddComponent(bullet, Position{spawnX, spawnY}); - - float vx = -baseSpeed; - float vy = 0.0f; - registry.AddComponent(bullet, Velocity{vx, vy}); - - registry.AddComponent(bullet, Bullet{bossEntity}); - registry.AddComponent(bullet, BossBullet{}); - registry.AddComponent(bullet, FireBullet{}); - - registry.AddComponent(bullet, CircleCollider{12.0f}); - registry.AddComponent(bullet, - CollisionLayer(CollisionLayers::ENEMY_BULLET, CollisionLayers::PLAYER)); - registry.AddComponent(bullet, Damage{12}); - - Core::Logger::Debug("[BossAttackSystem] Boss 3 fired single bullet"); - } - - void BossAttackSystem::CreateMine(Registry& registry, Entity bossEntity, float bossX, float bossY) { - const float minX = bossX + 200.0f; - const float maxX = bossX + 600.0f; - float spawnX = minX + (static_cast(rand()) / RAND_MAX) * (maxX - minX); - - const float minY = 100.0f; - const float maxY = 650.0f; - float spawnY = minY + (static_cast(rand()) / RAND_MAX) * (maxY - minY); - - Entity mine = registry.CreateEntity(); - - if (registry.HasComponent(mine)) { - registry.RemoveComponent(mine); - } - if (registry.HasComponent(mine)) { - registry.RemoveComponent(mine); - } - - registry.AddComponent(mine, Position{spawnX, spawnY}); - - const float scrollSpeed = -100.0f; - registry.AddComponent(mine, Velocity{scrollSpeed, 0.0f}); - - registry.AddComponent(mine, Mine{50.0f, 100.0f, 20.0f}); - - registry.AddComponent(mine, CircleCollider{15.0f}); - registry.AddComponent(mine, - CollisionLayer(CollisionLayers::OBSTACLE, CollisionLayers::PLAYER)); - registry.AddComponent(mine, Damage{35}); - - Core::Logger::Debug("[BossAttackSystem] Boss 3 deployed mine at ({}, {})", spawnX, spawnY); - } - - } -} diff --git a/libs/engine/src/ECS/CMakeLists.txt b/libs/engine/src/ECS/CMakeLists.txt deleted file mode 100644 index 72a199e..0000000 --- a/libs/engine/src/ECS/CMakeLists.txt +++ /dev/null @@ -1,91 +0,0 @@ -set(ECS_SOURCES - Registry.cpp - AudioSystem.cpp - MovementSystem.cpp - InputSystem.cpp - RenderingSystem.cpp - TextRenderingSystem.cpp - MenuSystem.cpp - PlayerSystem.cpp - PlayerFactory.cpp - CollisionDetectionSystem.cpp - BulletCollisionResponseSystem.cpp - PlayerCollisionResponseSystem.cpp - ObstacleCollisionResponseSystem.cpp - EnemySystem.cpp - EnemyFactory.cpp - HealthSystem.cpp - ScoreSystem.cpp - ShootingSystem.cpp - ScrollingSystem.cpp - LevelLoader.cpp - PowerUpFactory.cpp - PowerUpSpawnSystem.cpp - PowerUpCollisionSystem.cpp - ForcePodSystem.cpp - ShieldSystem.cpp - BossSystem.cpp - BossAttackSystem.cpp - MineSystem.cpp - BlackOrbSystem.cpp - ThirdBulletSystem.cpp - AnimationSystem.cpp - EffectFactory.cpp -) - -set(ECS_HEADERS - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/Component.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/AudioSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/Components/TextLabel.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/Components/Clickable.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/Entity.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/Registry.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/ISystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/SparseArray.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/MovementSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/InputSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/RenderingSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/TextRenderingSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/MenuSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/PlayerSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/PlayerFactory.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/CollisionDetectionSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/BulletCollisionResponseSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/PlayerCollisionResponseSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/ObstacleCollisionResponseSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/EnemySystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/EnemyFactory.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/HealthSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/ScoreSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/ShootingSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/ScrollingSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/LevelLoader.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/PowerUpFactory.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/PowerUpSpawnSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/PowerUpCollisionSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/ForcePodSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/ShieldSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/BossSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/BossAttackSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/MineSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/BlackOrbSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/ThirdBulletSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/AnimationSystem.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/EffectFactory.hpp -) - -add_library(rtype_ecs STATIC - ${ECS_SOURCES} - ${ECS_HEADERS} -) - -target_include_directories(rtype_ecs PUBLIC - $ - $ -) - -target_link_libraries(rtype_ecs PUBLIC nlohmann_json::nlohmann_json rtype_animation) - -set_target_properties(rtype_ecs PROPERTIES - POSITION_INDEPENDENT_CODE ON -) diff --git a/libs/engine/src/ECS/CollisionDetectionSystem.cpp b/libs/engine/src/ECS/CollisionDetectionSystem.cpp deleted file mode 100644 index 17fd2e5..0000000 --- a/libs/engine/src/ECS/CollisionDetectionSystem.cpp +++ /dev/null @@ -1,155 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** CollisionDetectionSystem implementation -*/ - -#include "ECS/CollisionDetectionSystem.hpp" -#include -#include - -namespace RType { - namespace ECS { - - void CollisionDetectionSystem::Update(Registry& registry, float deltaTime) { - (void)deltaTime; - ClearCollisionEvents(registry); - - auto entities = GetCollidableEntities(registry); - - for (size_t i = 0; i < entities.size(); ++i) { - for (size_t j = i + 1; j < entities.size(); ++j) { - Entity a = entities[i]; - Entity b = entities[j]; - - if (!ShouldCollide(registry, a, b)) { - continue; - } - - if (CheckCollision(registry, a, b)) { - registry.AddComponent(a, CollisionEvent(b)); - registry.AddComponent(b, CollisionEvent(a)); - } - } - } - } - - void CollisionDetectionSystem::ClearCollisionEvents(Registry& registry) { - auto entitiesWithEvents = registry.GetEntitiesWithComponent(); - for (auto entity : entitiesWithEvents) { - if (registry.IsEntityAlive(entity)) { - registry.RemoveComponent(entity); - } - } - } - - std::vector CollisionDetectionSystem::GetCollidableEntities(Registry& registry) { - std::vector collidableEntities; - - auto allEntities = registry.GetEntitiesWithComponent(); - - for (auto entity : allEntities) { - bool hasCircleCollider = registry.HasComponent(entity); - bool hasBoxCollider = registry.HasComponent(entity); - - if (hasCircleCollider || hasBoxCollider) { - collidableEntities.push_back(entity); - } - } - - return collidableEntities; - } - - bool CollisionDetectionSystem::ShouldCollide(Registry& registry, Entity a, Entity b) { - if (!registry.HasComponent(a) || !registry.HasComponent(b)) { - return true; - } - const auto& layerA = registry.GetComponent(a); - const auto& layerB = registry.GetComponent(b); - - bool aCanCollideWithB = (layerA.mask & layerB.layer) != 0; - bool bCanCollideWithA = (layerB.mask & layerA.layer) != 0; - - return aCanCollideWithB && bCanCollideWithA; - } - - bool CollisionDetectionSystem::CheckCollision(Registry& registry, Entity a, Entity b) { - if (!registry.HasComponent(a) || !registry.HasComponent(b)) { - return false; - } - - const auto& posA = registry.GetComponent(a); - const auto& posB = registry.GetComponent(b); - - bool aHasCircle = registry.HasComponent(a); - bool bHasCircle = registry.HasComponent(b); - bool aHasBox = registry.HasComponent(a); - bool bHasBox = registry.HasComponent(b); - - if (aHasCircle && bHasCircle) { - const auto& circleA = registry.GetComponent(a); - const auto& circleB = registry.GetComponent(b); - return CheckCircleCircle(posA.x, posA.y, circleA.radius, - posB.x, posB.y, circleB.radius); - } - - if (aHasBox && bHasBox) { - const auto& boxA = registry.GetComponent(a); - const auto& boxB = registry.GetComponent(b); - return CheckAABB(posA.x, posA.y, boxA.width, boxA.height, - posB.x, posB.y, boxB.width, boxB.height); - } - - if (aHasCircle && bHasBox) { - const auto& circle = registry.GetComponent(a); - const auto& box = registry.GetComponent(b); - return CheckCircleAABB(posA.x, posA.y, circle.radius, - posB.x, posB.y, box.width, box.height); - } - - if (aHasBox && bHasCircle) { - const auto& box = registry.GetComponent(a); - const auto& circle = registry.GetComponent(b); - return CheckCircleAABB(posB.x, posB.y, circle.radius, - posA.x, posA.y, box.width, box.height); - } - - return false; - } - - bool CollisionDetectionSystem::CheckCircleCircle(float x1, float y1, float r1, - float x2, float y2, float r2) { - float dx = x2 - x1; - float dy = y2 - y1; - float radiusSum = r1 + r2; - float distanceSquared = dx * dx + dy * dy; - float radiusSumSquared = radiusSum * radiusSum; - - return distanceSquared <= radiusSumSquared; - } - - bool CollisionDetectionSystem::CheckAABB(float x1, float y1, float w1, float h1, - float x2, float y2, float w2, float h2) { - bool separated = - (x1 + w1 <= x2) || - (x1 >= x2 + w2) || - (y1 + h1 <= y2) || - (y1 >= y2 + h2); - - return !separated; - } - - bool CollisionDetectionSystem::CheckCircleAABB(float cx, float cy, float radius, - float bx, float by, float bw, float bh) { - float closestX = std::max(bx, std::min(cx, bx + bw)); - float closestY = std::max(by, std::min(cy, by + bh)); - float dx = cx - closestX; - float dy = cy - closestY; - float distanceSquared = dx * dx + dy * dy; - - return distanceSquared <= (radius * radius); - } - - } -} diff --git a/libs/engine/src/ECS/EnemyFactory.cpp b/libs/engine/src/ECS/EnemyFactory.cpp deleted file mode 100644 index 05acd74..0000000 --- a/libs/engine/src/ECS/EnemyFactory.cpp +++ /dev/null @@ -1,111 +0,0 @@ -#include "ECS/EnemyFactory.hpp" -#include "Core/Logger.hpp" -#include -#include -#include - -namespace RType { - - namespace ECS { - - namespace { - - std::atomic s_enemyIdCounter{1}; - - struct EnemyData { - Math::Color color; - const char* spritePath; - float speed; - int health; - int damage; - uint32_t score; - }; - - const std::array ENEMY_DATA_TABLE = {{// BASIC - {Math::Color(1.0f, 1.0f, 1.0f, 1.0f), "../assets/spaceships/nave2.png", 100.0f, 100, 10, 500}, - // FAST - {Math::Color(1.0f, 0.3f, 0.3f, 1.0f), "../assets/spaceships/nave2_red.png", 200.0f, 50, 5, 500}, - // TANK - {Math::Color(0.3f, 0.3f, 1.0f, 1.0f), "../assets/spaceships/nave2_blue.png", 50.0f, 200, 20, 500}, - // BOSS - {Math::Color(1.0f, 0.0f, 1.0f, 1.0f), "../assets/spaceships/nave2.png", 75.0f, 1000, 50, 500}, - // FORMATION - {Math::Color(0.5f, 0.5f, 0.5f, 1.0f), "../assets/spaceships/nave2.png", 100.0f, 100, 10, 500}}}; - - const EnemyData& GetEnemyData(EnemyType type) { - size_t index = static_cast(type); - if (index >= ENEMY_DATA_TABLE.size()) { - return ENEMY_DATA_TABLE[0]; - } - return ENEMY_DATA_TABLE[index]; - } - } - - Entity EnemyFactory::CreateEnemy(Registry& registry, EnemyType type, float startX, float startY, - Renderer::IRenderer* renderer) { - Entity enemy = registry.CreateEntity(); - - registry.AddComponent(enemy, Position(startX, startY)); - - const EnemyData& data = GetEnemyData(type); - registry.AddComponent(enemy, Velocity(-data.speed, 0.0f)); - - uint32_t uniqueId = s_enemyIdCounter.fetch_add(1); - registry.AddComponent(enemy, Enemy(type, uniqueId)); - - registry.AddComponent(enemy, Health(data.health, data.health)); - registry.AddComponent(enemy, Damage(data.damage)); - registry.AddComponent(enemy, ScoreValue(data.score)); - registry.AddComponent(enemy, BoxCollider(50.0f, 50.0f)); - - registry.AddComponent(enemy, CircleCollider(25.0f)); - registry.AddComponent(enemy, - CollisionLayer(CollisionLayers::ENEMY, - CollisionLayers::PLAYER | CollisionLayers::PLAYER_BULLET | CollisionLayers::OBSTACLE)); - - if (renderer) { - std::string spritePath(data.spritePath); - Renderer::TextureId textureId = renderer->LoadTexture(spritePath); - - if (textureId == Renderer::INVALID_TEXTURE_ID) { - Core::Logger::Warning("Failed to load enemy texture: {}, using default", spritePath); - textureId = renderer->LoadTexture("../assets/spaceships/nave2.png"); - } - - if (textureId != Renderer::INVALID_TEXTURE_ID) { - Renderer::SpriteId spriteId = - renderer->CreateSprite(textureId, Renderer::Rectangle{{0.0f, 0.0f}, {256.0f, 256.0f}}); - - auto& drawable = registry.AddComponent(enemy, Drawable(spriteId, 1)); - drawable.tint = data.color; - drawable.scale = Math::Vector2(0.5f, 0.5f); - drawable.origin = Math::Vector2(128.0f, 128.0f); - } else { - Core::Logger::Error("Failed to load any enemy texture for type {}", static_cast(type)); - } - } - - Core::Logger::Info("Created enemy type {} at position ({}, {})", static_cast(type), startX, startY); - - return enemy; - } - - float EnemyFactory::GetEnemySpeed(EnemyType type) { - return GetEnemyData(type).speed; - } - - int EnemyFactory::GetEnemyHealth(EnemyType type) { - return GetEnemyData(type).health; - } - - int EnemyFactory::GetEnemyDamage(EnemyType type) { - return GetEnemyData(type).damage; - } - - uint32_t EnemyFactory::GetEnemyScore(EnemyType type) { - return GetEnemyData(type).score; - } - - } - -} diff --git a/libs/engine/src/ECS/EnemySystem.cpp b/libs/engine/src/ECS/EnemySystem.cpp deleted file mode 100644 index 5eed3cc..0000000 --- a/libs/engine/src/ECS/EnemySystem.cpp +++ /dev/null @@ -1,119 +0,0 @@ -#include "ECS/EnemySystem.hpp" -#include "ECS/EnemyFactory.hpp" -#include "ECS/Component.hpp" -#include "Core/Logger.hpp" -#include -#include -#include - -namespace RType { - - namespace ECS { - - constexpr float ENEMY_SPAWN_OFFSET_X = 50.0f; - constexpr float ENEMY_SPAWN_MARGIN_Y = 50.0f; - constexpr float ENEMY_DESTROY_OFFSET_X = -100.0f; - constexpr float ENEMY_FAST_SIN_AMPLITUDE = 50.0f; - constexpr float ENEMY_FAST_SIN_FREQUENCY = 0.01f; - constexpr float ENEMY_SPAWN_MIN_Y = 50.0f; - - EnemySystem::EnemySystem(Renderer::IRenderer* renderer, float screenWidth, float screenHeight) - : m_renderer(renderer), m_screenWidth(screenWidth), m_screenHeight(screenHeight), - m_rng(std::random_device{}()) {} - - void EnemySystem::Update(Registry& registry, float deltaTime) { - m_spawnTimer += deltaTime; - - if (m_spawnTimer >= m_spawnInterval) { - SpawnRandomEnemy(registry); - m_spawnTimer = 0.0f; - } - - auto enemies = registry.GetEntitiesWithComponent(); - - for (Entity enemy : enemies) { - if (!registry.HasComponent(enemy)) { - continue; - } - - ApplyMovementPattern(registry, enemy, deltaTime); - } - - DestroyEnemiesOffScreen(registry, m_screenWidth); - } - - void EnemySystem::SpawnRandomEnemy(Registry& registry) { - std::uniform_real_distribution yDist(ENEMY_SPAWN_MIN_Y, m_screenHeight - ENEMY_SPAWN_MARGIN_Y); - std::uniform_int_distribution typeDist(0, 2); - - float spawnX = m_screenWidth + ENEMY_SPAWN_OFFSET_X; - float spawnY = yDist(m_rng); - - EnemyType type = static_cast(typeDist(m_rng)); - - EnemyFactory::CreateEnemy(registry, type, spawnX, spawnY, m_renderer); - } - - void EnemySystem::DestroyEnemiesOffScreen(Registry& registry, float /* screenWidth */) { - auto enemies = registry.GetEntitiesWithComponent(); - - for (Entity enemy : enemies) { - if (!registry.HasComponent(enemy)) { - continue; - } - - const auto& pos = registry.GetComponent(enemy); - - if (pos.x < ENEMY_DESTROY_OFFSET_X) { - registry.DestroyEntity(enemy); - } - } - } - - void EnemySystem::ApplyMovementPattern(Registry& registry, Entity enemy, float deltaTime) { - if (!registry.HasComponent(enemy) || !registry.HasComponent(enemy)) { - return; - } - - const auto& enemyComp = registry.GetComponent(enemy); - auto& velocity = registry.GetComponent(enemy); - - switch (enemyComp.type) { - case EnemyType::BASIC: - velocity.dx = -EnemyFactory::GetEnemySpeed(EnemyType::BASIC); - velocity.dy = 0.0f; - break; - - case EnemyType::FAST: { - if (registry.HasComponent(enemy)) { - const auto& pos = registry.GetComponent(enemy); - float speed = EnemyFactory::GetEnemySpeed(EnemyType::FAST); - velocity.dx = -speed; - velocity.dy = std::sin(pos.x * ENEMY_FAST_SIN_FREQUENCY) * ENEMY_FAST_SIN_AMPLITUDE; - } - break; - } - - case EnemyType::TANK: - velocity.dx = -EnemyFactory::GetEnemySpeed(EnemyType::TANK); - velocity.dy = 0.0f; - break; - - case EnemyType::BOSS: - velocity.dx = -EnemyFactory::GetEnemySpeed(EnemyType::BOSS); - velocity.dy = 0.0f; - break; - - case EnemyType::FORMATION: - velocity.dx = -EnemyFactory::GetEnemySpeed(EnemyType::FORMATION); - velocity.dy = 0.0f; - break; - - default: - break; - } - } - - } - -} diff --git a/libs/engine/src/ECS/HealthSystem.cpp b/libs/engine/src/ECS/HealthSystem.cpp deleted file mode 100644 index 54d2637..0000000 --- a/libs/engine/src/ECS/HealthSystem.cpp +++ /dev/null @@ -1,45 +0,0 @@ -#include "ECS/HealthSystem.hpp" -#include "ECS/Component.hpp" -#include "Core/Logger.hpp" - -namespace RType { - - namespace ECS { - - void HealthSystem::Update(Registry& registry, float deltaTime) { - (void)deltaTime; - - checkAndDestroyDeadEntities(registry); - } - - void HealthSystem::checkAndDestroyDeadEntities(Registry& registry) { - auto entitiesWithHealth = registry.GetEntitiesWithComponent(); - - for (Entity entity : entitiesWithHealth) { - if (!registry.IsEntityAlive(entity)) { - continue; - } - - if (!registry.HasComponent(entity)) { - continue; - } - - const auto& health = registry.GetComponent(entity); - - if (health.current <= 0) { - if (registry.HasComponent(entity)) { - if (!registry.HasComponent(entity)) { - registry.AddComponent(entity, BossKilled{entity, 1}); - Core::Logger::Info("[HealthSystem] Boss defeated! Marked for level transition"); - } - continue; - } - - registry.DestroyEntity(entity); - } - } - } - - } - -} diff --git a/libs/engine/src/ECS/InputSystem.cpp b/libs/engine/src/ECS/InputSystem.cpp deleted file mode 100644 index 1e172c7..0000000 --- a/libs/engine/src/ECS/InputSystem.cpp +++ /dev/null @@ -1,61 +0,0 @@ -#include "../include/ECS/InputSystem.hpp" -#include "../include/ECS/Registry.hpp" -#include "../include/ECS/Component.hpp" -#include "../include/Core/InputMapping.hpp" - -namespace RType { - namespace ECS { - - InputSystem::InputSystem(Renderer::IRenderer* renderer) - : m_renderer(renderer) {} - - void InputSystem::Update(Registry& registry, float deltaTime) { - auto entities = registry.GetEntitiesWithComponent(); - - Renderer::Key moveUpKey = Core::InputMapping::GetKey("MOVE_UP"); - Renderer::Key moveDownKey = Core::InputMapping::GetKey("MOVE_DOWN"); - Renderer::Key moveLeftKey = Core::InputMapping::GetKey("MOVE_LEFT"); - Renderer::Key moveRightKey = Core::InputMapping::GetKey("MOVE_RIGHT"); - Renderer::Key shootKey = Core::InputMapping::GetKey("SHOOT"); - - if (moveUpKey == Renderer::Key::Unknown) moveUpKey = Renderer::Key::Up; - if (moveDownKey == Renderer::Key::Unknown) moveDownKey = Renderer::Key::Down; - if (moveLeftKey == Renderer::Key::Unknown) moveLeftKey = Renderer::Key::Left; - if (moveRightKey == Renderer::Key::Unknown) moveRightKey = Renderer::Key::Right; - if (shootKey == Renderer::Key::Unknown) shootKey = Renderer::Key::E; - - for (Entity entity : entities) { - if (!registry.HasComponent(entity)) { - continue; - } - - auto& controllable = registry.GetComponent(entity); - auto& vel = registry.GetComponent(entity); - - vel.dx = 0.0f; - vel.dy = 0.0f; - - if (m_renderer->IsKeyPressed(moveUpKey)) { - vel.dy = -controllable.speed; - } - - if (m_renderer->IsKeyPressed(moveDownKey)) { - vel.dy = controllable.speed; - } - - if (m_renderer->IsKeyPressed(moveLeftKey)) { - vel.dx = -controllable.speed; - } - - if (m_renderer->IsKeyPressed(moveRightKey)) { - vel.dx = controllable.speed; - } - - if (registry.HasComponent(entity)) { - auto& shootCmd = registry.GetComponent(entity); - shootCmd.wantsToShoot = m_renderer->IsKeyPressed(shootKey); - } - } - } - } -} diff --git a/libs/engine/src/ECS/MenuSystem.cpp b/libs/engine/src/ECS/MenuSystem.cpp deleted file mode 100644 index 9b3d9d1..0000000 --- a/libs/engine/src/ECS/MenuSystem.cpp +++ /dev/null @@ -1,73 +0,0 @@ -#include "ECS/MenuSystem.hpp" -#include "ECS/Component.hpp" -#include "ECS/Components/Clickable.hpp" -#include "ECS/Components/TextLabel.hpp" - -namespace RType { - namespace ECS { - - MenuSystem::MenuSystem(Renderer::IRenderer* renderer) - : m_renderer(renderer) {} - - void MenuSystem::Update(Registry& registry, float deltaTime) { - if (!m_renderer) - return; - - Renderer::Vector2 mousePos = m_renderer->GetMousePosition(); - bool isMouseDown = m_renderer->IsMouseButtonPressed(Renderer::IRenderer::MouseButton::Left); - - auto entities = registry.GetEntitiesWithComponent(); - - for (Entity entity : entities) { - if (!registry.HasComponent(entity)) - continue; - - const auto& pos = registry.GetComponent(entity); - auto& clickable = registry.GetComponent(entity); - - Drawable* drawable = nullptr; - if (registry.HasComponent(entity)) { - drawable = ®istry.GetComponent(entity); - } - - TextLabel* textLabel = nullptr; - if (registry.HasComponent(entity)) { - textLabel = ®istry.GetComponent(entity); - } - - bool hovered = (mousePos.x >= pos.x && mousePos.x <= pos.x + clickable.width && - mousePos.y >= pos.y && mousePos.y <= pos.y + clickable.height); - - if (hovered) { - if (isMouseDown) { - clickable.state = ButtonState::Active; - if (drawable) - drawable->tint = {0.5f, 0.5f, 0.5f, 1.0f}; - if (textLabel) - textLabel->color = {0.5f, 0.5f, 0.5f, 1.0f}; - } else { - if (m_wasMouseDown && clickable.state == ButtonState::Active) { - if (m_callback) { - m_callback(clickable.actionId); - } - } - clickable.state = ButtonState::Hover; - if (drawable) - drawable->tint = {0.8f, 0.8f, 0.8f, 1.0f}; - if (textLabel) - textLabel->color = {0.8f, 0.8f, 0.8f, 1.0f}; - } - } else { - clickable.state = ButtonState::Idle; - if (drawable) - drawable->tint = {1.0f, 1.0f, 1.0f, 1.0f}; - if (textLabel) - textLabel->color = {1.0f, 1.0f, 1.0f, 1.0f}; - } - } - - m_wasMouseDown = isMouseDown; - } - - } -} diff --git a/libs/engine/src/ECS/MovementSystem.cpp b/libs/engine/src/ECS/MovementSystem.cpp deleted file mode 100644 index 11d5bd9..0000000 --- a/libs/engine/src/ECS/MovementSystem.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include "ECS/MovementSystem.hpp" -#include "ECS/Component.hpp" -#include - -namespace RType { - - namespace ECS { - - void MovementSystem::Update(Registry& registry, float deltaTime) { - auto entities = registry.GetEntitiesWithComponent(); - - for (Entity entity : entities) { - if (!registry.HasComponent(entity)) { - continue; - } - - // CRITICAL FIX: Skip obstacles - they should NEVER move! - if (registry.HasComponent(entity)) { - static int obstacleVelocityLog = 0; - if (obstacleVelocityLog < 10) { - std::cerr << "[MOVEMENT BUG] Obstacle entity " << entity - << " has Velocity component! Removing it." << std::endl; - obstacleVelocityLog++; - } - registry.RemoveComponent(entity); - continue; - } - - auto& position = registry.GetComponent(entity); - const auto& velocity = registry.GetComponent(entity); - - position.x += velocity.dx * deltaTime; - position.y += velocity.dy * deltaTime; - } - } - } - -} diff --git a/libs/engine/src/ECS/PowerUpCollisionSystem.cpp b/libs/engine/src/ECS/PowerUpCollisionSystem.cpp deleted file mode 100644 index 1dcdbad..0000000 --- a/libs/engine/src/ECS/PowerUpCollisionSystem.cpp +++ /dev/null @@ -1,76 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** PowerUpCollisionSystem -*/ - -#include "ECS/PowerUpCollisionSystem.hpp" -#include "ECS/PowerUpFactory.hpp" -#include "ECS/CollisionDetectionSystem.hpp" -#include "ECS/Component.hpp" -#include "Core/Logger.hpp" - -namespace RType { - namespace ECS { - - PowerUpCollisionSystem::PowerUpCollisionSystem(Renderer::IRenderer* renderer) - : m_renderer(renderer) {} - - void PowerUpCollisionSystem::Update(Registry& registry, float deltaTime) { - (void)deltaTime; - - auto powerups = registry.GetEntitiesWithComponent(); - auto players = registry.GetEntitiesWithComponent(); - - if (powerups.empty() || players.empty()) { - return; - } - - std::vector powerupsToDestroy; - - for (Entity powerup : powerups) { - if (!registry.IsEntityAlive(powerup)) { - continue; - } - - for (Entity player : players) { - if (!registry.IsEntityAlive(player)) { - continue; - } - - if (CollisionDetectionSystem::CheckCollision(registry, powerup, player)) { - const auto& powerupComp = registry.GetComponent(powerup); - - PowerUpFactory::ApplyPowerUpToPlayer( - registry, - player, - powerupComp.type, - m_renderer - ); - - Core::Logger::Info( - "[PowerUpCollisionSystem] Player collected powerup: {}", - PowerUpFactory::GetPowerUpName(powerupComp.type) - ); - - if (m_powerUpSound != Audio::INVALID_SOUND_ID) { - auto sfx = registry.CreateEntity(); - auto& se = registry.AddComponent(sfx, SoundEffect(m_powerUpSound, 1.0f)); - se.pitch = 1.0f; - } - - powerupsToDestroy.push_back(powerup); - break; - } - } - } - - // Destroy collected powerups - for (Entity powerup : powerupsToDestroy) { - registry.DestroyEntity(powerup); - } - } - - } -} diff --git a/libs/engine/src/ECS/PowerUpFactory.cpp b/libs/engine/src/ECS/PowerUpFactory.cpp deleted file mode 100644 index bf97a9c..0000000 --- a/libs/engine/src/ECS/PowerUpFactory.cpp +++ /dev/null @@ -1,265 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** PowerUpFactory -*/ - -#include "ECS/PowerUpFactory.hpp" -#include "ECS/EffectFactory.hpp" -#include "Animation/AnimationTypes.hpp" -#include "Core/Logger.hpp" -#include -#include -#include - -namespace RType { - namespace ECS { - - namespace { - std::atomic s_powerupIdCounter{1}; - - struct PowerUpData { - Math::Color color; - const char* spritePath; - const char* name; - }; - - const std::array POWERUP_DATA_TABLE = {{ - // FIRE_RATE_BOOST - Red - {Math::Color(1.0f, 0.2f, 0.2f, 1.0f), - "../assets/powerups/spread.png", "Fire Rate Boost"}, - // SPREAD_SHOT - Yellow - {Math::Color(1.0f, 1.0f, 0.2f, 1.0f), - "../assets/powerups/spread.png", "Spread Shot"}, - // LASER_BEAM - Cyan - {Math::Color(0.2f, 1.0f, 1.0f, 1.0f), - "../assets/powerups/laser.png", "Laser Beam"}, - // FORCE_POD - Orange (iconic R-Type color) - {Math::Color(1.0f, 0.6f, 0.0f, 1.0f), - "../assets/powerups/force_pod.png", "Force Pod"}, - // SPEED_BOOST - Green - {Math::Color(0.2f, 1.0f, 0.2f, 1.0f), - "../assets/powerups/speed.png", "Speed Boost"}, - // SHIELD - Blue - {Math::Color(0.4f, 0.4f, 1.0f, 1.0f), - "../assets/powerups/shield.png", "Shield"} - }}; - } - - Entity PowerUpFactory::CreatePowerUp( - Registry& registry, - PowerUpType type, - float startX, float startY, - Renderer::IRenderer* renderer, - const EffectFactory* effectFactory - ) { - Entity powerup = registry.CreateEntity(); - - registry.AddComponent(powerup, Position(startX, startY)); - registry.AddComponent(powerup, Velocity(-50.0f, 0.0f)); - - uint32_t uniqueId = s_powerupIdCounter.fetch_add(1); - registry.AddComponent(powerup, PowerUp(type, uniqueId)); - - registry.AddComponent(powerup, BoxCollider(32.0f, 32.0f)); - registry.AddComponent(powerup, - CollisionLayer(CollisionLayers::POWERUP, - CollisionLayers::PLAYER)); - - if (renderer) { - const PowerUpData& data = POWERUP_DATA_TABLE[static_cast(type)]; - - Renderer::TextureId textureId = renderer->LoadTexture(data.spritePath); - if (textureId == Renderer::INVALID_TEXTURE_ID) { - textureId = renderer->LoadTexture("../assets/powerups/laser.png"); - } - - if (textureId != Renderer::INVALID_TEXTURE_ID) { - Renderer::SpriteId spriteId = renderer->CreateSprite( - textureId, - Renderer::Rectangle{{0.0f, 0.0f}, {64.0f, 64.0f}} - ); - - auto& drawable = registry.AddComponent(powerup, Drawable(spriteId, 5)); - drawable.tint = data.color; - float scale = GetPowerUpScale(type); - drawable.scale = Math::Vector2(scale, scale); - - if (type == PowerUpType::FORCE_POD && effectFactory) { - const auto& config = effectFactory->GetConfig(); - if (config.forcePodAnimation != Animation::INVALID_CLIP_ID) { - if (config.forcePodSprite != Renderer::INVALID_SPRITE_ID) { - drawable.spriteId = config.forcePodSprite; - } - - auto& anim = registry.AddComponent(powerup, - SpriteAnimation(config.forcePodAnimation, true, 1.0f)); - anim.looping = true; - - if (config.forcePodFirstFrameRegion.size.x > 0.0f && config.forcePodFirstFrameRegion.size.y > 0.0f) { - anim.currentRegion = config.forcePodFirstFrameRegion; - anim.currentFrameIndex = 0; - } - - auto& animatedSprite = registry.AddComponent(powerup); - animatedSprite.needsUpdate = true; - } else { - auto& glow = registry.AddComponent(powerup); - glow.baseScale = scale; - } - } else { - auto& glow = registry.AddComponent(powerup); - glow.baseScale = scale; - } - } - } - - return powerup; - } - - void PowerUpFactory::ApplyPowerUpToPlayer( - Registry& registry, - Entity player, - PowerUpType type, - Renderer::IRenderer* renderer - ) { - if (!registry.HasComponent(player)) { - registry.AddComponent(player, ActivePowerUps()); - } - - auto& activePowerUps = registry.GetComponent(player); - - switch (type) { - case PowerUpType::FIRE_RATE_BOOST: { - if (!activePowerUps.hasFireRateBoost) { - activePowerUps.hasFireRateBoost = true; - - if (registry.HasComponent(player)) { - auto& shooter = registry.GetComponent(player); - shooter.fireRate *= 0.5f; - } - } - break; - } - - case PowerUpType::SPREAD_SHOT: { - if (!activePowerUps.hasSpreadShot) { - activePowerUps.hasSpreadShot = true; - activePowerUps.hasLaserBeam = false; - - if (registry.HasComponent(player)) { - registry.RemoveComponent(player); - } - registry.AddComponent( - player, - WeaponSlot(WeaponType::SPREAD, 0.3f, 20) - ); - } - break; - } - - case PowerUpType::LASER_BEAM: { - if (!activePowerUps.hasLaserBeam) { - activePowerUps.hasLaserBeam = true; - activePowerUps.hasSpreadShot = false; - - if (registry.HasComponent(player)) { - registry.RemoveComponent(player); - } - registry.AddComponent( - player, - WeaponSlot(WeaponType::LASER, 0.15f, 40) - ); - } - break; - } - - case PowerUpType::FORCE_POD: { - CreateForcePod(registry, player, renderer); - break; - } - - case PowerUpType::SPEED_BOOST: { - activePowerUps.speedMultiplier += 0.3f; - - if (registry.HasComponent(player)) { - auto& controllable = registry.GetComponent(player); - controllable.speed = 200.0f * activePowerUps.speedMultiplier; - } - break; - } - - case PowerUpType::SHIELD: { - if (!activePowerUps.hasShield) { - activePowerUps.hasShield = true; - constexpr float SHIELD_DURATION_SECONDS = 5.0f; - registry.AddComponent(player, Shield(SHIELD_DURATION_SECONDS)); - } - break; - } - } - } - - Entity PowerUpFactory::CreateForcePod( - Registry& registry, - Entity owner, - Renderer::IRenderer* renderer - ) { - Entity forcePod = registry.CreateEntity(); - - if (registry.HasComponent(owner)) { - const auto& ownerPos = registry.GetComponent(owner); - registry.AddComponent(forcePod, Position(ownerPos.x - 60.0f, ownerPos.y)); - } else { - registry.AddComponent(forcePod, Position(0.0f, 0.0f)); - } - - registry.AddComponent(forcePod, ForcePod(owner, -60.0f, 0.0f)); - registry.AddComponent(forcePod, BoxCollider(40.0f, 40.0f)); - - registry.AddComponent(forcePod, Shooter(0.25f, 50.0f, 20.0f)); - registry.AddComponent(forcePod, ShootCommand()); - - if (renderer) { - Renderer::TextureId textureId = renderer->LoadTexture("../assets/powerups/force_pod.png"); - if (textureId == Renderer::INVALID_TEXTURE_ID) { - textureId = renderer->LoadTexture("../assets/spaceships/player_blue.png"); - } - - if (textureId != Renderer::INVALID_TEXTURE_ID) { - Renderer::SpriteId spriteId = renderer->CreateSprite( - textureId, - Renderer::Rectangle{{0.0f, 0.0f}, {96.0f, 96.0f}} - ); - - auto& drawable = registry.AddComponent(forcePod, Drawable(spriteId, 9)); - drawable.tint = Math::Color(1.0f, 0.6f, 0.0f, 1.0f); - float scale = GetPowerUpScale(PowerUpType::FORCE_POD); - drawable.scale = Math::Vector2(scale, scale); - } - } - - return forcePod; - } - - Math::Color PowerUpFactory::GetPowerUpColor(PowerUpType type) { - return POWERUP_DATA_TABLE[static_cast(type)].color; - } - - const char* PowerUpFactory::GetPowerUpSpritePath(PowerUpType type) { - return POWERUP_DATA_TABLE[static_cast(type)].spritePath; - } - - const char* PowerUpFactory::GetPowerUpName(PowerUpType type) { - return POWERUP_DATA_TABLE[static_cast(type)].name; - } - - float PowerUpFactory::GetPowerUpScale(PowerUpType type) { - if (type == PowerUpType::FORCE_POD) { - return 0.33f * 5.0f; // 5x scale for force pod (upscaled more) - } - return 2.5f; - } - } -} diff --git a/libs/engine/src/ECS/PowerUpSpawnSystem.cpp b/libs/engine/src/ECS/PowerUpSpawnSystem.cpp deleted file mode 100644 index a4016d7..0000000 --- a/libs/engine/src/ECS/PowerUpSpawnSystem.cpp +++ /dev/null @@ -1,83 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** PowerUpSpawnSystem -*/ - -#include "ECS/PowerUpSpawnSystem.hpp" -#include "ECS/PowerUpFactory.hpp" -#include "ECS/EffectFactory.hpp" -#include "ECS/Component.hpp" -#include "Core/Logger.hpp" -#include - -namespace RType { - namespace ECS { - - PowerUpSpawnSystem::PowerUpSpawnSystem( - Renderer::IRenderer* renderer, - float screenWidth, - float screenHeight - ) - : m_renderer(renderer), - m_screenWidth(screenWidth), - m_screenHeight(screenHeight), - m_rng(std::chrono::steady_clock::now().time_since_epoch().count()) - { - } - - void PowerUpSpawnSystem::Update(Registry& registry, float deltaTime) { - m_spawnTimer += deltaTime; - - if (m_spawnTimer >= m_spawnInterval) { - SpawnRandomPowerUp(registry); - m_spawnTimer = 0.0f; - } - - DestroyPowerUpsOffScreen(registry); - } - - void PowerUpSpawnSystem::SpawnRandomPowerUp(Registry& registry) { - // Random powerup type (0-5) - std::uniform_int_distribution typeDist(0, 5); - PowerUpType type = static_cast(typeDist(m_rng)); - - // Random Y position (50 to screenHeight - 50) - std::uniform_real_distribution yDist(50.0f, m_screenHeight - 50.0f); - float spawnY = yDist(m_rng); - - float spawnX = m_screenWidth + 50.0f; - - PowerUpFactory::CreatePowerUp( - registry, - type, - spawnX, - spawnY, - m_renderer, - m_effectFactory - ); - - Core::Logger::Info("[PowerUpSpawnSystem] Spawned {} powerup at ({}, {})", - PowerUpFactory::GetPowerUpName(type), spawnX, spawnY); - } - - void PowerUpSpawnSystem::DestroyPowerUpsOffScreen(Registry& registry) { - auto powerups = registry.GetEntitiesWithComponent(); - - for (Entity powerup : powerups) { - if (!registry.IsEntityAlive(powerup)) { - continue; - } - - if (registry.HasComponent(powerup)) { - const auto& pos = registry.GetComponent(powerup); - if (pos.x < -100.0f) { - registry.DestroyEntity(powerup); - } - } - } - } - - } -} diff --git a/libs/engine/src/ECS/Registry.cpp b/libs/engine/src/ECS/Registry.cpp deleted file mode 100644 index 96525e9..0000000 --- a/libs/engine/src/ECS/Registry.cpp +++ /dev/null @@ -1,80 +0,0 @@ -#include "ECS/Registry.hpp" -#include - -namespace RType { - - namespace ECS { - - Registry::Registry() - : m_nextEntityID(1), m_entityCount(0) {} - - Entity Registry::CreateEntity() { - Entity newEntity; - - if (!m_freeEntityIds.empty()) { - newEntity = m_freeEntityIds.back(); - m_freeEntityIds.pop_back(); - -#ifdef DEBUG - // CRITICAL: Verify entity has no components before reuse - // If this fires, DestroyEntity didn't clean up properly - for (const auto& [componentID, pool] : m_componentPools) { - if (pool->Has(newEntity)) { - std::cerr << "[CRITICAL] Entity " << newEntity - << " being reused but still has components!" << std::endl; - pool->Remove(newEntity); // Emergency cleanup - } - } -#endif - } else { - newEntity = m_nextEntityID++; - } - - m_aliveEntities.insert(newEntity); - m_entityCount++; - return newEntity; - } - - void Registry::DestroyEntity(Entity entity) { - if (entity == NULL_ENTITY) { - return; - } - if (!IsEntityAlive(entity)) { - return; - } - - // Remove all components from this entity - for (auto& [componentID, pool] : m_componentPools) { - if (pool->Has(entity)) { - pool->Remove(entity); - } - } - -#ifdef DEBUG - // Debug validation: Verify all components were actually removed - // This helps catch component persistence bugs that cause entity type confusion - size_t remainingComponents = 0; - for (const auto& [componentID, pool] : m_componentPools) { - if (pool->Has(entity)) { - remainingComponents++; - } - } - if (remainingComponents > 0) { - std::cerr << "[DEBUG] WARNING: Entity " << entity - << " still has " << remainingComponents - << " component(s) after DestroyEntity cleanup!" << std::endl; - } -#endif - - m_aliveEntities.erase(entity); - m_entityCount--; - m_freeEntityIds.push_back(entity); - } - - bool Registry::IsEntityAlive(Entity entity) const { - return m_aliveEntities.find(entity) != m_aliveEntities.end(); - } - - } - -} diff --git a/libs/engine/src/ECS/RenderingSystem.cpp b/libs/engine/src/ECS/RenderingSystem.cpp deleted file mode 100644 index 2e62614..0000000 --- a/libs/engine/src/ECS/RenderingSystem.cpp +++ /dev/null @@ -1,73 +0,0 @@ -#include "ECS/RenderingSystem.hpp" -#include "ECS/Component.hpp" -#include "Core/ColorFilter.hpp" -#include - -namespace RType { - - namespace ECS { - - RenderingSystem::RenderingSystem(Renderer::IRenderer* renderer) - : m_renderer(renderer) {} - - void RenderingSystem::Update(Registry& registry, float deltaTime) { - if (!m_renderer) { - return; - } - - auto entities = registry.GetEntitiesWithComponent(); - - std::vector> renderableEntities; - for (Entity entity : entities) { - if (!registry.IsEntityAlive(entity)) { - continue; - } - if (registry.HasComponent(entity)) { - const auto& drawable = registry.GetComponent(entity); - renderableEntities.emplace_back(entity, drawable.layer); - } - } - - std::sort(renderableEntities.begin(), renderableEntities.end(), - [](const std::pair& a, const std::pair& b) { - return a.second < b.second; - }); - - for (const auto& pair : renderableEntities) { - Entity entity = pair.first; - const auto& position = registry.GetComponent(entity); - const auto& drawable = registry.GetComponent(entity); - - if (drawable.spriteId == Renderer::INVALID_SPRITE_ID) { - continue; - } - - if (registry.HasComponent(entity) && registry.HasComponent(entity)) { - auto& animatedSprite = registry.GetComponent(entity); - const auto& anim = registry.GetComponent(entity); - - if (anim.currentRegion.size.x > 0 && anim.currentRegion.size.y > 0) { - if (animatedSprite.needsUpdate) { - m_renderer->SetSpriteRegion(drawable.spriteId, anim.currentRegion); - animatedSprite.needsUpdate = false; - } - } - } - - Renderer::Transform2D transform; - transform.position = Renderer::Vector2(position.x, position.y); - transform.scale = drawable.scale; - transform.rotation = drawable.rotation; - transform.origin = drawable.origin; - - Math::Color finalColor = drawable.tint; - if (RType::Core::ColorFilter::IsColourBlindModeEnabled()) { - finalColor = RType::Core::ColorFilter::ApplyColourBlindFilter(drawable.tint); - } - - m_renderer->DrawSprite(drawable.spriteId, transform, finalColor); - } - } - } - -} diff --git a/libs/engine/src/ECS/ScoreSystem.cpp b/libs/engine/src/ECS/ScoreSystem.cpp deleted file mode 100644 index 303ac93..0000000 --- a/libs/engine/src/ECS/ScoreSystem.cpp +++ /dev/null @@ -1,61 +0,0 @@ -#include "../../include/ECS/ScoreSystem.hpp" -#include "../../include/ECS/Component.hpp" - - -namespace RType { - namespace ECS { - - void ScoreSystem::Update(Registry& registry, float deltaTime) { - auto enemiesKilled = registry.GetEntitiesWithComponent(); - - if (enemiesKilled.empty()) { - } - - for (auto enemyKilled : enemiesKilled) { - if (!registry.IsEntityAlive(enemyKilled) || !registry.HasComponent(enemyKilled) || !registry.HasComponent(enemyKilled)) { - continue; - } - const auto& enemyKilledComp = registry.GetComponent(enemyKilled); - Entity killer = enemyKilledComp.killedBy; - const auto& ennemyScoreValue = registry.GetComponent(enemyKilled); - - if (killer == NULL_ENTITY || !registry.IsEntityAlive(killer) || !registry.HasComponent(killer)) { - registry.RemoveComponent(enemyKilled); - continue; - } - - auto& killerScoreComp = registry.GetComponent(killer); - killerScoreComp.points += ennemyScoreValue.points; - - registry.RemoveComponent(enemyKilled); - } - - // +10 toutes les 10 secondes - constexpr float SCORE_INTERVAL_SECONDS = 10.0f; - constexpr uint32_t SCORE_PER_INTERVAL = 10; - - auto timedEntities = registry.GetEntitiesWithComponent(); - for (auto entity : timedEntities) { - if (!registry.IsEntityAlive(entity) || - !registry.HasComponent(entity) || - !registry.HasComponent(entity) || - !registry.HasComponent(entity)) { - continue; - } - - auto& timer = registry.GetComponent(entity); - timer.elapsed += deltaTime; - - if (timer.elapsed < SCORE_INTERVAL_SECONDS) { - continue; - } - - uint32_t intervals = static_cast(timer.elapsed / SCORE_INTERVAL_SECONDS); - timer.elapsed -= static_cast(intervals) * SCORE_INTERVAL_SECONDS; - - auto& score = registry.GetComponent(entity); - score.points += intervals * SCORE_PER_INTERVAL; - } - } - } -} \ No newline at end of file diff --git a/libs/engine/src/ECS/ScrollingSystem.cpp b/libs/engine/src/ECS/ScrollingSystem.cpp deleted file mode 100644 index 608437b..0000000 --- a/libs/engine/src/ECS/ScrollingSystem.cpp +++ /dev/null @@ -1,115 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** ScrollingSystem -*/ - -#include "../../include/ECS/ScrollingSystem.hpp" -#include "../../include/ECS/Registry.hpp" -#include "../../include/ECS/Component.hpp" -#include - -namespace RType { - namespace ECS { - - void ScrollingSystem::Update(Registry& registry, float deltaTime) { - auto scrollables = registry.GetEntitiesWithComponent(); - - static bool loggedScrollingSystem = false; - if (!loggedScrollingSystem) { - std::cout << "[ScrollingSystem] Two-pass update running" << std::endl; - loggedScrollingSystem = true; - } - - // First pass: scroll visual entities (obstacles, backgrounds, etc.) - for (auto entity : scrollables) { - if (!registry.IsEntityAlive(entity)) { - continue; - } - - // Skip obstacle colliders in first pass (they'll be synced in second pass) - bool isObstacleCollider = registry.HasComponent(entity) && - !registry.HasComponent(entity); - if (isObstacleCollider) { - continue; - } - - const auto& scrollable = registry.GetComponent(entity); - - if (!registry.HasComponent(entity)) { - continue; - } - - auto& pos = registry.GetComponent(entity); - pos.x = pos.x + scrollable.speed * deltaTime; - - float destroyThreshold = -1500.0f; - if (pos.x < destroyThreshold) { - registry.DestroyEntity(entity); - } - } - - // Second pass: synchronize obstacle colliders to their visual entities - static int serverSyncDebug = 0; - for (auto entity : scrollables) { - if (!registry.IsEntityAlive(entity)) { - continue; - } - - bool isObstacleCollider = registry.HasComponent(entity) && - !registry.HasComponent(entity); - - if (!isObstacleCollider) { - continue; - } - - if (!registry.HasComponent(entity) || - !registry.HasComponent(entity)) { - continue; - } - - const auto& metadata = registry.GetComponent(entity); - auto& colliderPos = registry.GetComponent(entity); - - if (metadata.visualEntity != NULL_ENTITY && - registry.IsEntityAlive(metadata.visualEntity) && - registry.HasComponent(metadata.visualEntity) && - registry.HasComponent(metadata.visualEntity)) { - - const auto& visualPos = registry.GetComponent(metadata.visualEntity); - - // Debug first few server syncs - if (serverSyncDebug < 3) { - std::cout << "[ScrollingSystem SYNC] Collider " << entity - << ": visual=(" << visualPos.x << "," << visualPos.y << ")" - << " offset=(" << metadata.offsetX << "," << metadata.offsetY << ")" - << " -> collider=(" << visualPos.x + metadata.offsetX << "," - << visualPos.y + metadata.offsetY << ")" << std::endl; - serverSyncDebug++; - } - - // Collider position = visual position + stored offset - colliderPos.x = visualPos.x + metadata.offsetX; - colliderPos.y = visualPos.y + metadata.offsetY; - } - else if (metadata.visualEntity != NULL_ENTITY && - registry.IsEntityAlive(metadata.visualEntity) && - !registry.HasComponent(metadata.visualEntity)) { - registry.DestroyEntity(entity); - continue; - } - else if (registry.HasComponent(entity)) { - const auto& scrollable = registry.GetComponent(entity); - colliderPos.x = colliderPos.x + scrollable.speed * deltaTime; - } - - // Destroy threshold for obstacle colliders - float destroyThreshold = -2000.0f; - if (colliderPos.x < destroyThreshold) { - registry.DestroyEntity(entity); - } - } - } - } -} diff --git a/libs/engine/src/ECS/ShieldSystem.cpp b/libs/engine/src/ECS/ShieldSystem.cpp deleted file mode 100644 index 1ba20f4..0000000 --- a/libs/engine/src/ECS/ShieldSystem.cpp +++ /dev/null @@ -1,75 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** ShieldSystem -*/ - -#include "ECS/ShieldSystem.hpp" -#include "ECS/Component.hpp" - -namespace RType { - namespace ECS { - - void ShieldSystem::Update(Registry& registry, float deltaTime) { - auto shields = registry.GetEntitiesWithComponent(); - - std::vector expiredShields; - - for (Entity entity : shields) { - if (!registry.IsEntityAlive(entity)) { - continue; - } - - auto& shield = registry.GetComponent(entity); - - if (shield.duration > 0.0f) { - shield.timeRemaining -= deltaTime; - - if (shield.timeRemaining <= 0.0f) { - expiredShields.push_back(entity); - } - } - - if (registry.HasComponent(entity)) { - auto& drawable = registry.GetComponent(entity); - drawable.tint = Math::Color(0.7f, 0.7f, 1.0f, 1.0f); - } - } - - for (Entity entity : expiredShields) { - registry.RemoveComponent(entity); - - if (registry.HasComponent(entity)) { - auto& active = registry.GetComponent(entity); - active.hasShield = false; - } - - if (registry.HasComponent(entity)) { - auto& drawable = registry.GetComponent(entity); - drawable.tint = Math::Color(1.0f, 1.0f, 1.0f, 1.0f); - } - } - - auto players = registry.GetEntitiesWithComponent(); - for (Entity entity : players) { - if (!registry.IsEntityAlive(entity)) { - continue; - } - - if (!registry.HasComponent(entity) && registry.HasComponent(entity)) { - auto& drawable = registry.GetComponent(entity); - if (registry.HasComponent(entity)) { - const auto& active = registry.GetComponent(entity); - if (!active.hasShield && drawable.tint.r < 0.8f && drawable.tint.b > 0.8f) { - drawable.tint = Math::Color(1.0f, 1.0f, 1.0f, 1.0f); - } - } else if (drawable.tint.r < 0.8f && drawable.tint.b > 0.8f) { - drawable.tint = Math::Color(1.0f, 1.0f, 1.0f, 1.0f); - } - } - } - } - - } -} diff --git a/libs/engine/src/ECS/TextRenderingSystem.cpp b/libs/engine/src/ECS/TextRenderingSystem.cpp deleted file mode 100644 index d421c70..0000000 --- a/libs/engine/src/ECS/TextRenderingSystem.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#include "ECS/TextRenderingSystem.hpp" -#include "ECS/Component.hpp" -#include "ECS/Components/TextLabel.hpp" -#include "Core/ColorFilter.hpp" - -namespace RType { - namespace ECS { - - TextRenderingSystem::TextRenderingSystem(Renderer::IRenderer* renderer) - : m_renderer(renderer) {} - - void TextRenderingSystem::Update(Registry& registry, float deltaTime) { - if (!m_renderer) - return; - - auto entities = registry.GetEntitiesWithComponent(); - - for (Entity entity : entities) { - if (!registry.IsEntityAlive(entity)) { - continue; - } - if (!registry.HasComponent(entity)) - continue; - - const auto& pos = registry.GetComponent(entity); - const auto& label = registry.GetComponent(entity); - - Renderer::TextParams params; - params.position = {pos.x + label.offsetX, pos.y + label.offsetY}; - Math::Color finalColor = label.color; - if (RType::Core::ColorFilter::IsColourBlindModeEnabled()) { - finalColor = RType::Core::ColorFilter::ApplyColourBlindFilter(label.color); - } - params.color = finalColor; - params.scale = 1.0f; - params.rotation = 0.0f; - params.centered = label.centered; - - m_renderer->DrawText(label.fontId, label.text, params); - } - } - } -} diff --git a/libs/engine/src/ECS/ThirdBulletSystem.cpp b/libs/engine/src/ECS/ThirdBulletSystem.cpp deleted file mode 100644 index 7ea3e87..0000000 --- a/libs/engine/src/ECS/ThirdBulletSystem.cpp +++ /dev/null @@ -1,73 +0,0 @@ -#include "ECS/ThirdBulletSystem.hpp" -#include "ECS/Component.hpp" -#include "Core/Logger.hpp" -#include -#include - -namespace RType { - namespace ECS { - - void ThirdBulletSystem::Update(Registry& registry, float deltaTime) { - auto thirdBullets = registry.GetEntitiesWithComponent(); - - for (auto bulletEntity : thirdBullets) { - if (!registry.IsEntityAlive(bulletEntity)) { - continue; - } - - auto& thirdBullet = registry.GetComponent(bulletEntity); - if (!thirdBullet.isActive) { - continue; - } - - const auto& pos = registry.GetComponent(bulletEntity); - thirdBullet.timeSinceSpawn += deltaTime; - - if (thirdBullet.timeSinceSpawn >= thirdBullet.spawnInterval) { - thirdBullet.timeSinceSpawn = 0.0f; - SpawnSmallProjectile(registry, pos.x, pos.y); - } - } - } - - void ThirdBulletSystem::SpawnSmallProjectile(Registry& registry, float x, float y) { - const float speed = 300.0f; - - Direction directions[3] = { - {0.0f, -speed}, - {0.0f, speed}, - {speed, 0.0f} - }; - - for (int i = 0; i < 3; i++) { - Entity smallBullet = registry.CreateEntity(); - - // CRITICAL FIX: Clean up obstacle components from entity ID reuse - if (registry.HasComponent(smallBullet)) { - std::cerr << "[THIRDBULLET CLEANUP] Removing Obstacle from bullet entity " << smallBullet << std::endl; - registry.RemoveComponent(smallBullet); - } - if (registry.HasComponent(smallBullet)) { - std::cerr << "[THIRDBULLET CLEANUP] Removing ObstacleMetadata from bullet entity " << smallBullet << std::endl; - registry.RemoveComponent(smallBullet); - } - - registry.AddComponent(smallBullet, Position{x, y}); - - registry.AddComponent(smallBullet, Velocity{directions[i].vx, directions[i].vy}); - - registry.AddComponent(smallBullet, BossBullet{}); - registry.AddComponent(smallBullet, Bullet{}); - - registry.AddComponent(smallBullet, CircleCollider{10.0f}); - registry.AddComponent(smallBullet, - CollisionLayer(CollisionLayers::OBSTACLE, CollisionLayers::PLAYER)); - - registry.AddComponent(smallBullet, Damage{10}); - } - - Core::Logger::Debug("[ThirdBulletSystem] Spawned 3 cross projectiles at ({}, {})", x, y); - } - - } -} diff --git a/libs/engine/src/Renderer/CMakeLists.txt b/libs/engine/src/Renderer/CMakeLists.txt deleted file mode 100644 index ee397d2..0000000 --- a/libs/engine/src/Renderer/CMakeLists.txt +++ /dev/null @@ -1,114 +0,0 @@ -# Three-tier SFML 2.x dependency resolution: -# 1. vcpkg (if CMAKE_TOOLCHAIN_FILE is set) -# 2. System-installed SFML 2.x (via find_package) -# 3. FetchContent (download and build SFML 2.6.2 from source) - -set(SFML_FOUND FALSE) - -# Tier 1: Try vcpkg first (already configured by root CMakeLists.txt) -if(DEFINED CMAKE_TOOLCHAIN_FILE AND CMAKE_TOOLCHAIN_FILE MATCHES "vcpkg") - message(STATUS "Searching for SFML 2.6+ via vcpkg...") - find_package(SFML 2.6 COMPONENTS graphics window system QUIET) - if(SFML_FOUND) - message(STATUS "Found SFML ${SFML_VERSION} via vcpkg") - endif() -endif() - -# Tier 2: Try system-installed SFML 2.6+ -if(NOT SFML_FOUND) - message(STATUS "Searching for system-installed SFML 2.6+...") - find_package(SFML 2.6 COMPONENTS graphics window system QUIET) - if(SFML_FOUND) - message(STATUS "Found system-installed SFML ${SFML_VERSION}") - endif() -endif() - -# Tier 3: Fetch SFML 2.6.2 from source if not found -if(NOT SFML_FOUND) - message(STATUS "SFML 2.x not found, fetching SFML 2.6.2 from source...") - - include(FetchContent) - - # Save current BUILD_SHARED_LIBS to avoid polluting other dependencies - set(OLD_BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS}) - - FetchContent_Declare( - SFML - GIT_REPOSITORY https://github.com/SFML/SFML.git - GIT_TAG 2.6.2 - GIT_SHALLOW TRUE - ) - - # Don't build audio and network modules (we only need graphics/window/system) - set(SFML_BUILD_AUDIO OFF CACHE BOOL "" FORCE) - set(SFML_BUILD_NETWORK OFF CACHE BOOL "" FORCE) - set(BUILD_SHARED_LIBS ON CACHE BOOL "" FORCE) - - FetchContent_MakeAvailable(SFML) - - # Restore original BUILD_SHARED_LIBS setting - set(BUILD_SHARED_LIBS ${OLD_BUILD_SHARED_LIBS} CACHE BOOL "" FORCE) - - set(SFML_LIBRARIES sfml-graphics sfml-window sfml-system) - set(SFML_FOUND TRUE) - message(STATUS "SFML 2.6.2 fetched and configured") -endif() - -# Determine library names based on target availability -if(SFML_FOUND) - if(TARGET SFML::Graphics) - # vcpkg uses SFML:: namespace - set(SFML_LIBRARIES SFML::Graphics SFML::Window SFML::System) - message(STATUS "Using SFML 2.x with SFML:: namespace") - elseif(TARGET sfml-graphics) - # FetchContent or manual install uses sfml- prefix - set(SFML_LIBRARIES sfml-graphics sfml-window sfml-system) - message(STATUS "Using SFML 2.x with sfml- prefix") - else() - message(FATAL_ERROR "SFML found but could not determine target names. " - "Expected SFML::Graphics or sfml-graphics targets. " - "Please ensure SFML 2.6+ is properly installed.") - endif() -else() - message(FATAL_ERROR "SFML 2.6+ could not be found via vcpkg, system packages, or FetchContent. " - "Please check your CMake configuration.") -endif() - -# Create SFML Renderer library -add_library(rtype_sfml_renderer SHARED - SFMLRenderer.cpp -) - -target_include_directories(rtype_sfml_renderer PUBLIC - $ - $ -) - -target_link_libraries(rtype_sfml_renderer - PUBLIC - rtype_core - rtype_ecs - PRIVATE - ${SFML_LIBRARIES} -) - -target_compile_features(rtype_sfml_renderer PUBLIC cxx_std_17) - -if(WIN32) - target_compile_definitions(rtype_sfml_renderer PRIVATE - NOMINMAX - WIN32_LEAN_AND_MEAN - _CRT_SECURE_NO_WARNINGS - ) -endif() - -set_target_properties(rtype_sfml_renderer PROPERTIES - PREFIX "" - OUTPUT_NAME "SFMLRenderer" - POSITION_INDEPENDENT_CODE ON -) - -install(TARGETS rtype_sfml_renderer - LIBRARY DESTINATION lib/rtype/modules - RUNTIME DESTINATION bin/rtype/modules -) diff --git a/libs/engine/src/Renderer/SFMLRenderer.cpp b/libs/engine/src/Renderer/SFMLRenderer.cpp deleted file mode 100644 index c7b7fe9..0000000 --- a/libs/engine/src/Renderer/SFMLRenderer.cpp +++ /dev/null @@ -1,541 +0,0 @@ -#include "Renderer/SFMLRenderer.hpp" -#include "Core/Engine.hpp" -#include "Core/Logger.hpp" -#include -#include - -namespace Renderer { - - SFMLRenderer::SFMLRenderer() { - RType::Core::Logger::Debug("SFMLRenderer constructor called"); - } - - SFMLRenderer::~SFMLRenderer() { - RType::Core::Logger::Debug("SFMLRenderer destructor called"); - Shutdown(); - } - - const char* SFMLRenderer::GetName() const { - return "SFMLRenderer"; - } - - RType::Core::ModulePriority SFMLRenderer::GetPriority() const { - return RType::Core::ModulePriority::High; - } - - bool SFMLRenderer::Initialize(RType::Core::Engine* engine) { - RType::Core::Logger::Info("Initializing SFMLRenderer module (SFML 2.6)"); - m_engine = engine; - m_clock.restart(); - return true; - } - - void SFMLRenderer::Shutdown() { - RType::Core::Logger::Info("Shutting down SFMLRenderer module"); - - m_sprites.clear(); - m_textures.clear(); - m_fonts.clear(); - - Destroy(); - m_engine = nullptr; - } - - void SFMLRenderer::Update(float /* deltaTime */) { - if (!m_window || !m_window->isOpen()) { - return; - } - - ProcessEvents(); - - m_stats.drawCalls = 0; - m_stats.textureSwitches = 0; - } - - bool SFMLRenderer::ShouldUpdateInRenderThread() const { - return true; - } - - bool SFMLRenderer::CreateWindow(const WindowConfig& config) { - RType::Core::Logger::Info("Creating window: {}x{} - {}", config.width, config.height, config.title); - - m_windowConfig = config; - - sf::Uint32 style = sf::Style::Default; - if (config.fullscreen) { - style = sf::Style::Fullscreen; - } else if (!config.resizable) { - style = sf::Style::Titlebar | sf::Style::Close; - } - - m_window = std::make_unique( - sf::VideoMode(config.width, config.height), - config.title, - style); - - if (!m_window) { - RType::Core::Logger::Error("Failed to create SFML window"); - return false; - } - - if (config.targetFramerate > 0) { - m_window->setFramerateLimit(config.targetFramerate); - } - - m_defaultView.setSize(1280.0f, 720.0f); - m_defaultView.setCenter(640.0f, 360.0f); - m_currentView = m_defaultView; - m_window->setView(m_currentView); - - RType::Core::Logger::Info("Window created successfully"); - return true; - } - - void SFMLRenderer::Destroy() { - if (m_window) { - RType::Core::Logger::Info("Destroying window"); - m_window->close(); - m_window.reset(); - } - } - - bool SFMLRenderer::IsWindowOpen() const { - return m_window && m_window->isOpen(); - } - - void SFMLRenderer::Resize(std::uint32_t width, std::uint32_t height) { - if (!m_window) { - RType::Core::Logger::Warning("Cannot resize: window not created"); - return; - } - - if (width == 0 || height == 0) { - RType::Core::Logger::Warning("Invalid window dimensions: {}x{}", width, height); - return; - } - - m_windowConfig.width = width; - m_windowConfig.height = height; - - const float targetWidth = 1280.0f; - const float targetHeight = 720.0f; - - m_defaultView.setSize(targetWidth, targetHeight); - m_defaultView.setCenter(targetWidth / 2.0f, targetHeight / 2.0f); - m_defaultView.setViewport(sf::FloatRect(0.0f, 0.0f, 1.0f, 1.0f)); - - if (!m_usingCustomCamera) { - m_currentView = m_defaultView; - m_window->setView(m_currentView); - } - - RType::Core::Logger::Info("Window resized to {}x{}", width, height); - } - - void SFMLRenderer::SetWindowTitle(const std::string& title) { - if (!m_window) { - RType::Core::Logger::Warning("Cannot set title: window not created"); - return; - } - - m_windowConfig.title = title; - m_window->setTitle(title); - } - - void SFMLRenderer::BeginFrame() { - if (!m_window) { - return; - } - - m_stats.drawCalls = 0; - m_stats.textureSwitches = 0; - } - - void SFMLRenderer::EndFrame() { - if (!m_window) { - return; - } - - m_window->display(); - } - - void SFMLRenderer::Clear(const Color& color) { - if (!m_window) { - return; - } - - m_window->clear(ToSFMLColor(color)); - } - - TextureId SFMLRenderer::LoadTexture(const std::string& path, const TextureConfig& config) { - auto texture = std::make_shared(); - - if (!texture->loadFromFile(path)) { - RType::Core::Logger::Error("Failed to load texture: {}", path); - return 0; - } - texture->setSmooth(config.smooth); - texture->setRepeated(config.repeated); - - TextureId id = m_nextTextureId++; - m_textures[id] = {texture, config}; - - RType::Core::Logger::Debug("Loaded texture: {} (ID: {})", path, id); - return id; - } - - void SFMLRenderer::UnloadTexture(TextureId textureId) { - auto it = m_textures.find(textureId); - if (it == m_textures.end()) { - RType::Core::Logger::Warning("Attempted to unload non-existent texture ID: {}", textureId); - return; - } - - for (auto spriteIt = m_sprites.begin(); spriteIt != m_sprites.end();) { - if (spriteIt->second.textureId == textureId) { - spriteIt = m_sprites.erase(spriteIt); - } else { - ++spriteIt; - } - } - - m_textures.erase(it); - RType::Core::Logger::Debug("Unloaded texture ID: {}", textureId); - } - - Vector2 SFMLRenderer::GetTextureSize(TextureId textureId) const { - auto it = m_textures.find(textureId); - if (it == m_textures.end()) { - return {0.0f, 0.0f}; - } - sf::Vector2u size = it->second.texture->getSize(); - return {static_cast(size.x), static_cast(size.y)}; - } - - SpriteId SFMLRenderer::CreateSprite(TextureId textureId, const Rectangle& region) { - auto it = m_textures.find(textureId); - if (it == m_textures.end()) { - RType::Core::Logger::Error("Cannot create sprite: texture ID {} not found", textureId); - return 0; - } - - SpriteId id = m_nextSpriteId++; - - SpriteData spriteData; - spriteData.sprite.setTexture(*it->second.texture); - spriteData.textureId = textureId; - - if (region.size.x > 0 && region.size.y > 0) { - spriteData.sprite.setTextureRect(sf::IntRect( - static_cast(region.position.x), - static_cast(region.position.y), - static_cast(region.size.x), - static_cast(region.size.y))); - } - - m_sprites[id] = spriteData; - - RType::Core::Logger::Debug("Created sprite ID: {} from texture ID: {}", id, textureId); - return id; - } - - void SFMLRenderer::DestroySprite(SpriteId spriteId) { - auto it = m_sprites.find(spriteId); - if (it == m_sprites.end()) { - RType::Core::Logger::Warning("Attempted to destroy non-existent sprite ID: {}", spriteId); - return; - } - - m_sprites.erase(it); - RType::Core::Logger::Debug("Destroyed sprite ID: {}", spriteId); - } - - void SFMLRenderer::SetSpriteRegion(SpriteId spriteId, const Rectangle& region) { - auto it = m_sprites.find(spriteId); - if (it == m_sprites.end()) { - RType::Core::Logger::Warning("Attempted to set region for non-existent sprite ID: {}", spriteId); - return; - } - - it->second.sprite.setTextureRect(ToSFMLRect(region)); - } - - void SFMLRenderer::DrawSprite(SpriteId spriteId, const Transform2D& transform, const Color& tint) { - if (!m_window) { - return; - } - - auto it = m_sprites.find(spriteId); - if (it == m_sprites.end()) { - RType::Core::Logger::Warning("Cannot draw sprite: ID {} not found", spriteId); - return; - } - - sf::Sprite& sprite = it->second.sprite; - - sprite.setPosition(ToSFMLVector(transform.position)); - sprite.setScale(ToSFMLVector(transform.scale)); - sprite.setRotation(transform.rotation); - sprite.setOrigin(ToSFMLVector(transform.origin)); - - if (tint.r > 0 || tint.g > 0 || tint.b > 0 || tint.a > 0) { - sprite.setColor(ToSFMLColor(tint)); - } else { - sprite.setColor(sf::Color::White); - } - - m_window->draw(sprite); - m_stats.drawCalls++; - } - - void SFMLRenderer::DrawRectangle(const Rectangle& rectangle, const Color& color) { - if (!m_window) { - return; - } - - sf::RectangleShape rect(ToSFMLVector(rectangle.size)); - rect.setPosition(ToSFMLVector(rectangle.position)); - rect.setFillColor(ToSFMLColor(color)); - - m_window->draw(rect); - m_stats.drawCalls++; - } - - FontId SFMLRenderer::LoadFont(const std::string& path, std::uint32_t characterSize) { - auto font = std::make_shared(); - - if (!font->loadFromFile(path)) { - RType::Core::Logger::Error("Failed to load font: {}", path); - return 0; - } - - FontId id = m_nextFontId++; - m_fonts[id] = {font, characterSize}; - - RType::Core::Logger::Debug("Loaded font: {} (ID: {}, size: {})", path, id, characterSize); - return id; - } - - void SFMLRenderer::UnloadFont(FontId fontId) { - auto it = m_fonts.find(fontId); - if (it == m_fonts.end()) { - RType::Core::Logger::Warning("Attempted to unload non-existent font ID: {}", fontId); - return; - } - - m_fonts.erase(it); - RType::Core::Logger::Debug("Unloaded font ID: {}", fontId); - } - - void SFMLRenderer::DrawText(FontId fontId, const std::string& text, const TextParams& params) { - if (!m_window) { - return; - } - - auto it = m_fonts.find(fontId); - if (it == m_fonts.end()) { - RType::Core::Logger::Warning("Cannot draw text: font ID {} not found", fontId); - return; - } - - sf::Text sfText; - sfText.setFont(*it->second.font); - sfText.setString(text); - sfText.setCharacterSize(it->second.characterSize); - sfText.setRotation(params.rotation); - sfText.setFillColor(ToSFMLColor(params.color)); - sfText.setScale(sf::Vector2f(params.scale, params.scale)); - - if (params.centered) { - sf::FloatRect bounds = sfText.getLocalBounds(); - sfText.setOrigin(bounds.left + bounds.width / 2.0f, bounds.top + bounds.height / 2.0f); - } - - sfText.setPosition(ToSFMLVector(params.position)); - - m_window->draw(sfText); - m_stats.drawCalls++; - } - - void SFMLRenderer::SetCamera(const Camera2D& camera) { - if (!m_window) { - return; - } - - m_currentView.setCenter(ToSFMLVector(camera.center)); - m_currentView.setSize(camera.size.x, camera.size.y); - m_window->setView(m_currentView); - m_usingCustomCamera = true; - - RType::Core::Logger::Debug("Camera set to center: ({}, {}), size: ({}, {})", - camera.center.x, camera.center.y, - camera.size.x, camera.size.y); - } - - void SFMLRenderer::ResetCamera() { - if (!m_window) { - return; - } - - m_currentView = m_defaultView; - m_window->setView(m_currentView); - m_usingCustomCamera = false; - - RType::Core::Logger::Debug("Camera reset to default view"); - } - - RenderStats SFMLRenderer::GetRenderStats() const { - return m_stats; - } - - float SFMLRenderer::GetDeltaTime() { - m_lastDeltaTime = m_clock.restart().asSeconds(); - return m_lastDeltaTime; - } - - void SFMLRenderer::ProcessEvents() { - if (!m_window) { - return; - } - - sf::Event event; - while (m_window->pollEvent(event)) { - if (event.type == sf::Event::Closed) { - m_window->close(); - } else if (event.type == sf::Event::Resized) { - Resize(event.size.width, event.size.height); - } - } - } - - sf::Color SFMLRenderer::ToSFMLColor(const Color& color) { - return sf::Color( - static_cast(color.r * 255.0f), - static_cast(color.g * 255.0f), - static_cast(color.b * 255.0f), - static_cast(color.a * 255.0f)); - } - - sf::Vector2f SFMLRenderer::ToSFMLVector(const Vector2& vec) { - return sf::Vector2f(vec.x, vec.y); - } - - sf::IntRect SFMLRenderer::ToSFMLRect(const Rectangle& rect) { - return sf::IntRect( - static_cast(rect.position.x), - static_cast(rect.position.y), - static_cast(rect.size.x), - static_cast(rect.size.y)); - } - - sf::Keyboard::Key SFMLRenderer::ToSFMLKey(Key key) { - static const std::unordered_map keyMapping = { - {Key::A, sf::Keyboard::A}, - {Key::B, sf::Keyboard::B}, - {Key::C, sf::Keyboard::C}, - {Key::D, sf::Keyboard::D}, - {Key::E, sf::Keyboard::E}, - {Key::F, sf::Keyboard::F}, - {Key::G, sf::Keyboard::G}, - {Key::H, sf::Keyboard::H}, - {Key::I, sf::Keyboard::I}, - {Key::J, sf::Keyboard::J}, - {Key::K, sf::Keyboard::K}, - {Key::L, sf::Keyboard::L}, - {Key::M, sf::Keyboard::M}, - {Key::N, sf::Keyboard::N}, - {Key::O, sf::Keyboard::O}, - {Key::P, sf::Keyboard::P}, - {Key::Q, sf::Keyboard::Q}, - {Key::R, sf::Keyboard::R}, - {Key::S, sf::Keyboard::S}, - {Key::T, sf::Keyboard::T}, - {Key::U, sf::Keyboard::U}, - {Key::V, sf::Keyboard::V}, - {Key::W, sf::Keyboard::W}, - {Key::X, sf::Keyboard::X}, - {Key::Y, sf::Keyboard::Y}, - {Key::Z, sf::Keyboard::Z}, - {Key::Num0, sf::Keyboard::Num0}, - {Key::Num1, sf::Keyboard::Num1}, - {Key::Num2, sf::Keyboard::Num2}, - {Key::Num3, sf::Keyboard::Num3}, - {Key::Num4, sf::Keyboard::Num4}, - {Key::Num5, sf::Keyboard::Num5}, - {Key::Num6, sf::Keyboard::Num6}, - {Key::Num7, sf::Keyboard::Num7}, - {Key::Num8, sf::Keyboard::Num8}, - {Key::Num9, sf::Keyboard::Num9}, - {Key::Escape, sf::Keyboard::Escape}, - {Key::Space, sf::Keyboard::Space}, - {Key::Enter, sf::Keyboard::Return}, - {Key::Backspace, sf::Keyboard::BackSpace}, - {Key::Tab, sf::Keyboard::Tab}, - {Key::Left, sf::Keyboard::Left}, - {Key::Right, sf::Keyboard::Right}, - {Key::Up, sf::Keyboard::Up}, - {Key::Down, sf::Keyboard::Down}, - {Key::LShift, sf::Keyboard::LShift}, - {Key::RShift, sf::Keyboard::RShift}, - {Key::LControl, sf::Keyboard::LControl}, - {Key::RControl, sf::Keyboard::RControl}, - {Key::LAlt, sf::Keyboard::LAlt}, - {Key::RAlt, sf::Keyboard::RAlt}}; - - auto it = keyMapping.find(key); - return (it != keyMapping.end()) ? it->second : sf::Keyboard::Unknown; - } - - bool SFMLRenderer::IsKeyPressed(Key key) const { - return sf::Keyboard::isKeyPressed(ToSFMLKey(key)); - } - - bool SFMLRenderer::IsMouseButtonPressed(MouseButton button) const { - sf::Mouse::Button sfBtn; - switch (button) { - case MouseButton::Left: - sfBtn = sf::Mouse::Left; - break; - case MouseButton::Right: - sfBtn = sf::Mouse::Right; - break; - case MouseButton::Middle: - sfBtn = sf::Mouse::Middle; - break; - default: - return false; - } - return sf::Mouse::isButtonPressed(sfBtn); - } - - Vector2 SFMLRenderer::GetMousePosition() const { - if (!m_window) - return {0, 0}; - sf::Vector2i pos = sf::Mouse::getPosition(*m_window); - sf::Vector2f worldPos = m_window->mapPixelToCoords(pos, m_currentView); - return {worldPos.x, worldPos.y}; - } - -} - -extern "C" { -#ifdef _WIN32 -__declspec(dllexport) -#else -__attribute__((visibility("default"))) -#endif -RType::Core::IModule* -CreateModule() { - return new Renderer::SFMLRenderer(); -} - -#ifdef _WIN32 -__declspec(dllexport) -#else -__attribute__((visibility("default"))) -#endif -void -DestroyModule(RType::Core::IModule* module) { - delete module; -} -}; diff --git a/libs/engine/src/main.cpp b/libs/engine/src/main.cpp deleted file mode 100644 index 151bf6a..0000000 --- a/libs/engine/src/main.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include -#include "../include/Core/Engine.hpp" - -using namespace RType; - -int main() { - Core::Logger::Info("R-Type Engine starting..."); - - Core::EngineConfig config; - config.pluginPath = "./plugins"; - - auto engine = std::make_unique(config); - - if (!engine->Initialize()) { - Core::Logger::Error("Failed to initialize engine"); - return 1; - } - Core::Logger::Info("Engine initialized with {} modules", engine->GetAllModules().size()); - - auto& registry = engine->GetRegistry(); - Core::Logger::Info("Registry has {} entities", registry.GetEntityCount()); - - engine->Shutdown(); - - Core::Logger::Info("Engine stopped"); - return 0; -} diff --git a/libs/network/CMakeLists.txt b/libs/network/CMakeLists.txt index fae57fc..488f8b7 100644 --- a/libs/network/CMakeLists.txt +++ b/libs/network/CMakeLists.txt @@ -39,7 +39,8 @@ add_library(rtype_network target_include_directories(rtype_network PUBLIC $ - $ + $ + $ ) find_package(lz4 CONFIG REQUIRED) @@ -48,7 +49,7 @@ target_link_libraries(rtype_network PUBLIC asio Threads::Threads nlohmann_json::nlohmann_json - rtype_ecs + rtype_game lz4::lz4 ) diff --git a/libs/network/include/GameServer.hpp b/libs/network/include/GameServer.hpp index fee7267..d98c429 100644 --- a/libs/network/include/GameServer.hpp +++ b/libs/network/include/GameServer.hpp @@ -12,27 +12,27 @@ #include "ECS/Registry.hpp" #include "ECS/Component.hpp" #include "ECS/MovementSystem.hpp" -#include "ECS/EnemySystem.hpp" +#include "EnemySystem.hpp" #include "ECS/CollisionDetectionSystem.hpp" -#include "ECS/BulletCollisionResponseSystem.hpp" -#include "ECS/PlayerCollisionResponseSystem.hpp" -#include "ECS/ObstacleCollisionResponseSystem.hpp" +#include "BulletCollisionResponseSystem.hpp" +#include "PlayerCollisionResponseSystem.hpp" +#include "ObstacleCollisionResponseSystem.hpp" #include "ECS/ScrollingSystem.hpp" -#include "ECS/BossSystem.hpp" -#include "ECS/BossAttackSystem.hpp" -#include "ECS/MineSystem.hpp" -#include "ECS/BlackOrbSystem.hpp" -#include "ECS/ThirdBulletSystem.hpp" -#include "ECS/LevelLoader.hpp" +#include "BossSystem.hpp" +#include "BossAttackSystem.hpp" +#include "MineSystem.hpp" +#include "BlackOrbSystem.hpp" +#include "ThirdBulletSystem.hpp" +#include "LevelLoader.hpp" #include "ECS/HealthSystem.hpp" #include "ECS/ScoreSystem.hpp" -#include "ECS/PlayerFactory.hpp" -#include "ECS/EnemyFactory.hpp" -#include "ECS/PowerUpSpawnSystem.hpp" -#include "ECS/PowerUpCollisionSystem.hpp" -#include "ECS/ShootingSystem.hpp" -#include "ECS/ForcePodSystem.hpp" -#include "ECS/ShieldSystem.hpp" +#include "PlayerFactory.hpp" +#include "EnemyFactory.hpp" +#include "PowerUpSpawnSystem.hpp" +#include "PowerUpCollisionSystem.hpp" +#include "ShootingSystem.hpp" +#include "ForcePodSystem.hpp" +#include "ShieldSystem.hpp" #include #include #include diff --git a/libs/network/src/GameServer.cpp b/libs/network/src/GameServer.cpp index 766250e..7fad140 100644 --- a/libs/network/src/GameServer.cpp +++ b/libs/network/src/GameServer.cpp @@ -7,8 +7,8 @@ #include "GameServer.hpp" #include "Compression.hpp" -#include "ECS/BossSystem.hpp" -#include "ECS/MineSystem.hpp" +#include "BossSystem.hpp" +#include "MineSystem.hpp" #include #include #include diff --git a/tests/test_health_system.cpp b/tests/test_health_system.cpp index eeda349..693fbbf 100644 --- a/tests/test_health_system.cpp +++ b/tests/test_health_system.cpp @@ -11,10 +11,10 @@ #include "Renderer/IRenderer.hpp" #include "ECS/Component.hpp" #include "ECS/InputSystem.hpp" -#include "ECS/PlayerSystem.hpp" -#include "ECS/PlayerFactory.hpp" -#include "ECS/EnemySystem.hpp" -#include "ECS/EnemyFactory.hpp" +#include "PlayerSystem.hpp" +#include "PlayerFactory.hpp" +#include "EnemySystem.hpp" +#include "EnemyFactory.hpp" #include "ECS/MovementSystem.hpp" #include "ECS/CollisionDetectionSystem.hpp" #include "ECS/HealthSystem.hpp" diff --git a/tests/test_player_system.cpp b/tests/test_player_system.cpp index 238a2b1..7ee47ca 100644 --- a/tests/test_player_system.cpp +++ b/tests/test_player_system.cpp @@ -11,8 +11,8 @@ #include "Renderer/IRenderer.hpp" #include "ECS/Component.hpp" #include "ECS/InputSystem.hpp" -#include "ECS/PlayerSystem.hpp" -#include "ECS/PlayerFactory.hpp" +#include "PlayerSystem.hpp" +#include "PlayerFactory.hpp" #include "ECS/MovementSystem.hpp" #include "ECS/RenderingSystem.hpp" #include From b7bdc832ab5fb0ca44be211146447249a31791c7 Mon Sep 17 00:00:00 2001 From: erwan Date: Mon, 2 Feb 2026 00:23:15 +0100 Subject: [PATCH 5/7] chore: removed old breakout directory --- breakout/README.md | 33 --- breakout/include/BallAccelerationSystem.hpp | 23 -- breakout/include/BreakoutPhysicsSystem.hpp | 26 -- breakout/include/BreakoutRenderSystem.hpp | 22 -- breakout/src/BallAccelerationSystem.cpp | 45 --- breakout/src/BreakoutPhysicsSystem.cpp | 245 ---------------- breakout/src/BreakoutRenderSystem.cpp | 72 ----- breakout/src/main.cpp | 301 -------------------- 8 files changed, 767 deletions(-) delete mode 100644 breakout/README.md delete mode 100644 breakout/include/BallAccelerationSystem.hpp delete mode 100644 breakout/include/BreakoutPhysicsSystem.hpp delete mode 100644 breakout/include/BreakoutRenderSystem.hpp delete mode 100644 breakout/src/BallAccelerationSystem.cpp delete mode 100644 breakout/src/BreakoutPhysicsSystem.cpp delete mode 100644 breakout/src/BreakoutRenderSystem.cpp delete mode 100644 breakout/src/main.cpp diff --git a/breakout/README.md b/breakout/README.md deleted file mode 100644 index 41a0b80..0000000 --- a/breakout/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Breakout - -Breakout game demonstrating the reusability of the R-Type game engine. - -## Objective - -Destroy all bricks by bouncing the ball with the paddle. - -## Controls - -- **Left Arrow** : Move the paddle to the left -- **Right Arrow** : Move the paddle to the right -- **C** : Toggle colour-blind mode (ON/OFF) -- **Escape** : Quit the game - -## Game Mechanics - -- **Ball** : Bounces off walls (left, right, top) and the paddle -- **Paddle** : The bounce angle depends on where the ball hits the paddle -- **Bricks** : A brick is destroyed in one hit -- **Acceleration** : The ball gradually accelerates over time -- **Ball Loss** : If the ball goes out at the bottom, it reappears at the center with a random downward direction -- **Score** : Increases with each brick destroyed -- **Victory** : When all bricks are destroyed, a "WIN" message appears - -## Compilation - -```bash -cd build -make breakout -./breakout -``` - diff --git a/breakout/include/BallAccelerationSystem.hpp b/breakout/include/BallAccelerationSystem.hpp deleted file mode 100644 index c71bc04..0000000 --- a/breakout/include/BallAccelerationSystem.hpp +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include "ECS/ISystem.hpp" - -namespace Breakout { - namespace ECS { - - class BallAccelerationSystem : public RType::ECS::ISystem { - public: - BallAccelerationSystem() = default; - ~BallAccelerationSystem() override = default; - - const char* GetName() const override { return "BallAccelerationSystem"; } - void Update(RType::ECS::Registry& registry, float deltaTime) override; - - private: - float m_elapsedTime = 0.0f; - static constexpr float ACCELERATION_RATE = 2.5f; - static constexpr float MAX_SPEED_MULTIPLIER = 2.5f; - }; - - } -} diff --git a/breakout/include/BreakoutPhysicsSystem.hpp b/breakout/include/BreakoutPhysicsSystem.hpp deleted file mode 100644 index 932e035..0000000 --- a/breakout/include/BreakoutPhysicsSystem.hpp +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include "ECS/ISystem.hpp" -#include "ECS/Registry.hpp" -#include "ECS/Component.hpp" - -namespace Breakout { - namespace ECS { - - class BreakoutPhysicsSystem : public RType::ECS::ISystem { - public: - BreakoutPhysicsSystem() = default; - ~BreakoutPhysicsSystem() override = default; - - const char* GetName() const override { return "BreakoutPhysicsSystem"; } - void Update(RType::ECS::Registry& registry, float deltaTime) override; - - private: - void HandleBallPaddleCollision(RType::ECS::Registry& registry, RType::ECS::Entity ball, RType::ECS::Entity paddle); - void HandleBallBrickCollision(RType::ECS::Registry& registry, RType::ECS::Entity ball, RType::ECS::Entity brick); - void HandleBallWallCollision(RType::ECS::Registry& registry, RType::ECS::Entity ball, float screenWidth, float screenHeight); - }; - - } -} - diff --git a/breakout/include/BreakoutRenderSystem.hpp b/breakout/include/BreakoutRenderSystem.hpp deleted file mode 100644 index 68e518a..0000000 --- a/breakout/include/BreakoutRenderSystem.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include "ECS/ISystem.hpp" -#include "Renderer/IRenderer.hpp" - -namespace Breakout { - namespace ECS { - - class BreakoutRenderSystem : public RType::ECS::ISystem { - public: - explicit BreakoutRenderSystem(Renderer::IRenderer* renderer); - ~BreakoutRenderSystem() override = default; - - const char* GetName() const override { return "BreakoutRenderSystem"; } - void Update(RType::ECS::Registry& registry, float deltaTime) override; - - private: - Renderer::IRenderer* m_renderer; - }; - - } -} diff --git a/breakout/src/BallAccelerationSystem.cpp b/breakout/src/BallAccelerationSystem.cpp deleted file mode 100644 index 8d2b8e0..0000000 --- a/breakout/src/BallAccelerationSystem.cpp +++ /dev/null @@ -1,45 +0,0 @@ -#include "BallAccelerationSystem.hpp" -#include "ECS/Registry.hpp" -#include "ECS/Component.hpp" -#include - -namespace Breakout { - namespace ECS { - - void BallAccelerationSystem::Update(RType::ECS::Registry& registry, float deltaTime) { - m_elapsedTime += deltaTime; - - RType::ECS::Entity ballEntity = RType::ECS::NULL_ENTITY; - auto entities = registry.GetEntitiesWithComponent(); - for (auto entity : entities) { - if (registry.HasComponent(entity)) { - ballEntity = entity; - break; - } - } - - if (ballEntity == RType::ECS::NULL_ENTITY || !registry.HasComponent(ballEntity)) { - return; - } - - auto& ballVel = registry.GetComponent(ballEntity); - - float speedMultiplier = 1.0f + (m_elapsedTime * ACCELERATION_RATE / 50.0f); - if (speedMultiplier > MAX_SPEED_MULTIPLIER) { - speedMultiplier = MAX_SPEED_MULTIPLIER; - } - - float currentSpeed = std::sqrt(ballVel.dx * ballVel.dx + ballVel.dy * ballVel.dy); - float targetSpeed = 250.0f * speedMultiplier; - - if (currentSpeed < targetSpeed && currentSpeed > 0.0f) { - float newSpeed = currentSpeed + (targetSpeed - currentSpeed) * deltaTime; - float dirX = ballVel.dx / currentSpeed; - float dirY = ballVel.dy / currentSpeed; - ballVel.dx = dirX * newSpeed; - ballVel.dy = dirY * newSpeed; - } - } - - } -} diff --git a/breakout/src/BreakoutPhysicsSystem.cpp b/breakout/src/BreakoutPhysicsSystem.cpp deleted file mode 100644 index 4bd6125..0000000 --- a/breakout/src/BreakoutPhysicsSystem.cpp +++ /dev/null @@ -1,245 +0,0 @@ -#include "BreakoutPhysicsSystem.hpp" -#include "ECS/Registry.hpp" -#include "ECS/Component.hpp" -#include -#include - -namespace Breakout { - namespace ECS { - - void BreakoutPhysicsSystem::Update(RType::ECS::Registry& registry, float deltaTime) { - constexpr float SCREEN_WIDTH = 800.0f; - constexpr float SCREEN_HEIGHT = 900.0f; - - RType::ECS::Entity ballEntity = RType::ECS::NULL_ENTITY; - auto entities = registry.GetEntitiesWithComponent(); - for (auto entity : entities) { - if (registry.HasComponent(entity)) { - ballEntity = entity; - break; - } - } - - if (ballEntity == RType::ECS::NULL_ENTITY) { - return; - } - - if (!registry.HasComponent(ballEntity) || - !registry.HasComponent(ballEntity)) { - return; - } - - const auto& ballPos = registry.GetComponent(ballEntity); - const auto& ballCollider = registry.GetComponent(ballEntity); - float ballRadius = ballCollider.radius; - - RType::ECS::Entity paddleEntity = RType::ECS::NULL_ENTITY; - auto controllableEntities = registry.GetEntitiesWithComponent(); - for (auto entity : controllableEntities) { - if (registry.HasComponent(entity)) { - paddleEntity = entity; - break; - } - } - - if (paddleEntity != RType::ECS::NULL_ENTITY && - registry.HasComponent(paddleEntity) && - registry.HasComponent(paddleEntity)) { - const auto& paddlePos = registry.GetComponent(paddleEntity); - const auto& paddleCollider = registry.GetComponent(paddleEntity); - - float paddleLeft = paddlePos.x - paddleCollider.width / 2.0f; - float paddleRight = paddlePos.x + paddleCollider.width / 2.0f; - float paddleTop = paddlePos.y - paddleCollider.height / 2.0f; - float paddleBottom = paddlePos.y + paddleCollider.height / 2.0f; - - float closestX = std::max(paddleLeft, std::min(ballPos.x, paddleRight)); - float closestY = std::max(paddleTop, std::min(ballPos.y, paddleBottom)); - - float dx = ballPos.x - closestX; - float dy = ballPos.y - closestY; - float distanceSquared = dx * dx + dy * dy; - - if (distanceSquared < (ballRadius * ballRadius)) { - HandleBallPaddleCollision(registry, ballEntity, paddleEntity); - } - } - - auto brickEntities = registry.GetEntitiesWithComponent(); - bool ballCollided = false; - for (auto brick : brickEntities) { - if (ballCollided) { - break; - } - if (registry.HasComponent(brick)) { - continue; - } - if (!registry.HasComponent(brick) || - !registry.HasComponent(brick) || - !registry.IsEntityAlive(brick)) { - continue; - } - - const auto& brickPos = registry.GetComponent(brick); - const auto& brickCollider = registry.GetComponent(brick); - - float brickLeft = brickPos.x - brickCollider.width / 2.0f; - float brickRight = brickPos.x + brickCollider.width / 2.0f; - float brickTop = brickPos.y - brickCollider.height / 2.0f; - float brickBottom = brickPos.y + brickCollider.height / 2.0f; - - float closestX = std::max(brickLeft, std::min(ballPos.x, brickRight)); - float closestY = std::max(brickTop, std::min(ballPos.y, brickBottom)); - - float dx = ballPos.x - closestX; - float dy = ballPos.y - closestY; - float distanceSquared = dx * dx + dy * dy; - - if (distanceSquared < (ballRadius * ballRadius)) { - HandleBallBrickCollision(registry, ballEntity, brick); - ballCollided = true; - } - } - - HandleBallWallCollision(registry, ballEntity, SCREEN_WIDTH, SCREEN_HEIGHT); - } - - void BreakoutPhysicsSystem::HandleBallPaddleCollision(RType::ECS::Registry& registry, RType::ECS::Entity ball, RType::ECS::Entity paddle) { - if (!registry.HasComponent(ball) || !registry.HasComponent(paddle) || - !registry.HasComponent(ball) || !registry.HasComponent(paddle) || - !registry.HasComponent(ball)) { - return; - } - - const auto& ballPos = registry.GetComponent(ball); - const auto& paddlePos = registry.GetComponent(paddle); - const auto& paddleCollider = registry.GetComponent(paddle); - const auto& ballCollider = registry.GetComponent(ball); - auto& ballVel = registry.GetComponent(ball); - auto& ballPosRef = registry.GetComponent(ball); - - float ballRadius = ballCollider.radius; - float paddleLeft = paddlePos.x - paddleCollider.width / 2.0f; - float paddleRight = paddlePos.x + paddleCollider.width / 2.0f; - float paddleTop = paddlePos.y - paddleCollider.height / 2.0f; - float paddleBottom = paddlePos.y + paddleCollider.height / 2.0f; - - float relativeX = (ballPos.x - paddlePos.x) / (paddleCollider.width / 2.0f); - relativeX = std::max(-1.0f, std::min(1.0f, relativeX)); - - float angle = relativeX * 60.0f * (3.14159f / 180.0f); - float speed = std::sqrt(ballVel.dx * ballVel.dx + ballVel.dy * ballVel.dy); - if (speed < 250.0f) { - speed = 300.0f; - } - - ballVel.dx = speed * std::sin(angle); - ballVel.dy = -std::abs(speed * std::cos(angle)); - - if (ballPos.y > paddlePos.y) { - ballPosRef.y = paddleBottom + ballRadius + 0.1f; - } else { - ballPosRef.y = paddleTop - ballRadius - 0.1f; - } - - if (ballPos.x < paddleLeft) { - ballPosRef.x = paddleLeft - ballRadius - 0.1f; - } else if (ballPos.x > paddleRight) { - ballPosRef.x = paddleRight + ballRadius + 0.1f; - } - } - - void BreakoutPhysicsSystem::HandleBallBrickCollision(RType::ECS::Registry& registry, RType::ECS::Entity ball, RType::ECS::Entity brick) { - if (!registry.HasComponent(ball) || !registry.HasComponent(brick) || - !registry.HasComponent(ball) || !registry.HasComponent(brick) || - !registry.HasComponent(brick) || !registry.HasComponent(ball)) { - return; - } - - const auto& ballPos = registry.GetComponent(ball); - const auto& brickPos = registry.GetComponent(brick); - const auto& brickCollider = registry.GetComponent(brick); - const auto& ballCollider = registry.GetComponent(ball); - auto& ballVel = registry.GetComponent(ball); - auto& ballPosRef = registry.GetComponent(ball); - auto& brickHealth = registry.GetComponent(brick); - - float brickLeft = brickPos.x - brickCollider.width / 2.0f; - float brickRight = brickPos.x + brickCollider.width / 2.0f; - float brickTop = brickPos.y - brickCollider.height / 2.0f; - float brickBottom = brickPos.y + brickCollider.height / 2.0f; - - float ballRadius = ballCollider.radius; - float overlapLeft = (ballPos.x + ballRadius) - brickLeft; - float overlapRight = brickRight - (ballPos.x - ballRadius); - float overlapTop = (ballPos.y + ballRadius) - brickTop; - float overlapBottom = brickBottom - (ballPos.y - ballRadius); - - float minOverlap = std::min({overlapLeft, overlapRight, overlapTop, overlapBottom}); - - if (minOverlap == overlapLeft || minOverlap == overlapRight) { - ballVel.dx = -ballVel.dx; - if (minOverlap == overlapLeft) { - ballPosRef.x = brickLeft - ballRadius - 0.1f; - } else { - ballPosRef.x = brickRight + ballRadius + 0.1f; - } - } else { - ballVel.dy = std::abs(ballVel.dy); - if (minOverlap == overlapTop) { - ballPosRef.y = brickTop - ballRadius - 0.1f; - } else { - ballPosRef.y = brickBottom + ballRadius + 0.1f; - } - } - - brickHealth.current--; - if (brickHealth.current <= 0) { - registry.DestroyEntity(brick); - } - } - - void BreakoutPhysicsSystem::HandleBallWallCollision(RType::ECS::Registry& registry, RType::ECS::Entity ball, float screenWidth, float screenHeight) { - if (ball == RType::ECS::NULL_ENTITY || !registry.IsEntityAlive(ball)) { - return; - } - - if (!registry.HasComponent(ball) || !registry.HasComponent(ball) || - !registry.HasComponent(ball)) { - return; - } - - auto& ballPos = registry.GetComponent(ball); - auto& ballVel = registry.GetComponent(ball); - float radius = registry.GetComponent(ball).radius; - - if (ballPos.x - radius <= 0.0f) { - ballPos.x = radius; - ballVel.dx = std::abs(ballVel.dx); - } else if (ballPos.x + radius >= screenWidth) { - ballPos.x = screenWidth - radius; - ballVel.dx = -std::abs(ballVel.dx); - } - - if (ballPos.y - radius <= 0.0f) { - ballPos.y = radius; - ballVel.dy = std::abs(ballVel.dy); - } - - if (ballPos.y + radius >= screenHeight) { - ballPos.x = screenWidth / 2.0f; - ballPos.y = screenHeight / 2.0f; - - static std::random_device rd; - static std::mt19937 gen(rd()); - std::uniform_real_distribution angleDist(-45.0f, 45.0f); - - float angle = angleDist(gen) * (3.14159f / 180.0f); - ballVel.dx = 250.0f * std::sin(angle); - ballVel.dy = 250.0f * std::abs(std::cos(angle)); - } - } - - } -} - diff --git a/breakout/src/BreakoutRenderSystem.cpp b/breakout/src/BreakoutRenderSystem.cpp deleted file mode 100644 index 333a32f..0000000 --- a/breakout/src/BreakoutRenderSystem.cpp +++ /dev/null @@ -1,72 +0,0 @@ -#include "BreakoutRenderSystem.hpp" -#include "ECS/Registry.hpp" -#include "ECS/Component.hpp" -#include "Core/ColorFilter.hpp" - -namespace Breakout { - namespace ECS { - - BreakoutRenderSystem::BreakoutRenderSystem(Renderer::IRenderer* renderer) - : m_renderer(renderer) {} - - void BreakoutRenderSystem::Update(RType::ECS::Registry& registry, float deltaTime) { - if (!m_renderer) { - return; - } - - auto boxEntities = registry.GetEntitiesWithComponent(); - for (auto entity : boxEntities) { - if (!registry.IsEntityAlive(entity) || !registry.HasComponent(entity)) { - continue; - } - - const auto& pos = registry.GetComponent(entity); - const auto& collider = registry.GetComponent(entity); - - if (registry.HasComponent(entity)) { - const auto& drawable = registry.GetComponent(entity); - if (drawable.spriteId != Renderer::INVALID_SPRITE_ID) { - continue; - } - - Renderer::Rectangle rect; - rect.position = Renderer::Vector2(pos.x - collider.width / 2.0f, pos.y - collider.height / 2.0f); - rect.size = Renderer::Vector2(collider.width, collider.height); - Math::Color finalColor = drawable.tint; - if (RType::Core::ColorFilter::IsColourBlindModeEnabled()) { - finalColor = RType::Core::ColorFilter::ApplyColourBlindFilter(drawable.tint); - } - m_renderer->DrawRectangle(rect, finalColor); - } - } - - auto circleEntities = registry.GetEntitiesWithComponent(); - for (auto entity : circleEntities) { - if (!registry.IsEntityAlive(entity) || !registry.HasComponent(entity)) { - continue; - } - - const auto& pos = registry.GetComponent(entity); - const auto& collider = registry.GetComponent(entity); - - if (registry.HasComponent(entity)) { - const auto& drawable = registry.GetComponent(entity); - if (drawable.spriteId != Renderer::INVALID_SPRITE_ID) { - continue; - } - - float radius = collider.radius; - Renderer::Rectangle rect; - rect.position = Renderer::Vector2(pos.x - radius, pos.y - radius); - rect.size = Renderer::Vector2(radius * 2.0f, radius * 2.0f); - Math::Color finalColor = drawable.tint; - if (RType::Core::ColorFilter::IsColourBlindModeEnabled()) { - finalColor = RType::Core::ColorFilter::ApplyColourBlindFilter(drawable.tint); - } - m_renderer->DrawRectangle(rect, finalColor); - } - } - } - - } -} diff --git a/breakout/src/main.cpp b/breakout/src/main.cpp deleted file mode 100644 index 5c8ee70..0000000 --- a/breakout/src/main.cpp +++ /dev/null @@ -1,301 +0,0 @@ -#include "Core/Engine.hpp" -#include "Core/ColorFilter.hpp" -#include "ECS/Registry.hpp" -#include "ECS/Component.hpp" -#include "ECS/MovementSystem.hpp" -#include "ECS/RenderingSystem.hpp" -#include "ECS/InputSystem.hpp" -#include "ECS/TextRenderingSystem.hpp" -#include "ECS/Components/TextLabel.hpp" -#include "Renderer/SFMLRenderer.hpp" -#include "BreakoutPhysicsSystem.hpp" -#include "BreakoutRenderSystem.hpp" -#include "BallAccelerationSystem.hpp" -#include -#include -#include -#include -#include - -using namespace RType; -using namespace RType::ECS; -using namespace Breakout::ECS; - -const Math::Color BRICK_COLORS[] = { - {1.0f, 0.2f, 0.2f, 1.0f}, - {1.0f, 0.5f, 0.2f, 1.0f}, - {1.0f, 1.0f, 0.2f, 1.0f}, - {0.2f, 1.0f, 0.2f, 1.0f}, - {0.2f, 0.5f, 1.0f, 1.0f}, - {0.5f, 0.2f, 1.0f, 1.0f}, - {0.2f, 1.0f, 1.0f, 1.0f} -}; - -Entity CreatePaddle(Registry& registry, float x, float y) { - Entity paddle = registry.CreateEntity(); - registry.AddComponent(paddle, Position{x, y}); - registry.AddComponent(paddle, Velocity{0.0f, 0.0f}); - registry.AddComponent(paddle, Controllable{480.0f}); - registry.AddComponent(paddle, BoxCollider{100.0f, 20.0f}); - - Drawable drawable(Renderer::INVALID_SPRITE_ID, 10); - drawable.tint = {0.8f, 0.8f, 1.0f, 1.0f}; - registry.AddComponent(paddle, std::move(drawable)); - return paddle; -} - -Entity CreateBall(Registry& registry, float x, float y) { - Entity ball = registry.CreateEntity(); - registry.AddComponent(ball, Position{x, y}); - - static std::random_device rd; - static std::mt19937 gen(rd()); - std::uniform_real_distribution angleDist(-45.0f, 45.0f); - - float angle = angleDist(gen) * (3.14159f / 180.0f); - float velX = 250.0f * std::sin(angle); - float velY = 250.0f * std::abs(std::cos(angle)); - - registry.AddComponent(ball, Velocity{velX, velY}); - registry.AddComponent(ball, CircleCollider{10.0f}); - - Drawable drawable(Renderer::INVALID_SPRITE_ID, 12); - drawable.tint = {1.0f, 1.0f, 1.0f, 1.0f}; - registry.AddComponent(ball, std::move(drawable)); - return ball; -} - -Entity CreateBrick(Registry& registry, float x, float y, const Math::Color& color) { - Entity brick = registry.CreateEntity(); - registry.AddComponent(brick, Position{x, y}); - registry.AddComponent(brick, Health{1, 1}); - registry.AddComponent(brick, BoxCollider{80.0f, 30.0f}); - - Drawable drawable(Renderer::INVALID_SPRITE_ID, 5); - drawable.tint = color; - registry.AddComponent(brick, std::move(drawable)); - return brick; -} - -void CreateBrickWall(Registry& registry, float startX, float startY, int rows, int cols) { - const float BRICK_WIDTH = 80.0f; - const float BRICK_HEIGHT = 30.0f; - const float BRICK_SPACING = 5.0f; - - for (int row = 0; row < rows; row++) { - for (int col = 0; col < cols; col++) { - float x = startX + col * (BRICK_WIDTH + BRICK_SPACING); - float y = startY + row * (BRICK_HEIGHT + BRICK_SPACING); - CreateBrick(registry, x, y, BRICK_COLORS[row % 7]); - } - } -} - -int main() { - std::cout << "=== Breakout Game - Engine Reusability Demo ===" << std::endl; - - auto renderer = std::make_shared(); - Renderer::WindowConfig config; - config.title = "Breakout"; - config.width = 800; - config.height = 900; - config.resizable = false; - config.fullscreen = false; - config.targetFramerate = 60; - - if (!renderer->CreateWindow(config)) { - std::cerr << "Failed to create window" << std::endl; - return 1; - } - - Renderer::Camera2D camera; - camera.center = Renderer::Vector2(400.0f, 450.0f); - camera.size = Renderer::Vector2(800.0f, 900.0f); - renderer->SetCamera(camera); - - Core::EngineConfig engineConfig; - engineConfig.pluginPath = "./plugins"; - auto engine = std::make_unique(engineConfig); - - if (!engine->Initialize()) { - std::cerr << "Failed to initialize engine" << std::endl; - return 1; - } - - auto& registry = engine->GetRegistry(); - - const float SCREEN_WIDTH = 800.0f; - const float SCREEN_HEIGHT = 900.0f; - const int BRICK_COLS = 9; - const int BRICK_ROWS = 7; - const float BRICK_WIDTH = 80.0f; - const float BRICK_SPACING = 5.0f; - const float WALL_TOTAL_WIDTH = BRICK_COLS * BRICK_WIDTH + (BRICK_COLS - 1) * BRICK_SPACING; - const float WALL_START_X = (SCREEN_WIDTH - WALL_TOTAL_WIDTH) / 2.0f + BRICK_WIDTH / 2.0f; - - Entity paddle = CreatePaddle(registry, SCREEN_WIDTH / 2.0f, SCREEN_HEIGHT - 80.0f); - Entity ball = CreateBall(registry, SCREEN_WIDTH / 2.0f, SCREEN_HEIGHT / 2.0f); - CreateBrickWall(registry, WALL_START_X, 30.0f, BRICK_ROWS, BRICK_COLS); - - Entity scoreEntity = registry.CreateEntity(); - registry.AddComponent(scoreEntity, Position{20.0f, SCREEN_HEIGHT - 40.0f}); - - Renderer::FontId font = renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 16); - if (font == Renderer::INVALID_FONT_ID) { - font = renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 16); - } - - Renderer::FontId winFont = renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 80); - if (winFont == Renderer::INVALID_FONT_ID) { - winFont = renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 80); - } - if (winFont == Renderer::INVALID_FONT_ID) { - winFont = font; - } - - if (font != Renderer::INVALID_FONT_ID) { - TextLabel scoreLabel("SCORE: 0000", font, 16); - scoreLabel.color = {1.0f, 1.0f, 1.0f, 1.0f}; - registry.AddComponent(scoreEntity, std::move(scoreLabel)); - } - - Entity winWEntity = NULL_ENTITY; - Entity winIEntity = NULL_ENTITY; - Entity winNEntity = NULL_ENTITY; - - engine->RegisterSystem(std::make_unique(renderer.get())); - engine->RegisterSystem(std::make_unique()); - engine->RegisterSystem(std::make_unique()); - engine->RegisterSystem(std::make_unique()); - engine->RegisterSystem(std::make_unique(renderer.get())); - engine->RegisterSystem(std::make_unique(renderer.get())); - engine->RegisterSystem(std::make_unique(renderer.get())); - - int score = 0; - bool running = true; - bool cKeyPressed = false; - bool colourBlindMode = false; - - std::ifstream settingsFile("breakout_settings.json"); - if (!settingsFile.is_open()) { - settingsFile.open("../breakout_settings.json"); - } - if (settingsFile.is_open()) { - std::string line; - while (std::getline(settingsFile, line)) { - if (line.find("colourBlindMode=") == 0) { - std::string value = line.substr(17); - colourBlindMode = (value == "true" || value == "1"); - Core::ColorFilter::SetColourBlindMode(colourBlindMode); - break; - } - } - settingsFile.close(); - } - - while (renderer->IsWindowOpen() && running) { - float deltaTime = renderer->GetDeltaTime(); - renderer->Update(deltaTime); - renderer->BeginFrame(); - renderer->Clear({0.0f, 0.0f, 0.0f, 1.0f}); - - if (registry.IsEntityAlive(paddle) && registry.HasComponent(paddle)) { - registry.GetComponent(paddle).dy = 0.0f; - } - - engine->UpdateSystems(deltaTime); - - if (registry.IsEntityAlive(paddle) && registry.HasComponent(paddle) && registry.HasComponent(paddle)) { - auto& paddlePos = registry.GetComponent(paddle); - const auto& collider = registry.GetComponent(paddle); - paddlePos.y = SCREEN_HEIGHT - 80.0f; - float halfWidth = collider.width / 2.0f; - paddlePos.x = std::max(halfWidth, std::min(SCREEN_WIDTH - halfWidth, paddlePos.x)); - } - - auto brickEntities = registry.GetEntitiesWithComponent(); - int remainingBricks = 0; - for (auto entity : brickEntities) { - if (!registry.HasComponent(entity)) { - remainingBricks++; - } - } - - int newScore = (BRICK_ROWS * BRICK_COLS) - remainingBricks; - if (newScore != score) { - score = newScore; - if (registry.HasComponent(scoreEntity)) { - char scoreText[32]; - std::snprintf(scoreText, sizeof(scoreText), "SCORE: %04d", score); - registry.GetComponent(scoreEntity).text = scoreText; - } - } - - static bool winMessageShown = false; - if (remainingBricks == 0 && !winMessageShown) { - std::cout << "You won! All bricks destroyed!" << std::endl; - - if (registry.IsEntityAlive(paddle)) { - registry.DestroyEntity(paddle); - } - if (registry.IsEntityAlive(ball)) { - registry.DestroyEntity(ball); - } - - const float WIN_Y = SCREEN_HEIGHT / 2.0f - 50.0f; - const float LETTER_SPACING = 120.0f; - const float WIN_START_X = SCREEN_WIDTH / 2.0f - LETTER_SPACING; - - winWEntity = registry.CreateEntity(); - registry.AddComponent(winWEntity, Position{WIN_START_X, WIN_Y}); - TextLabel wLabel("W", winFont, 80); - wLabel.color = {1.0f, 0.2f, 0.2f, 1.0f}; - wLabel.centered = true; - registry.AddComponent(winWEntity, std::move(wLabel)); - - winIEntity = registry.CreateEntity(); - registry.AddComponent(winIEntity, Position{WIN_START_X + LETTER_SPACING, WIN_Y}); - TextLabel iLabel("I", winFont, 80); - iLabel.color = {1.0f, 0.5f, 0.2f, 1.0f}; - iLabel.centered = true; - registry.AddComponent(winIEntity, std::move(iLabel)); - - winNEntity = registry.CreateEntity(); - registry.AddComponent(winNEntity, Position{WIN_START_X + LETTER_SPACING * 2, WIN_Y}); - TextLabel nLabel("N", winFont, 80); - nLabel.color = {1.0f, 1.0f, 0.2f, 1.0f}; - nLabel.centered = true; - registry.AddComponent(winNEntity, std::move(nLabel)); - - winMessageShown = true; - } - - renderer->EndFrame(); - - if (renderer->IsKeyPressed(Renderer::Key::Escape)) { - running = false; - } - - if (renderer->IsKeyPressed(Renderer::Key::C) && !cKeyPressed) { - cKeyPressed = true; - colourBlindMode = !colourBlindMode; - Core::ColorFilter::SetColourBlindMode(colourBlindMode); - std::cout << "Colour-blind mode: " << (colourBlindMode ? "ON" : "OFF") << std::endl; - - std::ofstream outFile("breakout_settings.json"); - if (!outFile.is_open()) { - outFile.open("../breakout_settings.json"); - } - if (outFile.is_open()) { - outFile << "colourBlindMode=" << (colourBlindMode ? "true" : "false") << std::endl; - outFile.close(); - } - } else if (!renderer->IsKeyPressed(Renderer::Key::C)) { - cKeyPressed = false; - } - } - - std::cout << "Final Score: " << score << std::endl; - engine->Shutdown(); - return 0; -} From d058435fad2862e29b0a7f7b700e567320048c4f Mon Sep 17 00:00:00 2001 From: erwan Date: Mon, 2 Feb 2026 00:28:53 +0100 Subject: [PATCH 6/7] chore: removed all the old file architeture --- client/include/EditorState.hpp | 85 - client/include/GameState.hpp | 392 ----- client/include/GameStateMachine.hpp | 199 --- client/include/LobbyState.hpp | 93 -- client/include/MenuState.hpp | 81 - client/include/ResultsState.hpp | 77 - client/include/RoomListState.hpp | 88 -- client/include/SettingsState.hpp | 185 --- client/include/editor/EditorAssetLibrary.hpp | 52 - client/include/editor/EditorCanvasManager.hpp | 43 - .../include/editor/EditorColliderManager.hpp | 47 - client/include/editor/EditorConstants.hpp | 102 -- client/include/editor/EditorDrawing.hpp | 35 - client/include/editor/EditorEntityManager.hpp | 67 - client/include/editor/EditorFileManager.hpp | 57 - client/include/editor/EditorGeometry.hpp | 52 - client/include/editor/EditorInputHandler.hpp | 31 - .../include/editor/EditorPropertyManager.hpp | 53 - client/include/editor/EditorTypes.hpp | 116 -- client/include/editor/EditorUIManager.hpp | 96 -- client/main.cpp | 96 -- client/src/EditorState.cpp | 342 ----- client/src/GameState.cpp | 14 - client/src/LobbyState.cpp | 10 - client/src/MenuState.cpp | 316 ---- client/src/ResultsState.cpp | 267 ---- client/src/SettingsState.cpp | 1365 ----------------- client/src/editor/EditorAssetLibrary.cpp | 119 -- client/src/editor/EditorCanvasManager.cpp | 116 -- client/src/editor/EditorColliderManager.cpp | 189 --- client/src/editor/EditorDrawing.cpp | 79 - client/src/editor/EditorEntityManager.cpp | 334 ---- client/src/editor/EditorFileManager.cpp | 269 ---- client/src/editor/EditorInputHandler.cpp | 41 - client/src/editor/EditorPropertyManager.cpp | 219 --- client/src/editor/EditorUIManager.cpp | 433 ------ client/src/game/GameStateHelpers.cpp | 513 ------- client/src/game/GameStateInit.cpp | 782 ---------- client/src/game/GameStateNetwork.cpp | 1059 ------------- client/src/game/GameStateTransition.cpp | 303 ---- client/src/game/GameStateUI.cpp | 605 -------- client/src/game/GameStateUpdate.cpp | 800 ---------- client/src/lobby/LobbyStateInit.cpp | 203 --- client/src/lobby/LobbyStateUpdate.cpp | 253 --- client/src/room/RoomListState.cpp | 358 ----- 45 files changed, 11036 deletions(-) delete mode 100644 client/include/EditorState.hpp delete mode 100644 client/include/GameState.hpp delete mode 100644 client/include/GameStateMachine.hpp delete mode 100644 client/include/LobbyState.hpp delete mode 100644 client/include/MenuState.hpp delete mode 100644 client/include/ResultsState.hpp delete mode 100644 client/include/RoomListState.hpp delete mode 100644 client/include/SettingsState.hpp delete mode 100644 client/include/editor/EditorAssetLibrary.hpp delete mode 100644 client/include/editor/EditorCanvasManager.hpp delete mode 100644 client/include/editor/EditorColliderManager.hpp delete mode 100644 client/include/editor/EditorConstants.hpp delete mode 100644 client/include/editor/EditorDrawing.hpp delete mode 100644 client/include/editor/EditorEntityManager.hpp delete mode 100644 client/include/editor/EditorFileManager.hpp delete mode 100644 client/include/editor/EditorGeometry.hpp delete mode 100644 client/include/editor/EditorInputHandler.hpp delete mode 100644 client/include/editor/EditorPropertyManager.hpp delete mode 100644 client/include/editor/EditorTypes.hpp delete mode 100644 client/include/editor/EditorUIManager.hpp delete mode 100644 client/main.cpp delete mode 100644 client/src/EditorState.cpp delete mode 100644 client/src/GameState.cpp delete mode 100644 client/src/LobbyState.cpp delete mode 100644 client/src/MenuState.cpp delete mode 100644 client/src/ResultsState.cpp delete mode 100644 client/src/SettingsState.cpp delete mode 100644 client/src/editor/EditorAssetLibrary.cpp delete mode 100644 client/src/editor/EditorCanvasManager.cpp delete mode 100644 client/src/editor/EditorColliderManager.cpp delete mode 100644 client/src/editor/EditorDrawing.cpp delete mode 100644 client/src/editor/EditorEntityManager.cpp delete mode 100644 client/src/editor/EditorFileManager.cpp delete mode 100644 client/src/editor/EditorInputHandler.cpp delete mode 100644 client/src/editor/EditorPropertyManager.cpp delete mode 100644 client/src/editor/EditorUIManager.cpp delete mode 100644 client/src/game/GameStateHelpers.cpp delete mode 100644 client/src/game/GameStateInit.cpp delete mode 100644 client/src/game/GameStateNetwork.cpp delete mode 100644 client/src/game/GameStateTransition.cpp delete mode 100644 client/src/game/GameStateUI.cpp delete mode 100644 client/src/game/GameStateUpdate.cpp delete mode 100644 client/src/lobby/LobbyStateInit.cpp delete mode 100644 client/src/lobby/LobbyStateUpdate.cpp delete mode 100644 client/src/room/RoomListState.cpp diff --git a/client/include/EditorState.hpp b/client/include/EditorState.hpp deleted file mode 100644 index 6b5b2a8..0000000 --- a/client/include/EditorState.hpp +++ /dev/null @@ -1,85 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** EditorState -*/ - -#pragma once - -#include "GameStateMachine.hpp" -#include "editor/EditorTypes.hpp" -#include "editor/EditorUIManager.hpp" -#include "editor/EditorEntityManager.hpp" -#include "editor/EditorAssetLibrary.hpp" -#include "editor/EditorInputHandler.hpp" -#include "editor/EditorPropertyManager.hpp" -#include "ECS/Registry.hpp" -#include "ECS/RenderingSystem.hpp" -#include "ECS/TextRenderingSystem.hpp" -#include "Renderer/IRenderer.hpp" -#include -#include -#include -#include - -namespace RType { - namespace Client { - - class EditorCanvasManager; - class EditorFileManager; - - class EditorState : public IState { - public: - EditorState(GameStateMachine& machine, GameContext& context); - ~EditorState() override; - - void Init() override; - void Cleanup() override; - void HandleInput() override; - void Update(float dt) override; - void Draw() override; - - private: - GameStateMachine& m_machine; - GameContext& m_context; - - RType::ECS::Registry m_registry; - std::shared_ptr m_renderer; - std::unique_ptr m_renderingSystem; - std::unique_ptr m_textSystem; - - std::unique_ptr m_canvasManager; - std::unique_ptr m_assetLibrary; - std::unique_ptr m_uiManager; - std::unique_ptr m_entityManager; - std::unique_ptr m_inputHandler; - std::unique_ptr m_propertyManager; - std::unique_ptr m_fileManager; - - // Fonts - Renderer::FontId m_fontSmall = Renderer::INVALID_FONT_ID; - Renderer::FontId m_fontMedium = Renderer::INVALID_FONT_ID; - - // UI entities for cleanup - std::vector m_entities; - std::vector m_statusBarEntities; - - // Input state - bool m_leftMousePressed = false; - - // Editor state - EditorPaletteSelection m_selection; - Math::Vector2 m_lastMouseWorld{0.0f, 0.0f}; - std::string m_currentLevelPath; - std::string m_levelName = "Custom Level"; - bool m_hasUnsavedChanges = false; - - void handleSelectionAt(const Math::Vector2& mouseWorld); - void updatePropertyPanel(); - void deleteSelectedEntity(); - void saveCurrentLevel(); - }; - - } -} diff --git a/client/include/GameState.hpp b/client/include/GameState.hpp deleted file mode 100644 index b9575cf..0000000 --- a/client/include/GameState.hpp +++ /dev/null @@ -1,392 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** GameState -*/ - -#pragma once - -#include "GameStateMachine.hpp" -#include "ECS/Registry.hpp" -#include "ECS/RenderingSystem.hpp" -#include "ECS/TextRenderingSystem.hpp" -#include "ECS/ScrollingSystem.hpp" -#include "ECS/ShootingSystem.hpp" -#include "ECS/MovementSystem.hpp" -#include "ECS/InputSystem.hpp" -#include "ECS/CollisionDetectionSystem.hpp" -#include "ECS/BulletCollisionResponseSystem.hpp" -#include "ECS/PlayerCollisionResponseSystem.hpp" -#include "ECS/ObstacleCollisionResponseSystem.hpp" -#include "ECS/HealthSystem.hpp" -#include "ECS/ScoreSystem.hpp" -#include "ECS/ShieldSystem.hpp" -#include "ECS/ForcePodSystem.hpp" -#include "ECS/PowerUpSpawnSystem.hpp" -#include "ECS/PowerUpCollisionSystem.hpp" -#include "ECS/AudioSystem.hpp" -#include "ECS/AnimationSystem.hpp" -#include "ECS/EffectFactory.hpp" -#include "ECS/Component.hpp" -#include "ECS/PowerUpFactory.hpp" -#include "ECS/LevelLoader.hpp" -#include "Animation/AnimationModule.hpp" -#include "Renderer/IRenderer.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace RType { - namespace Client { - - // Player HUD data for multiplayer scoreboard - struct PlayerHUDData { - bool active = false; - uint32_t score = 0; - int lives = 3; - int health = 100; - int maxHealth = 100; - bool isDead = false; - RType::ECS::Entity scoreEntity = RType::ECS::NULL_ENTITY; - RType::ECS::Entity playerEntity = RType::ECS::NULL_ENTITY; - RType::ECS::Entity powerupSpreadEntity = RType::ECS::NULL_ENTITY; - RType::ECS::Entity powerupLaserEntity = RType::ECS::NULL_ENTITY; - RType::ECS::Entity powerupSpeedEntity = RType::ECS::NULL_ENTITY; - RType::ECS::Entity powerupShieldEntity = RType::ECS::NULL_ENTITY; - }; - - struct PredictedInput { - uint32_t sequence = 0; - uint8_t inputs = 0; - float predictedX = 0.0f; - float predictedY = 0.0f; - float deltaTime = 0.0f; - }; - - // Entity interpolation state for smooth rendering of remote entities - struct InterpolationState { - float prevX = 0.0f; - float prevY = 0.0f; - float targetX = 0.0f; - float targetY = 0.0f; - float interpTime = 0.0f; - float interpDuration = 1.0f / 60.0f; - }; - - class InGameState : public IState { - public: - InGameState(GameStateMachine& machine, GameContext& context, uint32_t seed, const std::string& levelPath = "assets/levels/level1.json"); - ~InGameState() override = default; - - void Init() override; - void Cleanup() override; - void HandleInput() override; - void Update(float dt) override; - void Draw() override; - private: - // Level loading - void loadLevel(const std::string& levelPath); - void initializeFromLevel(); - - // Player System (Keane) - void initializePlayers(); - - // Ennemy System (Dryss) - void spawnEnemies(); - - void initializeUI(); - void updateHUD(); - void updatePowerUpIcons(); - void updatePowerUpText(RType::ECS::Entity& textEntity, const std::string& text, - bool isActive, float x, float y); - void renderChargeBar(); - void renderHealthBars(); - void renderBossHealthBar(); - void updateBossHealthBar(); - void initializeBossHealthBar(); - void destroyBossHealthBar(); - void renderGameOverOverlay(); - void renderVictoryOverlay(); - void renderLevelTransition(); - void triggerGameOverIfNeeded(); - void triggerVictoryIfNeeded(); - void enterResultsScreen(); - void createBeamEntity(); - void updateBeam(float dt); - - // Level progression - void checkBossDefeated(); - - // ECS systems - void createSystems(); - - // Server state update handler - void OnServerStateUpdate(uint32_t tick, const std::vector& entities, const std::vector& inputAcks); - void ReconcileWithServer(const network::InputAck& ack); - void OnLevelComplete(uint8_t completedLevel, uint8_t nextLevel); - void ApplyPowerUpStateToPlayer(ECS::Entity playerEntity, const network::EntityState& entityState); - - // Component cleanup helper for entity type validation - void CleanupInvalidComponents(ECS::Entity entity, network::EntityType expectedType); - - void cleanupForLevelTransition(); - - struct EnemySpriteConfig { - Renderer::SpriteId sprite = Renderer::INVALID_SPRITE_ID; - Math::Color tint{1.0f, 1.0f, 1.0f, 1.0f}; - float rotation = 0.0f; - }; - - struct EnemyBulletSpriteConfig { - Renderer::SpriteId sprite; - Math::Color tint; - float scale; - }; - EnemySpriteConfig GetEnemySpriteConfig(uint8_t enemyType) const; - EnemyBulletSpriteConfig GetEnemyBulletSpriteConfig(uint8_t enemyType) const; - Renderer::SpriteId GetPowerUpSprite(ECS::PowerUpType type) const; - - std::pair FindPlayerNameAndNumber(uint64_t ownerHash, const std::unordered_set& assignedNumbers) const; - void CreatePlayerNameLabel(RType::ECS::Entity playerEntity, const std::string& playerName, float x, float y); - void UpdatePlayerNameLabelPosition(RType::ECS::Entity playerEntity, float x, float y); - void DestroyPlayerNameLabel(RType::ECS::Entity playerEntity); - - // Level transition - void UpdateLevelTransition(float dt); - void LoadNextLevel(); - private: - GameStateMachine& m_machine; - GameContext& m_context; - uint32_t m_gameSeed; - - RType::ECS::Registry m_registry; - std::shared_ptr m_renderer; - - // Systems - std::unique_ptr m_scrollingSystem; - std::unique_ptr m_renderingSystem; - std::unique_ptr m_textSystem; - std::unique_ptr m_movementSystem; - std::unique_ptr m_inputSystem; - std::unique_ptr m_collisionDetectionSystem; - std::unique_ptr m_bulletResponseSystem; - std::unique_ptr m_playerResponseSystem; - std::unique_ptr m_obstacleResponseSystem; - std::unique_ptr m_healthSystem; - std::unique_ptr m_scoreSystem; - std::unique_ptr m_shootingSystem; - std::unique_ptr m_audioSystem; - std::unique_ptr m_shieldSystem; - std::unique_ptr m_forcePodSystem; - std::unique_ptr m_powerUpSpawnSystem; - std::unique_ptr m_powerUpCollisionSystem; - std::unique_ptr m_animationSystem; - std::unique_ptr m_animationModule; - std::unique_ptr m_effectFactory; - - // Bullet textures and sprites - Renderer::TextureId m_bulletTexture = Renderer::INVALID_TEXTURE_ID; - Renderer::SpriteId m_bulletSprite = Renderer::INVALID_SPRITE_ID; - - Audio::SoundId m_playerShootSound = Audio::INVALID_SOUND_ID; - Audio::MusicId m_shootMusic = Audio::INVALID_MUSIC_ID; - Audio::SoundId m_powerUpSound = Audio::INVALID_SOUND_ID; - Audio::MusicId m_powerUpMusic = Audio::INVALID_MUSIC_ID; - float m_shootSfxCooldown = 0.0f; - Audio::MusicId m_gameMusic = Audio::INVALID_MUSIC_ID; - bool m_gameMusicPlaying = false; - Audio::MusicId m_bossMusic = Audio::INVALID_MUSIC_ID; - bool m_bossMusicPlaying = false; - Audio::MusicId m_gameOverMusic = Audio::INVALID_MUSIC_ID; - bool m_gameOverMusicPlaying = false; - Audio::MusicId m_victoryMusic = Audio::INVALID_MUSIC_ID; - bool m_victoryMusicPlaying = false; - - // Enemy bullet textures and sprites - Renderer::TextureId m_enemyBulletGreenTexture = Renderer::INVALID_TEXTURE_ID; - Renderer::TextureId m_enemyBulletYellowTexture = Renderer::INVALID_TEXTURE_ID; - Renderer::TextureId m_enemyBulletPurpleTexture = Renderer::INVALID_TEXTURE_ID; - Renderer::SpriteId m_enemyBulletGreenSprite = Renderer::INVALID_SPRITE_ID; - Renderer::SpriteId m_enemyBulletYellowSprite = Renderer::INVALID_SPRITE_ID; - Renderer::SpriteId m_enemyBulletPurpleSprite = Renderer::INVALID_SPRITE_ID; - - // Background and obstacles entities - std::vector m_backgroundEntities; - std::vector m_obstacleSpriteEntities; - std::vector m_obstacleColliderEntities; - - bool m_escapeKeyPressed = false; - - // Network synchronization - float m_localScrollOffset = 0.0f; - float m_serverScrollOffset = 0.0f; - - std::chrono::steady_clock::time_point m_lastInputTime; - uint8_t m_currentInputs = 0; - uint8_t m_previousInputs = 0; - - std::deque m_inputHistory; - uint32_t m_inputSequence = 0; - uint32_t m_lastAckedSequence = 0; - float m_predictedX = 0.0f; - float m_predictedY = 0.0f; - static constexpr size_t MAX_INPUT_HISTORY = 120; - static constexpr float PREDICTION_SPEED = 200.0f; - - // Entity interpolation for remote entities - std::unordered_map m_interpolationStates; - - // Player ships tracking (network entities → ECS entities) - std::unordered_map m_networkEntityMap; - std::unordered_map m_bulletFlagsMap; // Track bullet flags to detect type changes - RType::ECS::Entity m_localPlayerEntity = RType::ECS::NULL_ENTITY; // Local player entity mirrored from server - - // Level progression tracking with visual transition - enum class TransitionPhase { - NONE, - FADE_OUT, - LOADING, - FADE_IN - }; - - struct LevelProgressionState { - bool bossDefeated = false; - bool levelComplete = false; - bool allLevelsComplete = false; - float transitionTimer = 0.0f; - float victoryElapsed = 0.0f; - int currentLevelNumber = 1; - int nextLevelNumber = 2; - int totalLevels = 3; - TransitionPhase transitionPhase = TransitionPhase::NONE; - float fadeAlpha = 0.0f; - }; - LevelProgressionState m_levelProgress; - - // Individual player ship sprites - Renderer::TextureId m_playerGreenTexture = Renderer::INVALID_TEXTURE_ID; - Renderer::TextureId m_playerBlueTexture = Renderer::INVALID_TEXTURE_ID; - Renderer::TextureId m_playerRedTexture = Renderer::INVALID_TEXTURE_ID; - Renderer::SpriteId m_playerGreenSprite = Renderer::INVALID_SPRITE_ID; - Renderer::SpriteId m_playerBlueSprite = Renderer::INVALID_SPRITE_ID; - Renderer::SpriteId m_playerRedSprite = Renderer::INVALID_SPRITE_ID; - - // Enemy ship sprites - Renderer::TextureId m_enemyGreenTexture = Renderer::INVALID_TEXTURE_ID; - Renderer::TextureId m_enemyRedTexture = Renderer::INVALID_TEXTURE_ID; - Renderer::TextureId m_enemyBlueTexture = Renderer::INVALID_TEXTURE_ID; - Renderer::SpriteId m_enemyGreenSprite = Renderer::INVALID_SPRITE_ID; - Renderer::SpriteId m_enemyRedSprite = Renderer::INVALID_SPRITE_ID; - Renderer::SpriteId m_enemyBlueSprite = Renderer::INVALID_SPRITE_ID; - - // Power-up sprites - Renderer::SpriteId m_powerupSpreadSprite = Renderer::INVALID_SPRITE_ID; - Renderer::SpriteId m_powerupLaserSprite = Renderer::INVALID_SPRITE_ID; - Renderer::SpriteId m_powerupForcePodSprite = Renderer::INVALID_SPRITE_ID; - Renderer::SpriteId m_powerupSpeedSprite = Renderer::INVALID_SPRITE_ID; - Renderer::SpriteId m_powerupShieldSprite = Renderer::INVALID_SPRITE_ID; - - // Explosion animation - Renderer::TextureId m_explosionTexture = Renderer::INVALID_TEXTURE_ID; - Renderer::SpriteId m_explosionSprite = Renderer::INVALID_SPRITE_ID; - Animation::AnimationClipId m_explosionClipId = Animation::INVALID_CLIP_ID; - Renderer::TextureId m_shootingTexture = Renderer::INVALID_TEXTURE_ID; - Renderer::SpriteId m_shootingSprite = Renderer::INVALID_SPRITE_ID; - Animation::AnimationClipId m_shootingClipId = Animation::INVALID_CLIP_ID; - - Renderer::TextureId m_forcePodTexture = Renderer::INVALID_TEXTURE_ID; - Renderer::SpriteId m_forcePodSprite = Renderer::INVALID_SPRITE_ID; - Animation::AnimationClipId m_forcePodClipId = Animation::INVALID_CLIP_ID; - Renderer::TextureId m_beamTexture = Renderer::INVALID_TEXTURE_ID; - Renderer::SpriteId m_beamSprite = Renderer::INVALID_SPRITE_ID; - Animation::AnimationClipId m_beamClipId = Animation::INVALID_CLIP_ID; - Renderer::TextureId m_hitTexture = Renderer::INVALID_TEXTURE_ID; - Renderer::SpriteId m_hitSprite = Renderer::INVALID_SPRITE_ID; - Animation::AnimationClipId m_hitClipId = Animation::INVALID_CLIP_ID; - Animation::AnimationClipId m_waveAttackClipId = Animation::INVALID_CLIP_ID; - Animation::AnimationClipId m_secondAttackClipId = Animation::INVALID_CLIP_ID; - Animation::AnimationClipId m_fireBulletClipId = Animation::INVALID_CLIP_ID; - Animation::AnimationClipId m_mineClipId = Animation::INVALID_CLIP_ID; - Animation::AnimationClipId m_mineExplosionClipId = Animation::INVALID_CLIP_ID; - - // HUD fonts - Renderer::FontId m_hudFont = Renderer::INVALID_FONT_ID; - Renderer::FontId m_hudFontSmall = Renderer::INVALID_FONT_ID; - Renderer::FontId m_gameOverFontLarge = Renderer::INVALID_FONT_ID; - Renderer::FontId m_gameOverFontMedium = Renderer::INVALID_FONT_ID; - - // HUD entities - local player info (left side) - RType::ECS::Entity m_hudPlayerEntity = RType::ECS::NULL_ENTITY; - RType::ECS::Entity m_hudScoreEntity = RType::ECS::NULL_ENTITY; - RType::ECS::Entity m_hudLivesEntity = RType::ECS::NULL_ENTITY; - - // HUD entities - all players scoreboard (right side) - RType::ECS::Entity m_hudScoreboardTitle = RType::ECS::NULL_ENTITY; - std::array m_playersHUD; - - // Local player state for HUD - uint32_t m_playerScore = 0; - int m_playerLives = 3; - float m_scoreAccumulator = 0.0f; // For time-based score testing - - // Game Over overlay - bool m_isGameOver = false; - float m_gameOverElapsed = 0.0f; - bool m_gameOverEnterPressed = false; - bool m_gameOverEscapePressed = false; - RType::ECS::Entity m_gameOverTitleEntity = RType::ECS::NULL_ENTITY; - RType::ECS::Entity m_gameOverScoreEntity = RType::ECS::NULL_ENTITY; - RType::ECS::Entity m_gameOverHintEntity = RType::ECS::NULL_ENTITY; - - RType::ECS::Entity m_victoryTitleEntity = RType::ECS::NULL_ENTITY; - RType::ECS::Entity m_victoryScoreEntity = RType::ECS::NULL_ENTITY; - RType::ECS::Entity m_victoryHintEntity = RType::ECS::NULL_ENTITY; - - bool m_isCharging = false; - float m_chargeTime = 0.0f; - RType::ECS::Entity m_beamEntity = RType::ECS::NULL_ENTITY; - float m_beamDuration = 0.0f; - static constexpr float MAX_CHARGE_TIME = 2.0f; // 2 seconds for full charge - - bool m_isNetworkSession = false; - std::unordered_map m_obstacleIdToCollider; - - // Player name tracking - std::unordered_map m_playerNameMap; - std::unordered_map m_playerNameLabels; - std::unordered_set m_assignedPlayerNumbers; - - // Level loader data - RType::ECS::LevelData m_levelData; - RType::ECS::LoadedAssets m_levelAssets; - RType::ECS::CreatedEntities m_levelEntities; - std::string m_currentLevelPath = "assets/levels/level1.json"; - - bool m_bossWarningActive = false; - bool m_bossWarningTriggered = false; - float m_bossWarningTimer = 0.0f; - static constexpr float BOSS_WARNING_DURATION = 4.0f; - bool m_bossWarningFlashState = false; - void renderBossWarning(); - - // Boss health bar - struct { - bool active = false; - int currentHealth = 0; - int maxHealth = 1000; - uint32_t bossNetworkId = 0; - RType::ECS::Entity titleEntity = RType::ECS::NULL_ENTITY; - RType::ECS::Entity barBackgroundEntity = RType::ECS::NULL_ENTITY; - RType::ECS::Entity barForegroundEntity = RType::ECS::NULL_ENTITY; - } m_bossHealthBar; - }; - - } -} diff --git a/client/include/GameStateMachine.hpp b/client/include/GameStateMachine.hpp deleted file mode 100644 index 8796ac6..0000000 --- a/client/include/GameStateMachine.hpp +++ /dev/null @@ -1,199 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** GameStateMachine -*/ - -#pragma once - -#include -#include -#include -#include -#include -#include "Renderer/IRenderer.hpp" -#include "ECS/Registry.hpp" -#include "Audio/IAudio.hpp" -#include "../../libs/network/include/GameClient.hpp" -#include "../../libs/network/include/Protocol.hpp" -#include "../../libs/network/include/INetworkModule.hpp" -#include "Core/Logger.hpp" - -namespace RType { - namespace Client { - - enum class GameState { - None, - Menu, - Lobby, - Game, - Exit - }; - - struct GameContext { - std::shared_ptr renderer; - std::shared_ptr registry; - std::shared_ptr networkClient; - std::shared_ptr networkModule; - std::shared_ptr audio; - - std::string playerName; - std::string serverIp; - uint16_t serverPort; - uint64_t playerHash; - uint8_t playerNumber; - std::vector allPlayers; - - uint32_t roomId = 0; - std::string roomName; - }; - - class IState { - public: - virtual ~IState() = default; - - virtual void Init() = 0; - virtual void Cleanup() {} - virtual void HandleInput() = 0; - virtual void Update(float dt) = 0; - virtual void Draw() = 0; - }; - - class GameStateMachine { - public: - GameStateMachine() = default; - ~GameStateMachine() { - while (!m_states.empty()) { - PopStateImmediate(); - } - } - - void PushState(std::unique_ptr state) { - if (m_isDispatching) { - m_pending = PendingOp{OpType::Push, std::move(state)}; - return; - } - PushStateImmediate(std::move(state)); - } - - void PopState() { - if (m_isDispatching) { - m_pending = PendingOp{OpType::Pop, nullptr}; - return; - } - PopStateImmediate(); - } - - void ChangeState(std::unique_ptr state) { - if (m_isDispatching) { - m_pending = PendingOp{OpType::Change, std::move(state)}; - return; - } - ChangeStateImmediate(std::move(state)); - } - - IState* GetCurrentState() { - return m_states.empty() ? nullptr : m_states.top().get(); - } - - void Update(float dt) { - if (GetCurrentState()) { - m_isDispatching = true; - GetCurrentState()->Update(dt); - m_isDispatching = false; - ApplyPending(); - if (m_states.empty()) { - Core::Logger::Warning("[GameStateMachine] State stack is empty after Update!"); - } - } - } - - void Draw() { - if (GetCurrentState()) { - m_isDispatching = true; - GetCurrentState()->Draw(); - m_isDispatching = false; - ApplyPending(); - if (m_states.empty()) { - Core::Logger::Warning("[GameStateMachine] State stack is empty after Draw!"); - } - } - } - - void HandleInput() { - if (GetCurrentState()) { - m_isDispatching = true; - GetCurrentState()->HandleInput(); - m_isDispatching = false; - ApplyPending(); - - if (m_states.empty()) { - Core::Logger::Warning("[GameStateMachine] State stack is empty after HandleInput!"); - } - } - } - - bool IsRunning() const { return !m_states.empty(); } - size_t GetStateCount() const { return m_states.size(); } - private: - enum class OpType { - Push, - Pop, - Change - }; - - struct PendingOp { - OpType type; - std::unique_ptr state; - }; - - void ApplyPending() { - if (!m_pending.has_value()) { - return; - } - PendingOp op = std::move(*m_pending); - m_pending.reset(); - - switch (op.type) { - case OpType::Push: - PushStateImmediate(std::move(op.state)); - break; - case OpType::Pop: - PopStateImmediate(); - break; - case OpType::Change: - ChangeStateImmediate(std::move(op.state)); - break; - } - } - - void PushStateImmediate(std::unique_ptr state) { - m_states.push(std::move(state)); - m_states.top()->Init(); - } - - void PopStateImmediate() { - if (!m_states.empty()) { - m_states.top()->Cleanup(); - m_states.pop(); - } else { - Core::Logger::Warning("[GameStateMachine] Attempted to pop state from empty stack!"); - } - } - - void ChangeStateImmediate(std::unique_ptr state) { - if (!m_states.empty()) { - m_states.top()->Cleanup(); - m_states.pop(); - } - PushStateImmediate(std::move(state)); - } - - std::stack> m_states; - bool m_isDispatching = false; - std::optional m_pending; - }; - - } -} diff --git a/client/include/LobbyState.hpp b/client/include/LobbyState.hpp deleted file mode 100644 index 03bf537..0000000 --- a/client/include/LobbyState.hpp +++ /dev/null @@ -1,93 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** LobbyState -*/ - -#pragma once - -#include "GameStateMachine.hpp" -#include "LobbyClient.hpp" -#include "ECS/Component.hpp" -#include "ECS/Registry.hpp" -#include "ECS/RenderingSystem.hpp" -#include "ECS/TextRenderingSystem.hpp" -#include "ECS/AudioSystem.hpp" -#include "Renderer/IRenderer.hpp" -#include "GameState.hpp" -#include "Audio/IAudio.hpp" - -#include -#include -#include -#include -#include - -namespace RType { - namespace Client { - - class LobbyState : public IState { - public: - LobbyState(GameStateMachine& machine, GameContext& context); - LobbyState(GameStateMachine& machine, GameContext& context, network::NetworkTcpSocket&& socket); - ~LobbyState() override = default; - - void Init() override; - void Cleanup() override; - void HandleInput() override; - void Update(float dt) override; - void Draw() override; - private: - void createUI(); - void updateLobbyState(); - void updateOrCreatePlayerEntity(const network::PlayerInfo& player); - void updatePlayerCardVisuals(uint8_t playerNum, const network::PlayerInfo& player); - void removePlayer(uint8_t playerNum); - Renderer::TextureId getPlayerTexture(uint8_t playerNum); - - GameStateMachine& m_machine; - GameContext& m_context; - - std::unique_ptr m_client; - std::string m_playerName; - - RType::ECS::Registry m_registry; - std::shared_ptr m_renderer; - std::unique_ptr m_renderingSystem; - std::unique_ptr m_textSystem; - std::unique_ptr m_audioSystem; - - Audio::MusicId m_lobbyMusic = Audio::INVALID_MUSIC_ID; - bool m_lobbyMusicPlaying = false; - - Renderer::FontId m_fontLarge = Renderer::INVALID_FONT_ID; - Renderer::FontId m_fontMedium = Renderer::INVALID_FONT_ID; - Renderer::FontId m_fontSmall = Renderer::INVALID_FONT_ID; - - Renderer::TextureId m_playerBlueTexture = Renderer::INVALID_TEXTURE_ID; - Renderer::TextureId m_playerGreenTexture = Renderer::INVALID_TEXTURE_ID; - Renderer::TextureId m_nave2BlueTexture = Renderer::INVALID_TEXTURE_ID; - Renderer::TextureId m_nave2Texture = Renderer::INVALID_TEXTURE_ID; - Renderer::TextureId m_bgTexture = Renderer::INVALID_TEXTURE_ID; - - RType::ECS::Entity m_titleEntity = RType::ECS::NULL_ENTITY; - RType::ECS::Entity m_instructionsEntity = RType::ECS::NULL_ENTITY; - RType::ECS::Entity m_bgEntity = RType::ECS::NULL_ENTITY; - RType::ECS::Entity m_errorEntity = RType::ECS::NULL_ENTITY; - RType::ECS::Entity m_countdownEntity = RType::ECS::NULL_ENTITY; - - std::unordered_map m_playerEntities; - std::unordered_map m_playerSprites; - - std::string m_lastStatus; - std::string m_errorMessage; - float m_errorTimer = 0.0f; - uint8_t m_countdownSeconds = 0; - bool m_rKeyPressed = false; - bool m_escapeKeyPressed = false; - bool m_hasError = false; - }; - - } -} diff --git a/client/include/MenuState.hpp b/client/include/MenuState.hpp deleted file mode 100644 index 6af39f7..0000000 --- a/client/include/MenuState.hpp +++ /dev/null @@ -1,81 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** MenuState -*/ - -#pragma once - -#include "GameStateMachine.hpp" -#include "ECS/Registry.hpp" -#include "ECS/RenderingSystem.hpp" -#include "ECS/TextRenderingSystem.hpp" -#include "ECS/AudioSystem.hpp" -#include "Renderer/IRenderer.hpp" -#include "Audio/IAudio.hpp" -#include -#include - -namespace RType { - namespace Client { - - class MenuState : public IState { - public: - MenuState(GameStateMachine& machine, GameContext& context); - ~MenuState() override = default; - - void Init() override; - void Cleanup() override; - void HandleInput() override; - void Update(float dt) override; - void Draw() override; - private: - void createUI(); - void updateAnimations(float dt); - void updateMenuSelection(); - - enum class MenuItem { - PLAY = 0, - EDITOR = 1, - SETTINGS = 2, - QUIT = 3, - COUNT = 4 - }; - - GameStateMachine& m_machine; - GameContext& m_context; - - RType::ECS::Registry m_registry; - std::shared_ptr m_renderer; - std::unique_ptr m_renderingSystem; - std::unique_ptr m_textSystem; - std::unique_ptr m_audioSystem; - - Audio::MusicId m_menuMusic = Audio::INVALID_MUSIC_ID; - bool m_menuMusicPlaying = false; - Audio::MusicId m_selectMusic = Audio::INVALID_MUSIC_ID; - - Renderer::FontId m_fontLarge = Renderer::INVALID_FONT_ID; - Renderer::FontId m_fontMedium = Renderer::INVALID_FONT_ID; - Renderer::FontId m_fontSmall = Renderer::INVALID_FONT_ID; - Renderer::TextureId m_bgTexture = Renderer::INVALID_TEXTURE_ID; - - std::vector m_entities; - RType::ECS::Entity m_titleEntity = RType::ECS::NULL_ENTITY; - RType::ECS::Entity m_subtitleEntity = RType::ECS::NULL_ENTITY; - std::vector m_menuItems; - - int m_selectedIndex = 0; - bool m_upKeyPressed = false; - bool m_downKeyPressed = false; - bool m_enterKeyPressed = false; - bool m_escKeyPressed = false; - int m_ignoreInputFrames = 1; - - float m_animTime = 0.0f; - float m_titlePulse = 0.0f; - }; - - } -} diff --git a/client/include/ResultsState.hpp b/client/include/ResultsState.hpp deleted file mode 100644 index 8df39c0..0000000 --- a/client/include/ResultsState.hpp +++ /dev/null @@ -1,77 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** ResultsState -*/ - -#pragma once - -#include "GameStateMachine.hpp" -#include "ECS/Registry.hpp" -#include "ECS/RenderingSystem.hpp" -#include "ECS/TextRenderingSystem.hpp" -#include "Renderer/IRenderer.hpp" - -#include -#include -#include -#include - -namespace RType { - namespace Client { - - class ResultsState : public IState { - public: - ResultsState(GameStateMachine& machine, - GameContext& context, - std::vector> scores); - ~ResultsState() override = default; - - void Init() override; - void Cleanup() override; - void HandleInput() override; - void Update(float dt) override; - void Draw() override; - - private: - void createUI(); - - private: - GameStateMachine& m_machine; - GameContext& m_context; - std::vector> m_scores; - - RType::ECS::Registry m_registry; - std::shared_ptr m_renderer; - std::unique_ptr m_renderingSystem; - std::unique_ptr m_textSystem; - - Renderer::FontId m_fontLarge = Renderer::INVALID_FONT_ID; - Renderer::FontId m_fontMedium = Renderer::INVALID_FONT_ID; - Renderer::FontId m_fontSmall = Renderer::INVALID_FONT_ID; - - Renderer::TextureId m_bgTexture = Renderer::INVALID_TEXTURE_ID; - Renderer::SpriteId m_bgSprite = Renderer::INVALID_SPRITE_ID; - Renderer::Vector2 m_bgTextureSize{0.0f, 0.0f}; - - RType::ECS::Entity m_bgEntity = RType::ECS::NULL_ENTITY; - - bool m_drawScorePanel = false; - Renderer::Rectangle m_scorePanelRect{}; - float m_scorePanelHeaderHeight = 44.0f; - float m_scorePanelRowStartY = 0.0f; - float m_scorePanelRowStepY = 36.0f; - size_t m_scorePanelRowCount = 0; - float m_colRankX = 0.0f; - float m_colNameX = 0.0f; - float m_colScoreX = 0.0f; - bool m_escapePressed = false; - - Audio::MusicId m_endMusic = Audio::INVALID_MUSIC_ID; - }; - - } -} - - diff --git a/client/include/RoomListState.hpp b/client/include/RoomListState.hpp deleted file mode 100644 index 28926d0..0000000 --- a/client/include/RoomListState.hpp +++ /dev/null @@ -1,88 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** RoomListState - Room selection screen -*/ - -#pragma once - -#include "GameStateMachine.hpp" -#include "RoomClient.hpp" -#include "ECS/Component.hpp" -#include "ECS/Registry.hpp" -#include "ECS/RenderingSystem.hpp" -#include "ECS/TextRenderingSystem.hpp" -#include "Renderer/IRenderer.hpp" - -#include -#include -#include -#include - -namespace RType { - namespace Client { - - class RoomListState : public IState { - public: - RoomListState(GameStateMachine& machine, GameContext& context); - ~RoomListState() override = default; - - void Init() override; - void Cleanup() override; - void HandleInput() override; - void Update(float dt) override; - void Draw() override; - - private: - void createUI(); - void updateRoomList(); - void createRoomEntities(); - void clearRoomEntities(); - void handleCreateRoom(); - void handleJoinRoom(); - - GameStateMachine& m_machine; - GameContext& m_context; - - std::unique_ptr m_roomClient; - - RType::ECS::Registry m_registry; - std::shared_ptr m_renderer; - std::unique_ptr m_renderingSystem; - std::unique_ptr m_textSystem; - - Renderer::FontId m_fontLarge = Renderer::INVALID_FONT_ID; - Renderer::FontId m_fontMedium = Renderer::INVALID_FONT_ID; - Renderer::FontId m_fontSmall = Renderer::INVALID_FONT_ID; - Renderer::TextureId m_bgTexture = Renderer::INVALID_TEXTURE_ID; - - RType::ECS::Entity m_titleEntity = RType::ECS::NULL_ENTITY; - RType::ECS::Entity m_instructionsEntity = RType::ECS::NULL_ENTITY; - RType::ECS::Entity m_bgEntity = RType::ECS::NULL_ENTITY; - RType::ECS::Entity m_noRoomsEntity = RType::ECS::NULL_ENTITY; - RType::ECS::Entity m_errorEntity = RType::ECS::NULL_ENTITY; - - std::vector m_roomEntities; - std::vector m_rooms; - - int m_selectedIndex = 0; - bool m_upKeyPressed = false; - bool m_downKeyPressed = false; - bool m_enterKeyPressed = false; - bool m_cKeyPressed = false; - bool m_escapeKeyPressed = false; - - bool m_hasError = false; - std::string m_errorMessage; - float m_errorTimer = 0.0f; - - float m_refreshTimer = 0.0f; - static constexpr float REFRESH_INTERVAL = 2.0f; - - bool m_creatingRoom = false; - std::string m_newRoomName; - }; - - } -} diff --git a/client/include/SettingsState.hpp b/client/include/SettingsState.hpp deleted file mode 100644 index 7245c4c..0000000 --- a/client/include/SettingsState.hpp +++ /dev/null @@ -1,185 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** SettingsState -*/ - -#pragma once - -#include "GameStateMachine.hpp" -#include "ECS/Registry.hpp" -#include "ECS/RenderingSystem.hpp" -#include "ECS/TextRenderingSystem.hpp" -#include "ECS/AudioSystem.hpp" -#include "Renderer/IRenderer.hpp" -#include "Audio/IAudio.hpp" -#include "Core/ColorFilter.hpp" -#include -#include -#include -#include - -namespace RType { - namespace Client { - - class SettingsState : public IState { - public: - SettingsState(GameStateMachine& machine, GameContext& context); - ~SettingsState() override = default; - - void Init() override; - void Cleanup() override; - void HandleInput() override; - void Update(float dt) override; - void Draw() override; - - static bool IsColourBlindModeEnabled(); - static void SetColourBlindMode(bool enabled); - static void LoadSettingsFromFile(); - static std::uint32_t GetScreenWidth(); - static std::uint32_t GetScreenHeight(); - static bool GetFullscreen(); - static std::uint32_t GetTargetFramerate(); - static float GetMasterVolume(); - static bool IsMuted(); - - private: - void createUI(); - void createScreenUI(); - void createAudioUI(); - void createCommandsUI(); - void updateAnimations(float dt); - void updateMenuSelection(); - void updateScreenMenuSelection(); - void updateAudioMenuSelection(); - void updateCommandsMenuSelection(); - void toggleColourBlindMode(); - void enterSubMenu(bool screen); - void enterCommandsMenu(); - void exitSubMenu(); - void changeResolution(int direction); - void toggleFullscreen(); - void changeFrameRate(int direction); - void changeMasterVolume(int direction); - void toggleMute(); - void startRebind(int actionIndex); - void processRebind(); - void applyScreenSettings(); - void saveSettings(); - void loadSettings(); - void clearMenuUI(); - static std::string keyToString(Renderer::Key key); - static Renderer::Key stringToKey(const std::string& str); - - enum class SettingsItem { - COLOUR_BLIND = 0, - SCREEN = 1, - AUDIO = 2, - COMMANDS = 3, - BACK = 4, - COUNT = 5 - }; - - enum class ScreenItem { - RESOLUTION = 0, - FULLSCREEN = 1, - FRAME_RATE = 2, - BACK = 3, - COUNT = 4 - }; - - enum class AudioItem { - MASTER_VOLUME = 0, - MUTE = 1, - BACK = 2, - COUNT = 3 - }; - - enum class CommandsItem { - MOVE_UP = 0, - MOVE_DOWN = 1, - MOVE_LEFT = 2, - MOVE_RIGHT = 3, - SHOOT = 4, - BACK = 5, - COUNT = 6 - }; - - struct Resolution { - std::uint32_t width; - std::uint32_t height; - std::string name; - }; - - GameStateMachine& m_machine; - GameContext& m_context; - - RType::ECS::Registry m_registry; - std::shared_ptr m_renderer; - std::unique_ptr m_renderingSystem; - std::unique_ptr m_textSystem; - std::unique_ptr m_audioSystem; - - Audio::MusicId m_selectMusic = Audio::INVALID_MUSIC_ID; - - Renderer::FontId m_fontLarge = Renderer::INVALID_FONT_ID; - Renderer::FontId m_fontMedium = Renderer::INVALID_FONT_ID; - Renderer::FontId m_fontSmall = Renderer::INVALID_FONT_ID; - Renderer::TextureId m_bgTexture = Renderer::INVALID_TEXTURE_ID; - - std::vector m_entities; - RType::ECS::Entity m_titleEntity = RType::ECS::NULL_ENTITY; - std::vector m_menuItems; - std::vector m_screenMenuItems; - std::vector m_audioMenuItems; - std::vector m_commandsMenuItems; - - int m_selectedIndex = 0; - int m_screenSelectedIndex = 0; - int m_audioSelectedIndex = 0; - int m_commandsSelectedIndex = 0; - bool m_upKeyPressed = false; - bool m_downKeyPressed = false; - bool m_leftKeyPressed = false; - bool m_rightKeyPressed = false; - bool m_enterKeyPressed = false; - bool m_escKeyPressed = false; - - float m_animTime = 0.0f; - - bool m_colourBlindMode = false; - bool m_inScreenMenu = false; - bool m_inAudioMenu = false; - bool m_inCommandsMenu = false; - bool m_waitingForRebind = false; - int m_rebindingActionIndex = -1; - - std::uint32_t m_screenWidth = 1280; - std::uint32_t m_screenHeight = 720; - bool m_fullscreen = false; - std::uint32_t m_targetFramerate = 60; - int m_resolutionIndex = 0; - int m_framerateIndex = 2; - - static const std::vector RESOLUTIONS; - static const std::vector FRAMERATES; - static const std::vector FRAMERATE_NAMES; - - float m_masterVolume = 1.0f; - float m_volumeBeforeMute = 1.0f; - bool m_muted = false; - - static bool s_colourBlindMode; - static std::uint32_t s_screenWidth; - static std::uint32_t s_screenHeight; - static bool s_fullscreen; - static std::uint32_t s_targetFramerate; - static float s_masterVolume; - static bool s_muted; - static const std::string SETTINGS_FILE_PATH; - }; - - } -} - diff --git a/client/include/editor/EditorAssetLibrary.hpp b/client/include/editor/EditorAssetLibrary.hpp deleted file mode 100644 index 8397b97..0000000 --- a/client/include/editor/EditorAssetLibrary.hpp +++ /dev/null @@ -1,52 +0,0 @@ -#pragma once - -#include "editor/EditorTypes.hpp" -#include "Renderer/IRenderer.hpp" -#include "Math/Types.hpp" -#include -#include -#include - -namespace RType { - namespace Client { - - struct EditorAssetDefinition { - std::string id; - std::string displayName; - EditorEntityType type = EditorEntityType::OBSTACLE; - std::string texturePath; - Math::Vector2 defaultSize{0.0f, 0.0f}; - float defaultScrollSpeed = -50.0f; - int defaultLayer = 1; - std::string enemyType; - std::string powerUpType; - }; - - struct EditorAssetResource { - EditorAssetDefinition definition; - Renderer::TextureId textureId = Renderer::INVALID_TEXTURE_ID; - Renderer::SpriteId spriteId = Renderer::INVALID_SPRITE_ID; - Math::Vector2 textureSize{128.0f, 128.0f}; - }; - - class EditorAssetLibrary { - public: - explicit EditorAssetLibrary(Renderer::IRenderer* renderer); - - bool Initialize(); - - const EditorAssetResource* GetResource(const std::string& id) const; - const std::vector& GetResources(EditorEntityType type) const; - - private: - Renderer::IRenderer* m_renderer; - std::unordered_map m_resourcesById; - std::unordered_map> m_resourcesByType; - - std::vector buildDefaultDefinitions() const; - bool loadResource(EditorAssetResource& resource); - }; - - } -} - diff --git a/client/include/editor/EditorCanvasManager.hpp b/client/include/editor/EditorCanvasManager.hpp deleted file mode 100644 index 59815df..0000000 --- a/client/include/editor/EditorCanvasManager.hpp +++ /dev/null @@ -1,43 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** EditorCanvasManager -*/ - -#pragma once - -#include "EditorTypes.hpp" -#include "Renderer/IRenderer.hpp" -#include "Math/Types.hpp" -#include - -namespace RType { - namespace Client { - - class EditorCanvasManager { - public: - explicit EditorCanvasManager(Renderer::IRenderer* renderer); - ~EditorCanvasManager() = default; - - void HandleCameraInput(); - void ApplyCamera(); - void DrawGrid(); - - Math::Vector2 ScreenToWorld(Math::Vector2 screenPos) const; - Math::Vector2 WorldToScreen(Math::Vector2 worldPos) const; - - const EditorCamera& GetCamera() const { return m_camera; } - const EditorGrid& GetGrid() const { return m_grid; } - - void ToggleGridSnap() { m_grid.snapToGrid = !m_grid.snapToGrid; } - float SnapToGrid(float value) const; - - private: - Renderer::IRenderer* m_renderer; - EditorCamera m_camera; - EditorGrid m_grid; - }; - - } -} diff --git a/client/include/editor/EditorColliderManager.hpp b/client/include/editor/EditorColliderManager.hpp deleted file mode 100644 index 11fb700..0000000 --- a/client/include/editor/EditorColliderManager.hpp +++ /dev/null @@ -1,47 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** EditorColliderManager -*/ - -#pragma once - -#include "editor/EditorTypes.hpp" -#include "ECS/LevelLoader.hpp" -#include "Renderer/IRenderer.hpp" -#include "Math/Types.hpp" -#include - -namespace RType { - namespace Client { - - class EditorColliderManager { - public: - explicit EditorColliderManager(Renderer::IRenderer* renderer); - ~EditorColliderManager() = default; - - void DrawColliders(const EditorEntityData* entity, int selectedIndex = -1) const; - void DrawHandles(const EditorEntityData* entity, int colliderIndex) const; - ColliderHandle GetHandleAt(const EditorEntityData* entity, - const Math::Vector2& worldPos) const; - int AddCollider(EditorEntityData& entity, const Math::Vector2& worldPos); - bool RemoveCollider(EditorEntityData& entity, int colliderIndex); - void ResizeCollider(EditorEntityData& entity, int colliderIndex, - ColliderHandle::Type handleType, - const Math::Vector2& worldPos); - - void SetDragStart(const Math::Vector2& pos) { m_dragStart = pos; } - const Math::Vector2& GetDragStart() const { return m_dragStart; } - - private: - Renderer::IRenderer* m_renderer; - Math::Vector2 m_dragStart{0.0f, 0.0f}; - - void drawCollider(const ECS::ColliderDef& collider, const Math::Color& color) const; - void drawHandle(const Math::Vector2& pos) const; - Math::Rectangle getColliderRect(const ECS::ColliderDef& collider) const; - }; - - } -} diff --git a/client/include/editor/EditorConstants.hpp b/client/include/editor/EditorConstants.hpp deleted file mode 100644 index 69036b6..0000000 --- a/client/include/editor/EditorConstants.hpp +++ /dev/null @@ -1,102 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** EditorConstants -*/ - -#pragma once - -#include "Math/Types.hpp" - -namespace RType { - namespace Client { - namespace EditorConstants { - - namespace UI { - constexpr float PALETTE_PANEL_LEFT = 10.0f; - constexpr float PALETTE_PANEL_WIDTH = 220.0f; - constexpr float PALETTE_BUTTON_HEIGHT = 22.0f; - constexpr float PALETTE_START_Y = 80.0f; - constexpr float TOOLBAR_START_Y = 45.0f; - constexpr float TOOLBAR_BUTTON_HEIGHT = 24.0f; - constexpr float TOOLBAR_BUTTON_GAP = 8.0f; - - constexpr float PROPERTY_PANEL_X = 920.0f; - constexpr float PROPERTY_PANEL_WIDTH = 340.0f; - constexpr float PROPERTY_PANEL_START_Y = 90.0f; - constexpr float PROPERTY_ROW_HEIGHT = 28.0f; - constexpr float PROPERTY_VALUE_OFFSET_X = 100.0f; - constexpr float TOOLBAR_RIGHT_X = PROPERTY_PANEL_X; - constexpr float TOOLBAR_RIGHT_WIDTH = PROPERTY_PANEL_WIDTH; - - constexpr float STATUS_BAR_X = 10.0f; - constexpr float STATUS_BAR_Y = 10.0f; - constexpr float STATUS_BAR_LINE_HEIGHT = 14.0f; - } - - namespace Colors { - inline const Math::Color BACKGROUND{0.1f, 0.1f, 0.15f, 1.0f}; - inline const Math::Color GRID{0.3f, 0.3f, 0.3f, 0.5f}; - inline const Math::Color SELECTION_OUTLINE{1.0f, 1.0f, 0.0f, 1.0f}; - constexpr float PREVIEW_ALPHA = 0.35f; - - inline const Math::Color UI_HEADER{0.4f, 0.86f, 0.9f, 1.0f}; - inline const Math::Color UI_TEXT{0.75f, 0.75f, 0.8f, 1.0f}; - inline const Math::Color UI_HINT{0.5f, 0.86f, 1.0f, 0.7f}; - inline const Math::Color UI_ACTIVE{1.0f, 0.7f, 0.0f, 1.0f}; - inline const Math::Color UI_HOVER{0.9f, 0.9f, 0.95f, 1.0f}; - } - - namespace Camera { - constexpr float PAN_SPEED = 500.0f; - constexpr float ZOOM_SPEED = 0.1f; - constexpr float INITIAL_X = 640.0f; - constexpr float INITIAL_Y = 360.0f; - constexpr float INITIAL_ZOOM = 1.0f; - - constexpr float MIN_X = -1000.0f; - constexpr float MAX_X = 15000.0f; - constexpr float MIN_Y = -500.0f; - constexpr float MAX_Y = 1200.0f; - constexpr float MIN_ZOOM = 0.25f; - constexpr float MAX_ZOOM = 2.0f; - } - - namespace Grid { - constexpr float CELL_SIZE = 50.0f; - constexpr float LINE_THICKNESS = 1.0f; - } - - namespace PropertySteps { - constexpr float POSITION_STEP = 25.0f; - constexpr float SCALE_STEP = 10.0f; - constexpr float LAYER_STEP = 1.0f; - constexpr float SCROLL_SPEED_STEP = 5.0f; - constexpr float MIN_SCALE = 10.0f; - } - - namespace Collider { - constexpr float HANDLE_SIZE = 8.0f; - constexpr float MIN_COLLIDER_SIZE = 10.0f; - constexpr float COLLIDER_LINE_THICKNESS = 2.0f; - constexpr float SNAP_DISTANCE = 5.0f; - - inline const Math::Color COLLIDER_NORMAL{0.0f, 1.0f, 0.0f, 0.6f}; - inline const Math::Color COLLIDER_SELECTED{1.0f, 0.5f, 0.0f, 0.8f}; - inline const Math::Color COLLIDER_HANDLE{1.0f, 1.0f, 0.0f, 1.0f}; - } - - namespace Input { - constexpr size_t MAX_INPUT_BUFFER_SIZE = 8; - constexpr int NUMBER_KEY_COUNT = 10; - } - - namespace Selection { - constexpr float OUTLINE_THICKNESS = 3.0f; - inline const Math::Color OUTLINE_COLOR{1.0f, 0.85f, 0.35f, 1.0f}; - } - - } - } -} diff --git a/client/include/editor/EditorDrawing.hpp b/client/include/editor/EditorDrawing.hpp deleted file mode 100644 index 4645baa..0000000 --- a/client/include/editor/EditorDrawing.hpp +++ /dev/null @@ -1,35 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** EditorDrawing -*/ - -#pragma once - -#include "Math/Types.hpp" -#include "Renderer/IRenderer.hpp" -#include "ECS/LevelLoader.hpp" - -namespace RType { - namespace Client { - namespace EditorDrawing { - - void DrawRectangleOutline(Renderer::IRenderer* renderer, - const Math::Rectangle& rect, - const Math::Color& color, - float thickness = 3.0f); - - void DrawCollider(Renderer::IRenderer* renderer, - const ECS::ColliderDef& collider, - const Math::Color& color, - float lineThickness = 2.0f); - - void DrawHandle(Renderer::IRenderer* renderer, - const Math::Vector2& position, - const Math::Color& color, - float size = 8.0f); - - } - } -} diff --git a/client/include/editor/EditorEntityManager.hpp b/client/include/editor/EditorEntityManager.hpp deleted file mode 100644 index f72118f..0000000 --- a/client/include/editor/EditorEntityManager.hpp +++ /dev/null @@ -1,67 +0,0 @@ -#pragma once - -#include "editor/EditorTypes.hpp" -#include "editor/EditorAssetLibrary.hpp" -#include "ECS/Registry.hpp" -#include "Renderer/IRenderer.hpp" -#include -#include - -namespace RType { - namespace Client { - - class EditorColliderManager; - - class EditorEntityManager { - public: - EditorEntityManager(Renderer::IRenderer* renderer, RType::ECS::Registry& registry, EditorAssetLibrary& assets); - ~EditorEntityManager(); - - EditorEntityData* PlaceEntity(const EditorPaletteSelection& selection, const Math::Vector2& worldPos); - void DrawPlacementPreview(EditorMode mode, - EditorEntityType type, - const std::string& identifier, - const Math::Vector2& worldPos) const; - void DrawSelectionOutline() const; - void DrawColliders(int selectedColliderIndex = -1) const; - void DrawColliderHandles(int colliderIndex) const; - - bool SelectAt(const Math::Vector2& worldPos); - void ClearSelection(); - bool DeleteSelected(); - - ColliderHandle GetColliderHandleAt(const Math::Vector2& worldPos) const; - void AddCollider(const Math::Vector2& worldPos); - bool RemoveCollider(int colliderIndex); - void ResizeCollider(int colliderIndex, ColliderHandle::Type handleType, const Math::Vector2& worldPos); - int GetSelectedColliderIndex() const { return m_selectedColliderIndex; } - void SetSelectedCollider(int index) { m_selectedColliderIndex = index; } - - const std::vector& GetEntities() const { return m_entities; } - EditorEntityData* GetSelectedEntity(); - const EditorEntityData* GetSelectedEntity() const; - - void SyncEntity(EditorEntityData& data); - void RebuildDefaultCollider(EditorEntityData& data); - - private: - Math::Vector2 getDefaultSize(EditorEntityType type) const; - Math::Color getColor(EditorEntityType type) const; - const EditorAssetResource* getResource(const std::string& id) const; - void applyEntityToComponents(EditorEntityData& data); - void destroyEntity(EditorEntityData& data); - void drawOutline(const EditorEntityData& entity) const; - void initializeDefaultCollider(EditorEntityData& data) const; - void createColliderEntities(EditorEntityData& data); - - Renderer::IRenderer* m_renderer; - RType::ECS::Registry& m_registry; - EditorAssetLibrary& m_assets; - std::unique_ptr m_colliderManager; - std::vector m_entities; - int m_selectedIndex = -1; - int m_selectedColliderIndex = -1; - }; - - } -} diff --git a/client/include/editor/EditorFileManager.hpp b/client/include/editor/EditorFileManager.hpp deleted file mode 100644 index e0140fa..0000000 --- a/client/include/editor/EditorFileManager.hpp +++ /dev/null @@ -1,57 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** EditorFileManager - Save/Load level files -*/ - -#pragma once - -#include "editor/EditorTypes.hpp" -#include "editor/EditorAssetLibrary.hpp" -#include "ECS/LevelLoader.hpp" -#include "ECS/Registry.hpp" -#include "Renderer/IRenderer.hpp" -#include -#include - -namespace RType { - namespace Client { - - class EditorFileManager { - public: - explicit EditorFileManager(EditorAssetLibrary& assets); - - bool SaveLevel(const std::string& path, - const std::vector& entities, - const std::string& levelName); - - std::vector LoadLevel(const std::string& path, - ECS::Registry& registry, - Renderer::IRenderer* renderer); - - const std::string& GetLastError() const { return m_lastError; } - - private: - ECS::LevelData GatherLevelData(const std::vector& entities, - const std::string& levelName); - - EditorEntityData ConvertObstacleToEditor(const ECS::ObstacleDef& obs, - ECS::Registry& registry, - Renderer::IRenderer* renderer); - EditorEntityData ConvertEnemyToEditor(const ECS::EnemyDef& enemy, - ECS::Registry& registry, - Renderer::IRenderer* renderer); - EditorEntityData ConvertSpawnToEditor(const ECS::PlayerSpawnDef& spawn, - ECS::Registry& registry, - Renderer::IRenderer* renderer); - EditorEntityData ConvertBackgroundToEditor(const ECS::BackgroundDef& bg, - ECS::Registry& registry, - Renderer::IRenderer* renderer); - - EditorAssetLibrary& m_assets; - std::string m_lastError; - }; - - } -} diff --git a/client/include/editor/EditorGeometry.hpp b/client/include/editor/EditorGeometry.hpp deleted file mode 100644 index 21f6d1a..0000000 --- a/client/include/editor/EditorGeometry.hpp +++ /dev/null @@ -1,52 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** EditorGeometry -*/ - -#pragma once - -#include "Math/Types.hpp" - -namespace RType { - namespace Client { - namespace EditorGeometry { - - inline Math::Rectangle BuildRect(float x, float y, float width, float height) { - Math::Rectangle rect; - rect.position = {x, y}; - rect.size = {width, height}; - return rect; - } - - inline bool PointInRect(const Math::Vector2& point, const Math::Rectangle& rect) { - return point.x >= rect.position.x && - point.x <= rect.position.x + rect.size.x && - point.y >= rect.position.y && - point.y <= rect.position.y + rect.size.y; - } - - inline Math::Rectangle GetEntityBounds(float centerX, float centerY, float width, float height) { - return BuildRect(centerX - width / 2.0f, centerY - height / 2.0f, width, height); - } - - inline Math::Rectangle GetHandleBounds(float x, float y, float handleSize) { - return BuildRect(x - handleSize / 2.0f, y - handleSize / 2.0f, handleSize, handleSize); - } - - inline bool PointInEntityBounds(const Math::Vector2& point, - float centerX, float centerY, - float width, float height) { - float left = centerX - width / 2.0f; - float right = centerX + width / 2.0f; - float top = centerY - height / 2.0f; - float bottom = centerY + height / 2.0f; - - return point.x >= left && point.x <= right && - point.y >= top && point.y <= bottom; - } - - } - } -} diff --git a/client/include/editor/EditorInputHandler.hpp b/client/include/editor/EditorInputHandler.hpp deleted file mode 100644 index bd578e5..0000000 --- a/client/include/editor/EditorInputHandler.hpp +++ /dev/null @@ -1,31 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** EditorInputHandler -*/ - -#pragma once - -#include "Renderer/IRenderer.hpp" -#include -#include - -namespace RType { - namespace Client { - - class EditorInputHandler { - public: - explicit EditorInputHandler(Renderer::IRenderer* renderer); - ~EditorInputHandler() = default; - - void HandleKeyPress(Renderer::Key key, const std::function& action); - void Update(); - - private: - Renderer::IRenderer* m_renderer; - std::unordered_map m_keyStates; - }; - - } -} diff --git a/client/include/editor/EditorPropertyManager.hpp b/client/include/editor/EditorPropertyManager.hpp deleted file mode 100644 index 170b8fa..0000000 --- a/client/include/editor/EditorPropertyManager.hpp +++ /dev/null @@ -1,53 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** EditorPropertyManager -*/ - -#pragma once - -#include "editor/EditorTypes.hpp" -#include "Renderer/IRenderer.hpp" -#include -#include - -namespace RType { - namespace Client { - - class EditorPropertyManager { - public: - explicit EditorPropertyManager(Renderer::IRenderer* renderer); - ~EditorPropertyManager() = default; - - bool HandleInput(EditorEntityData* selectedEntity, - std::function)> handleKeyPress); - - EditableProperty GetActiveProperty() const { return m_activeProperty; } - const std::string& GetInputBuffer() const { return m_inputBuffer; } - void ClearInput(); - void SetOnPropertyChanged(std::function callback); - void SetOnEntityDeleted(std::function callback); - void SetOnPropertyCycled(std::function callback); - - private: - Renderer::IRenderer* m_renderer; - - EditableProperty m_activeProperty = EditableProperty::POSITION_X; - std::string m_inputBuffer; - - std::function m_onPropertyChanged; - std::function m_onEntityDeleted; - std::function m_onPropertyCycled; - - void cycleProperty(); - void applyPropertyDelta(EditorEntityData& entity, float delta); - void setPropertyValue(EditorEntityData& entity, float value); - float getPropertyValue(const EditorEntityData& entity) const; - float getPropertyStep() const; - void handleNumberInput(Renderer::Key key); - void handleBackspace(EditorEntityData* entity); - }; - - } -} diff --git a/client/include/editor/EditorTypes.hpp b/client/include/editor/EditorTypes.hpp deleted file mode 100644 index 8b18fd4..0000000 --- a/client/include/editor/EditorTypes.hpp +++ /dev/null @@ -1,116 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** EditorTypes -*/ - -#pragma once - -#include "ECS/Entity.hpp" -#include "ECS/LevelLoader.hpp" -#include "Math/Types.hpp" -#include -#include - -namespace RType { - namespace Client { - - enum class EditorEntityType { - BACKGROUND, - OBSTACLE, - ENEMY, - POWERUP, - PLAYER_SPAWN - }; - - enum class EditorMode { - SELECT, - PLACE_ENEMY, - PLACE_OBSTACLE, - PLACE_POWERUP, - PLACE_PLAYER_SPAWN, - PLACE_BACKGROUND, - EDIT_COLLIDERS - }; - - enum class EditableProperty { - POSITION_X, - POSITION_Y, - SCALE_WIDTH, - SCALE_HEIGHT, - LAYER, - SCROLL_SPEED, - COLLIDER_X, - COLLIDER_Y, - COLLIDER_WIDTH, - COLLIDER_HEIGHT, - COUNT - }; - - enum class ColliderEditMode { - NONE, - ADD, - REMOVE, - RESIZE, - MOVE - }; - - struct ColliderHandle { - int colliderIndex = -1; - enum class Type { NONE, BODY, TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT } type = Type::NONE; - }; - - struct EditorEntityData { - ECS::Entity entity = ECS::NULL_ENTITY; - EditorEntityType type = EditorEntityType::OBSTACLE; - std::vector colliderEntities; - std::string presetId; - - // Common properties - float x = 0.0f; - float y = 0.0f; - float scaleWidth = 100.0f; - float scaleHeight = 100.0f; - int layer = 1; - float scrollSpeed = -50.0f; - - // Type-specific data - std::string textureKey; - std::string enemyType; - std::string powerUpType; - std::vector colliders; - - // Selection state - bool isSelected = false; - }; - - struct EditorCamera { - float x = 640.0f; - float y = 360.0f; - float zoom = 1.0f; - - float minX = -1000.0f; - float maxX = 15000.0f; - float minY = -500.0f; - float maxY = 1200.0f; - - float minZoom = 0.25f; - float maxZoom = 2.0f; - }; - - struct EditorGrid { - bool enabled = true; - float cellSize = 50.0f; - bool snapToGrid = false; - Math::Color gridColor{0.3f, 0.3f, 0.3f, 0.5f}; - }; - - struct EditorPaletteSelection { - EditorMode mode = EditorMode::SELECT; - EditorEntityType entityType = EditorEntityType::OBSTACLE; - std::string subtype; - }; - - } -} diff --git a/client/include/editor/EditorUIManager.hpp b/client/include/editor/EditorUIManager.hpp deleted file mode 100644 index e422f29..0000000 --- a/client/include/editor/EditorUIManager.hpp +++ /dev/null @@ -1,96 +0,0 @@ -#pragma once - -#include "editor/EditorTypes.hpp" -#include "editor/EditorAssetLibrary.hpp" -#include "ECS/Component.hpp" -#include "ECS/Registry.hpp" -#include "Renderer/IRenderer.hpp" -#include "Math/Types.hpp" -#include -#include -#include -#include - -namespace RType { - namespace Client { - - class EditorUIManager { - public: - EditorUIManager(Renderer::IRenderer* renderer, - EditorAssetLibrary& assets, - ECS::Registry& registry, - std::vector& trackedEntities, - Renderer::FontId fontSmall, - Renderer::FontId fontMedium); - - void InitializePalette(); - void InitializePropertiesPanel(); - void InitializeColliderPanel(); - void UpdateHover(Math::Vector2 mouseScreen); - bool HandleActionClick(Math::Vector2 mouseScreen); - std::optional HandleClick(Math::Vector2 mouseScreen); - void SetActiveSelection(const EditorPaletteSelection& selection); - const EditorPaletteSelection& GetActiveSelection() const { return m_activeSelection; } - void UpdatePropertyPanel(const EditorEntityData* selected, - EditableProperty activeProperty, - const std::string& inputBuffer); - void UpdateColliderPanel(const EditorEntityData* selected, int selectedColliderIndex); - void SetOnSaveRequested(const std::function& callback); - - private: - struct PaletteEntry { - std::string label; - EditorMode mode = EditorMode::SELECT; - EditorEntityType entityType = EditorEntityType::OBSTACLE; - std::string subtype; - Math::Rectangle bounds; - ECS::Entity textEntity = ECS::NULL_ENTITY; - bool hovered = false; - const EditorAssetResource* resource = nullptr; - }; - - struct PropertyField { - EditableProperty property = EditableProperty::POSITION_X; - ECS::Entity nameEntity = ECS::NULL_ENTITY; - ECS::Entity valueEntity = ECS::NULL_ENTITY; - }; - - struct ActionButton { - std::string label; - Math::Rectangle bounds; - ECS::Entity textEntity = ECS::NULL_ENTITY; - bool hovered = false; - std::function onClick; - }; - - void createCategoryLabel(const std::string& label, float y); - void createPaletteButton(PaletteEntry entry, float y); - void InitializeToolbar(); - void refreshPaletteVisuals(); - void refreshActionButtons(); - - Renderer::IRenderer* m_renderer; - EditorAssetLibrary& m_assets; - ECS::Registry& m_registry; - std::vector& m_trackedEntities; - Renderer::FontId m_fontSmall; - Renderer::FontId m_fontMedium; - - std::vector m_entries; - std::vector m_actionButtons; - EditorPaletteSelection m_activeSelection; - std::vector m_propertyFields; - ECS::Entity m_propertiesHeader = ECS::NULL_ENTITY; - ECS::Entity m_selectedInfoEntity = ECS::NULL_ENTITY; - ECS::Entity m_propertyHintEntity = ECS::NULL_ENTITY; - - ECS::Entity m_colliderPanelHeader = ECS::NULL_ENTITY; - ECS::Entity m_colliderCountEntity = ECS::NULL_ENTITY; - ECS::Entity m_colliderHintEntity = ECS::NULL_ENTITY; - - std::function m_onSaveRequested; - }; - - } -} - diff --git a/client/main.cpp b/client/main.cpp deleted file mode 100644 index 85743d6..0000000 --- a/client/main.cpp +++ /dev/null @@ -1,96 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** main -*/ - -#include "GameStateMachine.hpp" -#include "MenuState.hpp" -#include "SettingsState.hpp" -#include "Renderer/SFMLRenderer.hpp" -#include "AsioNetworkModule.hpp" -#include "Audio/SFMLAudio.hpp" -#include -#include - -int main(int argc, char* argv[]) { - std::string serverIp = "127.0.0.1"; - uint16_t serverPort = 4242; - std::string playerName = "Player1"; - - if (argc > 1) { - serverIp = argv[1]; - } - if (argc > 2) { - serverPort = static_cast(std::stoi(argv[2])); - } - if (argc > 3) { - playerName = argv[3]; - } - - std::cout << "=== R-Type Client ===" << std::endl; - std::cout << "Server IP: " << serverIp << std::endl; - std::cout << "Server Port: " << serverPort << std::endl; - std::cout << "Player Name: " << playerName << std::endl; - std::cout << "=====================" << std::endl; - - RType::Client::SettingsState::LoadSettingsFromFile(); - - auto renderer = std::make_shared(); - auto networkModule = std::make_shared(); - networkModule->Initialize(nullptr); - auto audio = std::make_shared(); - - Renderer::WindowConfig config; - config.title = "R-Type - " + playerName; - config.width = RType::Client::SettingsState::GetScreenWidth(); - config.height = RType::Client::SettingsState::GetScreenHeight(); - config.fullscreen = RType::Client::SettingsState::GetFullscreen(); - config.resizable = !config.fullscreen; - config.targetFramerate = RType::Client::SettingsState::GetTargetFramerate(); - - if (!renderer->CreateWindow(config)) { - std::cerr << "Failed to create window" << std::endl; - return 1; - } - - RType::Client::GameContext context; - context.renderer = renderer; - context.networkModule = networkModule; - context.audio = audio; - context.serverIp = serverIp; - context.serverPort = serverPort; - context.playerName = playerName; - - Audio::AudioConfig audioConfig; - audioConfig.masterVolume = RType::Client::SettingsState::GetMasterVolume(); - audio->ConfigureDevice(audioConfig); - - if (RType::Client::SettingsState::IsMuted()) { - audio->SetMasterVolume(0.0f); - } - - RType::Client::GameStateMachine machine; - - machine.PushState(std::make_unique(machine, context)); - - while (renderer->IsWindowOpen() && machine.IsRunning()) { - float dt = renderer->GetDeltaTime(); - - renderer->Update(dt); - if (audio) { - audio->Update(dt); - } - renderer->BeginFrame(); - - machine.HandleInput(); - machine.Update(dt); - machine.Draw(); - - renderer->EndFrame(); - } - - std::cout << "Exiting R-Type client..." << std::endl; - return 0; -} diff --git a/client/src/EditorState.cpp b/client/src/EditorState.cpp deleted file mode 100644 index 8eb5475..0000000 --- a/client/src/EditorState.cpp +++ /dev/null @@ -1,342 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** EditorState -*/ - -#include "EditorState.hpp" -#include "editor/EditorCanvasManager.hpp" -#include "editor/EditorUIManager.hpp" -#include "editor/EditorEntityManager.hpp" -#include "editor/EditorFileManager.hpp" -#include "editor/EditorConstants.hpp" -#include "ECS/Component.hpp" -#include "ECS/Components/TextLabel.hpp" -#include "Core/Logger.hpp" -#include -#include -#include -#include - -using namespace RType::Client::EditorConstants; - -namespace RType { - namespace Client { - - EditorState::EditorState(GameStateMachine& machine, GameContext& context) - : m_machine(machine) - , m_context(context) - , m_renderer(context.renderer) - { - } - - EditorState::~EditorState() = default; - - void EditorState::Init() { - Core::Logger::Info("[EditorState] Initializing level editor..."); - - m_renderingSystem = std::make_unique(m_renderer.get()); - m_textSystem = std::make_unique(m_renderer.get()); - - m_fontSmall = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 12); - if (m_fontSmall == Renderer::INVALID_FONT_ID) { - m_fontSmall = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 12); - } - - m_fontMedium = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 16); - if (m_fontMedium == Renderer::INVALID_FONT_ID) { - m_fontMedium = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 16); - } - - for (int i = 0; i < 3; ++i) { - ECS::Entity statusEntity = m_registry.CreateEntity(); - m_entities.push_back(statusEntity); - m_statusBarEntities.push_back(statusEntity); - - float yPos = UI::STATUS_BAR_Y + (i * UI::STATUS_BAR_LINE_HEIGHT); - m_registry.AddComponent(statusEntity, ECS::Position{UI::STATUS_BAR_X, yPos}); - - ECS::TextLabel statusLabel; - statusLabel.text = ""; - statusLabel.fontId = m_fontSmall; - statusLabel.characterSize = 10; - statusLabel.color = Colors::UI_TEXT; - m_registry.AddComponent(statusEntity, std::move(statusLabel)); - } - - m_inputHandler = std::make_unique(m_renderer.get()); - m_propertyManager = std::make_unique(m_renderer.get()); - m_canvasManager = std::make_unique(m_renderer.get()); - m_assetLibrary = std::make_unique(m_renderer.get()); - m_assetLibrary->Initialize(); - - m_fileManager = std::make_unique(*m_assetLibrary); - m_entityManager = std::make_unique(m_renderer.get(), m_registry, *m_assetLibrary); - m_uiManager = std::make_unique(m_renderer.get(), *m_assetLibrary, m_registry, m_entities, m_fontSmall, m_fontMedium); - m_uiManager->InitializePalette(); - m_uiManager->SetOnSaveRequested([this]() { - saveCurrentLevel(); - }); - m_selection = m_uiManager->GetActiveSelection(); - - m_propertyManager->SetOnPropertyChanged([this](EditorEntityData& entity) { - m_entityManager->RebuildDefaultCollider(entity); - m_entityManager->SyncEntity(entity); - m_hasUnsavedChanges = true; - updatePropertyPanel(); - }); - - m_propertyManager->SetOnEntityDeleted([this]() { - deleteSelectedEntity(); - }); - - m_propertyManager->SetOnPropertyCycled([this]() { - updatePropertyPanel(); - }); - - updatePropertyPanel(); - - Core::Logger::Info("[EditorState] Level editor initialized"); - } - - void EditorState::Cleanup() { - Core::Logger::Info("[EditorState] Cleaning up level editor..."); - - Renderer::Camera2D defaultCam{{0.0f, 0.0f}, {1280.0f, 720.0f}}; - m_renderer->SetCamera(defaultCam); - m_renderer->ResetCamera(); - - for (ECS::Entity entity : m_entities) { - if (m_registry.IsEntityAlive(entity)) { - m_registry.DestroyEntity(entity); - } - } - m_entities.clear(); - - Core::Logger::Info("[EditorState] Level editor cleaned up"); - } - - void EditorState::HandleInput() { - m_inputHandler->HandleKeyPress(Renderer::Key::Escape, [this]() { - if (m_hasUnsavedChanges) { - Core::Logger::Warning("[EditorState] Exiting with unsaved changes"); - } - Core::Logger::Info("[EditorState] Returning to menu..."); - if (m_machine.IsRunning() && m_machine.GetCurrentState() == this) { - m_machine.PopState(); - } else { - Core::Logger::Error("[EditorState] Cannot pop state - state machine is empty or invalid"); - } - }); - - m_inputHandler->HandleKeyPress(Renderer::Key::S, [this]() { - if (m_renderer->IsKeyPressed(Renderer::Key::LControl) || - m_renderer->IsKeyPressed(Renderer::Key::RControl)) { - saveCurrentLevel(); - } - }); - - m_inputHandler->HandleKeyPress(Renderer::Key::O, [this]() { - if (m_renderer->IsKeyPressed(Renderer::Key::LControl) || - m_renderer->IsKeyPressed(Renderer::Key::RControl)) { - - std::string path = m_currentLevelPath.empty() - ? "assets/levels/level1.json" - : m_currentLevelPath; - - auto loadedEntities = m_fileManager->LoadLevel(path, m_registry, m_renderer.get()); - if (!loadedEntities.empty()) { - Core::Logger::Info("[EditorState] Level loaded from {}", path); - m_currentLevelPath = path; - m_hasUnsavedChanges = false; - } else { - Core::Logger::Error("[EditorState] Failed to load: {}", m_fileManager->GetLastError()); - } - } - }); - - bool blockCameraInput = false; - if (m_propertyManager && m_entityManager && m_entityManager->GetSelectedEntity()) { - auto handleKeyPress = [this](Renderer::Key key, std::function action) { - m_inputHandler->HandleKeyPress(key, action); - }; - blockCameraInput = m_propertyManager->HandleInput(m_entityManager->GetSelectedEntity(), handleKeyPress); - } - - Math::Vector2 mouseScreen = m_renderer->GetMousePosition(); - if (m_uiManager) { - m_uiManager->UpdateHover(mouseScreen); - } - - if (m_canvasManager && !blockCameraInput) { - m_canvasManager->HandleCameraInput(); - } - - bool isLeftPressed = m_renderer->IsMouseButtonPressed(Renderer::IRenderer::MouseButton::Left); - if (isLeftPressed && !m_leftMousePressed) { - bool consumedByUI = false; - - if (m_uiManager) { - if (m_uiManager->HandleActionClick(mouseScreen)) { - consumedByUI = true; - } else { - auto selection = m_uiManager->HandleClick(mouseScreen); - if (selection.has_value()) { - m_selection = selection.value(); - consumedByUI = true; - if (m_propertyManager) { - m_propertyManager->ClearInput(); - } - } - } - } - - if (!consumedByUI && m_canvasManager && m_entityManager) { - Math::Vector2 mouseWorld = m_canvasManager->ScreenToWorld(mouseScreen); - if (m_selection.mode != EditorMode::SELECT) { - if (m_entityManager->PlaceEntity(m_selection, mouseWorld)) { - m_hasUnsavedChanges = true; - if (m_propertyManager) { - m_propertyManager->ClearInput(); - } - - m_selection.mode = EditorMode::SELECT; - m_selection.subtype.clear(); - if (m_uiManager) { - m_uiManager->SetActiveSelection(m_selection); - } - updatePropertyPanel(); - } - } else { - handleSelectionAt(mouseWorld); - } - } - } - m_leftMousePressed = isLeftPressed; - } - - void EditorState::Update(float dt) { - (void)dt; - - if (!m_canvasManager || m_statusBarEntities.size() < 3) { - return; - } - - const auto& camera = m_canvasManager->GetCamera(); - Math::Vector2 mouseScreen = m_renderer->GetMousePosition(); - Math::Vector2 mouseWorld = m_canvasManager->ScreenToWorld(mouseScreen); - m_lastMouseWorld = mouseWorld; - - auto formatLine = [](int index, const auto&... args) { - std::ostringstream oss; - oss << std::fixed << std::setprecision(1); - (oss << ... << args); - return oss.str(); - }; - - if (m_registry.IsEntityAlive(m_statusBarEntities[0])) { - auto& label0 = m_registry.GetComponent(m_statusBarEntities[0]); - label0.text = formatLine(0, "camera: (", camera.x, ", ", camera.y, ")"); - } - - if (m_registry.IsEntityAlive(m_statusBarEntities[1])) { - auto& label1 = m_registry.GetComponent(m_statusBarEntities[1]); - label1.text = formatLine(1, "zoom: ", camera.zoom, "x"); - } - - if (m_registry.IsEntityAlive(m_statusBarEntities[2])) { - auto& label2 = m_registry.GetComponent(m_statusBarEntities[2]); - label2.text = formatLine(2, "mouse: (", mouseWorld.x, ", ", mouseWorld.y, ")"); - } - } - - void EditorState::Draw() { - m_renderer->Clear(Colors::BACKGROUND); - - if (m_canvasManager) { - m_canvasManager->ApplyCamera(); - m_canvasManager->DrawGrid(); - } - - m_renderingSystem->Update(m_registry, 0.0f); - - if (m_entityManager) { - m_entityManager->DrawSelectionOutline(); - - if (m_entityManager->GetSelectedEntity()) { - m_entityManager->DrawColliders(m_entityManager->GetSelectedColliderIndex()); - } - - m_entityManager->DrawPlacementPreview(m_selection.mode, - m_selection.entityType, - m_selection.subtype, - m_lastMouseWorld); - } - - if (m_canvasManager) { - m_renderer->ResetCamera(); - } - - m_textSystem->Update(m_registry, 0.0f); - } - - void EditorState::handleSelectionAt(const Math::Vector2& mouseWorld) { - if (!m_entityManager) { - return; - } - - if (!m_entityManager->SelectAt(mouseWorld)) { - m_entityManager->ClearSelection(); - } - - if (m_propertyManager) { - m_propertyManager->ClearInput(); - } - updatePropertyPanel(); - } - - void EditorState::updatePropertyPanel() { - if (!m_uiManager || !m_entityManager || !m_propertyManager) { - return; - } - const EditorEntityData* selected = m_entityManager->GetSelectedEntity(); - m_uiManager->UpdatePropertyPanel(selected, m_propertyManager->GetActiveProperty(), m_propertyManager->GetInputBuffer()); - m_uiManager->UpdateColliderPanel(selected, m_entityManager->GetSelectedColliderIndex()); - } - - void EditorState::deleteSelectedEntity() { - if (!m_entityManager) { - return; - } - - if (m_entityManager->DeleteSelected()) { - m_hasUnsavedChanges = true; - if (m_propertyManager) { - m_propertyManager->ClearInput(); - } - updatePropertyPanel(); - } - } - - void EditorState::saveCurrentLevel() { - if (!m_fileManager || !m_entityManager) { - return; - } - - std::string path = m_currentLevelPath.empty() - ? "assets/levels/custom_level.json" - : m_currentLevelPath; - - if (m_fileManager->SaveLevel(path, m_entityManager->GetEntities(), m_levelName)) { - Core::Logger::Info("[EditorState] Level saved to {}", path); - m_hasUnsavedChanges = false; - m_currentLevelPath = path; - } else { - Core::Logger::Error("[EditorState] Failed to save: {}", m_fileManager->GetLastError()); - } - } - - - } -} diff --git a/client/src/GameState.cpp b/client/src/GameState.cpp deleted file mode 100644 index 9a98acb..0000000 --- a/client/src/GameState.cpp +++ /dev/null @@ -1,14 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** GameState - Main aggregator file -** This file includes all the split GameState implementation files -*/ - -#include "game/GameStateInit.cpp" -#include "game/GameStateUI.cpp" -#include "game/GameStateNetwork.cpp" -#include "game/GameStateUpdate.cpp" -#include "game/GameStateHelpers.cpp" -#include "game/GameStateTransition.cpp" diff --git a/client/src/LobbyState.cpp b/client/src/LobbyState.cpp deleted file mode 100644 index 878cfa2..0000000 --- a/client/src/LobbyState.cpp +++ /dev/null @@ -1,10 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** LobbyState - Main aggregator file -** This file includes all the split LobbyState implementation files -*/ - -#include "lobby/LobbyStateInit.cpp" -#include "lobby/LobbyStateUpdate.cpp" diff --git a/client/src/MenuState.cpp b/client/src/MenuState.cpp deleted file mode 100644 index 3e0f61f..0000000 --- a/client/src/MenuState.cpp +++ /dev/null @@ -1,316 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** MenuState -*/ - -#include "MenuState.hpp" -#include "LobbyState.hpp" -#include "EditorState.hpp" -#include "RoomListState.hpp" -#include "SettingsState.hpp" -#include "ECS/Components/TextLabel.hpp" -#include "ECS/Component.hpp" -#include "ECS/AudioSystem.hpp" -#include "Core/Logger.hpp" -#include -#include - -using namespace RType::ECS; - -namespace RType { - namespace Client { - - MenuState::MenuState(GameStateMachine& machine, GameContext& context) : m_machine(machine), m_context(context) { - m_renderer = context.renderer; - m_renderingSystem = std::make_unique(m_renderer.get()); - m_textSystem = std::make_unique(m_renderer.get()); - } - - void MenuState::Init() { - std::cout << "[MenuState] Initializing modern UI..." << std::endl; - m_ignoreInputFrames = 1; - - if (m_context.audio) { - m_audioSystem = std::make_unique(m_context.audio.get()); - m_menuMusic = m_context.audio->LoadMusic("assets/sounds/menu.flac"); - if (m_menuMusic == Audio::INVALID_MUSIC_ID) { - m_menuMusic = m_context.audio->LoadMusic("../assets/sounds/menu.flac"); - } - m_menuMusicPlaying = false; - - m_selectMusic = m_context.audio->LoadMusic("assets/sounds/select.flac"); - if (m_selectMusic == Audio::INVALID_MUSIC_ID) { - m_selectMusic = m_context.audio->LoadMusic("../assets/sounds/select.flac"); - } - } - - m_fontLarge = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 48); - if (m_fontLarge == Renderer::INVALID_FONT_ID) { - m_fontLarge = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 48); - } - - m_fontMedium = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 24); - if (m_fontMedium == Renderer::INVALID_FONT_ID) { - m_fontMedium = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 24); - } - - m_fontSmall = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 16); - if (m_fontSmall == Renderer::INVALID_FONT_ID) { - m_fontSmall = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 16); - } - - m_bgTexture = m_renderer->LoadTexture("assets/backgrounds/1.jpg"); - if (m_bgTexture == Renderer::INVALID_TEXTURE_ID) { - m_bgTexture = m_renderer->LoadTexture("../assets/backgrounds/1.jpg"); - } - - createUI(); - } - - void MenuState::Cleanup() { - std::cout << "[MenuState] Cleaning up..." << std::endl; - - if (m_context.audio && m_menuMusic != Audio::INVALID_MUSIC_ID) { - m_context.audio->StopMusic(m_menuMusic); - m_context.audio->UnloadMusic(m_menuMusic); - m_menuMusic = Audio::INVALID_MUSIC_ID; - m_menuMusicPlaying = false; - } - - if (m_context.audio && m_selectMusic != Audio::INVALID_MUSIC_ID) { - m_context.audio->StopMusic(m_selectMusic); - m_context.audio->UnloadMusic(m_selectMusic); - m_selectMusic = Audio::INVALID_MUSIC_ID; - } - - for (Entity entity : m_entities) { - if (m_registry.IsEntityAlive(entity)) { - m_registry.DestroyEntity(entity); - } - } - m_entities.clear(); - } - - void MenuState::createUI() { - if (m_bgTexture != Renderer::INVALID_TEXTURE_ID) { - Entity bg = m_registry.CreateEntity(); - m_entities.push_back(bg); - m_registry.AddComponent(bg, Position{0.0f, 0.0f}); - - Renderer::SpriteId spriteId = m_renderer->CreateSprite(m_bgTexture, {}); - Drawable drawable(spriteId, -10); - - Renderer::Vector2 texSize = m_renderer->GetTextureSize(m_bgTexture); - if (texSize.x > 0 && texSize.y > 0) { - drawable.scale = {1280.0f / texSize.x, 720.0f / texSize.y}; - } - - m_registry.AddComponent(bg, std::move(drawable)); - } - - if (m_fontLarge == Renderer::INVALID_FONT_ID) - return; - - m_titleEntity = m_registry.CreateEntity(); - m_entities.push_back(m_titleEntity); - m_registry.AddComponent(m_titleEntity, Position{640.0f, 200.0f}); - TextLabel titleLabel("R-TYPE", m_fontLarge, 72); - titleLabel.color = {0.0f, 1.0f, 1.0f, 1.0f}; - titleLabel.centered = true; - m_registry.AddComponent(m_titleEntity, std::move(titleLabel)); - - m_subtitleEntity = m_registry.CreateEntity(); - m_entities.push_back(m_subtitleEntity); - m_registry.AddComponent(m_subtitleEntity, Position{640.0f, 280.0f}); - TextLabel subtitleLabel("EPIC SPACE SHOOTER", m_fontSmall, 16); - subtitleLabel.color = {0.5f, 0.86f, 1.0f, 0.95f}; - subtitleLabel.centered = true; - m_registry.AddComponent(m_subtitleEntity, std::move(subtitleLabel)); - - const char* menuLabels[] = {"PLAY", "LEVEL EDITOR", "SETTINGS", "QUIT"}; - float startY = 360.0f; - float itemSpacing = 60.0f; - - for (int i = 0; i < 4; i++) { - Entity menuItem = m_registry.CreateEntity(); - m_entities.push_back(menuItem); - m_menuItems.push_back(menuItem); - - m_registry.AddComponent(menuItem, Position{640.0f, startY + i * itemSpacing}); - TextLabel label(menuLabels[i], m_fontMedium, 24); - label.color = {0.7f, 0.7f, 0.7f, 1.0f}; - label.centered = true; - m_registry.AddComponent(menuItem, std::move(label)); - } - - Entity controlsText = m_registry.CreateEntity(); - m_entities.push_back(controlsText); - m_registry.AddComponent(controlsText, Position{640.0f, 620.0f}); - TextLabel controlsLabel("USE ARROWS TO NAVIGATE | ENTER TO SELECT", m_fontSmall, 12); - controlsLabel.color = {0.5f, 0.86f, 1.0f, 0.85f}; - controlsLabel.centered = true; - m_registry.AddComponent(controlsText, std::move(controlsLabel)); - - Entity versionText = m_registry.CreateEntity(); - m_entities.push_back(versionText); - m_registry.AddComponent(versionText, Position{20.0f, 680.0f}); - TextLabel versionLabel("v1.0.0 ALPHA", m_fontSmall, 12); - versionLabel.color = {0.42f, 0.18f, 0.48f, 0.8f}; - m_registry.AddComponent(versionText, std::move(versionLabel)); - } - - void MenuState::updateAnimations(float dt) { - m_animTime += dt; - - m_titlePulse = std::sin(m_animTime * 2.0f) * 0.3f + 1.0f; - - if (m_titleEntity != NULL_ENTITY && m_registry.IsEntityAlive(m_titleEntity)) { - auto& titleLabel = m_registry.GetComponent(m_titleEntity); - titleLabel.color.a = 0.7f + (m_titlePulse - 1.0f); - } - } - - void MenuState::updateMenuSelection() { - for (size_t i = 0; i < m_menuItems.size(); i++) { - if (m_registry.IsEntityAlive(m_menuItems[i])) { - auto& label = m_registry.GetComponent(m_menuItems[i]); - - if (static_cast(i) == m_selectedIndex) { - float pulse = std::sin(m_animTime * 4.0f) * 0.3f + 0.7f; - label.color = {1.0f, 0.08f + pulse * 0.5f, 0.58f, 1.0f}; - } else { - label.color = {0.5f, 0.5f, 0.5f, 0.8f}; - } - } - } - } - - void MenuState::HandleInput() { - if (m_ignoreInputFrames == 0 && m_renderer->IsKeyPressed(Renderer::Key::Escape) && !m_escKeyPressed) { - m_ignoreInputFrames = 1; - } - - if (m_ignoreInputFrames > 0) { - m_upKeyPressed = m_renderer->IsKeyPressed(Renderer::Key::Up); - m_downKeyPressed = m_renderer->IsKeyPressed(Renderer::Key::Down); - m_enterKeyPressed = m_renderer->IsKeyPressed(Renderer::Key::Enter); - m_escKeyPressed = m_renderer->IsKeyPressed(Renderer::Key::Escape); - m_ignoreInputFrames--; - return; - } - - auto playSelectSound = [this]() { - if (m_context.audio && m_selectMusic != Audio::INVALID_MUSIC_ID) { - m_context.audio->StopMusic(m_selectMusic); - Audio::PlaybackOptions opts; - opts.volume = 1.0f; - opts.loop = false; - m_context.audio->PlayMusic(m_selectMusic, opts); - } - }; - - if (m_renderer->IsKeyPressed(Renderer::Key::Up) && !m_upKeyPressed) { - m_upKeyPressed = true; - playSelectSound(); - m_selectedIndex--; - if (m_selectedIndex < 0) { - m_selectedIndex = static_cast(MenuItem::COUNT) - 1; - } - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Up)) { - m_upKeyPressed = false; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Down) && !m_downKeyPressed) { - m_downKeyPressed = true; - playSelectSound(); - m_selectedIndex++; - if (m_selectedIndex >= static_cast(MenuItem::COUNT)) { - m_selectedIndex = 0; - } - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Down)) { - m_downKeyPressed = false; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Enter) && !m_enterKeyPressed) { - m_enterKeyPressed = true; - playSelectSound(); - - switch (static_cast(m_selectedIndex)) { - case MenuItem::PLAY: - std::cout << "[MenuState] Starting game... Transitioning to Room Selection" << std::endl; - std::cout << "[MenuState] Connecting to " << m_context.serverIp << ":" << m_context.serverPort << " as '" << m_context.playerName << "'" << std::endl; - if (m_context.audio && m_menuMusic != Audio::INVALID_MUSIC_ID) { - m_context.audio->StopMusic(m_menuMusic); - m_menuMusicPlaying = false; - } - m_machine.PushState(std::make_unique(m_machine, m_context)); - break; - - case MenuItem::EDITOR: - std::cout << "[MenuState] Opening Level Editor..." << std::endl; - if (m_context.audio && m_menuMusic != Audio::INVALID_MUSIC_ID) { - m_context.audio->StopMusic(m_menuMusic); - m_menuMusicPlaying = false; - } - m_machine.PushState(std::make_unique(m_machine, m_context)); - break; - - case MenuItem::SETTINGS: - std::cout << "[MenuState] Opening Settings..." << std::endl; - m_machine.PushState(std::make_unique(m_machine, m_context)); - break; - - case MenuItem::QUIT: - std::cout << "[MenuState] Quitting..." << std::endl; - m_machine.PopState(); - break; - - default: - break; - } - - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Enter)) { - m_enterKeyPressed = false; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Escape) && !m_escKeyPressed) { - m_escKeyPressed = true; - playSelectSound(); - Core::Logger::Info("[MenuState] Escape pressed. Quitting..."); - m_machine.PopState(); - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Escape)) { - m_escKeyPressed = false; - } - } - - void MenuState::Update(float dt) { - updateAnimations(dt); - updateMenuSelection(); - - if (m_audioSystem && m_menuMusic != Audio::INVALID_MUSIC_ID && !m_menuMusicPlaying) { - auto cmd = m_registry.CreateEntity(); - auto& me = m_registry.AddComponent(cmd, MusicEffect(m_menuMusic)); - me.play = true; - me.stop = false; - me.loop = true; - me.volume = 0.35f; - me.pitch = 1.0f; - m_menuMusicPlaying = true; - } - - if (m_audioSystem) { - m_audioSystem->Update(m_registry, dt); - } - } - - void MenuState::Draw() { - m_renderer->Clear({0.05f, 0.05f, 0.1f, 1.0f}); - - m_renderingSystem->Update(m_registry, 0.0f); - m_textSystem->Update(m_registry, 0.0f); - } - - } -} diff --git a/client/src/ResultsState.cpp b/client/src/ResultsState.cpp deleted file mode 100644 index a9887e2..0000000 --- a/client/src/ResultsState.cpp +++ /dev/null @@ -1,267 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** ResultsState -*/ - -#include "ResultsState.hpp" -#include "RoomListState.hpp" -#include "ECS/Components/TextLabel.hpp" - -#include -#include -#include -#include - -using namespace RType::ECS; - -namespace RType { - namespace Client { - - ResultsState::ResultsState(GameStateMachine& machine, GameContext& context, std::vector> scores) : m_machine(machine), m_context(context), m_scores(std::move(scores)) { - m_renderer = context.renderer; - m_renderingSystem = std::make_unique(m_renderer.get()); - m_textSystem = std::make_unique(m_renderer.get()); - } - - void ResultsState::Init() { - m_fontLarge = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 36); - if (m_fontLarge == Renderer::INVALID_FONT_ID) { - m_fontLarge = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 36); - } - - m_fontMedium = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 18); - if (m_fontMedium == Renderer::INVALID_FONT_ID) { - m_fontMedium = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 18); - } - - m_fontSmall = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 14); - if (m_fontSmall == Renderer::INVALID_FONT_ID) { - m_fontSmall = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 14); - } - - m_bgTexture = m_renderer->LoadTexture("assets/backgrounds/game over/preview.png"); - if (m_bgTexture == Renderer::INVALID_TEXTURE_ID) { - m_bgTexture = m_renderer->LoadTexture("../assets/backgrounds/game over/preview.png"); - } - if (m_bgTexture != Renderer::INVALID_TEXTURE_ID) { - m_bgSprite = m_renderer->CreateSprite(m_bgTexture, {}); - m_bgTextureSize = m_renderer->GetTextureSize(m_bgTexture); - } else { - m_bgTexture = m_renderer->LoadTexture("assets/backgrounds/1.jpg"); - if (m_bgTexture == Renderer::INVALID_TEXTURE_ID) { - m_bgTexture = m_renderer->LoadTexture("../assets/backgrounds/1.jpg"); - } - if (m_bgTexture != Renderer::INVALID_TEXTURE_ID) { - m_bgSprite = m_renderer->CreateSprite(m_bgTexture, {}); - m_bgTextureSize = m_renderer->GetTextureSize(m_bgTexture); - } - } - m_escapePressed = m_renderer->IsKeyPressed(Renderer::Key::Escape); - - if (m_context.audio) { - m_endMusic = m_context.audio->LoadMusic("assets/sounds/end.flac"); - if (m_endMusic == Audio::INVALID_MUSIC_ID) { - m_endMusic = m_context.audio->LoadMusic("../assets/sounds/end.flac"); - } - if (m_endMusic != Audio::INVALID_MUSIC_ID) { - Audio::PlaybackOptions opts; - opts.loop = true; - opts.volume = 0.5f; - m_context.audio->PlayMusic(m_endMusic, opts); - } - } - - std::sort(m_scores.begin(), m_scores.end(), [](const auto& a, const auto& b) { return a.second > b.second; }); - - createUI(); - } - - void ResultsState::Cleanup() { - if (m_context.audio && m_endMusic != Audio::INVALID_MUSIC_ID) { - m_context.audio->StopMusic(m_endMusic); - m_context.audio->UnloadMusic(m_endMusic); - m_endMusic = Audio::INVALID_MUSIC_ID; - } - - auto entities = m_registry.GetEntitiesWithComponent(); - for (auto e : entities) { - if (m_registry.IsEntityAlive(e)) { - m_registry.DestroyEntity(e); - } - } - } - - void ResultsState::HandleInput() { - if (m_renderer->IsKeyPressed(Renderer::Key::Escape) && !m_escapePressed) { - m_escapePressed = true; - if (m_context.audio && m_endMusic != Audio::INVALID_MUSIC_ID) { - m_context.audio->StopMusic(m_endMusic); - } - m_machine.ChangeState(std::make_unique(m_machine, m_context)); - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Escape)) { - m_escapePressed = false; - } - } - - void ResultsState::Update(float dt) { - (void)dt; - } - - void ResultsState::Draw() { - m_renderer->Clear({0.05f, 0.05f, 0.12f, 1.0f}); - m_renderingSystem->Update(m_registry, 0.0f); - - if (m_drawScorePanel) { - Renderer::Rectangle border = m_scorePanelRect; - border.position.x -= 4.0f; - border.position.y -= 4.0f; - border.size.x += 8.0f; - border.size.y += 8.0f; - m_renderer->DrawRectangle(border, Renderer::Color(0.0f, 0.0f, 0.0f, 0.90f)); - m_renderer->DrawRectangle(m_scorePanelRect, Renderer::Color(0.0f, 0.0f, 0.0f, 0.62f)); - Renderer::Rectangle headerRect = m_scorePanelRect; - headerRect.size.y = m_scorePanelHeaderHeight; - m_renderer->DrawRectangle(headerRect, Renderer::Color(0.0f, 0.0f, 0.0f, 0.72f)); - Renderer::Rectangle sep = m_scorePanelRect; - sep.position.y += m_scorePanelHeaderHeight; - sep.size.y = 2.0f; - m_renderer->DrawRectangle(sep, Renderer::Color(0.20f, 0.60f, 1.00f, 0.45f)); - - for (size_t i = 0; i < m_scorePanelRowCount; ++i) { - Renderer::Rectangle rowRect; - rowRect.position = Renderer::Vector2(m_scorePanelRect.position.x + 12.0f, m_scorePanelRowStartY + static_cast(i) * m_scorePanelRowStepY - 16.0f); - rowRect.size = Renderer::Vector2(m_scorePanelRect.size.x - 24.0f, m_scorePanelRowStepY); - float a = (i % 2 == 0) ? 0.10f : 0.06f; - m_renderer->DrawRectangle(rowRect, Renderer::Color(0.02f, 0.08f, 0.18f, a)); - } - - Renderer::Rectangle hintStrip; - hintStrip.position = Renderer::Vector2(0.0f, 630.0f); - hintStrip.size = Renderer::Vector2(1280.0f, 60.0f); - m_renderer->DrawRectangle(hintStrip, Renderer::Color(0.0f, 0.0f, 0.0f, 0.30f)); - } - - m_textSystem->Update(m_registry, 0.0f); - } - - void ResultsState::createUI() { - if (m_bgSprite != Renderer::INVALID_SPRITE_ID) { - m_bgEntity = m_registry.CreateEntity(); - m_registry.AddComponent(m_bgEntity, Position{0.0f, 0.0f}); - - Drawable drawable(m_bgSprite, -10); - - if (m_bgTextureSize.x > 0 && m_bgTextureSize.y > 0) { - drawable.scale = {1280.0f / m_bgTextureSize.x, 720.0f / m_bgTextureSize.y}; - } - - m_registry.AddComponent(m_bgEntity, std::move(drawable)); - } - - if (m_fontLarge == Renderer::INVALID_FONT_ID) { - return; - } - - const float screenW = 1280.0f; - const float panelW = 920.0f; - const float panelH = 430.0f; - const float panelX = (screenW - panelW) * 0.5f; - const float panelY = 185.0f; - - m_drawScorePanel = true; - m_scorePanelRect.position = Renderer::Vector2(panelX, panelY); - m_scorePanelRect.size = Renderer::Vector2(panelW, panelH); - m_scorePanelRowStepY = 36.0f; - m_scorePanelRowStartY = panelY + m_scorePanelHeaderHeight + 46.0f; - - m_colRankX = panelX + 70.0f; - m_colNameX = panelX + 170.0f; - m_colScoreX = panelX + panelW - 130.0f; - - Entity titleEntity = m_registry.CreateEntity(); - m_registry.AddComponent(titleEntity, Position{640.0f, 90.0f}); - TextLabel titleLabel("RESULTS", m_fontLarge, 40); - titleLabel.centered = true; - titleLabel.color = {0.10f, 0.45f, 1.00f, 1.0f}; - m_registry.AddComponent(titleEntity, std::move(titleLabel)); - - Entity subtitleEntity = m_registry.CreateEntity(); - m_registry.AddComponent(subtitleEntity, Position{640.0f, 140.0f}); - TextLabel subtitle("Final scores", m_fontSmall != Renderer::INVALID_FONT_ID ? m_fontSmall : m_fontMedium, 16); - subtitle.centered = true; - subtitle.color = {0.20f, 0.60f, 1.00f, 0.95f}; - m_registry.AddComponent(subtitleEntity, std::move(subtitle)); - - { - Entity headerRank = m_registry.CreateEntity(); - m_registry.AddComponent(headerRank, Position{m_colRankX, panelY + 26.0f}); - TextLabel hRank("RANK", m_fontSmall != Renderer::INVALID_FONT_ID ? m_fontSmall : m_fontMedium, 14); - hRank.centered = true; - hRank.color = {0.75f, 0.88f, 1.00f, 0.95f}; - m_registry.AddComponent(headerRank, std::move(hRank)); - - Entity headerName = m_registry.CreateEntity(); - m_registry.AddComponent(headerName, Position{m_colNameX, panelY + 26.0f}); - TextLabel hName("NAME", m_fontSmall != Renderer::INVALID_FONT_ID ? m_fontSmall : m_fontMedium, 14); - hName.centered = false; - hName.color = {0.75f, 0.88f, 1.00f, 0.95f}; - m_registry.AddComponent(headerName, std::move(hName)); - - Entity headerScore = m_registry.CreateEntity(); - m_registry.AddComponent(headerScore, Position{m_colScoreX, panelY + 26.0f}); - TextLabel hScore("SCORE", m_fontSmall != Renderer::INVALID_FONT_ID ? m_fontSmall : m_fontMedium, 14); - hScore.centered = true; - hScore.color = {0.75f, 0.88f, 1.00f, 0.95f}; - m_registry.AddComponent(headerScore, std::move(hScore)); - } - - size_t maxLines = std::min(m_scores.size(), 8); - m_scorePanelRowCount = maxLines; - - for (size_t i = 0; i < maxLines; i++) { - const auto& [name, score] = m_scores[i]; - - const float y = m_scorePanelRowStartY + static_cast(i) * m_scorePanelRowStepY; - - Entity rankEnt = m_registry.CreateEntity(); - m_registry.AddComponent(rankEnt, Position{m_colRankX, y}); - TextLabel rankLabel(std::to_string(i + 1), m_fontMedium != Renderer::INVALID_FONT_ID ? m_fontMedium : m_fontLarge, 18); - rankLabel.centered = true; - rankLabel.color = {0.75f, 0.88f, 1.00f, 1.0f}; - m_registry.AddComponent(rankEnt, std::move(rankLabel)); - - Entity nameEnt = m_registry.CreateEntity(); - m_registry.AddComponent(nameEnt, Position{m_colNameX, y}); - std::string displayName = name; - if (displayName.size() > 18) { - displayName = displayName.substr(0, 18); - } - TextLabel nameLabel(displayName, m_fontMedium != Renderer::INVALID_FONT_ID ? m_fontMedium : m_fontLarge, 18); - nameLabel.centered = false; - nameLabel.color = {0.90f, 0.95f, 1.00f, 1.0f}; - m_registry.AddComponent(nameEnt, std::move(nameLabel)); - - Entity scoreEnt = m_registry.CreateEntity(); - m_registry.AddComponent(scoreEnt, Position{m_colScoreX, y}); - std::ostringstream ss; - ss << std::setw(8) << std::setfill('0') << score; - TextLabel scoreLabel(ss.str(), m_fontMedium != Renderer::INVALID_FONT_ID ? m_fontMedium : m_fontLarge, 18); - scoreLabel.centered = true; - scoreLabel.color = {0.75f, 0.88f, 1.00f, 1.0f}; - m_registry.AddComponent(scoreEnt, std::move(scoreLabel)); - } - - Entity hint = m_registry.CreateEntity(); - m_registry.AddComponent(hint, Position{640.0f, 650.0f}); - TextLabel hintLabel("Press ESC to return to room selection", m_fontSmall != Renderer::INVALID_FONT_ID ? m_fontSmall : m_fontMedium, 14); - hintLabel.centered = true; - hintLabel.color = {0.20f, 0.60f, 1.00f, 0.95f}; - m_registry.AddComponent(hint, std::move(hintLabel)); - } - - } -} - - diff --git a/client/src/SettingsState.cpp b/client/src/SettingsState.cpp deleted file mode 100644 index f107d81..0000000 --- a/client/src/SettingsState.cpp +++ /dev/null @@ -1,1365 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** SettingsState -*/ - -#include "SettingsState.hpp" -#include "ECS/Components/TextLabel.hpp" -#include "ECS/Component.hpp" -#include "Core/Logger.hpp" -#include "Core/InputMapping.hpp" -#include -#include -#include -#include - -using namespace RType::ECS; -using json = nlohmann::json; - -namespace { - const std::unordered_map KEY_TO_STRING = { - {Renderer::Key::A, "A"}, {Renderer::Key::B, "B"}, {Renderer::Key::C, "C"}, - {Renderer::Key::D, "D"}, {Renderer::Key::E, "E"}, {Renderer::Key::F, "F"}, - {Renderer::Key::G, "G"}, {Renderer::Key::H, "H"}, {Renderer::Key::I, "I"}, - {Renderer::Key::J, "J"}, {Renderer::Key::K, "K"}, {Renderer::Key::L, "L"}, - {Renderer::Key::M, "M"}, {Renderer::Key::N, "N"}, {Renderer::Key::O, "O"}, - {Renderer::Key::P, "P"}, {Renderer::Key::Q, "Q"}, {Renderer::Key::R, "R"}, - {Renderer::Key::S, "S"}, {Renderer::Key::T, "T"}, {Renderer::Key::U, "U"}, - {Renderer::Key::V, "V"}, {Renderer::Key::W, "W"}, {Renderer::Key::X, "X"}, - {Renderer::Key::Y, "Y"}, {Renderer::Key::Z, "Z"}, - {Renderer::Key::Num0, "0"}, {Renderer::Key::Num1, "1"}, {Renderer::Key::Num2, "2"}, - {Renderer::Key::Num3, "3"}, {Renderer::Key::Num4, "4"}, {Renderer::Key::Num5, "5"}, - {Renderer::Key::Num6, "6"}, {Renderer::Key::Num7, "7"}, {Renderer::Key::Num8, "8"}, - {Renderer::Key::Num9, "9"}, - {Renderer::Key::Space, "SPACE"}, {Renderer::Key::Enter, "ENTER"}, - {Renderer::Key::Escape, "ESC"}, {Renderer::Key::Tab, "TAB"}, - {Renderer::Key::Left, "LEFT"}, {Renderer::Key::Right, "RIGHT"}, - {Renderer::Key::Up, "UP"}, {Renderer::Key::Down, "DOWN"}, - {Renderer::Key::LShift, "LSHIFT"}, {Renderer::Key::RShift, "RSHIFT"}, - {Renderer::Key::LControl, "LCTRL"}, {Renderer::Key::RControl, "RCTRL"}, - {Renderer::Key::LAlt, "LALT"}, {Renderer::Key::RAlt, "RALT"} - }; - - const std::unordered_map STRING_TO_KEY = { - {"A", Renderer::Key::A}, {"B", Renderer::Key::B}, {"C", Renderer::Key::C}, - {"D", Renderer::Key::D}, {"E", Renderer::Key::E}, {"F", Renderer::Key::F}, - {"G", Renderer::Key::G}, {"H", Renderer::Key::H}, {"I", Renderer::Key::I}, - {"J", Renderer::Key::J}, {"K", Renderer::Key::K}, {"L", Renderer::Key::L}, - {"M", Renderer::Key::M}, {"N", Renderer::Key::N}, {"O", Renderer::Key::O}, - {"P", Renderer::Key::P}, {"Q", Renderer::Key::Q}, {"R", Renderer::Key::R}, - {"S", Renderer::Key::S}, {"T", Renderer::Key::T}, {"U", Renderer::Key::U}, - {"V", Renderer::Key::V}, {"W", Renderer::Key::W}, {"X", Renderer::Key::X}, - {"Y", Renderer::Key::Y}, {"Z", Renderer::Key::Z}, - {"0", Renderer::Key::Num0}, {"1", Renderer::Key::Num1}, {"2", Renderer::Key::Num2}, - {"3", Renderer::Key::Num3}, {"4", Renderer::Key::Num4}, {"5", Renderer::Key::Num5}, - {"6", Renderer::Key::Num6}, {"7", Renderer::Key::Num7}, {"8", Renderer::Key::Num8}, - {"9", Renderer::Key::Num9}, - {"SPACE", Renderer::Key::Space}, {"ENTER", Renderer::Key::Enter}, - {"ESC", Renderer::Key::Escape}, {"TAB", Renderer::Key::Tab}, - {"LEFT", Renderer::Key::Left}, {"RIGHT", Renderer::Key::Right}, - {"UP", Renderer::Key::Up}, {"DOWN", Renderer::Key::Down}, - {"LSHIFT", Renderer::Key::LShift}, {"RSHIFT", Renderer::Key::RShift}, - {"LCTRL", Renderer::Key::LControl}, {"RCTRL", Renderer::Key::RControl}, - {"LALT", Renderer::Key::LAlt}, {"RALT", Renderer::Key::RAlt} - }; - - const std::vector REBINDABLE_KEYS = { - Renderer::Key::A, Renderer::Key::B, Renderer::Key::C, Renderer::Key::D, Renderer::Key::E, - Renderer::Key::F, Renderer::Key::G, Renderer::Key::H, Renderer::Key::I, Renderer::Key::J, - Renderer::Key::K, Renderer::Key::L, Renderer::Key::M, Renderer::Key::N, Renderer::Key::O, - Renderer::Key::P, Renderer::Key::Q, Renderer::Key::R, Renderer::Key::S, Renderer::Key::T, - Renderer::Key::U, Renderer::Key::V, Renderer::Key::W, Renderer::Key::X, Renderer::Key::Y, - Renderer::Key::Z, - Renderer::Key::Num0, Renderer::Key::Num1, Renderer::Key::Num2, Renderer::Key::Num3, - Renderer::Key::Num4, Renderer::Key::Num5, Renderer::Key::Num6, Renderer::Key::Num7, - Renderer::Key::Num8, Renderer::Key::Num9, - Renderer::Key::Space, Renderer::Key::Enter, Renderer::Key::Escape, Renderer::Key::Tab, - Renderer::Key::Left, Renderer::Key::Right, Renderer::Key::Up, Renderer::Key::Down, - Renderer::Key::LShift, Renderer::Key::RShift, Renderer::Key::LControl, Renderer::Key::RControl, - Renderer::Key::LAlt, Renderer::Key::RAlt - }; - - const std::vector ACTION_KEYS = {"MOVE_UP", "MOVE_DOWN", "MOVE_LEFT", "MOVE_RIGHT", "SHOOT"}; -} - -namespace RType { - namespace Client { - - const std::string SettingsState::SETTINGS_FILE_PATH = "settings.json"; - bool SettingsState::s_colourBlindMode = false; - std::uint32_t SettingsState::s_screenWidth = 1280; - std::uint32_t SettingsState::s_screenHeight = 720; - bool SettingsState::s_fullscreen = false; - std::uint32_t SettingsState::s_targetFramerate = 60; - float SettingsState::s_masterVolume = 1.0f; - bool SettingsState::s_muted = false; - - const std::vector SettingsState::RESOLUTIONS = { - {1280, 720, "1280x720"}, - {1920, 1080, "1920x1080"}, - {2560, 1440, "2560x1440"}, - {3840, 2160, "3840x2160"} - }; - - const std::vector SettingsState::FRAMERATES = {30, 60, 120, 0}; - const std::vector SettingsState::FRAMERATE_NAMES = {"30", "60", "120", "Unlimited"}; - - SettingsState::SettingsState(GameStateMachine& machine, GameContext& context) - : m_machine(machine), m_context(context) { - m_renderer = context.renderer; - m_renderingSystem = std::make_unique(m_renderer.get()); - m_textSystem = std::make_unique(m_renderer.get()); - } - - void SettingsState::Init() { - std::cout << "[SettingsState] Initializing..." << std::endl; - - if (m_context.audio) { - m_audioSystem = std::make_unique(m_context.audio.get()); - m_selectMusic = m_context.audio->LoadMusic("assets/sounds/select.flac"); - if (m_selectMusic == Audio::INVALID_MUSIC_ID) { - m_selectMusic = m_context.audio->LoadMusic("../assets/sounds/select.flac"); - } - } - - m_fontLarge = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 48); - if (m_fontLarge == Renderer::INVALID_FONT_ID) { - m_fontLarge = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 48); - } - - m_fontMedium = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 24); - if (m_fontMedium == Renderer::INVALID_FONT_ID) { - m_fontMedium = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 24); - } - - m_fontSmall = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 16); - if (m_fontSmall == Renderer::INVALID_FONT_ID) { - m_fontSmall = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 16); - } - - m_bgTexture = m_renderer->LoadTexture("assets/backgrounds/1.jpg"); - if (m_bgTexture == Renderer::INVALID_TEXTURE_ID) { - m_bgTexture = m_renderer->LoadTexture("../assets/backgrounds/1.jpg"); - } - - loadSettings(); - createUI(); - } - - void SettingsState::Cleanup() { - std::cout << "[SettingsState] Cleaning up..." << std::endl; - - if (m_context.audio && m_selectMusic != Audio::INVALID_MUSIC_ID) { - m_context.audio->StopMusic(m_selectMusic); - m_context.audio->UnloadMusic(m_selectMusic); - m_selectMusic = Audio::INVALID_MUSIC_ID; - } - - for (Entity entity : m_entities) { - if (m_registry.IsEntityAlive(entity)) { - m_registry.DestroyEntity(entity); - } - } - m_entities.clear(); - } - - void SettingsState::createUI() { - m_menuItems.clear(); - - if (m_bgTexture != Renderer::INVALID_TEXTURE_ID) { - Entity bg = m_registry.CreateEntity(); - m_entities.push_back(bg); - m_registry.AddComponent(bg, Position{0.0f, 0.0f}); - - Renderer::SpriteId spriteId = m_renderer->CreateSprite(m_bgTexture, {}); - Drawable drawable(spriteId, -10); - - Renderer::Vector2 texSize = m_renderer->GetTextureSize(m_bgTexture); - if (texSize.x > 0 && texSize.y > 0) { - drawable.scale = {1280.0f / texSize.x, 720.0f / texSize.y}; - } - - m_registry.AddComponent(bg, std::move(drawable)); - } - - if (m_fontLarge == Renderer::INVALID_FONT_ID) - return; - - m_titleEntity = m_registry.CreateEntity(); - m_entities.push_back(m_titleEntity); - m_registry.AddComponent(m_titleEntity, Position{640.0f, 150.0f}); - TextLabel titleLabel("SETTINGS", m_fontLarge, 72); - titleLabel.color = {0.0f, 1.0f, 1.0f, 1.0f}; - titleLabel.centered = true; - m_registry.AddComponent(m_titleEntity, std::move(titleLabel)); - - float startY = 250.0f; - float itemSpacing = 70.0f; - - Entity colourBlindItem = m_registry.CreateEntity(); - m_entities.push_back(colourBlindItem); - m_menuItems.push_back(colourBlindItem); - m_registry.AddComponent(colourBlindItem, Position{640.0f, startY}); - std::string colourBlindText = "COLOUR-BLIND MODE: " + std::string(m_colourBlindMode ? "ON" : "OFF"); - TextLabel colourBlindLabel(colourBlindText, m_fontMedium, 24); - colourBlindLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; - colourBlindLabel.centered = true; - m_registry.AddComponent(colourBlindItem, std::move(colourBlindLabel)); - - Entity screenItem = m_registry.CreateEntity(); - m_entities.push_back(screenItem); - m_menuItems.push_back(screenItem); - m_registry.AddComponent(screenItem, Position{640.0f, startY + itemSpacing}); - TextLabel screenLabel("SCREEN", m_fontMedium, 24); - screenLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; - screenLabel.centered = true; - m_registry.AddComponent(screenItem, std::move(screenLabel)); - - Entity audioItem = m_registry.CreateEntity(); - m_entities.push_back(audioItem); - m_menuItems.push_back(audioItem); - m_registry.AddComponent(audioItem, Position{640.0f, startY + itemSpacing * 2}); - TextLabel audioLabel("AUDIO", m_fontMedium, 24); - audioLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; - audioLabel.centered = true; - m_registry.AddComponent(audioItem, std::move(audioLabel)); - - Entity commandsItem = m_registry.CreateEntity(); - m_entities.push_back(commandsItem); - m_menuItems.push_back(commandsItem); - m_registry.AddComponent(commandsItem, Position{640.0f, startY + itemSpacing * 3}); - TextLabel commandsLabel("COMMANDS", m_fontMedium, 24); - commandsLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; - commandsLabel.centered = true; - m_registry.AddComponent(commandsItem, std::move(commandsLabel)); - - Entity backItem = m_registry.CreateEntity(); - m_entities.push_back(backItem); - m_menuItems.push_back(backItem); - m_registry.AddComponent(backItem, Position{640.0f, startY + itemSpacing * 4}); - TextLabel backLabel("BACK", m_fontMedium, 24); - backLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; - backLabel.centered = true; - m_registry.AddComponent(backItem, std::move(backLabel)); - - Entity controlsText = m_registry.CreateEntity(); - m_entities.push_back(controlsText); - m_registry.AddComponent(controlsText, Position{640.0f, 600.0f}); - TextLabel controlsLabel("USE ARROWS TO NAVIGATE | ENTER TO SELECT | ESC TO GO BACK", m_fontSmall, 12); - controlsLabel.color = {0.5f, 0.86f, 1.0f, 0.85f}; - controlsLabel.centered = true; - m_registry.AddComponent(controlsText, std::move(controlsLabel)); - } - - void SettingsState::createScreenUI() { - for (Entity entity : m_screenMenuItems) { - if (m_registry.IsEntityAlive(entity)) { - m_registry.DestroyEntity(entity); - } - } - m_screenMenuItems.clear(); - - if (m_fontLarge == Renderer::INVALID_FONT_ID) - return; - - Entity titleEntity = m_registry.CreateEntity(); - m_entities.push_back(titleEntity); - m_screenMenuItems.push_back(titleEntity); - m_registry.AddComponent(titleEntity, Position{640.0f, 150.0f}); - TextLabel titleLabel("SCREEN SETTINGS", m_fontLarge, 72); - titleLabel.color = {0.0f, 1.0f, 1.0f, 1.0f}; - titleLabel.centered = true; - m_registry.AddComponent(titleEntity, std::move(titleLabel)); - - float startY = 280.0f; - float itemSpacing = 70.0f; - - Entity resolutionItem = m_registry.CreateEntity(); - m_entities.push_back(resolutionItem); - m_screenMenuItems.push_back(resolutionItem); - m_registry.AddComponent(resolutionItem, Position{640.0f, startY}); - std::string resolutionText = "RESOLUTION: " + RESOLUTIONS[m_resolutionIndex].name; - TextLabel resolutionLabel(resolutionText, m_fontMedium, 24); - resolutionLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; - resolutionLabel.centered = true; - m_registry.AddComponent(resolutionItem, std::move(resolutionLabel)); - - Entity fullscreenItem = m_registry.CreateEntity(); - m_entities.push_back(fullscreenItem); - m_screenMenuItems.push_back(fullscreenItem); - m_registry.AddComponent(fullscreenItem, Position{640.0f, startY + itemSpacing}); - std::string fullscreenText = "FULLSCREEN: " + std::string(m_fullscreen ? "ON" : "OFF"); - TextLabel fullscreenLabel(fullscreenText, m_fontMedium, 24); - fullscreenLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; - fullscreenLabel.centered = true; - m_registry.AddComponent(fullscreenItem, std::move(fullscreenLabel)); - - Entity framerateItem = m_registry.CreateEntity(); - m_entities.push_back(framerateItem); - m_screenMenuItems.push_back(framerateItem); - m_registry.AddComponent(framerateItem, Position{640.0f, startY + itemSpacing * 2}); - std::string framerateText = "FRAME RATE: " + FRAMERATE_NAMES[m_framerateIndex]; - TextLabel framerateLabel(framerateText, m_fontMedium, 24); - framerateLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; - framerateLabel.centered = true; - m_registry.AddComponent(framerateItem, std::move(framerateLabel)); - - Entity backItem = m_registry.CreateEntity(); - m_entities.push_back(backItem); - m_screenMenuItems.push_back(backItem); - m_registry.AddComponent(backItem, Position{640.0f, startY + itemSpacing * 3}); - TextLabel backLabel("BACK", m_fontMedium, 24); - backLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; - backLabel.centered = true; - m_registry.AddComponent(backItem, std::move(backLabel)); - - Entity controlsText = m_registry.CreateEntity(); - m_entities.push_back(controlsText); - m_screenMenuItems.push_back(controlsText); - m_registry.AddComponent(controlsText, Position{640.0f, 600.0f}); - TextLabel controlsLabel("UP/DOWN: NAVIGATE | LEFT/RIGHT: CHANGE VALUE | ESC: BACK", m_fontSmall, 12); - controlsLabel.color = {0.5f, 0.86f, 1.0f, 0.85f}; - controlsLabel.centered = true; - m_registry.AddComponent(controlsText, std::move(controlsLabel)); - } - - void SettingsState::createAudioUI() { - for (Entity entity : m_audioMenuItems) { - if (m_registry.IsEntityAlive(entity)) { - m_registry.DestroyEntity(entity); - } - } - m_audioMenuItems.clear(); - - if (m_fontLarge == Renderer::INVALID_FONT_ID) - return; - - Entity titleEntity = m_registry.CreateEntity(); - m_entities.push_back(titleEntity); - m_audioMenuItems.push_back(titleEntity); - m_registry.AddComponent(titleEntity, Position{640.0f, 150.0f}); - TextLabel titleLabel("AUDIO SETTINGS", m_fontLarge, 72); - titleLabel.color = {0.0f, 1.0f, 1.0f, 1.0f}; - titleLabel.centered = true; - m_registry.AddComponent(titleEntity, std::move(titleLabel)); - - float startY = 280.0f; - float itemSpacing = 70.0f; - - Entity volumeItem = m_registry.CreateEntity(); - m_entities.push_back(volumeItem); - m_audioMenuItems.push_back(volumeItem); - m_registry.AddComponent(volumeItem, Position{640.0f, startY}); - int volumePercent = static_cast(std::round(m_masterVolume * 100.0f)); - volumePercent = (volumePercent / 5) * 5; - std::string volumeText = "MASTER VOLUME: " + std::to_string(volumePercent) + "%"; - TextLabel volumeLabel(volumeText, m_fontMedium, 24); - volumeLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; - volumeLabel.centered = true; - m_registry.AddComponent(volumeItem, std::move(volumeLabel)); - - Entity muteItem = m_registry.CreateEntity(); - m_entities.push_back(muteItem); - m_audioMenuItems.push_back(muteItem); - m_registry.AddComponent(muteItem, Position{640.0f, startY + itemSpacing}); - std::string muteText = "MUTE: " + std::string(m_muted ? "ON" : "OFF"); - TextLabel muteLabel(muteText, m_fontMedium, 24); - muteLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; - muteLabel.centered = true; - m_registry.AddComponent(muteItem, std::move(muteLabel)); - - Entity backItem = m_registry.CreateEntity(); - m_entities.push_back(backItem); - m_audioMenuItems.push_back(backItem); - m_registry.AddComponent(backItem, Position{640.0f, startY + itemSpacing * 2}); - TextLabel backLabel("BACK", m_fontMedium, 24); - backLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; - backLabel.centered = true; - m_registry.AddComponent(backItem, std::move(backLabel)); - - Entity controlsText = m_registry.CreateEntity(); - m_entities.push_back(controlsText); - m_audioMenuItems.push_back(controlsText); - m_registry.AddComponent(controlsText, Position{640.0f, 600.0f}); - TextLabel controlsLabel("UP/DOWN: NAVIGATE | LEFT/RIGHT: CHANGE VALUE | ESC: BACK", m_fontSmall, 12); - controlsLabel.color = {0.5f, 0.86f, 1.0f, 0.85f}; - controlsLabel.centered = true; - m_registry.AddComponent(controlsText, std::move(controlsLabel)); - } - - void SettingsState::createCommandsUI() { - for (Entity entity : m_commandsMenuItems) { - if (m_registry.IsEntityAlive(entity)) { - m_registry.DestroyEntity(entity); - } - } - m_commandsMenuItems.clear(); - - if (m_fontLarge == Renderer::INVALID_FONT_ID) - return; - - Entity titleEntity = m_registry.CreateEntity(); - m_entities.push_back(titleEntity); - m_commandsMenuItems.push_back(titleEntity); - m_registry.AddComponent(titleEntity, Position{640.0f, 150.0f}); - TextLabel titleLabel("COMMANDS", m_fontLarge, 72); - titleLabel.color = {0.0f, 1.0f, 1.0f, 1.0f}; - titleLabel.centered = true; - m_registry.AddComponent(titleEntity, std::move(titleLabel)); - - float startY = 260.0f; - float itemSpacing = 55.0f; - - std::vector actionNames = {"MOVE UP", "MOVE DOWN", "MOVE LEFT", "MOVE RIGHT", "SHOOT"}; - - for (size_t i = 0; i < actionNames.size(); i++) { - Entity actionItem = m_registry.CreateEntity(); - m_entities.push_back(actionItem); - m_commandsMenuItems.push_back(actionItem); - m_registry.AddComponent(actionItem, Position{640.0f, startY + itemSpacing * i}); - - Renderer::Key currentKey = Core::InputMapping::GetKey(ACTION_KEYS[i]); - std::string keyName = keyToString(currentKey); - std::string actionText = actionNames[i] + ": " + keyName; - if (m_waitingForRebind && m_rebindingActionIndex == static_cast(i)) { - actionText = actionNames[i] + ": PRESS A KEY..."; - } - - TextLabel actionLabel(actionText, m_fontMedium, 24); - actionLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; - actionLabel.centered = true; - m_registry.AddComponent(actionItem, std::move(actionLabel)); - } - - Entity backItem = m_registry.CreateEntity(); - m_entities.push_back(backItem); - m_commandsMenuItems.push_back(backItem); - m_registry.AddComponent(backItem, Position{640.0f, startY + itemSpacing * 5}); - TextLabel backLabel("BACK", m_fontMedium, 24); - backLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; - backLabel.centered = true; - m_registry.AddComponent(backItem, std::move(backLabel)); - - Entity controlsText = m_registry.CreateEntity(); - m_entities.push_back(controlsText); - m_commandsMenuItems.push_back(controlsText); - m_registry.AddComponent(controlsText, Position{640.0f, 600.0f}); - std::string controlsTextStr = m_waitingForRebind ? - "PRESS ANY KEY TO REBIND | ESC TO CANCEL" : - "UP/DOWN: NAVIGATE | ENTER: REBIND | ESC: BACK"; - TextLabel controlsLabel(controlsTextStr, m_fontSmall, 12); - controlsLabel.color = {0.5f, 0.86f, 1.0f, 0.85f}; - controlsLabel.centered = true; - m_registry.AddComponent(controlsText, std::move(controlsLabel)); - } - - void SettingsState::updateAnimations(float dt) { - m_animTime += dt; - } - - void SettingsState::updateMenuSelection() { - if (m_inScreenMenu) { - updateScreenMenuSelection(); - return; - } - if (m_inAudioMenu) { - updateAudioMenuSelection(); - return; - } - if (m_inCommandsMenu) { - updateCommandsMenuSelection(); - return; - } - - for (size_t i = 0; i < m_menuItems.size(); i++) { - if (m_registry.IsEntityAlive(m_menuItems[i])) { - auto& label = m_registry.GetComponent(m_menuItems[i]); - - if (static_cast(i) == m_selectedIndex) { - float pulse = std::sin(m_animTime * 4.0f) * 0.3f + 0.7f; - label.color = {1.0f, 0.08f + pulse * 0.5f, 0.58f, 1.0f}; - } else { - label.color = {0.7f, 0.7f, 0.7f, 1.0f}; - } - } - } - - if (m_menuItems.size() > 0 && m_registry.IsEntityAlive(m_menuItems[0])) { - auto& label = m_registry.GetComponent(m_menuItems[0]); - std::string colourBlindText = "COLOUR-BLIND MODE: " + std::string(m_colourBlindMode ? "ON" : "OFF"); - label.text = colourBlindText; - } - } - - void SettingsState::updateScreenMenuSelection() { - for (size_t i = 0; i < m_screenMenuItems.size(); i++) { - if (m_registry.IsEntityAlive(m_screenMenuItems[i])) { - auto& label = m_registry.GetComponent(m_screenMenuItems[i]); - - if (i == 0) { - continue; - } - - int itemIndex = static_cast(i) - 1; - if (itemIndex == m_screenSelectedIndex) { - float pulse = std::sin(m_animTime * 4.0f) * 0.3f + 0.7f; - label.color = {1.0f, 0.08f + pulse * 0.5f, 0.58f, 1.0f}; - } else { - label.color = {0.7f, 0.7f, 0.7f, 1.0f}; - } - } - } - - if (m_screenMenuItems.size() > 1 && m_registry.IsEntityAlive(m_screenMenuItems[1])) { - auto& label = m_registry.GetComponent(m_screenMenuItems[1]); - std::string resolutionText = "RESOLUTION: " + RESOLUTIONS[m_resolutionIndex].name; - label.text = resolutionText; - } - - if (m_screenMenuItems.size() > 2 && m_registry.IsEntityAlive(m_screenMenuItems[2])) { - auto& label = m_registry.GetComponent(m_screenMenuItems[2]); - std::string fullscreenText = "FULLSCREEN: " + std::string(m_fullscreen ? "ON" : "OFF"); - label.text = fullscreenText; - } - - if (m_screenMenuItems.size() > 3 && m_registry.IsEntityAlive(m_screenMenuItems[3])) { - auto& label = m_registry.GetComponent(m_screenMenuItems[3]); - std::string framerateText = "FRAME RATE: " + FRAMERATE_NAMES[m_framerateIndex]; - label.text = framerateText; - } - } - - void SettingsState::updateAudioMenuSelection() { - for (size_t i = 0; i < m_audioMenuItems.size(); i++) { - if (m_registry.IsEntityAlive(m_audioMenuItems[i])) { - auto& label = m_registry.GetComponent(m_audioMenuItems[i]); - - if (i == 0) { - continue; - } - - int itemIndex = static_cast(i) - 1; - if (itemIndex == m_audioSelectedIndex) { - float pulse = std::sin(m_animTime * 4.0f) * 0.3f + 0.7f; - label.color = {1.0f, 0.08f + pulse * 0.5f, 0.58f, 1.0f}; - } else { - label.color = {0.7f, 0.7f, 0.7f, 1.0f}; - } - } - } - - if (m_audioMenuItems.size() > 1 && m_registry.IsEntityAlive(m_audioMenuItems[1])) { - auto& label = m_registry.GetComponent(m_audioMenuItems[1]); - int volumePercent = static_cast(std::round(m_masterVolume * 100.0f)); - volumePercent = (volumePercent / 5) * 5; - std::string volumeText = "MASTER VOLUME: " + std::to_string(volumePercent) + "%"; - label.text = volumeText; - } - - if (m_audioMenuItems.size() > 2 && m_registry.IsEntityAlive(m_audioMenuItems[2])) { - auto& label = m_registry.GetComponent(m_audioMenuItems[2]); - std::string muteText = "MUTE: " + std::string(m_muted ? "ON" : "OFF"); - label.text = muteText; - } - } - - void SettingsState::updateCommandsMenuSelection() { - std::vector actionNames = {"MOVE UP", "MOVE DOWN", "MOVE LEFT", "MOVE RIGHT", "SHOOT"}; - - for (size_t i = 0; i < m_commandsMenuItems.size(); i++) { - if (m_registry.IsEntityAlive(m_commandsMenuItems[i])) { - auto& label = m_registry.GetComponent(m_commandsMenuItems[i]); - - if (i == 0) { - continue; - } - - int itemIndex = static_cast(i) - 1; - if (itemIndex == m_commandsSelectedIndex) { - float pulse = std::sin(m_animTime * 4.0f) * 0.3f + 0.7f; - label.color = {1.0f, 0.08f + pulse * 0.5f, 0.58f, 1.0f}; - } else { - label.color = {0.7f, 0.7f, 0.7f, 1.0f}; - } - - if (itemIndex >= 0 && itemIndex < static_cast(actionNames.size())) { - Renderer::Key currentKey = Core::InputMapping::GetKey(ACTION_KEYS[itemIndex]); - std::string keyName = keyToString(currentKey); - std::string actionText = actionNames[itemIndex] + ": " + keyName; - if (m_waitingForRebind && m_rebindingActionIndex == itemIndex) { - actionText = actionNames[itemIndex] + ": PRESS A KEY..."; - } - label.text = actionText; - } else if (itemIndex == static_cast(actionNames.size())) { - label.text = "BACK"; - } - } - } - } - - void SettingsState::HandleInput() { - auto playSelectSound = [this]() { - if (m_context.audio && m_selectMusic != Audio::INVALID_MUSIC_ID) { - m_context.audio->StopMusic(m_selectMusic); - Audio::PlaybackOptions opts; - opts.volume = 1.0f; - opts.loop = false; - m_context.audio->PlayMusic(m_selectMusic, opts); - } - }; - - if (m_inScreenMenu) { - if (m_renderer->IsKeyPressed(Renderer::Key::Up) && !m_upKeyPressed) { - m_upKeyPressed = true; - playSelectSound(); - m_screenSelectedIndex--; - if (m_screenSelectedIndex < 0) { - m_screenSelectedIndex = static_cast(ScreenItem::COUNT) - 1; - } - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Up)) { - m_upKeyPressed = false; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Down) && !m_downKeyPressed) { - m_downKeyPressed = true; - playSelectSound(); - m_screenSelectedIndex++; - if (m_screenSelectedIndex >= static_cast(ScreenItem::COUNT)) { - m_screenSelectedIndex = 0; - } - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Down)) { - m_downKeyPressed = false; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Left) && !m_leftKeyPressed) { - m_leftKeyPressed = true; - playSelectSound(); - switch (static_cast(m_screenSelectedIndex)) { - case ScreenItem::RESOLUTION: - changeResolution(-1); - break; - case ScreenItem::FULLSCREEN: - toggleFullscreen(); - break; - case ScreenItem::FRAME_RATE: - changeFrameRate(-1); - break; - default: - break; - } - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Left)) { - m_leftKeyPressed = false; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Right) && !m_rightKeyPressed) { - m_rightKeyPressed = true; - playSelectSound(); - switch (static_cast(m_screenSelectedIndex)) { - case ScreenItem::RESOLUTION: - changeResolution(1); - break; - case ScreenItem::FULLSCREEN: - toggleFullscreen(); - break; - case ScreenItem::FRAME_RATE: - changeFrameRate(1); - break; - default: - break; - } - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Right)) { - m_rightKeyPressed = false; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Enter) && !m_enterKeyPressed) { - m_enterKeyPressed = true; - playSelectSound(); - if (static_cast(m_screenSelectedIndex) == ScreenItem::BACK) { - exitSubMenu(); - } - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Enter)) { - m_enterKeyPressed = false; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Escape) && !m_escKeyPressed) { - m_escKeyPressed = true; - playSelectSound(); - exitSubMenu(); - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Escape)) { - m_escKeyPressed = false; - } - return; - } - - if (m_inAudioMenu) { - if (m_renderer->IsKeyPressed(Renderer::Key::Up) && !m_upKeyPressed) { - m_upKeyPressed = true; - playSelectSound(); - m_audioSelectedIndex--; - if (m_audioSelectedIndex < 0) { - m_audioSelectedIndex = static_cast(AudioItem::COUNT) - 1; - } - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Up)) { - m_upKeyPressed = false; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Down) && !m_downKeyPressed) { - m_downKeyPressed = true; - playSelectSound(); - m_audioSelectedIndex++; - if (m_audioSelectedIndex >= static_cast(AudioItem::COUNT)) { - m_audioSelectedIndex = 0; - } - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Down)) { - m_downKeyPressed = false; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Left) && !m_leftKeyPressed) { - m_leftKeyPressed = true; - playSelectSound(); - switch (static_cast(m_audioSelectedIndex)) { - case AudioItem::MASTER_VOLUME: - changeMasterVolume(-1); - break; - case AudioItem::MUTE: - toggleMute(); - break; - default: - break; - } - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Left)) { - m_leftKeyPressed = false; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Right) && !m_rightKeyPressed) { - m_rightKeyPressed = true; - playSelectSound(); - switch (static_cast(m_audioSelectedIndex)) { - case AudioItem::MASTER_VOLUME: - changeMasterVolume(1); - break; - case AudioItem::MUTE: - toggleMute(); - break; - default: - break; - } - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Right)) { - m_rightKeyPressed = false; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Enter) && !m_enterKeyPressed) { - m_enterKeyPressed = true; - playSelectSound(); - if (static_cast(m_audioSelectedIndex) == AudioItem::BACK) { - exitSubMenu(); - } - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Enter)) { - m_enterKeyPressed = false; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Escape) && !m_escKeyPressed) { - m_escKeyPressed = true; - playSelectSound(); - exitSubMenu(); - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Escape)) { - m_escKeyPressed = false; - } - return; - } - - if (m_inCommandsMenu) { - if (m_waitingForRebind) { - processRebind(); - return; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Up) && !m_upKeyPressed) { - m_upKeyPressed = true; - playSelectSound(); - m_commandsSelectedIndex--; - if (m_commandsSelectedIndex < 0) { - m_commandsSelectedIndex = static_cast(CommandsItem::COUNT) - 1; - } - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Up)) { - m_upKeyPressed = false; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Down) && !m_downKeyPressed) { - m_downKeyPressed = true; - playSelectSound(); - m_commandsSelectedIndex++; - if (m_commandsSelectedIndex >= static_cast(CommandsItem::COUNT)) { - m_commandsSelectedIndex = 0; - } - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Down)) { - m_downKeyPressed = false; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Enter) && !m_enterKeyPressed) { - m_enterKeyPressed = true; - playSelectSound(); - if (static_cast(m_commandsSelectedIndex) == CommandsItem::BACK) { - exitSubMenu(); - } else if (m_commandsSelectedIndex >= 0 && m_commandsSelectedIndex < 5) { - startRebind(m_commandsSelectedIndex); - } - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Enter)) { - m_enterKeyPressed = false; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Escape) && !m_escKeyPressed) { - m_escKeyPressed = true; - playSelectSound(); - exitSubMenu(); - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Escape)) { - m_escKeyPressed = false; - } - return; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Up) && !m_upKeyPressed) { - m_upKeyPressed = true; - playSelectSound(); - m_selectedIndex--; - if (m_selectedIndex < 0) { - m_selectedIndex = static_cast(SettingsItem::COUNT) - 1; - } - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Up)) { - m_upKeyPressed = false; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Down) && !m_downKeyPressed) { - m_downKeyPressed = true; - playSelectSound(); - m_selectedIndex++; - if (m_selectedIndex >= static_cast(SettingsItem::COUNT)) { - m_selectedIndex = 0; - } - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Down)) { - m_downKeyPressed = false; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Enter) && !m_enterKeyPressed) { - m_enterKeyPressed = true; - playSelectSound(); - - switch (static_cast(m_selectedIndex)) { - case SettingsItem::COLOUR_BLIND: - toggleColourBlindMode(); - break; - - case SettingsItem::SCREEN: - enterSubMenu(true); - break; - - case SettingsItem::AUDIO: - enterSubMenu(false); - break; - - case SettingsItem::COMMANDS: - enterCommandsMenu(); - break; - - case SettingsItem::BACK: - m_machine.PopState(); - break; - - default: - break; - } - - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Enter)) { - m_enterKeyPressed = false; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Escape) && !m_escKeyPressed) { - m_escKeyPressed = true; - playSelectSound(); - m_machine.PopState(); - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Escape)) { - m_escKeyPressed = false; - } - } - - void SettingsState::Update(float dt) { - updateAnimations(dt); - updateMenuSelection(); - - if (m_audioSystem) { - m_audioSystem->Update(m_registry, dt); - } - } - - void SettingsState::Draw() { - m_renderer->Clear({0.05f, 0.05f, 0.1f, 1.0f}); - - m_renderingSystem->Update(m_registry, 0.0f); - m_textSystem->Update(m_registry, 0.0f); - } - - void SettingsState::toggleColourBlindMode() { - m_colourBlindMode = !m_colourBlindMode; - s_colourBlindMode = m_colourBlindMode; - SetColourBlindMode(m_colourBlindMode); - saveSettings(); - std::cout << "[SettingsState] Colour-blind mode: " << (m_colourBlindMode ? "ON" : "OFF") << std::endl; - } - - void SettingsState::saveSettings() { - try { - json j; - j["colourBlindMode"] = m_colourBlindMode; - j["screenWidth"] = m_screenWidth; - j["screenHeight"] = m_screenHeight; - j["fullscreen"] = m_fullscreen; - j["targetFramerate"] = m_targetFramerate; - j["masterVolume"] = m_masterVolume; - j["muted"] = m_muted; - - json inputMappings; - std::vector actionKeys = {"MOVE_UP", "MOVE_DOWN", "MOVE_LEFT", "MOVE_RIGHT", "SHOOT"}; - for (const auto& action : actionKeys) { - Renderer::Key key = Core::InputMapping::GetKey(action); - if (key != Renderer::Key::Unknown) { - inputMappings[action] = keyToString(key); - } - } - j["inputMappings"] = inputMappings; - - std::string filePath = SETTINGS_FILE_PATH; - std::ofstream file(filePath); - if (!file.is_open()) { - filePath = "../" + SETTINGS_FILE_PATH; - file.open(filePath); - } - - if (file.is_open()) { - file << j.dump(4); - file.close(); - Core::Logger::Info("[SettingsState] Settings saved to {}", filePath); - } else { - Core::Logger::Warning("[SettingsState] Failed to save settings to {}", SETTINGS_FILE_PATH); - } - } catch (const std::exception& e) { - Core::Logger::Error("[SettingsState] Error saving settings: {}", e.what()); - } - } - - void SettingsState::loadSettings() { - m_colourBlindMode = s_colourBlindMode; - m_screenWidth = s_screenWidth; - m_screenHeight = s_screenHeight; - m_fullscreen = s_fullscreen; - m_targetFramerate = s_targetFramerate; - m_masterVolume = s_masterVolume; - m_muted = s_muted; - m_volumeBeforeMute = m_masterVolume; - - if (m_context.audio) { - if (m_muted) { - m_context.audio->SetMasterVolume(0.0f); - } else { - m_context.audio->SetMasterVolume(m_masterVolume); - } - } - - m_resolutionIndex = 0; - for (size_t i = 0; i < RESOLUTIONS.size(); i++) { - if (RESOLUTIONS[i].width == m_screenWidth && RESOLUTIONS[i].height == m_screenHeight) { - m_resolutionIndex = static_cast(i); - break; - } - } - - m_framerateIndex = 2; - for (size_t i = 0; i < FRAMERATES.size(); i++) { - if (FRAMERATES[i] == m_targetFramerate) { - m_framerateIndex = static_cast(i); - break; - } - } - } - - void SettingsState::LoadSettingsFromFile() { - try { - std::string filePath = SETTINGS_FILE_PATH; - std::ifstream file(filePath); - if (!file.is_open()) { - filePath = "../" + SETTINGS_FILE_PATH; - file.open(filePath); - } - - if (!file.is_open()) { - Core::Logger::Info("[SettingsState] Settings file not found, using defaults"); - s_colourBlindMode = false; - SetColourBlindMode(false); - s_screenWidth = 1280; - s_screenHeight = 720; - s_fullscreen = false; - s_targetFramerate = 60; - s_masterVolume = 1.0f; - s_muted = false; - return; - } - - json j; - file >> j; - file.close(); - - if (j.contains("colourBlindMode")) { - s_colourBlindMode = j["colourBlindMode"].get(); - SetColourBlindMode(s_colourBlindMode); - } else { - s_colourBlindMode = false; - SetColourBlindMode(false); - } - - if (j.contains("screenWidth")) { - s_screenWidth = j["screenWidth"].get(); - } else { - s_screenWidth = 1280; - } - if (j.contains("screenHeight")) { - s_screenHeight = j["screenHeight"].get(); - } else { - s_screenHeight = 720; - } - if (j.contains("fullscreen")) { - s_fullscreen = j["fullscreen"].get(); - } else { - s_fullscreen = false; - } - if (j.contains("targetFramerate")) { - s_targetFramerate = j["targetFramerate"].get(); - } else { - s_targetFramerate = 60; - } - - if (j.contains("masterVolume")) { - s_masterVolume = j["masterVolume"].get(); - } else { - s_masterVolume = 1.0f; - } - - if (j.contains("muted")) { - s_muted = j["muted"].get(); - } else { - s_muted = false; - } - - if (j.contains("inputMappings") && j["inputMappings"].is_object()) { - for (auto& [action, keyStr] : j["inputMappings"].items()) { - Renderer::Key key = stringToKey(keyStr.get()); - if (key != Renderer::Key::Unknown) { - Core::InputMapping::SetKey(action, key); - } - } - } - - Core::Logger::Info("[SettingsState] Settings loaded: colourBlindMode={}, resolution={}x{}, fullscreen={}, framerate={}, masterVolume={}, muted={}", - s_colourBlindMode, s_screenWidth, s_screenHeight, s_fullscreen, s_targetFramerate, s_masterVolume, s_muted); - } catch (const std::exception& e) { - Core::Logger::Error("[SettingsState] Error loading settings: {}", e.what()); - s_colourBlindMode = false; - SetColourBlindMode(false); - s_screenWidth = 1280; - s_screenHeight = 720; - s_fullscreen = false; - s_targetFramerate = 60; - s_masterVolume = 1.0f; - s_muted = false; - } - } - - bool SettingsState::IsColourBlindModeEnabled() { - return s_colourBlindMode; - } - - void SettingsState::SetColourBlindMode(bool enabled) { - s_colourBlindMode = enabled; - Core::ColorFilter::SetColourBlindMode(enabled); - } - - void SettingsState::clearMenuUI() { - for (auto it = m_entities.begin(); it != m_entities.end();) { - Entity entity = *it; - if (m_registry.IsEntityAlive(entity)) { - bool isBackground = false; - if (m_registry.HasComponent(entity)) { - const auto& drawable = m_registry.GetComponent(entity); - if (drawable.layer == -10) { - isBackground = true; - } - } - if (!isBackground) { - m_registry.DestroyEntity(entity); - it = m_entities.erase(it); - } else { - ++it; - } - } else { - it = m_entities.erase(it); - } - } - } - - void SettingsState::enterSubMenu(bool screen) { - for (Entity entity : m_menuItems) { - if (m_registry.IsEntityAlive(entity)) { - m_registry.DestroyEntity(entity); - } - } - m_menuItems.clear(); - - if (m_titleEntity != RType::ECS::NULL_ENTITY && m_registry.IsEntityAlive(m_titleEntity)) { - m_registry.DestroyEntity(m_titleEntity); - m_titleEntity = RType::ECS::NULL_ENTITY; - } - - clearMenuUI(); - - if (screen) { - m_inScreenMenu = true; - m_screenSelectedIndex = 0; - createScreenUI(); - } else { - m_inAudioMenu = true; - m_audioSelectedIndex = 0; - createAudioUI(); - } - } - - void SettingsState::exitSubMenu() { - std::vector* menuItems = nullptr; - if (m_inScreenMenu) { - menuItems = &m_screenMenuItems; - } else if (m_inAudioMenu) { - menuItems = &m_audioMenuItems; - } else if (m_inCommandsMenu) { - menuItems = &m_commandsMenuItems; - } - - if (menuItems) { - for (Entity entity : *menuItems) { - if (m_registry.IsEntityAlive(entity)) { - m_registry.DestroyEntity(entity); - } - } - menuItems->clear(); - } - - clearMenuUI(); - - int savedSelectedIndex = m_selectedIndex; - if (m_inScreenMenu) { - savedSelectedIndex = static_cast(SettingsItem::SCREEN); - } else if (m_inAudioMenu) { - savedSelectedIndex = static_cast(SettingsItem::AUDIO); - } else if (m_inCommandsMenu) { - savedSelectedIndex = static_cast(SettingsItem::COMMANDS); - } - - m_inScreenMenu = false; - m_inAudioMenu = false; - m_inCommandsMenu = false; - m_waitingForRebind = false; - m_rebindingActionIndex = -1; - m_screenSelectedIndex = 0; - m_audioSelectedIndex = 0; - m_commandsSelectedIndex = 0; - m_selectedIndex = savedSelectedIndex; - createUI(); - updateMenuSelection(); - } - - void SettingsState::enterCommandsMenu() { - clearMenuUI(); - m_inCommandsMenu = true; - m_commandsSelectedIndex = 0; - m_waitingForRebind = false; - m_rebindingActionIndex = -1; - createCommandsUI(); - } - - void SettingsState::startRebind(int actionIndex) { - m_waitingForRebind = true; - m_rebindingActionIndex = actionIndex; - m_enterKeyPressed = true; - createCommandsUI(); - } - - void SettingsState::processRebind() { - if (m_renderer->IsKeyPressed(Renderer::Key::Enter)) { - m_enterKeyPressed = true; - return; - } - - if (m_enterKeyPressed && !m_renderer->IsKeyPressed(Renderer::Key::Enter)) { - m_enterKeyPressed = false; - return; - } - - for (Renderer::Key key : REBINDABLE_KEYS) { - if (m_renderer->IsKeyPressed(key)) { - if (key == Renderer::Key::Escape) { - m_waitingForRebind = false; - m_rebindingActionIndex = -1; - m_enterKeyPressed = false; - createCommandsUI(); - return; - } - - if (m_rebindingActionIndex >= 0 && m_rebindingActionIndex < static_cast(ACTION_KEYS.size())) { - Core::InputMapping::SetKey(ACTION_KEYS[m_rebindingActionIndex], key); - saveSettings(); - m_waitingForRebind = false; - m_rebindingActionIndex = -1; - m_enterKeyPressed = false; - createCommandsUI(); - } - return; - } - } - } - - std::string SettingsState::keyToString(Renderer::Key key) { - auto it = KEY_TO_STRING.find(key); - return (it != KEY_TO_STRING.end()) ? it->second : "UNKNOWN"; - } - - Renderer::Key SettingsState::stringToKey(const std::string& str) { - auto it = STRING_TO_KEY.find(str); - return (it != STRING_TO_KEY.end()) ? it->second : Renderer::Key::Unknown; - } - - - void SettingsState::changeResolution(int direction) { - m_resolutionIndex += direction; - if (m_resolutionIndex < 0) { - m_resolutionIndex = static_cast(RESOLUTIONS.size()) - 1; - } else if (m_resolutionIndex >= static_cast(RESOLUTIONS.size())) { - m_resolutionIndex = 0; - } - m_screenWidth = RESOLUTIONS[m_resolutionIndex].width; - m_screenHeight = RESOLUTIONS[m_resolutionIndex].height; - applyScreenSettings(); - } - - void SettingsState::toggleFullscreen() { - m_fullscreen = !m_fullscreen; - applyScreenSettings(); - } - - void SettingsState::changeFrameRate(int direction) { - m_framerateIndex += direction; - if (m_framerateIndex < 0) { - m_framerateIndex = static_cast(FRAMERATES.size()) - 1; - } else if (m_framerateIndex >= static_cast(FRAMERATES.size())) { - m_framerateIndex = 0; - } - m_targetFramerate = FRAMERATES[m_framerateIndex]; - applyScreenSettings(); - } - - void SettingsState::changeMasterVolume(int direction) { - if (m_muted) { - return; - } - - float step = 0.05f; - m_masterVolume += direction * step; - m_masterVolume = std::clamp(m_masterVolume, 0.0f, 1.0f); - - if (m_context.audio) { - m_context.audio->SetMasterVolume(m_masterVolume); - } - - s_masterVolume = m_masterVolume; - saveSettings(); - } - - void SettingsState::toggleMute() { - m_muted = !m_muted; - - if (m_context.audio) { - if (m_muted) { - m_volumeBeforeMute = m_masterVolume; - m_context.audio->SetMasterVolume(0.0f); - } else { - m_masterVolume = m_volumeBeforeMute; - m_context.audio->SetMasterVolume(m_masterVolume); - } - } - - s_muted = m_muted; - s_masterVolume = m_masterVolume; - saveSettings(); - } - - void SettingsState::applyScreenSettings() { - Renderer::WindowConfig config; - config.title = "R-Type - " + m_context.playerName; - config.width = m_screenWidth; - config.height = m_screenHeight; - config.fullscreen = m_fullscreen; - config.resizable = !m_fullscreen; - config.targetFramerate = m_targetFramerate; - - s_screenWidth = m_screenWidth; - s_screenHeight = m_screenHeight; - s_fullscreen = m_fullscreen; - s_targetFramerate = m_targetFramerate; - - m_renderer->Destroy(); - if (!m_renderer->CreateWindow(config)) { - Core::Logger::Error("[SettingsState] Failed to recreate window with new settings"); - m_screenWidth = 1280; - m_screenHeight = 720; - m_fullscreen = false; - m_targetFramerate = 60; - m_resolutionIndex = 0; - m_framerateIndex = 2; - s_screenWidth = 1280; - s_screenHeight = 720; - s_fullscreen = false; - s_targetFramerate = 60; - config.width = 1280; - config.height = 720; - config.fullscreen = false; - config.targetFramerate = 60; - m_renderer->CreateWindow(config); - } - - saveSettings(); - } - - std::uint32_t SettingsState::GetScreenWidth() { - return s_screenWidth; - } - - std::uint32_t SettingsState::GetScreenHeight() { - return s_screenHeight; - } - - bool SettingsState::GetFullscreen() { - return s_fullscreen; - } - - std::uint32_t SettingsState::GetTargetFramerate() { - return s_targetFramerate; - } - - float SettingsState::GetMasterVolume() { - return s_masterVolume; - } - - bool SettingsState::IsMuted() { - return s_muted; - } - - } -} - diff --git a/client/src/editor/EditorAssetLibrary.cpp b/client/src/editor/EditorAssetLibrary.cpp deleted file mode 100644 index d06c885..0000000 --- a/client/src/editor/EditorAssetLibrary.cpp +++ /dev/null @@ -1,119 +0,0 @@ -#include "editor/EditorAssetLibrary.hpp" -#include "Core/Logger.hpp" - -namespace RType { - namespace Client { - - namespace { - std::string ResolvePath(const std::string& relativePath) { - return "../" + relativePath; - } - } - - EditorAssetLibrary::EditorAssetLibrary(Renderer::IRenderer* renderer) - : m_renderer(renderer) - { - } - - bool EditorAssetLibrary::Initialize() { - auto definitions = buildDefaultDefinitions(); - bool anyLoaded = false; - - for (auto& def : definitions) { - EditorAssetResource resource; - resource.definition = std::move(def); - if (loadResource(resource)) { - const std::string& id = resource.definition.id; - m_resourcesById.emplace(id, std::move(resource)); - anyLoaded = true; - } else { - Core::Logger::Warning("[EditorAssetLibrary] Failed to load asset '{}'", resource.definition.id); - } - } - - m_resourcesByType.clear(); - for (auto& [id, resource] : m_resourcesById) { - m_resourcesByType[static_cast(resource.definition.type)].push_back(&resource); - } - - return anyLoaded; - } - - const EditorAssetResource* EditorAssetLibrary::GetResource(const std::string& id) const { - auto it = m_resourcesById.find(id); - if (it == m_resourcesById.end()) { - return nullptr; - } - return &it->second; - } - - const std::vector& EditorAssetLibrary::GetResources(EditorEntityType type) const { - static const std::vector empty; - auto it = m_resourcesByType.find(static_cast(type)); - if (it == m_resourcesByType.end()) { - return empty; - } - return it->second; - } - - bool EditorAssetLibrary::loadResource(EditorAssetResource& resource) { - if (!m_renderer) { - return false; - } - - const std::string fullPath = ResolvePath(resource.definition.texturePath); - resource.textureId = m_renderer->LoadTexture(fullPath); - if (resource.textureId == Renderer::INVALID_TEXTURE_ID) { - resource.textureId = m_renderer->LoadTexture(resource.definition.texturePath); - } - - if (resource.textureId == Renderer::INVALID_TEXTURE_ID) { - return false; - } - - resource.spriteId = m_renderer->CreateSprite(resource.textureId, {}); - resource.textureSize = m_renderer->GetTextureSize(resource.textureId); - - if (resource.definition.defaultSize.x <= 0.0f || resource.definition.defaultSize.y <= 0.0f) { - resource.definition.defaultSize = resource.textureSize; - } - - return resource.spriteId != Renderer::INVALID_SPRITE_ID; - } - - std::vector EditorAssetLibrary::buildDefaultDefinitions() const { - return { - // Obstacles - {"obstacle1", "Obstacle 1", EditorEntityType::OBSTACLE, "assets/backgrounds/obstacles/middle_obstacle.png", {441.0f, 200.0f}, -50.0f, 1, "", ""}, - {"obstacle2", "Obstacle 2", EditorEntityType::OBSTACLE, "assets/backgrounds/obstacles/obstacle_bas.png", {441.0f, 120.0f}, -50.0f, 1, "", ""}, - {"obstacle3", "Obstacle 3", EditorEntityType::OBSTACLE, "assets/backgrounds/obstacles/little_obstacle.png", {250.0f, 150.0f}, -50.0f, 1, "", ""}, - {"obstacle4", "Obstacle 4", EditorEntityType::OBSTACLE, "assets/backgrounds/obstacles/obstacle_square.png", {300.0f, 300.0f}, -50.0f, 1, "", ""}, - {"obstacle5", "Obstacle 5", EditorEntityType::OBSTACLE, "assets/backgrounds/obstacles/obstacle_bas_two.png", {441.0f, 120.0f}, -50.0f, 1, "", ""}, - {"obstacle6", "Obstacle 6", EditorEntityType::OBSTACLE, "assets/backgrounds/obstacles/obstacle_haut_two.png", {441.0f, 100.0f}, -50.0f, 1, "", ""}, - {"obstacle7", "Obstacle 7", EditorEntityType::OBSTACLE, "assets/backgrounds/obstacles/obstacle_yellow.png", {350.0f, 220.0f}, -50.0f, 1, "", ""}, - {"obstacle8", "Obstacle 8", EditorEntityType::OBSTACLE, "assets/backgrounds/obstacles/little_obstacle_haut.png", {250.0f, 120.0f}, -50.0f, 1, "", ""}, - - // Enemies - {"enemy_blue", "Enemy Blue", EditorEntityType::ENEMY, "assets/spaceships/enemy-blue.png", {96.0f, 96.0f}, -10.0f, 5, "BASIC", ""}, - {"enemy_red", "Enemy Red", EditorEntityType::ENEMY, "assets/spaceships/enemy-red.png", {96.0f, 96.0f}, -10.0f, 5, "FAST", ""}, - {"enemy_green", "Enemy Green", EditorEntityType::ENEMY, "assets/spaceships/enemy-green.png", {96.0f, 96.0f}, -10.0f, 5, "TANK", ""}, - {"enemy_boss", "Enemy Boss", EditorEntityType::ENEMY, "assets/spaceships/enemy-purple-boss.png", {200.0f, 200.0f}, -15.0f, 5, "BOSS", ""}, - - // Power-ups - {"power_spread", "Power Spread", EditorEntityType::POWERUP, "assets/powerups/spread.png", {64.0f, 64.0f}, -50.0f, 2, "", "SPREAD_SHOT"}, - {"power_laser", "Power Laser", EditorEntityType::POWERUP, "assets/powerups/laser.png", {64.0f, 64.0f}, -50.0f, 2, "", "LASER_BEAM"}, - {"power_force", "Power Force Pod", EditorEntityType::POWERUP, "assets/powerups/force_pod.png", {64.0f, 64.0f}, -50.0f, 2, "", "FORCE_POD"}, - {"power_speed", "Power Speed", EditorEntityType::POWERUP, "assets/powerups/speed.png", {64.0f, 64.0f}, -50.0f, 2, "", "SPEED_BOOST"}, - {"power_shield", "Power Shield", EditorEntityType::POWERUP, "assets/powerups/shield.png", {64.0f, 64.0f}, -50.0f, 2, "", "SHIELD"}, - - // Player spawn - {"player_spawn", "Player Spawn", EditorEntityType::PLAYER_SPAWN, "assets/spaceships/player_blue.png", {96.0f, 96.0f}, 0.0f, 10, "", ""}, - - // Background - {"background_space", "Space Background", EditorEntityType::BACKGROUND, "assets/backgrounds/Cave_one.png", {1280.0f, 720.0f}, -20.0f, -100, "", ""}, - }; - } - - } -} - diff --git a/client/src/editor/EditorCanvasManager.cpp b/client/src/editor/EditorCanvasManager.cpp deleted file mode 100644 index 0478121..0000000 --- a/client/src/editor/EditorCanvasManager.cpp +++ /dev/null @@ -1,116 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** EditorCanvasManager -*/ - -#include "editor/EditorCanvasManager.hpp" -#include "editor/EditorConstants.hpp" -#include -#include - -using namespace RType::Client::EditorConstants; - -namespace RType { - namespace Client { - - EditorCanvasManager::EditorCanvasManager(Renderer::IRenderer* renderer) - : m_renderer(renderer) - { - } - - void EditorCanvasManager::HandleCameraInput() { - const float panSpeed = Camera::PAN_SPEED; - const float zoomSpeed = Camera::ZOOM_SPEED; - - // Camera panning (continuous while key is held) - if (m_renderer->IsKeyPressed(Renderer::Key::Up)) { - m_camera.y -= panSpeed * (1.0f / 60.0f) / m_camera.zoom; - } - if (m_renderer->IsKeyPressed(Renderer::Key::Down)) { - m_camera.y += panSpeed * (1.0f / 60.0f) / m_camera.zoom; - } - if (m_renderer->IsKeyPressed(Renderer::Key::Left)) { - m_camera.x -= panSpeed * (1.0f / 60.0f) / m_camera.zoom; - } - if (m_renderer->IsKeyPressed(Renderer::Key::Right)) { - m_camera.x += panSpeed * (1.0f / 60.0f) / m_camera.zoom; - } - - static bool zoomInKeyPressed = false; - static bool zoomOutKeyPressed = false; - - bool isZoomInPressed = m_renderer->IsKeyPressed(Renderer::Key::X); - if (isZoomInPressed && !zoomInKeyPressed) { - zoomInKeyPressed = true; - m_camera.zoom += zoomSpeed; - } else if (!isZoomInPressed) { - zoomInKeyPressed = false; - } - - bool isZoomOutPressed = m_renderer->IsKeyPressed(Renderer::Key::C); - if (isZoomOutPressed && !zoomOutKeyPressed) { - zoomOutKeyPressed = true; - m_camera.zoom -= zoomSpeed; - } else if (!isZoomOutPressed) { - zoomOutKeyPressed = false; - } - - m_camera.x = std::clamp(m_camera.x, m_camera.minX, m_camera.maxX); - m_camera.y = std::clamp(m_camera.y, m_camera.minY, m_camera.maxY); - m_camera.zoom = std::clamp(m_camera.zoom, m_camera.minZoom, m_camera.maxZoom); - } - - void EditorCanvasManager::ApplyCamera() { - Renderer::Camera2D cam; - cam.center = {m_camera.x, m_camera.y}; - cam.size = {1280.0f / m_camera.zoom, 720.0f / m_camera.zoom}; - m_renderer->SetCamera(cam); - } - - void EditorCanvasManager::DrawGrid() { - if (!m_grid.enabled) { - return; - } - - Math::Vector2 topLeft = ScreenToWorld({0.0f, 0.0f}); - Math::Vector2 bottomRight = ScreenToWorld({1280.0f, 720.0f}); - - float startX = std::floor(topLeft.x / m_grid.cellSize) * m_grid.cellSize; - float startY = std::floor(topLeft.y / m_grid.cellSize) * m_grid.cellSize; - float endX = std::ceil(bottomRight.x / m_grid.cellSize) * m_grid.cellSize; - float endY = std::ceil(bottomRight.y / m_grid.cellSize) * m_grid.cellSize; - - for (float x = startX; x <= endX; x += m_grid.cellSize) { - Math::Rectangle rect = {{x, startY}, {1.0f, endY - startY}}; - m_renderer->DrawRectangle(rect, m_grid.gridColor); - } - - for (float y = startY; y <= endY; y += m_grid.cellSize) { - Math::Rectangle rect = {{startX, y}, {endX - startX, 1.0f}}; - m_renderer->DrawRectangle(rect, m_grid.gridColor); - } - } - - Math::Vector2 EditorCanvasManager::ScreenToWorld(Math::Vector2 screenPos) const { - float worldX = (screenPos.x - 640.0f) / m_camera.zoom + m_camera.x; - float worldY = (screenPos.y - 360.0f) / m_camera.zoom + m_camera.y; - return {worldX, worldY}; - } - - Math::Vector2 EditorCanvasManager::WorldToScreen(Math::Vector2 worldPos) const { - float screenX = (worldPos.x - m_camera.x) * m_camera.zoom + 640.0f; - float screenY = (worldPos.y - m_camera.y) * m_camera.zoom + 360.0f; - return {screenX, screenY}; - } - - float EditorCanvasManager::SnapToGrid(float value) const { - if (!m_grid.snapToGrid) { - return value; - } - return std::round(value / m_grid.cellSize) * m_grid.cellSize; - } - - } -} diff --git a/client/src/editor/EditorColliderManager.cpp b/client/src/editor/EditorColliderManager.cpp deleted file mode 100644 index 8e474d6..0000000 --- a/client/src/editor/EditorColliderManager.cpp +++ /dev/null @@ -1,189 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** EditorColliderManager -*/ - -#include "editor/EditorColliderManager.hpp" -#include "editor/EditorConstants.hpp" -#include "editor/EditorGeometry.hpp" -#include "editor/EditorDrawing.hpp" -#include - -using namespace RType::Client::EditorConstants; - -namespace RType { - namespace Client { - - EditorColliderManager::EditorColliderManager(Renderer::IRenderer* renderer) - : m_renderer(renderer) - { - } - - void EditorColliderManager::DrawColliders(const EditorEntityData* entity, int selectedIndex) const { - if (!entity || !m_renderer) { - return; - } - - for (size_t i = 0; i < entity->colliders.size(); ++i) { - const auto& collider = entity->colliders[i]; - bool isSelected = (static_cast(i) == selectedIndex); - Math::Color color = isSelected ? Collider::COLLIDER_SELECTED : Collider::COLLIDER_NORMAL; - drawCollider(collider, color); - } - } - - void EditorColliderManager::DrawHandles(const EditorEntityData* entity, int colliderIndex) const { - if (!entity || !m_renderer || colliderIndex < 0 || colliderIndex >= static_cast(entity->colliders.size())) { - return; - } - - const auto& collider = entity->colliders[static_cast(colliderIndex)]; - - // Draw handles at the four corners - drawHandle({collider.x, collider.y}); - drawHandle({collider.x + collider.width, collider.y}); - drawHandle({collider.x, collider.y + collider.height}); - drawHandle({collider.x + collider.width, collider.y + collider.height}); - } - - ColliderHandle EditorColliderManager::GetHandleAt(const EditorEntityData* entity, - const Math::Vector2& worldPos) const { - if (!entity) { - return {}; - } - - const float hs = Collider::HANDLE_SIZE / 2.0f; - - for (size_t i = 0; i < entity->colliders.size(); ++i) { - const auto& collider = entity->colliders[i]; - - // Check corner handles first (they have priority over body) - Math::Rectangle tlHandle = EditorGeometry::BuildRect( - collider.x - hs, collider.y - hs, Collider::HANDLE_SIZE, Collider::HANDLE_SIZE); - Math::Rectangle trHandle = EditorGeometry::BuildRect( - collider.x + collider.width - hs, collider.y - hs, Collider::HANDLE_SIZE, Collider::HANDLE_SIZE); - Math::Rectangle blHandle = EditorGeometry::BuildRect( - collider.x - hs, collider.y + collider.height - hs, Collider::HANDLE_SIZE, Collider::HANDLE_SIZE); - Math::Rectangle brHandle = EditorGeometry::BuildRect( - collider.x + collider.width - hs, collider.y + collider.height - hs, Collider::HANDLE_SIZE, Collider::HANDLE_SIZE); - - if (EditorGeometry::PointInRect(worldPos, tlHandle)) { - return {static_cast(i), ColliderHandle::Type::TOP_LEFT}; - } - if (EditorGeometry::PointInRect(worldPos, trHandle)) { - return {static_cast(i), ColliderHandle::Type::TOP_RIGHT}; - } - if (EditorGeometry::PointInRect(worldPos, blHandle)) { - return {static_cast(i), ColliderHandle::Type::BOTTOM_LEFT}; - } - if (EditorGeometry::PointInRect(worldPos, brHandle)) { - return {static_cast(i), ColliderHandle::Type::BOTTOM_RIGHT}; - } - - // Check body last - Math::Rectangle bodyRect = getColliderRect(collider); - if (EditorGeometry::PointInRect(worldPos, bodyRect)) { - return {static_cast(i), ColliderHandle::Type::BODY}; - } - } - - return {}; - } - - int EditorColliderManager::AddCollider(EditorEntityData& entity, const Math::Vector2& worldPos) { - // Create a default-sized collider at the specified position - ECS::ColliderDef newCollider; - newCollider.x = worldPos.x - 25.0f; - newCollider.y = worldPos.y - 25.0f; - newCollider.width = 50.0f; - newCollider.height = 50.0f; - - entity.colliders.push_back(newCollider); - - return static_cast(entity.colliders.size()) - 1; - } - - bool EditorColliderManager::RemoveCollider(EditorEntityData& entity, int colliderIndex) { - if (colliderIndex < 0 || colliderIndex >= static_cast(entity.colliders.size())) { - return false; - } - - entity.colliders.erase(entity.colliders.begin() + colliderIndex); - return true; - } - - void EditorColliderManager::ResizeCollider(EditorEntityData& entity, int colliderIndex, - ColliderHandle::Type handleType, - const Math::Vector2& worldPos) { - if (colliderIndex < 0 || colliderIndex >= static_cast(entity.colliders.size())) { - return; - } - - auto& collider = entity.colliders[static_cast(colliderIndex)]; - const float minSize = Collider::MIN_COLLIDER_SIZE; - - float originalRight = collider.x + collider.width; - float originalBottom = collider.y + collider.height; - - switch (handleType) { - case ColliderHandle::Type::TOP_LEFT: - collider.x = std::min(worldPos.x, originalRight - minSize); - collider.y = std::min(worldPos.y, originalBottom - minSize); - collider.width = originalRight - collider.x; - collider.height = originalBottom - collider.y; - break; - - case ColliderHandle::Type::TOP_RIGHT: - collider.y = std::min(worldPos.y, originalBottom - minSize); - collider.width = std::max(worldPos.x - collider.x, minSize); - collider.height = originalBottom - collider.y; - break; - - case ColliderHandle::Type::BOTTOM_LEFT: - collider.x = std::min(worldPos.x, originalRight - minSize); - collider.width = originalRight - collider.x; - collider.height = std::max(worldPos.y - collider.y, minSize); - break; - - case ColliderHandle::Type::BOTTOM_RIGHT: - collider.width = std::max(worldPos.x - collider.x, minSize); - collider.height = std::max(worldPos.y - collider.y, minSize); - break; - - case ColliderHandle::Type::BODY: { - Math::Vector2 delta = {worldPos.x - m_dragStart.x, worldPos.y - m_dragStart.y}; - collider.x += delta.x; - collider.y += delta.y; - m_dragStart = worldPos; - break; - } - - case ColliderHandle::Type::NONE: - break; - } - } - - void EditorColliderManager::drawCollider(const ECS::ColliderDef& collider, const Math::Color& color) const { - // Draw semi-transparent fill - Math::Rectangle colliderRect = EditorGeometry::BuildRect( - collider.x, collider.y, collider.width, collider.height); - Math::Color fillColor = color; - fillColor.a = 0.2f; - m_renderer->DrawRectangle(colliderRect, fillColor); - - // Draw outline using utility - EditorDrawing::DrawCollider(m_renderer, collider, color, Collider::COLLIDER_LINE_THICKNESS); - } - - void EditorColliderManager::drawHandle(const Math::Vector2& pos) const { - EditorDrawing::DrawHandle(m_renderer, pos, Collider::COLLIDER_HANDLE, Collider::HANDLE_SIZE); - } - - Math::Rectangle EditorColliderManager::getColliderRect(const ECS::ColliderDef& collider) const { - return EditorGeometry::BuildRect(collider.x, collider.y, collider.width, collider.height); - } - - } -} diff --git a/client/src/editor/EditorDrawing.cpp b/client/src/editor/EditorDrawing.cpp deleted file mode 100644 index 27acb77..0000000 --- a/client/src/editor/EditorDrawing.cpp +++ /dev/null @@ -1,79 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** EditorDrawing -*/ - -#include "editor/EditorDrawing.hpp" -#include "editor/EditorGeometry.hpp" - -namespace RType { - namespace Client { - namespace EditorDrawing { - - void DrawRectangleOutline(Renderer::IRenderer* renderer, - const Math::Rectangle& rect, - const Math::Color& color, - float thickness) { - if (!renderer) { - return; - } - - // Draw four rectangles for the border (top, bottom, left, right) - Math::Rectangle top = EditorGeometry::BuildRect( - rect.position.x, rect.position.y, rect.size.x, thickness); - Math::Rectangle bottom = EditorGeometry::BuildRect( - rect.position.x, rect.position.y + rect.size.y - thickness, rect.size.x, thickness); - Math::Rectangle left = EditorGeometry::BuildRect( - rect.position.x, rect.position.y, thickness, rect.size.y); - Math::Rectangle right = EditorGeometry::BuildRect( - rect.position.x + rect.size.x - thickness, rect.position.y, thickness, rect.size.y); - - renderer->DrawRectangle(top, color); - renderer->DrawRectangle(bottom, color); - renderer->DrawRectangle(left, color); - renderer->DrawRectangle(right, color); - } - - void DrawCollider(Renderer::IRenderer* renderer, - const ECS::ColliderDef& collider, - const Math::Color& color, - float lineThickness) { - if (!renderer) { - return; - } - - // Draw collider as an outline (same pattern as DrawRectangleOutline) - Math::Rectangle top = EditorGeometry::BuildRect( - collider.x, collider.y, collider.width, lineThickness); - Math::Rectangle bottom = EditorGeometry::BuildRect( - collider.x, collider.y + collider.height - lineThickness, collider.width, lineThickness); - Math::Rectangle left = EditorGeometry::BuildRect( - collider.x, collider.y, lineThickness, collider.height); - Math::Rectangle right = EditorGeometry::BuildRect( - collider.x + collider.width - lineThickness, collider.y, lineThickness, collider.height); - - renderer->DrawRectangle(top, color); - renderer->DrawRectangle(bottom, color); - renderer->DrawRectangle(left, color); - renderer->DrawRectangle(right, color); - } - - void DrawHandle(Renderer::IRenderer* renderer, - const Math::Vector2& position, - const Math::Color& color, - float size) { - if (!renderer) { - return; - } - - const float halfSize = size / 2.0f; - Math::Rectangle handle = EditorGeometry::BuildRect( - position.x - halfSize, position.y - halfSize, size, size); - renderer->DrawRectangle(handle, color); - } - - } - } -} diff --git a/client/src/editor/EditorEntityManager.cpp b/client/src/editor/EditorEntityManager.cpp deleted file mode 100644 index a67e310..0000000 --- a/client/src/editor/EditorEntityManager.cpp +++ /dev/null @@ -1,334 +0,0 @@ -#include "editor/EditorEntityManager.hpp" -#include "editor/EditorConstants.hpp" -#include "editor/EditorGeometry.hpp" -#include "editor/EditorDrawing.hpp" -#include "editor/EditorColliderManager.hpp" -#include "ECS/Component.hpp" -#include "Core/Logger.hpp" -#include -#include - -using namespace RType::Client::EditorConstants; - -namespace RType { - namespace Client { - - EditorEntityManager::EditorEntityManager(Renderer::IRenderer* renderer, - RType::ECS::Registry& registry, - EditorAssetLibrary& assets) - : m_renderer(renderer) - , m_registry(registry) - , m_assets(assets) - , m_colliderManager(std::make_unique(renderer)) - { - } - - EditorEntityManager::~EditorEntityManager() = default; - - EditorEntityData* EditorEntityManager::PlaceEntity(const EditorPaletteSelection& selection, const Math::Vector2& worldPos) { - auto* resource = getResource(selection.subtype); - if (!resource) { - Core::Logger::Warning("[EditorEntityManager] Unknown asset '{}'", selection.subtype); - return nullptr; - } - - EditorEntityData data; - data.type = selection.entityType; - data.presetId = resource->definition.id; - data.textureKey = resource->definition.id; - data.scaleWidth = resource->definition.defaultSize.x; - data.scaleHeight = resource->definition.defaultSize.y; - data.x = worldPos.x; - data.y = worldPos.y; - data.scrollSpeed = resource->definition.defaultScrollSpeed; - data.layer = resource->definition.defaultLayer; - data.enemyType = resource->definition.enemyType; - data.powerUpType = resource->definition.powerUpType; - - initializeDefaultCollider(data); - - m_entities.push_back(data); - EditorEntityData& stored = m_entities.back(); - applyEntityToComponents(stored); - - m_selectedIndex = static_cast(m_entities.size()) - 1; - stored.isSelected = true; - - return &stored; - } - - void EditorEntityManager::DrawPlacementPreview(EditorMode mode, - EditorEntityType type, - const std::string& identifier, - const Math::Vector2& worldPos) const { - if (!m_renderer || mode == EditorMode::SELECT) { - return; - } - - Math::Vector2 size = getDefaultSize(type); - if (!identifier.empty()) { - if (const auto* resource = getResource(identifier)) { - size = resource->definition.defaultSize; - } - } - Math::Rectangle rect; - rect.position = {worldPos.x - size.x / 2.0f, worldPos.y - size.y / 2.0f}; - rect.size = {size.x, size.y}; - - Math::Color color = getColor(type); - color.a = 0.35f; - m_renderer->DrawRectangle(rect, color); - } - - void EditorEntityManager::DrawSelectionOutline() const { - if (!m_renderer || !GetSelectedEntity()) { - return; - } - drawOutline(*GetSelectedEntity()); - } - - bool EditorEntityManager::SelectAt(const Math::Vector2& worldPos) { - bool found = false; - for (auto& entity : m_entities) { - entity.isSelected = false; - } - - for (int i = static_cast(m_entities.size()) - 1; i >= 0; --i) { - const auto& entity = m_entities[static_cast(i)]; - if (EditorGeometry::PointInEntityBounds(worldPos, entity.x, entity.y, entity.scaleWidth, entity.scaleHeight)) { - m_entities[static_cast(i)].isSelected = true; - m_selectedIndex = i; - found = true; - break; - } - } - - if (!found) { - m_selectedIndex = -1; - } - - return found; - } - - void EditorEntityManager::ClearSelection() { - for (auto& entity : m_entities) { - entity.isSelected = false; - } - m_selectedIndex = -1; - } - - bool EditorEntityManager::DeleteSelected() { - if (!GetSelectedEntity()) { - return false; - } - - destroyEntity(m_entities[static_cast(m_selectedIndex)]); - m_entities.erase(m_entities.begin() + m_selectedIndex); - - if (m_entities.empty()) { - m_selectedIndex = -1; - } else { - m_selectedIndex = std::min(m_selectedIndex, static_cast(m_entities.size()) - 1); - if (m_selectedIndex >= 0) { - m_entities[static_cast(m_selectedIndex)].isSelected = true; - } - } - - return true; - } - - EditorEntityData* EditorEntityManager::GetSelectedEntity() { - if (m_selectedIndex < 0 || m_selectedIndex >= static_cast(m_entities.size())) { - return nullptr; - } - return &m_entities[static_cast(m_selectedIndex)]; - } - - const EditorEntityData* EditorEntityManager::GetSelectedEntity() const { - if (m_selectedIndex < 0 || m_selectedIndex >= static_cast(m_entities.size())) { - return nullptr; - } - return &m_entities[static_cast(m_selectedIndex)]; - } - - void EditorEntityManager::SyncEntity(EditorEntityData& data) { - applyEntityToComponents(data); - } - - void EditorEntityManager::RebuildDefaultCollider(EditorEntityData& data) { - initializeDefaultCollider(data); - } - - const EditorAssetResource* EditorEntityManager::getResource(const std::string& id) const { - return m_assets.GetResource(id); - } - - void EditorEntityManager::applyEntityToComponents(EditorEntityData& data) { - destroyEntity(data); - - auto* resource = getResource(data.presetId.empty() ? data.textureKey : data.presetId); - if (!resource || !m_renderer) { - return; - } - - float left = data.x - data.scaleWidth / 2.0f; - float top = data.y - data.scaleHeight / 2.0f; - - data.entity = m_registry.CreateEntity(); - m_registry.AddComponent(data.entity, ECS::Position{left, top}); - - auto& drawable = m_registry.AddComponent(data.entity, ECS::Drawable(resource->spriteId, data.layer)); - drawable.scale = {data.scaleWidth / resource->textureSize.x, data.scaleHeight / resource->textureSize.y}; - drawable.origin = {0.0f, 0.0f}; - - if (data.scrollSpeed != 0.0f) { - m_registry.AddComponent(data.entity, ECS::Scrollable(data.scrollSpeed)); - } - - createColliderEntities(data); - } - - void EditorEntityManager::destroyEntity(EditorEntityData& data) { - if (data.entity != ECS::NULL_ENTITY && m_registry.IsEntityAlive(data.entity)) { - m_registry.DestroyEntity(data.entity); - } - data.entity = ECS::NULL_ENTITY; - - for (ECS::Entity collider : data.colliderEntities) { - if (m_registry.IsEntityAlive(collider)) { - m_registry.DestroyEntity(collider); - } - } - data.colliderEntities.clear(); - } - - void EditorEntityManager::drawOutline(const EditorEntityData& entity) const { - Math::Rectangle rect = EditorGeometry::BuildRect( - entity.x - entity.scaleWidth / 2.0f, - entity.y - entity.scaleHeight / 2.0f, - entity.scaleWidth, - entity.scaleHeight); - - EditorDrawing::DrawRectangleOutline(m_renderer, rect, - Selection::OUTLINE_COLOR, Selection::OUTLINE_THICKNESS); - } - - Math::Vector2 EditorEntityManager::getDefaultSize(EditorEntityType type) const { - switch (type) { - case EditorEntityType::ENEMY: - return {80.0f, 60.0f}; - case EditorEntityType::POWERUP: - return {50.0f, 50.0f}; - case EditorEntityType::PLAYER_SPAWN: - return {40.0f, 40.0f}; - case EditorEntityType::BACKGROUND: - return {1280.0f, 720.0f}; - case EditorEntityType::OBSTACLE: - default: - return {120.0f, 80.0f}; - } - } - - Math::Color EditorEntityManager::getColor(EditorEntityType type) const { - switch (type) { - case EditorEntityType::ENEMY: - return {0.95f, 0.35f, 0.35f, 1.0f}; - case EditorEntityType::POWERUP: - return {0.4f, 0.9f, 0.6f, 1.0f}; - case EditorEntityType::PLAYER_SPAWN: - return {0.9f, 0.9f, 0.1f, 1.0f}; - case EditorEntityType::BACKGROUND: - return {0.4f, 0.5f, 0.95f, 1.0f}; - case EditorEntityType::OBSTACLE: - default: - return {0.6f, 0.7f, 0.95f, 1.0f}; - } - } - - void EditorEntityManager::DrawColliders(int selectedColliderIndex) const { - if (m_colliderManager) { - m_colliderManager->DrawColliders(GetSelectedEntity(), selectedColliderIndex); - } - } - - void EditorEntityManager::DrawColliderHandles(int colliderIndex) const { - if (m_colliderManager) { - m_colliderManager->DrawHandles(GetSelectedEntity(), colliderIndex); - } - } - - ColliderHandle EditorEntityManager::GetColliderHandleAt(const Math::Vector2& worldPos) const { - if (m_colliderManager) { - return m_colliderManager->GetHandleAt(GetSelectedEntity(), worldPos); - } - return {}; - } - - void EditorEntityManager::AddCollider(const Math::Vector2& worldPos) { - auto* entity = GetSelectedEntity(); - if (!entity || !m_colliderManager) { - return; - } - - m_selectedColliderIndex = m_colliderManager->AddCollider(*entity, worldPos); - SyncEntity(*entity); - } - - bool EditorEntityManager::RemoveCollider(int colliderIndex) { - auto* entity = GetSelectedEntity(); - if (!entity || !m_colliderManager) { - return false; - } - - if (m_colliderManager->RemoveCollider(*entity, colliderIndex)) { - SyncEntity(*entity); - - // Update selected collider index - if (m_selectedColliderIndex == colliderIndex) { - m_selectedColliderIndex = -1; - } else if (m_selectedColliderIndex > colliderIndex) { - m_selectedColliderIndex--; - } - - return true; - } - - return false; - } - - void EditorEntityManager::ResizeCollider(int colliderIndex, ColliderHandle::Type handleType, const Math::Vector2& worldPos) { - auto* entity = GetSelectedEntity(); - if (!entity || !m_colliderManager) { - return; - } - - m_colliderManager->ResizeCollider(*entity, colliderIndex, handleType, worldPos); - SyncEntity(*entity); - } - - void EditorEntityManager::initializeDefaultCollider(EditorEntityData& data) const { - data.colliders.clear(); - ECS::ColliderDef collider; - collider.x = data.x - data.scaleWidth / 2.0f; - collider.y = data.y - data.scaleHeight / 2.0f; - collider.width = data.scaleWidth; - collider.height = data.scaleHeight; - data.colliders.push_back(collider); - } - - void EditorEntityManager::createColliderEntities(EditorEntityData& data) { - data.colliderEntities.clear(); - for (const auto& collider : data.colliders) { - ECS::Entity colliderEntity = m_registry.CreateEntity(); - m_registry.AddComponent(colliderEntity, ECS::Position{collider.x, collider.y}); - m_registry.AddComponent(colliderEntity, ECS::BoxCollider{collider.width, collider.height}); - m_registry.AddComponent(colliderEntity, ECS::Scrollable(data.scrollSpeed)); - m_registry.AddComponent(colliderEntity, ECS::Obstacle(true)); - m_registry.AddComponent(colliderEntity, ECS::CollisionLayer(ECS::CollisionLayers::OBSTACLE, ECS::CollisionLayers::ALL)); - data.colliderEntities.push_back(colliderEntity); - } - } - - } -} - diff --git a/client/src/editor/EditorFileManager.cpp b/client/src/editor/EditorFileManager.cpp deleted file mode 100644 index 9eec9a1..0000000 --- a/client/src/editor/EditorFileManager.cpp +++ /dev/null @@ -1,269 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** EditorFileManager - Save/Load level files implementation -*/ - -#include "editor/EditorFileManager.hpp" -#include "ECS/Component.hpp" -#include "Core/Logger.hpp" -#include -#include - -namespace RType { - namespace Client { - - EditorFileManager::EditorFileManager(EditorAssetLibrary& assets) - : m_assets(assets), m_lastError("") { - } - - bool EditorFileManager::SaveLevel(const std::string& path, - const std::vector& entities, - const std::string& levelName) { - m_lastError.clear(); - ECS::LevelData levelData = GatherLevelData(entities, levelName); - - std::vector candidatePaths; - candidatePaths.emplace_back(std::filesystem::path("..") / path); - candidatePaths.emplace_back(path); - - std::string lastFailure; - - for (const auto& candidate : candidatePaths) { - try { - if (candidate.has_parent_path()) { - std::error_code ec; - std::filesystem::create_directories(candidate.parent_path(), ec); - if (ec) { - throw std::runtime_error("Failed to create directories for " - + candidate.parent_path().string() + ": " + ec.message()); - } - } - - ECS::LevelLoader::SaveToFile(levelData, candidate.string()); - - Core::Logger::Info("[EditorFileManager] Level '{}' saved to {}", - levelName, candidate.string()); - return true; - - } catch (const std::exception& e) { - lastFailure = e.what(); - Core::Logger::Warning("[EditorFileManager] Save attempt failed for {}: {}", - candidate.string(), e.what()); - } - } - - m_lastError = lastFailure.empty() - ? "Save failed: no valid path candidates" - : std::string("Save failed: ") + lastFailure; - Core::Logger::Error("[EditorFileManager] {}", m_lastError); - return false; - } - - std::vector EditorFileManager::LoadLevel(const std::string& path, - ECS::Registry& registry, - Renderer::IRenderer* renderer) { - std::vector result; - - try { - m_lastError.clear(); - - ECS::LevelData levelData = ECS::LevelLoader::LoadFromFile(path); - - if (!levelData.background.texture.empty()) { - result.push_back(ConvertBackgroundToEditor(levelData.background, registry, renderer)); - } - - for (const auto& obs : levelData.obstacles) { - result.push_back(ConvertObstacleToEditor(obs, registry, renderer)); - } - - for (const auto& enemy : levelData.enemies) { - result.push_back(ConvertEnemyToEditor(enemy, registry, renderer)); - } - - for (const auto& spawn : levelData.playerSpawns) { - result.push_back(ConvertSpawnToEditor(spawn, registry, renderer)); - } - - Core::Logger::Info("[EditorFileManager] Successfully loaded {} entities from {}", - result.size(), path); - - } catch (const std::exception& e) { - m_lastError = std::string("Load failed: ") + e.what(); - Core::Logger::Error("[EditorFileManager] {}", m_lastError); - result.clear(); - } - - return result; - } - - ECS::LevelData EditorFileManager::GatherLevelData(const std::vector& entities, - const std::string& levelName) { - ECS::LevelData level; - level.name = levelName; - - level.textures["background"] = "assets/backgrounds/Cave_one.png"; - level.textures["obstacle1"] = "assets/backgrounds/obstacles/middle_obstacle.png"; - level.textures["obstacle2"] = "assets/backgrounds/obstacles/obstacle_bas.png"; - level.textures["obstacle3"] = "assets/backgrounds/obstacles/little_obstacle.png"; - level.textures["obstacle4"] = "assets/backgrounds/obstacles/obstacle_square.png"; - level.textures["obstacle5"] = "assets/backgrounds/obstacles/obstacle_bas_two.png"; - level.textures["obstacle6"] = "assets/backgrounds/obstacles/obstacle_haut_two.png"; - level.textures["obstacle7"] = "assets/backgrounds/obstacles/obstacle_yellow.png"; - level.textures["obstacle8"] = "assets/backgrounds/obstacles/little_obstacle_haut.png"; - level.textures["player_blue"] = "assets/spaceships/player_blue.png"; - level.textures["player_green"] = "assets/spaceships/player_green.png"; - level.textures["player_red"] = "assets/spaceships/player_red.png"; - level.textures["enemy-green"] = "assets/spaceships/enemy-green.png"; - level.textures["enemy-red"] = "assets/spaceships/enemy-red.png"; - level.textures["enemy-blue"] = "assets/spaceships/enemy-blue.png"; - level.textures["bullet"] = "assets/projectiles/bullet.png"; - level.textures["powerup-spread"] = "assets/powerups/spread.png"; - level.textures["powerup-laser"] = "assets/powerups/laser.png"; - level.textures["powerup-force-pod"] = "assets/powerups/force_pod.png"; - level.textures["powerup-speed"] = "assets/powerups/speed.png"; - level.textures["powerup-shield"] = "assets/powerups/shield.png"; - - level.fonts["main"] = {"assets/fonts/PressStart2P-Regular.ttf", 16}; - level.fonts["small"] = {"assets/fonts/PressStart2P-Regular.ttf", 12}; - - bool hasBackground = false; - - for (const auto& entity : entities) { - switch (entity.type) { - case EditorEntityType::BACKGROUND: { - if (!hasBackground) { - level.background.texture = entity.textureKey; - level.background.scrollSpeed = entity.scrollSpeed; - level.background.copies = 3; - level.background.layer = entity.layer; - hasBackground = true; - } - break; - } - - case EditorEntityType::OBSTACLE: { - ECS::ObstacleDef obs; - obs.texture = entity.textureKey; - obs.x = entity.x; - obs.y = entity.y; - obs.scaleWidth = entity.scaleWidth; - obs.scaleHeight = entity.scaleHeight; - obs.scrollSpeed = entity.scrollSpeed; - obs.layer = entity.layer; - obs.colliders = entity.colliders; - level.obstacles.push_back(obs); - break; - } - - case EditorEntityType::ENEMY: { - ECS::EnemyDef enemy; - enemy.type = entity.enemyType; - enemy.x = entity.x; - enemy.y = entity.y; - level.enemies.push_back(enemy); - break; - } - - case EditorEntityType::PLAYER_SPAWN: { - ECS::PlayerSpawnDef spawn; - spawn.x = entity.x; - spawn.y = entity.y; - level.playerSpawns.push_back(spawn); - break; - } - - case EditorEntityType::POWERUP: - break; - } - } - - if (!hasBackground) { - level.background.texture = "background"; - level.background.scrollSpeed = -50.0f; - level.background.copies = 3; - level.background.layer = -100; - } - - if (level.playerSpawns.empty()) { - level.playerSpawns.push_back({100.0f, 200.0f}); - level.playerSpawns.push_back({100.0f, 360.0f}); - level.playerSpawns.push_back({100.0f, 520.0f}); - level.playerSpawns.push_back({100.0f, 680.0f}); - } - - return level; - } - - EditorEntityData EditorFileManager::ConvertObstacleToEditor(const ECS::ObstacleDef& obs, - ECS::Registry&, - Renderer::IRenderer*) { - EditorEntityData data; - data.type = EditorEntityType::OBSTACLE; - data.textureKey = obs.texture; - data.presetId = obs.texture; - data.x = obs.x; - data.y = obs.y; - data.scaleWidth = obs.scaleWidth; - data.scaleHeight = obs.scaleHeight; - data.scrollSpeed = obs.scrollSpeed; - data.layer = obs.layer; - data.colliders = obs.colliders; - data.entity = ECS::NULL_ENTITY; - - return data; - } - - EditorEntityData EditorFileManager::ConvertEnemyToEditor(const ECS::EnemyDef& enemy, - ECS::Registry& registry, - Renderer::IRenderer*) { - EditorEntityData data; - data.type = EditorEntityType::ENEMY; - data.enemyType = enemy.type; - data.x = enemy.x; - data.y = enemy.y; - data.scaleWidth = 50.0f; - data.scaleHeight = 50.0f; - data.layer = 10; - data.entity = ECS::NULL_ENTITY; - - return data; - } - - EditorEntityData EditorFileManager::ConvertSpawnToEditor(const ECS::PlayerSpawnDef& spawn, - ECS::Registry&, - Renderer::IRenderer*) { - EditorEntityData data; - data.type = EditorEntityType::PLAYER_SPAWN; - data.x = spawn.x; - data.y = spawn.y; - data.scaleWidth = 40.0f; - data.scaleHeight = 40.0f; - data.layer = 5; - data.entity = ECS::NULL_ENTITY; - - return data; - } - - EditorEntityData EditorFileManager::ConvertBackgroundToEditor(const ECS::BackgroundDef& bg, - ECS::Registry&, - Renderer::IRenderer*) { - EditorEntityData data; - data.type = EditorEntityType::BACKGROUND; - data.textureKey = bg.texture; - data.presetId = bg.texture; - data.x = 0.0f; - data.y = 0.0f; - data.scaleWidth = 1280.0f; - data.scaleHeight = 720.0f; - data.scrollSpeed = bg.scrollSpeed; - data.layer = bg.layer; - data.entity = ECS::NULL_ENTITY; - - return data; - } - - } -} diff --git a/client/src/editor/EditorInputHandler.cpp b/client/src/editor/EditorInputHandler.cpp deleted file mode 100644 index 721a3dc..0000000 --- a/client/src/editor/EditorInputHandler.cpp +++ /dev/null @@ -1,41 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** EditorInputHandler -*/ - -#include "editor/EditorInputHandler.hpp" - -namespace RType { - namespace Client { - - EditorInputHandler::EditorInputHandler(Renderer::IRenderer* renderer) - : m_renderer(renderer) - { - } - - void EditorInputHandler::HandleKeyPress(Renderer::Key key, const std::function& action) { - if (!m_renderer) { - return; - } - - bool isCurrentlyPressed = m_renderer->IsKeyPressed(key); - bool wasPreviouslyPressed = m_keyStates[key]; - - // Action triggers only on transition from not pressed to pressed - if (isCurrentlyPressed && !wasPreviouslyPressed) { - action(); - } - - m_keyStates[key] = isCurrentlyPressed; - } - - void EditorInputHandler::Update() { - // Update is now handled within HandleKeyPress itself - // This method is kept for potential future frame-based reset logic - // if needed, but currently the state is updated immediately - } - - } -} diff --git a/client/src/editor/EditorPropertyManager.cpp b/client/src/editor/EditorPropertyManager.cpp deleted file mode 100644 index 04678c3..0000000 --- a/client/src/editor/EditorPropertyManager.cpp +++ /dev/null @@ -1,219 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** EditorPropertyManager -*/ - -#include "editor/EditorPropertyManager.hpp" -#include "editor/EditorConstants.hpp" -#include - -using namespace RType::Client::EditorConstants; - -namespace RType { - namespace Client { - - EditorPropertyManager::EditorPropertyManager(Renderer::IRenderer* renderer) - : m_renderer(renderer) - { - } - - bool EditorPropertyManager::HandleInput(EditorEntityData* selectedEntity, - std::function)> handleKeyPress) { - if (!selectedEntity) { - return false; - } - - bool consumedDirectional = m_renderer->IsKeyPressed(Renderer::Key::Up) || - m_renderer->IsKeyPressed(Renderer::Key::Down); - - // Tab: cycle through properties - handleKeyPress(Renderer::Key::Tab, [this]() { - cycleProperty(); - }); - - // Arrow keys: adjust property values - handleKeyPress(Renderer::Key::Up, [this, selectedEntity]() { - applyPropertyDelta(*selectedEntity, getPropertyStep()); - }); - handleKeyPress(Renderer::Key::Down, [this, selectedEntity]() { - applyPropertyDelta(*selectedEntity, -getPropertyStep()); - }); - - // Number keys: direct value input - const Renderer::Key digitKeys[10] = { - Renderer::Key::Num0, Renderer::Key::Num1, Renderer::Key::Num2, Renderer::Key::Num3, Renderer::Key::Num4, - Renderer::Key::Num5, Renderer::Key::Num6, Renderer::Key::Num7, Renderer::Key::Num8, Renderer::Key::Num9 - }; - - for (int i = 0; i < 10; ++i) { - handleKeyPress(digitKeys[i], [this, selectedEntity, key = digitKeys[i]]() { - handleNumberInput(key); - // Apply the new value immediately if buffer is not empty - if (!m_inputBuffer.empty()) { - setPropertyValue(*selectedEntity, std::stof(m_inputBuffer)); - } - }); - } - - // Backspace: remove digit or delete entity - handleKeyPress(Renderer::Key::Backspace, [this, selectedEntity]() { - handleBackspace(selectedEntity); - }); - - // Enter: commit current value - handleKeyPress(Renderer::Key::Enter, [this]() { - ClearInput(); - }); - - return consumedDirectional; - } - - void EditorPropertyManager::ClearInput() { - m_inputBuffer.clear(); - } - - void EditorPropertyManager::SetOnPropertyChanged(std::function callback) { - m_onPropertyChanged = callback; - } - - void EditorPropertyManager::SetOnEntityDeleted(std::function callback) { - m_onEntityDeleted = callback; - } - - void EditorPropertyManager::SetOnPropertyCycled(std::function callback) { - m_onPropertyCycled = callback; - } - - void EditorPropertyManager::cycleProperty() { - // Only cycle through the 6 visible properties (POSITION_X to SCROLL_SPEED) - // Skip collider properties (COLLIDER_X to COLLIDER_HEIGHT) as they have their own UI - const int numVisibleProperties = static_cast(EditableProperty::SCROLL_SPEED) + 1; - int current = static_cast(m_activeProperty); - current = (current + 1) % numVisibleProperties; - m_activeProperty = static_cast(current); - ClearInput(); - - if (m_onPropertyCycled) { - m_onPropertyCycled(); - } - } - - void EditorPropertyManager::applyPropertyDelta(EditorEntityData& entity, float delta) { - float newValue = getPropertyValue(entity) + delta; - setPropertyValue(entity, newValue); - ClearInput(); - } - - void EditorPropertyManager::setPropertyValue(EditorEntityData& entity, float value) { - switch (m_activeProperty) { - case EditableProperty::POSITION_X: - entity.x = value; - break; - case EditableProperty::POSITION_Y: - entity.y = value; - break; - case EditableProperty::SCALE_WIDTH: - entity.scaleWidth = std::max(PropertySteps::MIN_SCALE, value); - break; - case EditableProperty::SCALE_HEIGHT: - entity.scaleHeight = std::max(PropertySteps::MIN_SCALE, value); - break; - case EditableProperty::LAYER: - entity.layer = static_cast(value); - break; - case EditableProperty::SCROLL_SPEED: - entity.scrollSpeed = value; - break; - case EditableProperty::COUNT: - break; - } - - // Notify that property changed (for collider rebuild and UI update) - if (m_onPropertyChanged) { - m_onPropertyChanged(entity); - } - } - - float EditorPropertyManager::getPropertyValue(const EditorEntityData& entity) const { - switch (m_activeProperty) { - case EditableProperty::POSITION_X: - return entity.x; - case EditableProperty::POSITION_Y: - return entity.y; - case EditableProperty::SCALE_WIDTH: - return entity.scaleWidth; - case EditableProperty::SCALE_HEIGHT: - return entity.scaleHeight; - case EditableProperty::LAYER: - return static_cast(entity.layer); - case EditableProperty::SCROLL_SPEED: - return entity.scrollSpeed; - case EditableProperty::COUNT: - break; - } - return 0.0f; - } - - float EditorPropertyManager::getPropertyStep() const { - switch (m_activeProperty) { - case EditableProperty::POSITION_X: - case EditableProperty::POSITION_Y: - return PropertySteps::POSITION_STEP; - case EditableProperty::SCALE_WIDTH: - case EditableProperty::SCALE_HEIGHT: - return PropertySteps::SCALE_STEP; - case EditableProperty::LAYER: - return PropertySteps::LAYER_STEP; - case EditableProperty::SCROLL_SPEED: - return PropertySteps::SCROLL_SPEED_STEP; - case EditableProperty::COUNT: - break; - } - return PropertySteps::LAYER_STEP; - } - - void EditorPropertyManager::handleNumberInput(Renderer::Key key) { - if (m_inputBuffer.size() >= Input::MAX_INPUT_BUFFER_SIZE) { - return; - } - - char digit = '0'; - switch (key) { - case Renderer::Key::Num0: digit = '0'; break; - case Renderer::Key::Num1: digit = '1'; break; - case Renderer::Key::Num2: digit = '2'; break; - case Renderer::Key::Num3: digit = '3'; break; - case Renderer::Key::Num4: digit = '4'; break; - case Renderer::Key::Num5: digit = '5'; break; - case Renderer::Key::Num6: digit = '6'; break; - case Renderer::Key::Num7: digit = '7'; break; - case Renderer::Key::Num8: digit = '8'; break; - case Renderer::Key::Num9: digit = '9'; break; - default: - return; - } - - m_inputBuffer.push_back(digit); - // Note: We don't have access to selectedEntity here, so we rely on the - // callback pattern where EditorState will call setPropertyValue - } - - void EditorPropertyManager::handleBackspace(EditorEntityData* entity) { - if (!m_inputBuffer.empty()) { - m_inputBuffer.pop_back(); - if (!m_inputBuffer.empty() && entity) { - // Update property with new buffer value - setPropertyValue(*entity, std::stof(m_inputBuffer)); - } - } else { - // Empty buffer + backspace = delete entity - if (m_onEntityDeleted) { - m_onEntityDeleted(); - } - } - } - - } -} diff --git a/client/src/editor/EditorUIManager.cpp b/client/src/editor/EditorUIManager.cpp deleted file mode 100644 index bb2f344..0000000 --- a/client/src/editor/EditorUIManager.cpp +++ /dev/null @@ -1,433 +0,0 @@ -#include "editor/EditorUIManager.hpp" -#include "editor/EditorConstants.hpp" -#include "editor/EditorGeometry.hpp" -#include "ECS/Components/TextLabel.hpp" -#include - -using namespace RType::Client::EditorConstants; - -namespace RType { - namespace Client { - - EditorUIManager::EditorUIManager(Renderer::IRenderer* renderer, - EditorAssetLibrary& assets, - ECS::Registry& registry, - std::vector& trackedEntities, - Renderer::FontId fontSmall, - Renderer::FontId fontMedium) - : m_renderer(renderer) - , m_assets(assets) - , m_registry(registry) - , m_trackedEntities(trackedEntities) - , m_fontSmall(fontSmall) - , m_fontMedium(fontMedium) - { - } - - void EditorUIManager::InitializePalette() { - m_entries.clear(); - m_actionButtons.clear(); - float cursorY = UI::PALETTE_START_Y; - m_activeSelection = EditorPaletteSelection{}; - - InitializeToolbar(); - - createCategoryLabel("TOOLS", cursorY); - cursorY += UI::PALETTE_BUTTON_HEIGHT; - PaletteEntry selectEntry; - selectEntry.label = "SELECT"; - selectEntry.mode = EditorMode::SELECT; - selectEntry.entityType = EditorEntityType::OBSTACLE; - createPaletteButton(selectEntry, cursorY); - cursorY += UI::PALETTE_BUTTON_HEIGHT * 1.5f; - - auto createSection = [&](const std::string& label, EditorEntityType type, EditorMode mode) { - createCategoryLabel(label, cursorY); - cursorY += UI::PALETTE_BUTTON_HEIGHT; - const auto& resources = m_assets.GetResources(type); - for (const auto* resource : resources) { - PaletteEntry entry; - entry.label = resource->definition.displayName; - entry.mode = mode; - entry.entityType = type; - entry.subtype = resource->definition.id; - entry.resource = resource; - createPaletteButton(entry, cursorY); - cursorY += UI::PALETTE_BUTTON_HEIGHT; - } - cursorY += UI::PALETTE_BUTTON_HEIGHT * 0.5f; - }; - - createSection("ENEMIES", EditorEntityType::ENEMY, EditorMode::PLACE_ENEMY); - createSection("OBSTACLES", EditorEntityType::OBSTACLE, EditorMode::PLACE_OBSTACLE); - createSection("POWERUPS", EditorEntityType::POWERUP, EditorMode::PLACE_POWERUP); - createSection("PLAYER SPAWNS", EditorEntityType::PLAYER_SPAWN, EditorMode::PLACE_PLAYER_SPAWN); - createSection("BACKGROUNDS", EditorEntityType::BACKGROUND, EditorMode::PLACE_BACKGROUND); - - SetActiveSelection(m_activeSelection); - InitializePropertiesPanel(); - InitializeColliderPanel(); - } - - void EditorUIManager::InitializeColliderPanel() { - float startY = UI::PROPERTY_PANEL_START_Y + 320.0f; - - m_colliderPanelHeader = m_registry.CreateEntity(); - m_trackedEntities.push_back(m_colliderPanelHeader); - m_registry.AddComponent(m_colliderPanelHeader, ECS::Position{UI::PROPERTY_PANEL_X, startY}); - - ECS::TextLabel headerLabel("COLLIDERS", m_fontMedium, 18); - headerLabel.centered = false; - headerLabel.color = Colors::UI_HEADER; - m_registry.AddComponent(m_colliderPanelHeader, std::move(headerLabel)); - - m_colliderCountEntity = m_registry.CreateEntity(); - m_trackedEntities.push_back(m_colliderCountEntity); - m_registry.AddComponent(m_colliderCountEntity, ECS::Position{UI::PROPERTY_PANEL_X, startY + 32.0f}); - - ECS::TextLabel countLabel("No colliders", m_fontSmall, 13); - countLabel.centered = false; - countLabel.color = Colors::UI_TEXT; - m_registry.AddComponent(m_colliderCountEntity, std::move(countLabel)); - - m_colliderHintEntity = m_registry.CreateEntity(); - m_trackedEntities.push_back(m_colliderHintEntity); - m_registry.AddComponent(m_colliderHintEntity, ECS::Position{UI::PROPERTY_PANEL_X, startY + 120.0f}); - - ECS::TextLabel hintLabel("Shown in green", m_fontSmall, 11); - hintLabel.centered = false; - hintLabel.color = Colors::UI_HINT; - m_registry.AddComponent(m_colliderHintEntity, std::move(hintLabel)); - } - - void EditorUIManager::InitializePropertiesPanel() { - m_propertyFields.clear(); - - float headerY = UI::PROPERTY_PANEL_START_Y; - m_propertiesHeader = m_registry.CreateEntity(); - m_trackedEntities.push_back(m_propertiesHeader); - m_registry.AddComponent(m_propertiesHeader, ECS::Position{UI::PROPERTY_PANEL_X, headerY}); - - ECS::TextLabel headerLabel("PROPERTIES", m_fontMedium, 18); - headerLabel.centered = false; - headerLabel.color = Colors::UI_HEADER; - m_registry.AddComponent(m_propertiesHeader, std::move(headerLabel)); - - m_selectedInfoEntity = m_registry.CreateEntity(); - m_trackedEntities.push_back(m_selectedInfoEntity); - m_registry.AddComponent(m_selectedInfoEntity, ECS::Position{UI::PROPERTY_PANEL_X, headerY + 32.0f}); - - ECS::TextLabel infoLabel("No entity selected", m_fontSmall, 14); - infoLabel.centered = false; - infoLabel.color = Colors::UI_TEXT; - m_registry.AddComponent(m_selectedInfoEntity, std::move(infoLabel)); - - m_propertyHintEntity = m_registry.CreateEntity(); - m_trackedEntities.push_back(m_propertyHintEntity); - m_registry.AddComponent(m_propertyHintEntity, ECS::Position{UI::PROPERTY_PANEL_X, headerY + 240.0f}); - - ECS::TextLabel hintLabel("Tab | ↑↓ | 0-9 | Backspace", m_fontSmall, 11); - hintLabel.centered = false; - hintLabel.color = Colors::UI_HINT; - m_registry.AddComponent(m_propertyHintEntity, std::move(hintLabel)); - - const std::vector> propertyRows = { - {"POS X", EditableProperty::POSITION_X}, - {"POS Y", EditableProperty::POSITION_Y}, - {"WIDTH", EditableProperty::SCALE_WIDTH}, - {"HEIGHT", EditableProperty::SCALE_HEIGHT}, - {"LAYER", EditableProperty::LAYER}, - {"SCROLL", EditableProperty::SCROLL_SPEED}, - }; - - float rowY = headerY + 80.0f; - for (const auto& [label, property] : propertyRows) { - PropertyField field; - field.property = property; - - field.nameEntity = m_registry.CreateEntity(); - m_trackedEntities.push_back(field.nameEntity); - m_registry.AddComponent(field.nameEntity, ECS::Position{UI::PROPERTY_PANEL_X, rowY}); - - ECS::TextLabel nameLabel(label, m_fontSmall, 13); - nameLabel.centered = false; - nameLabel.color = Colors::UI_TEXT; - m_registry.AddComponent(field.nameEntity, std::move(nameLabel)); - - field.valueEntity = m_registry.CreateEntity(); - m_trackedEntities.push_back(field.valueEntity); - m_registry.AddComponent(field.valueEntity, ECS::Position{UI::PROPERTY_PANEL_X + UI::PROPERTY_VALUE_OFFSET_X, rowY}); - - ECS::TextLabel valueLabel("--", m_fontSmall, 13); - valueLabel.centered = false; - valueLabel.color = Colors::UI_TEXT; - m_registry.AddComponent(field.valueEntity, std::move(valueLabel)); - - m_propertyFields.push_back(field); - rowY += UI::PROPERTY_ROW_HEIGHT; - } - } - - void EditorUIManager::UpdateHover(Math::Vector2 mouseScreen) { - bool hoverChanged = false; - for (auto& entry : m_entries) { - bool inside = EditorGeometry::PointInRect(mouseScreen, entry.bounds); - if (entry.hovered != inside) { - entry.hovered = inside; - hoverChanged = true; - } - } - - for (auto& button : m_actionButtons) { - bool inside = EditorGeometry::PointInRect(mouseScreen, button.bounds); - if (button.hovered != inside) { - button.hovered = inside; - hoverChanged = true; - } - } - - if (hoverChanged) { - refreshPaletteVisuals(); - refreshActionButtons(); - } - } - - bool EditorUIManager::HandleActionClick(Math::Vector2 mouseScreen) { - for (const auto& button : m_actionButtons) { - if (EditorGeometry::PointInRect(mouseScreen, button.bounds)) { - if (button.onClick) { - button.onClick(); - } - return true; - } - } - return false; - } - - std::optional EditorUIManager::HandleClick(Math::Vector2 mouseScreen) { - for (const auto& entry : m_entries) { - if (EditorGeometry::PointInRect(mouseScreen, entry.bounds)) { - EditorPaletteSelection selection; - selection.mode = entry.mode; - selection.entityType = entry.entityType; - selection.subtype = entry.subtype; - SetActiveSelection(selection); - return selection; - } - } - - return std::nullopt; - } - - void EditorUIManager::SetActiveSelection(const EditorPaletteSelection& selection) { - m_activeSelection = selection; - refreshPaletteVisuals(); - refreshActionButtons(); - } - - void EditorUIManager::UpdatePropertyPanel(const EditorEntityData* selected, - EditableProperty activeProperty, - const std::string& inputBuffer) { - std::string infoText = "No entity selected"; - if (selected) { - std::ostringstream oss; - oss << "Type: "; - switch (selected->type) { - case EditorEntityType::ENEMY: - oss << "Enemy (" << (selected->enemyType.empty() ? "BASIC" : selected->enemyType) << ")"; - break; - case EditorEntityType::POWERUP: - oss << "PowerUp (" << (selected->powerUpType.empty() ? "DEFAULT" : selected->powerUpType) << ")"; - break; - case EditorEntityType::PLAYER_SPAWN: - oss << "Player Spawn"; - break; - case EditorEntityType::BACKGROUND: - oss << "Background"; - break; - case EditorEntityType::OBSTACLE: - default: - oss << "Obstacle"; - break; - } - infoText = oss.str(); - } - - if (m_selectedInfoEntity != ECS::NULL_ENTITY && m_registry.IsEntityAlive(m_selectedInfoEntity)) { - auto& label = m_registry.GetComponent(m_selectedInfoEntity); - label.text = infoText; - } - - for (auto& field : m_propertyFields) { - if (!m_registry.IsEntityAlive(field.valueEntity)) { - continue; - } - - auto& valueLabel = m_registry.GetComponent(field.valueEntity); - if (!selected) { - valueLabel.text = "--"; - valueLabel.color = {0.5f, 0.5f, 0.5f, 0.7f}; - continue; - } - - float value = 0.0f; - switch (field.property) { - case EditableProperty::POSITION_X: value = selected->x; break; - case EditableProperty::POSITION_Y: value = selected->y; break; - case EditableProperty::SCALE_WIDTH: value = selected->scaleWidth; break; - case EditableProperty::SCALE_HEIGHT: value = selected->scaleHeight; break; - case EditableProperty::LAYER: value = static_cast(selected->layer); break; - case EditableProperty::SCROLL_SPEED: value = selected->scrollSpeed; break; - case EditableProperty::COUNT: break; - } - - if (!inputBuffer.empty() && field.property == activeProperty) { - valueLabel.text = inputBuffer; - } else { - std::ostringstream oss; - oss.setf(std::ios::fixed); - oss.precision(field.property == EditableProperty::LAYER ? 0 : 1); - oss << value; - valueLabel.text = oss.str(); - } - - if (field.property == activeProperty) { - valueLabel.color = {1.0f, 0.85f, 0.35f, 1.0f}; - } else { - valueLabel.color = {0.95f, 0.95f, 0.95f, 1.0f}; - } - } - } - - void EditorUIManager::createCategoryLabel(const std::string& label, float y) { - float textX = UI::PALETTE_PANEL_LEFT + 10.0f; - ECS::Entity entity = m_registry.CreateEntity(); - m_trackedEntities.push_back(entity); - m_registry.AddComponent(entity, ECS::Position{textX, y}); - - ECS::TextLabel textLabel(label, m_fontSmall, 12); - textLabel.centered = false; - textLabel.color = Colors::UI_HEADER; - m_registry.AddComponent(entity, std::move(textLabel)); - } - - void EditorUIManager::createPaletteButton(PaletteEntry entry, float y) { - entry.bounds.position = {UI::PALETTE_PANEL_LEFT, y - (UI::PALETTE_BUTTON_HEIGHT / 2.0f)}; - entry.bounds.size = {UI::PALETTE_PANEL_WIDTH, UI::PALETTE_BUTTON_HEIGHT}; - - float textX = UI::PALETTE_PANEL_LEFT + 10.0f; - ECS::Entity entity = m_registry.CreateEntity(); - m_trackedEntities.push_back(entity); - m_registry.AddComponent(entity, ECS::Position{textX, y}); - - std::string labelText = entry.label; - if (entry.resource) { - labelText = entry.resource->definition.displayName; - } - - ECS::TextLabel label(labelText, m_fontSmall, 13); - label.centered = false; - label.color = {0.75f, 0.75f, 0.8f, 1.0f}; - m_registry.AddComponent(entity, std::move(label)); - - entry.textEntity = entity; - m_entries.push_back(std::move(entry)); - } - - void EditorUIManager::InitializeToolbar() { - float buttonY = UI::TOOLBAR_START_Y; - ActionButton saveButton; - saveButton.label = "SAVE LEVEL"; - saveButton.bounds.position = {UI::TOOLBAR_RIGHT_X, buttonY - (UI::TOOLBAR_BUTTON_HEIGHT / 2.0f)}; - saveButton.bounds.size = {UI::TOOLBAR_RIGHT_WIDTH, UI::TOOLBAR_BUTTON_HEIGHT}; - - float textX = UI::TOOLBAR_RIGHT_X + 10.0f; - ECS::Entity entity = m_registry.CreateEntity(); - m_trackedEntities.push_back(entity); - m_registry.AddComponent(entity, ECS::Position{textX, buttonY}); - - ECS::TextLabel label(saveButton.label, m_fontMedium, 14); - label.centered = false; - label.color = Colors::UI_TEXT; - m_registry.AddComponent(entity, std::move(label)); - - saveButton.textEntity = entity; - saveButton.onClick = [this]() { - if (m_onSaveRequested) { - m_onSaveRequested(); - } - }; - - m_actionButtons.push_back(std::move(saveButton)); - } - - - void EditorUIManager::refreshPaletteVisuals() { - for (auto& entry : m_entries) { - if (!m_registry.IsEntityAlive(entry.textEntity)) { - continue; - } - - auto& label = m_registry.GetComponent(entry.textEntity); - bool isSelected = entry.mode == m_activeSelection.mode; - if (!entry.subtype.empty() || !m_activeSelection.subtype.empty()) { - isSelected = isSelected && entry.subtype == m_activeSelection.subtype; - } - - if (isSelected) { - label.color = {1.0f, 0.85f, 0.35f, 1.0f}; - } else if (entry.hovered) { - label.color = {0.95f, 0.95f, 0.95f, 1.0f}; - } else { - label.color = {0.75f, 0.75f, 0.8f, 1.0f}; - } - } - } - - void EditorUIManager::refreshActionButtons() { - for (auto& button : m_actionButtons) { - if (!m_registry.IsEntityAlive(button.textEntity)) { - continue; - } - auto& label = m_registry.GetComponent(button.textEntity); - label.color = button.hovered ? Colors::UI_HOVER : Colors::UI_TEXT; - } - } - - void EditorUIManager::SetOnSaveRequested(const std::function& callback) { - m_onSaveRequested = callback; - } - - void EditorUIManager::UpdateColliderPanel(const EditorEntityData* selected, int selectedColliderIndex) { - if (!m_registry.IsEntityAlive(m_colliderCountEntity)) { - return; - } - - auto& countLabel = m_registry.GetComponent(m_colliderCountEntity); - - if (!selected || selected->colliders.empty()) { - countLabel.text = "No colliders"; - countLabel.color = Colors::UI_TEXT; - return; - } - - std::ostringstream oss; - oss << selected->colliders.size() << " collider"; - if (selected->colliders.size() > 1) { - oss << "s"; - } - - if (selectedColliderIndex >= 0 && selectedColliderIndex < static_cast(selected->colliders.size())) { - const auto& collider = selected->colliders[static_cast(selectedColliderIndex)]; - oss << "\n\nSelected: #" << selectedColliderIndex; - oss << "\nPos: (" << static_cast(collider.x) << ", " << static_cast(collider.y) << ")"; - oss << "\nSize: " << static_cast(collider.width) << "x" << static_cast(collider.height); - } - - countLabel.text = oss.str(); - countLabel.color = Colors::UI_TEXT; - } - - } -} - diff --git a/client/src/game/GameStateHelpers.cpp b/client/src/game/GameStateHelpers.cpp deleted file mode 100644 index 9d42a42..0000000 --- a/client/src/game/GameStateHelpers.cpp +++ /dev/null @@ -1,513 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** GameState - Helper functions (sprite configs, power-ups, player labels) -*/ - -#include "../../include/GameState.hpp" - -#include "ECS/Components/TextLabel.hpp" -#include "ECS/Component.hpp" -#include "Core/Logger.hpp" - -using namespace RType::ECS; - -namespace RType { - namespace Client { - - InGameState::EnemySpriteConfig InGameState::GetEnemySpriteConfig(uint8_t enemyType) const { - const Renderer::SpriteId* sprites[] = { - &m_enemyGreenSprite, - &m_enemyRedSprite, - &m_enemyBlueSprite, - &m_enemyGreenSprite, - &m_enemyGreenSprite}; - - static const Math::Color tints[] = { - {1.0f, 1.0f, 1.0f, 1.0f}, - {1.0f, 1.0f, 1.0f, 1.0f}, - {1.0f, 1.0f, 1.0f, 1.0f}, - {1.0f, 1.0f, 1.0f, 1.0f}, - {1.0f, 1.0f, 1.0f, 1.0f}}; - static const float rotations[] = { - 180.0f, - 180.0f, - 180.0f, - 180.0f, - 180.0f}; - - size_t index = (enemyType < 5) ? enemyType : 0; - EnemySpriteConfig result; - result.sprite = *sprites[index]; - result.tint = tints[index]; - result.rotation = rotations[index]; - return result; - } - - Renderer::SpriteId InGameState::GetPowerUpSprite(ECS::PowerUpType type) const { - switch (type) { - case ECS::PowerUpType::FIRE_RATE_BOOST: - case ECS::PowerUpType::SPREAD_SHOT: - return m_powerupSpreadSprite; - case ECS::PowerUpType::LASER_BEAM: - return m_powerupLaserSprite; - case ECS::PowerUpType::FORCE_POD: - return m_powerupForcePodSprite; - case ECS::PowerUpType::SPEED_BOOST: - return m_powerupSpeedSprite; - case ECS::PowerUpType::SHIELD: - return m_powerupShieldSprite; - default: - return m_powerupSpreadSprite; - } - } - - InGameState::EnemyBulletSpriteConfig InGameState::GetEnemyBulletSpriteConfig(uint8_t enemyType) const { - const Renderer::SpriteId* sprites[] = { - &m_enemyBulletGreenSprite, - &m_enemyBulletYellowSprite, - &m_enemyBulletPurpleSprite}; - - static const Math::Color tints[] = { - {1.0f, 1.0f, 1.0f, 1.0f}, - {1.0f, 0.2f, 0.2f, 1.0f}, - {0.8f, 0.3f, 1.0f, 1.0f}}; - - static const float scales[] = { - 0.14f, - 0.09f, - 0.18f}; - - size_t index = (enemyType < 3) ? enemyType : 0; - EnemyBulletSpriteConfig result; - result.sprite = *sprites[index]; - result.tint = tints[index]; - result.scale = scales[index]; - return result; - } - - void InGameState::ApplyPowerUpStateToPlayer(ECS::Entity playerEntity, const network::EntityState& entityState) { - using namespace RType::ECS; - using namespace network; - - if (!m_registry.IsEntityAlive(playerEntity)) { - return; - } - - if (!m_registry.HasComponent(playerEntity)) { - m_registry.AddComponent(playerEntity, ActivePowerUps()); - } - auto& activePowerUps = m_registry.GetComponent(playerEntity); - - bool gainedPowerUp = false; - bool isLocal = (playerEntity == m_localPlayerEntity); - - if (isLocal) { - bool newFireRate = (entityState.powerUpFlags & PowerUpFlags::POWERUP_FIRE_RATE_BOOST) != 0; - bool newSpread = (entityState.powerUpFlags & PowerUpFlags::POWERUP_SPREAD_SHOT) != 0; - bool newLaser = (entityState.powerUpFlags & PowerUpFlags::POWERUP_LASER_BEAM) != 0; - bool newShield = (entityState.powerUpFlags & PowerUpFlags::POWERUP_SHIELD) != 0; - - if (!activePowerUps.hasFireRateBoost && newFireRate) gainedPowerUp = true; - if (!activePowerUps.hasSpreadShot && newSpread) gainedPowerUp = true; - if (!activePowerUps.hasLaserBeam && newLaser) gainedPowerUp = true; - if (!activePowerUps.hasShield && newShield) gainedPowerUp = true; - - } - - activePowerUps.hasFireRateBoost = (entityState.powerUpFlags & PowerUpFlags::POWERUP_FIRE_RATE_BOOST) != 0; - activePowerUps.hasSpreadShot = (entityState.powerUpFlags & PowerUpFlags::POWERUP_SPREAD_SHOT) != 0; - activePowerUps.hasLaserBeam = (entityState.powerUpFlags & PowerUpFlags::POWERUP_LASER_BEAM) != 0; - activePowerUps.hasShield = (entityState.powerUpFlags & PowerUpFlags::POWERUP_SHIELD) != 0; - - float speedMult = static_cast(entityState.speedMultiplier) / 10.0f; - activePowerUps.speedMultiplier = speedMult; - - if (m_registry.HasComponent(playerEntity)) { - auto& controllable = m_registry.GetComponent(playerEntity); - controllable.speed = 200.0f * speedMult; - } - - WeaponType weaponType = static_cast(entityState.weaponType); - - if (isLocal) { - bool hadSpecialWeapon = m_registry.HasComponent(playerEntity); - bool hasSpecialWeaponNow = (weaponType != WeaponType::STANDARD); - - - if (!hadSpecialWeapon && hasSpecialWeaponNow) { - std::cout << "[DEBUG] PowerUp: Acquired special weapon!" << std::endl; - gainedPowerUp = true; - } else if (hadSpecialWeapon && hasSpecialWeaponNow) { - auto& currentWeapon = m_registry.GetComponent(playerEntity); - if (currentWeapon.type != weaponType) { - std::cout << "[DEBUG] PowerUp: Changed special weapon!" << std::endl; - gainedPowerUp = true; - } - } - - bool newFireRate = (entityState.powerUpFlags & PowerUpFlags::POWERUP_FIRE_RATE_BOOST) != 0; - bool newSpread = (entityState.powerUpFlags & PowerUpFlags::POWERUP_SPREAD_SHOT) != 0; - if (!activePowerUps.hasFireRateBoost && newFireRate) { - std::cout << "[DEBUG] PowerUp: FireRate Boost!" << std::endl; - gainedPowerUp = true; - } - if (!activePowerUps.hasSpreadShot && newSpread) { - std::cout << "[DEBUG] PowerUp: Spread Shot!" << std::endl; - gainedPowerUp = true; - } - } - - if (gainedPowerUp) { - std::cout << "[DEBUG] PowerUp GAINED! Playing sound..." << std::endl; - if (m_context.audio) { - if (m_powerUpMusic != Audio::INVALID_MUSIC_ID) { - Audio::PlaybackOptions opts; - opts.volume = 1.0f; - opts.loop = false; - m_context.audio->PlayMusic(m_powerUpMusic, opts); - } else if (m_powerUpSound != Audio::INVALID_SOUND_ID) { - Audio::PlaybackOptions opts; - opts.volume = 1.0f; - m_context.audio->PlaySound(m_powerUpSound, opts); - } - } - } - - float fireRate = static_cast(entityState.fireRate) / 10.0f; - - if (weaponType != WeaponType::STANDARD) { - if (m_registry.HasComponent(playerEntity)) { - auto& weaponSlot = m_registry.GetComponent(playerEntity); - weaponSlot.type = weaponType; - weaponSlot.fireRate = fireRate; - } else { - int damage = (weaponType == WeaponType::LASER) ? 40 : 20; - m_registry.AddComponent(playerEntity, WeaponSlot(weaponType, fireRate, damage)); - } - activePowerUps.hasSpreadShot = (weaponType == WeaponType::SPREAD); - activePowerUps.hasLaserBeam = (weaponType == WeaponType::LASER); - } else { - if (m_registry.HasComponent(playerEntity)) { - m_registry.RemoveComponent(playerEntity); - } - activePowerUps.hasSpreadShot = false; - activePowerUps.hasLaserBeam = false; - } - - if (!m_registry.HasComponent(playerEntity) && m_registry.HasComponent(playerEntity)) { - auto& shooter = m_registry.GetComponent(playerEntity); - shooter.fireRate = fireRate; - } - - constexpr float SHIELD_DURATION_SECONDS = 5.0f; - if (activePowerUps.hasShield) { - if (!m_registry.HasComponent(playerEntity)) { - m_registry.AddComponent(playerEntity, Shield(SHIELD_DURATION_SECONDS)); - } else { - auto& shield = m_registry.GetComponent(playerEntity); - if (shield.timeRemaining <= 0.0f) { - shield.timeRemaining = SHIELD_DURATION_SECONDS; - } - } - } else { - if (m_registry.HasComponent(playerEntity)) { - m_registry.RemoveComponent(playerEntity); - if (m_registry.HasComponent(playerEntity)) { - auto& drawable = m_registry.GetComponent(playerEntity); - drawable.tint = Math::Color(1.0f, 1.0f, 1.0f, 1.0f); - } - } - } - - } - - void InGameState::createBeamEntity() { - if (m_localPlayerEntity == ECS::NULL_ENTITY || !m_registry.IsEntityAlive(m_localPlayerEntity)) { - return; - } - - if (!m_registry.HasComponent(m_localPlayerEntity) || - !m_registry.HasComponent(m_localPlayerEntity) || - !m_effectFactory) { - return; - } - - const auto& playerPos = m_registry.GetComponent(m_localPlayerEntity); - const auto& shooter = m_registry.GetComponent(m_localPlayerEntity); - - float beamX = playerPos.x + shooter.offsetX; - float beamY = playerPos.y + shooter.offsetY; - - float screenWidth = 1280.0f; - if (m_levelData.config.screenWidth > 0.0f) { - screenWidth = m_levelData.config.screenWidth; - } - - float beamHeight = 40.0f; - if (m_registry.HasComponent(m_localPlayerEntity)) { - const auto& playerDrawable = m_registry.GetComponent(m_localPlayerEntity); - beamHeight = 80.0f * playerDrawable.scale.y; - } - - float savedChargeTime = m_chargeTime; - m_beamEntity = m_effectFactory->CreateBeam(m_registry, beamX, beamY, m_localPlayerEntity, savedChargeTime, screenWidth, beamHeight); - } - - void InGameState::updateBeam(float dt) { - if (m_beamEntity == ECS::NULL_ENTITY) { - return; - } - - if (!m_registry.IsEntityAlive(m_beamEntity)) { - m_beamEntity = ECS::NULL_ENTITY; - return; - } - - // Update beam width dynamically as player moves - if (m_registry.HasComponent(m_beamEntity)) { - const auto& beamPos = m_registry.GetComponent(m_beamEntity); - - float screenWidth = 1280.0f; - if (m_levelData.config.screenWidth > 0.0f) { - screenWidth = m_levelData.config.screenWidth; - } - - // Get frame width from config or use default - float frameWidth = 200.0f; - if (m_effectFactory) { - const auto& config = m_effectFactory->GetConfig(); - if (config.beamFirstFrameRegion.size.x > 0.0f) { - frameWidth = config.beamFirstFrameRegion.size.x; - } - } - - // Calculate new beam width (extend from current position to screen edge) - float newBeamWidth = screenWidth - beamPos.x; - if (newBeamWidth < frameWidth) { - newBeamWidth = frameWidth; - } - - // Update Drawable scale if it exists - if (m_registry.HasComponent(m_beamEntity)) { - auto& drawable = m_registry.GetComponent(m_beamEntity); - float newScaleX = newBeamWidth / frameWidth; - // Preserve the Y scale - drawable.scale.x = newScaleX; - } - - // Update BoxCollider width if it exists - if (m_registry.HasComponent(m_beamEntity)) { - auto& collider = m_registry.GetComponent(m_beamEntity); - // Get frame height to calculate the scaled height - float frameHeight = 64.0f; - if (m_effectFactory) { - const auto& config = m_effectFactory->GetConfig(); - if (config.beamFirstFrameRegion.size.y > 0.0f) { - frameHeight = config.beamFirstFrameRegion.size.y; - } - } - - // Get current scale Y from drawable or use default - float scaleY = 1.0f; - if (m_registry.HasComponent(m_beamEntity)) { - scaleY = m_registry.GetComponent(m_beamEntity).scale.y; - } - - collider.width = newBeamWidth; - collider.height = frameHeight * scaleY; - } - } - - if (m_registry.HasComponent(m_beamEntity)) { - auto& anim = m_registry.GetComponent(m_beamEntity); - if (anim.looping && m_animationSystem) { - anim.destroyOnComplete = false; - } - } - - m_beamDuration -= dt; - if (m_beamDuration <= 0.0f) { - if (m_registry.IsEntityAlive(m_beamEntity)) { - m_registry.DestroyEntity(m_beamEntity); - } - m_beamEntity = ECS::NULL_ENTITY; - m_beamDuration = 0.0f; - } - } - - std::pair InGameState::FindPlayerNameAndNumber(uint64_t ownerHash, const std::unordered_set& assignedNumbers) const { - std::string playerName = "Player"; - uint8_t playerNum = 0; - - if (ownerHash != 0) { - auto nameIt = m_playerNameMap.find(ownerHash); - if (nameIt != m_playerNameMap.end()) { - playerName = nameIt->second; - } - for (const auto& p : m_context.allPlayers) { - if (p.hash == ownerHash) { - playerNum = p.number; - break; - } - } - } - - if (playerName == "Player" || playerNum == 0) { - for (const auto& p : m_context.allPlayers) { - if (p.number > 0 && p.number <= MAX_PLAYERS) { - if (assignedNumbers.find(p.number) == assignedNumbers.end()) { - auto fallbackIt = m_playerNameMap.find(static_cast(p.number)); - if (fallbackIt != m_playerNameMap.end() && !fallbackIt->second.empty()) { - playerName = fallbackIt->second; - playerNum = p.number; - break; - } - } - } - } - } - - return {playerName, playerNum}; - } - - void InGameState::CreatePlayerNameLabel(Entity playerEntity, const std::string& playerName, float x, float y) { - Renderer::FontId nameFont = (m_hudFontSmall != Renderer::INVALID_FONT_ID) ? m_hudFontSmall : m_hudFont; - if (nameFont == Renderer::INVALID_FONT_ID) { - return; - } - - std::string displayName = playerName; - if (displayName.length() > 16) { - displayName = displayName.substr(0, 16); - } - - Entity nameLabelEntity = m_registry.CreateEntity(); - m_registry.AddComponent(nameLabelEntity, Position{x, y}); - TextLabel nameLabel(displayName, nameFont, 12); - nameLabel.color = {1.0f, 1.0f, 1.0f, 1.0f}; - nameLabel.centered = true; - nameLabel.offsetY = -30.0f; - nameLabel.offsetX = 0.0f; - m_registry.AddComponent(nameLabelEntity, std::move(nameLabel)); - m_playerNameLabels[playerEntity] = nameLabelEntity; - } - - void InGameState::UpdatePlayerNameLabelPosition(Entity playerEntity, float x, float y) { - auto labelIt = m_playerNameLabels.find(playerEntity); - if (labelIt == m_playerNameLabels.end()) { - return; - } - - Entity labelEntity = labelIt->second; - if (!m_registry.IsEntityAlive(labelEntity) || !m_registry.HasComponent(labelEntity)) { - return; - } - - auto& labelPos = m_registry.GetComponent(labelEntity); - labelPos.x = x; - labelPos.y = y; - } - - void InGameState::DestroyPlayerNameLabel(Entity playerEntity) { - auto labelIt = m_playerNameLabels.find(playerEntity); - if (labelIt == m_playerNameLabels.end()) { - return; - } - - Entity labelEntity = labelIt->second; - if (m_registry.IsEntityAlive(labelEntity)) { - m_registry.DestroyEntity(labelEntity); - } - m_playerNameLabels.erase(labelIt); - } - - void InGameState::CleanupInvalidComponents(ECS::Entity entity, network::EntityType expectedType) { - if (!m_registry.IsEntityAlive(entity)) { - return; - } - - if (expectedType == network::EntityType::OBSTACLE) { - if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); - if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); - if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); - if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); - if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); - if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); - } else if (expectedType == network::EntityType::BULLET) { - if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); - if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); - if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); - if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); - if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); - if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); - if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); - if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); - } else { - if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); - if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); - if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); - if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); - - if (expectedType == network::EntityType::PLAYER || expectedType == network::EntityType::POWERUP) { - if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); - } - if (expectedType == network::EntityType::ENEMY || expectedType == network::EntityType::POWERUP) { - if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); - } - if (expectedType == network::EntityType::BOSS) { - if (m_registry.HasComponent(entity)) m_registry.RemoveComponent(entity); - } - } - } - - void InGameState::cleanupForLevelTransition() { - Core::Logger::Info("[GameState] Cleaning up for level transition..."); - - auto positionEntities = m_registry.GetEntitiesWithComponent(); - std::vector entitiesToDestroy(positionEntities.begin(), positionEntities.end()); - - auto drawableEntities = m_registry.GetEntitiesWithComponent(); - for (auto e : drawableEntities) { - if (std::find(entitiesToDestroy.begin(), entitiesToDestroy.end(), e) == entitiesToDestroy.end()) { - entitiesToDestroy.push_back(e); - } - } - - Core::Logger::Info("[GameState] Destroying {} entities from previous level", entitiesToDestroy.size()); - for (Entity entity : entitiesToDestroy) { - if (m_registry.IsEntityAlive(entity)) { - m_registry.DestroyEntity(entity); - } - } - - m_networkEntityMap.clear(); - m_obstacleColliderEntities.clear(); - m_obstacleIdToCollider.clear(); - m_playerNameLabels.clear(); - - for (auto& hud : m_playersHUD) { - hud = PlayerHUDData{}; - } - - m_bossHealthBar.active = false; - m_bossHealthBar.currentHealth = 0; - m_bossHealthBar.maxHealth = 1000; - m_bossHealthBar.bossNetworkId = 0; - m_bossHealthBar.titleEntity = NULL_ENTITY; - m_bossHealthBar.barBackgroundEntity = NULL_ENTITY; - m_bossHealthBar.barForegroundEntity = NULL_ENTITY; - - m_bossWarningActive = false; - m_bossWarningTriggered = false; - m_bossWarningTimer = 0.0f; - - m_isGameOver = false; - m_localScrollOffset = 0.0f; - - Core::Logger::Info("[GameState] Level transition cleanup complete"); - } - - } -} diff --git a/client/src/game/GameStateInit.cpp b/client/src/game/GameStateInit.cpp deleted file mode 100644 index 8f5cbb9..0000000 --- a/client/src/game/GameStateInit.cpp +++ /dev/null @@ -1,782 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** GameState - Initialization functions -*/ - -#include "../../include/GameState.hpp" - -#include "ECS/Components/TextLabel.hpp" -#include "ECS/Component.hpp" -#include "Core/Logger.hpp" -#include "ECS/PlayerFactory.hpp" -#include "Animation/AnimationTypes.hpp" - -using namespace RType::ECS; -using namespace Animation; - -namespace RType { - namespace Client { - - InGameState::InGameState(GameStateMachine& machine, GameContext& context, uint32_t seed, const std::string& levelPath) - : m_machine(machine), m_context(context), m_gameSeed(seed), m_currentLevelPath(levelPath) { - m_renderer = context.renderer; - } - - void InGameState::Init() { - Core::Logger::Info("[GameState] Initializing game"); - - if (!m_networkEntityMap.empty() || !m_obstacleColliderEntities.empty()) { - Core::Logger::Info("[GameState] Detected level transition, cleaning up previous level..."); - cleanupForLevelTransition(); - } - - if (m_context.audio) { - m_audioSystem = std::make_unique(m_context.audio.get()); - - m_shootMusic = m_context.audio->LoadMusic("assets/sounds/players_shoot.flac"); - if (m_shootMusic == Audio::INVALID_MUSIC_ID) { - m_shootMusic = m_context.audio->LoadMusic("../assets/sounds/players_shoot.flac"); - } - - m_powerUpSound = m_context.audio->LoadSound("assets/sounds/powerup.flac"); - if (m_powerUpSound == Audio::INVALID_SOUND_ID) { - m_powerUpSound = m_context.audio->LoadSound("../assets/sounds/powerup.flac"); - } - m_powerUpMusic = m_context.audio->LoadMusic("assets/sounds/powerup.flac"); - if (m_powerUpMusic == Audio::INVALID_MUSIC_ID) { - m_powerUpMusic = m_context.audio->LoadMusic("../assets/sounds/powerup.flac"); - } - - std::string musicPath = "assets/sounds/stage1.flac"; - if (m_currentLevelPath.find("level2.json") != std::string::npos) { - musicPath = "assets/sounds/stage2.flac"; - } else if (m_currentLevelPath.find("level3.json") != std::string::npos) { - musicPath = "assets/sounds/stage3.flac"; - } - - m_gameMusic = m_context.audio->LoadMusic(musicPath); - if (m_gameMusic == Audio::INVALID_MUSIC_ID) { - m_gameMusic = m_context.audio->LoadMusic("../" + musicPath); - } - - m_bossMusic = m_context.audio->LoadMusic("assets/sounds/BOSS.flac"); - if (m_bossMusic == Audio::INVALID_MUSIC_ID) { - m_bossMusic = m_context.audio->LoadMusic("../assets/sounds/BOSS.flac"); - } - - m_gameOverMusic = m_context.audio->LoadMusic("assets/sounds/gameover.flac"); - if (m_gameOverMusic == Audio::INVALID_MUSIC_ID) { - m_gameOverMusic = m_context.audio->LoadMusic("../assets/sounds/gameover.flac"); - } - - m_victoryMusic = m_context.audio->LoadMusic("assets/sounds/victory.flac"); - if (m_victoryMusic == Audio::INVALID_MUSIC_ID) { - m_victoryMusic = m_context.audio->LoadMusic("../assets/sounds/victory.flac"); - } - - if (m_gameMusic != Audio::INVALID_MUSIC_ID) { - auto cmd = m_registry.CreateEntity(); - auto& me = m_registry.AddComponent(cmd, MusicEffect(m_gameMusic)); - me.play = true; - me.stop = false; - me.loop = true; - me.volume = 0.35f; - me.pitch = 1.0f; - m_gameMusicPlaying = true; - } - } - - if (m_context.networkClient) { - m_context.networkClient->SetStateCallback([this](uint32_t tick, const std::vector& entities, const std::vector& inputAcks) { this->OnServerStateUpdate(tick, entities, inputAcks); }); - m_context.networkClient->SetLevelCompleteCallback([this](uint8_t completedLevel, uint8_t nextLevel) { this->OnLevelComplete(completedLevel, nextLevel); }); - - if (!m_context.networkClient->IsConnected()) { - Core::Logger::Info("[GameState] Reconnecting to server after level transition..."); - if (!m_context.networkClient->ConnectToServer()) { - Core::Logger::Error("[GameState] Failed to reconnect to server!"); - } else { - Core::Logger::Info("[GameState] Reconnected to server successfully"); - } - } - } else { - Core::Logger::Warning("[GameState] No network client available"); - } - - m_isNetworkSession = (m_context.networkClient != nullptr); - - for (const auto& player : m_context.allPlayers) { - if (player.hash != 0) { - m_playerNameMap[player.hash] = player.name; - } - m_playerNameMap[static_cast(player.number)] = player.name; - } - - loadLevel(m_currentLevelPath); - createSystems(); - - if (m_shootingSystem && m_playerShootSound != Audio::INVALID_SOUND_ID) { - m_shootingSystem->SetShootSound(m_playerShootSound); - } - - if (m_powerUpCollisionSystem && m_powerUpSound != Audio::INVALID_SOUND_ID) { - m_powerUpCollisionSystem->SetPowerUpSound(m_powerUpSound); - } - - initializeFromLevel(); - if (!m_isNetworkSession) { - initializePlayers(); - } - initializeUI(); - - Core::Logger::Info("[GameState] Initialization complete"); - } - - void InGameState::loadLevel(const std::string& levelPath) { - m_levelData = ECS::LevelLoader::LoadFromFile(levelPath); - m_levelAssets = ECS::LevelLoader::LoadAssets(m_levelData, m_renderer.get()); - Core::Logger::Info("[GameState] Loaded level '{}' with {} textures, {} obstacle definitions", m_levelData.name, m_levelAssets.textures.size(), m_levelData.obstacles.size()); - - // Load explosion spritesheet for death animations - m_explosionTexture = m_renderer->LoadTexture("assets/SFX/explosion.png"); - if (m_explosionTexture == Renderer::INVALID_TEXTURE_ID) { - m_explosionTexture = m_renderer->LoadTexture("../assets/SFX/explosion.png"); - } - if (m_explosionTexture != Renderer::INVALID_TEXTURE_ID) { - m_explosionSprite = m_renderer->CreateSprite(m_explosionTexture, {}); - Core::Logger::Info("[GameState] Explosion spritesheet loaded"); - } else { - Core::Logger::Warning("[GameState] Failed to load explosion spritesheet"); - } - - m_shootingTexture = m_renderer->LoadTexture("assets/SFX/shooting.png"); - if (m_shootingTexture == Renderer::INVALID_TEXTURE_ID) { - m_shootingTexture = m_renderer->LoadTexture("../assets/SFX/shooting.png"); - } - if (m_shootingTexture != Renderer::INVALID_TEXTURE_ID) { - m_shootingSprite = m_renderer->CreateSprite(m_shootingTexture, {}); - Core::Logger::Info("[GameState] Shooting animation spritesheet loaded"); - } else { - Core::Logger::Warning("[GameState] Failed to load shooting animation spritesheet"); - } - - m_forcePodTexture = m_renderer->LoadTexture("assets/SFX/forcepod.png"); - if (m_forcePodTexture == Renderer::INVALID_TEXTURE_ID) { - m_forcePodTexture = m_renderer->LoadTexture("../assets/SFX/forcepod.png"); - } - if (m_forcePodTexture != Renderer::INVALID_TEXTURE_ID) { - m_forcePodSprite = m_renderer->CreateSprite(m_forcePodTexture, {}); - Core::Logger::Info("[GameState] Force pod animation spritesheet loaded"); - } else { - Core::Logger::Warning("[GameState] Failed to load force pod animation spritesheet"); - } - - m_beamTexture = m_renderer->LoadTexture("assets/SFX/beam.png"); - if (m_beamTexture == Renderer::INVALID_TEXTURE_ID) { - m_beamTexture = m_renderer->LoadTexture("../assets/SFX/beam.png"); - } - if (m_beamTexture != Renderer::INVALID_TEXTURE_ID) { - m_beamSprite = m_renderer->CreateSprite(m_beamTexture, {}); - Core::Logger::Info("[GameState] Beam animation spritesheet loaded"); - } else { - Core::Logger::Warning("[GameState] Failed to load beam animation spritesheet"); - } - - m_enemyBulletGreenTexture = m_renderer->LoadTexture("assets/projectiles/bullet-green.png"); - if (m_enemyBulletGreenTexture == Renderer::INVALID_TEXTURE_ID) { - m_enemyBulletGreenTexture = m_renderer->LoadTexture("../assets/projectiles/bullet-green.png"); - } - if (m_enemyBulletGreenTexture != Renderer::INVALID_TEXTURE_ID) { - m_enemyBulletGreenSprite = m_renderer->CreateSprite(m_enemyBulletGreenTexture, {}); - Core::Logger::Info("[GameState] Enemy bullet green sprite loaded"); - } - - m_enemyBulletYellowTexture = m_renderer->LoadTexture("assets/projectiles/bullet-yellow.png"); - if (m_enemyBulletYellowTexture == Renderer::INVALID_TEXTURE_ID) { - m_enemyBulletYellowTexture = m_renderer->LoadTexture("../assets/projectiles/bullet-yellow.png"); - } - if (m_enemyBulletYellowTexture != Renderer::INVALID_TEXTURE_ID) { - m_enemyBulletYellowSprite = m_renderer->CreateSprite(m_enemyBulletYellowTexture, {}); - Core::Logger::Info("[GameState] Enemy bullet yellow sprite loaded"); - } - - m_enemyBulletPurpleTexture = m_renderer->LoadTexture("assets/projectiles/bullet-purple.png"); - if (m_enemyBulletPurpleTexture == Renderer::INVALID_TEXTURE_ID) { - m_enemyBulletPurpleTexture = m_renderer->LoadTexture("../assets/projectiles/bullet-purple.png"); - } - if (m_enemyBulletPurpleTexture != Renderer::INVALID_TEXTURE_ID) { - m_enemyBulletPurpleSprite = m_renderer->CreateSprite(m_enemyBulletPurpleTexture, {}); - Core::Logger::Info("[GameState] Enemy bullet purple sprite loaded"); - } - - auto enemyGreenIt = m_levelAssets.sprites.find("enemy-green"); - if (enemyGreenIt != m_levelAssets.sprites.end()) { - m_enemyGreenSprite = enemyGreenIt->second; - } else { - auto textureIt = m_levelAssets.textures.find("enemy-green"); - if (textureIt != m_levelAssets.textures.end()) { - m_enemyGreenSprite = m_renderer->CreateSprite(textureIt->second, {}); - } - } - - auto enemyRedIt = m_levelAssets.sprites.find("enemy-red"); - if (enemyRedIt != m_levelAssets.sprites.end()) { - m_enemyRedSprite = enemyRedIt->second; - } else { - auto textureIt = m_levelAssets.textures.find("enemy-red"); - if (textureIt != m_levelAssets.textures.end()) { - m_enemyRedSprite = m_renderer->CreateSprite(textureIt->second, {}); - } - } - - auto enemyBlueIt = m_levelAssets.sprites.find("enemy-blue"); - if (enemyBlueIt != m_levelAssets.sprites.end()) { - m_enemyBlueSprite = enemyBlueIt->second; - } else { - auto textureIt = m_levelAssets.textures.find("enemy-blue"); - if (textureIt != m_levelAssets.textures.end()) { - m_enemyBlueSprite = m_renderer->CreateSprite(textureIt->second, {}); - } - } - - auto ensureEnemySprite = [this](Renderer::SpriteId& target, Renderer::SpriteId fallbackPlayerSprite, const std::string& path) { - if (target != Renderer::INVALID_SPRITE_ID) { - return; - } - Renderer::TextureId textureId = m_renderer->LoadTexture(path); - if (textureId == Renderer::INVALID_TEXTURE_ID) { - textureId = m_renderer->LoadTexture("../" + path); - } - if (textureId != Renderer::INVALID_TEXTURE_ID) { - target = m_renderer->CreateSprite(textureId, {}); - } else if (fallbackPlayerSprite != Renderer::INVALID_SPRITE_ID) { - target = fallbackPlayerSprite; - } - }; - - ensureEnemySprite(m_enemyGreenSprite, m_playerGreenSprite, "assets/spaceships/enemy-green.png"); - ensureEnemySprite(m_enemyRedSprite, m_playerRedSprite, "assets/spaceships/enemy-red.png"); - ensureEnemySprite(m_enemyBlueSprite, m_playerBlueSprite, "assets/spaceships/enemy-blue.png"); - - auto powerupSpreadIt = m_levelAssets.sprites.find("powerup-spread"); - if (powerupSpreadIt != m_levelAssets.sprites.end()) { - m_powerupSpreadSprite = powerupSpreadIt->second; - } else { - auto textureIt = m_levelAssets.textures.find("powerup-spread"); - if (textureIt != m_levelAssets.textures.end()) { - m_powerupSpreadSprite = m_renderer->CreateSprite(textureIt->second, {}); - } - } - - auto powerupLaserIt = m_levelAssets.sprites.find("powerup-laser"); - if (powerupLaserIt != m_levelAssets.sprites.end()) { - m_powerupLaserSprite = powerupLaserIt->second; - } else { - auto textureIt = m_levelAssets.textures.find("powerup-laser"); - if (textureIt != m_levelAssets.textures.end()) { - m_powerupLaserSprite = m_renderer->CreateSprite(textureIt->second, {}); - } - } - - auto powerupForcePodIt = m_levelAssets.sprites.find("powerup-force-pod"); - if (powerupForcePodIt != m_levelAssets.sprites.end()) { - m_powerupForcePodSprite = powerupForcePodIt->second; - } else { - auto textureIt = m_levelAssets.textures.find("powerup-force-pod"); - if (textureIt != m_levelAssets.textures.end()) { - m_powerupForcePodSprite = m_renderer->CreateSprite(textureIt->second, {}); - } - } - - auto powerupSpeedIt = m_levelAssets.sprites.find("powerup-speed"); - if (powerupSpeedIt != m_levelAssets.sprites.end()) { - m_powerupSpeedSprite = powerupSpeedIt->second; - } else { - auto textureIt = m_levelAssets.textures.find("powerup-speed"); - if (textureIt != m_levelAssets.textures.end()) { - m_powerupSpeedSprite = m_renderer->CreateSprite(textureIt->second, {}); - } - } - - auto powerupShieldIt = m_levelAssets.sprites.find("powerup-shield"); - if (powerupShieldIt != m_levelAssets.sprites.end()) { - m_powerupShieldSprite = powerupShieldIt->second; - } else { - auto textureIt = m_levelAssets.textures.find("powerup-shield"); - if (textureIt != m_levelAssets.textures.end()) { - m_powerupShieldSprite = m_renderer->CreateSprite(textureIt->second, {}); - } - } - } - - void InGameState::initializeFromLevel() { - m_levelEntities = ECS::LevelLoader::CreateEntities( - m_registry, - m_levelData, - m_levelAssets, - m_renderer.get()); - - m_backgroundEntities = m_levelEntities.backgrounds; - m_obstacleSpriteEntities = m_levelEntities.obstacleVisuals; - m_obstacleColliderEntities = m_levelEntities.obstacleColliders; - m_obstacleIdToCollider.clear(); - - // Initialize collider positions based on their visual entities - for (auto collider : m_obstacleColliderEntities) { - if (!m_registry.IsEntityAlive(collider) || - !m_registry.HasComponent(collider)) { - continue; - } - const auto& metadata = m_registry.GetComponent(collider); - m_obstacleIdToCollider[metadata.uniqueId] = collider; - - // Sync collider to visual entity position on initialization - if (metadata.visualEntity != ECS::NULL_ENTITY && - m_registry.IsEntityAlive(metadata.visualEntity) && - m_registry.HasComponent(metadata.visualEntity) && - m_registry.HasComponent(collider)) { - - const auto& visualPos = m_registry.GetComponent(metadata.visualEntity); - auto& colliderPos = m_registry.GetComponent(collider); - - // Set absolute position = visual position + offset - colliderPos.x = visualPos.x + metadata.offsetX; - colliderPos.y = visualPos.y + metadata.offsetY; - - } - } - - } - - void InGameState::createSystems() { - auto bulletSpriteIt = m_levelAssets.sprites.find("bullet"); - Renderer::SpriteId bulletSprite = (bulletSpriteIt != m_levelAssets.sprites.end()) ? bulletSpriteIt->second : Renderer::INVALID_SPRITE_ID; - - if (bulletSprite == Renderer::INVALID_SPRITE_ID) { - Core::Logger::Error("[GameState] Bullet sprite not found in level assets"); - } - - m_scrollingSystem = std::make_unique(); - m_renderingSystem = std::make_unique(m_renderer.get()); - m_textSystem = std::make_unique(m_renderer.get()); - - // Initialize animation module and system - m_animationModule = std::make_unique(); - - if (m_explosionTexture != Renderer::INVALID_TEXTURE_ID && m_animationModule) { - Animation::GridLayout layout; - layout.columns = 6; - layout.rows = 1; - layout.frameCount = 6; - layout.frameWidth = 45.0f; - layout.frameHeight = 40.0f; - layout.defaultDuration = 0.005f; - - m_explosionClipId = m_animationModule->CreateClipFromGrid( - "explosion_small", - "assets/SFX/explosion.png", - layout, - false); - Core::Logger::Info("[GameState] Created explosion animation clip with {} frames", 6); - } - - if (m_shootingTexture != Renderer::INVALID_TEXTURE_ID && m_animationModule) { - const int frameCount = 8; - - Renderer::Vector2 textureSize = m_renderer->GetTextureSize(m_shootingTexture); - float singleFrameWidth = textureSize.x / static_cast(frameCount); - float frameHeight = textureSize.y; - - Animation::AnimationClipConfig shootingConfig; - shootingConfig.name = "shooting_animation"; - shootingConfig.texturePath = "assets/SFX/shooting.png"; - shootingConfig.looping = false; - shootingConfig.playbackSpeed = 1.0f; - - for (int i = frameCount - 1; i >= 0; --i) { - Animation::FrameDef frame; - frame.region.position.x = static_cast(i) * singleFrameWidth; - frame.region.position.y = 0.0f; - frame.region.size.x = singleFrameWidth; - frame.region.size.y = frameHeight; - frame.duration = 0.05f; - shootingConfig.frames.push_back(frame); - } - - m_shootingClipId = m_animationModule->CreateClip(shootingConfig); - Core::Logger::Info("[GameState] Created shooting animation clip with {} frames (frame size: {}x{}, texture size: {}x{}, reversed for right-to-left)", - frameCount, singleFrameWidth, frameHeight, textureSize.x, textureSize.y); - } - - if (m_forcePodTexture != Renderer::INVALID_TEXTURE_ID && m_animationModule) { - const int totalFramesInSheet = 12; - const int frameCount = 6; - - Renderer::Vector2 textureSize = m_renderer->GetTextureSize(m_forcePodTexture); - float singleFrameWidth = textureSize.x / static_cast(totalFramesInSheet); - float frameHeight = textureSize.y; - - Animation::AnimationClipConfig forcePodConfig; - forcePodConfig.name = "forcepod_rotation"; - forcePodConfig.texturePath = "assets/SFX/forcepod.png"; - forcePodConfig.looping = true; - forcePodConfig.playbackSpeed = 1.0f; - - for (int i = 0; i < frameCount; ++i) { - Animation::FrameDef frame; - frame.region.position.x = static_cast(i) * singleFrameWidth; - frame.region.position.y = 0.0f; - frame.region.size.x = singleFrameWidth; - frame.region.size.y = frameHeight; - frame.duration = 0.1f; - forcePodConfig.frames.push_back(frame); - } - - m_forcePodClipId = m_animationModule->CreateClip(forcePodConfig); - Core::Logger::Info("[GameState] Created force pod rotation animation clip with {} frames (frame size: {}x{}, texture size: {}x{})", - frameCount, singleFrameWidth, frameHeight, textureSize.x, textureSize.y); - } - - if (m_beamTexture != Renderer::INVALID_TEXTURE_ID && m_animationModule) { - const int frameCount = 5; - - Renderer::Vector2 textureSize = m_renderer->GetTextureSize(m_beamTexture); - float singleFrameWidth = textureSize.x / static_cast(frameCount); - float frameHeight = textureSize.y; - - Animation::AnimationClipConfig beamConfig; - beamConfig.name = "beam_animation"; - beamConfig.texturePath = "assets/SFX/beam.png"; - beamConfig.looping = true; - beamConfig.playbackSpeed = 1.0f; - - for (int i = 0; i < frameCount; ++i) { - Animation::FrameDef frame; - frame.region.position.x = static_cast(i) * singleFrameWidth; - frame.region.position.y = 0.0f; - frame.region.size.x = singleFrameWidth; - frame.region.size.y = frameHeight; - frame.duration = 0.1f; - beamConfig.frames.push_back(frame); - } - - m_beamClipId = m_animationModule->CreateClip(beamConfig); - Core::Logger::Info("[GameState] Created beam animation clip with {} frames (frame size: {}x{}, texture size: {}x{})", - frameCount, singleFrameWidth, frameHeight, textureSize.x, textureSize.y); - } - - m_hitTexture = m_renderer->LoadTexture("assets/SFX/hit.png"); - if (m_hitTexture == Renderer::INVALID_TEXTURE_ID) { - m_hitTexture = m_renderer->LoadTexture("../assets/SFX/hit.png"); - } - if (m_hitTexture != Renderer::INVALID_TEXTURE_ID) { - Renderer::Vector2 textureSize = m_renderer->GetTextureSize(m_hitTexture); - m_hitSprite = m_renderer->CreateSprite(m_hitTexture, Renderer::Rectangle{{0.0f, 0.0f}, {textureSize.x, textureSize.y}}); - Core::Logger::Info("[GameState] Hit animation spritesheet loaded (texture size: {}x{})", textureSize.x, textureSize.y); - } else { - Core::Logger::Warning("[GameState] Failed to load hit animation spritesheet"); - } - - if (m_hitTexture != Renderer::INVALID_TEXTURE_ID && m_animationModule) { - const int frameCount = 5; - - Renderer::Vector2 textureSize = m_renderer->GetTextureSize(m_hitTexture); - float singleFrameWidth = textureSize.x / static_cast(frameCount); - float frameHeight = textureSize.y; - - Animation::AnimationClipConfig hitConfig; - hitConfig.name = "hit_animation"; - hitConfig.texturePath = "assets/SFX/hit.png"; - hitConfig.looping = false; - hitConfig.playbackSpeed = 1.0f; - - for (int i = 0; i < frameCount; ++i) { - Animation::FrameDef frame; - frame.region.position.x = static_cast(i) * singleFrameWidth; - frame.region.position.y = 0.0f; - frame.region.size.x = singleFrameWidth; - frame.region.size.y = frameHeight; - frame.duration = 0.1f; - hitConfig.frames.push_back(frame); - } - - m_hitClipId = m_animationModule->CreateClip(hitConfig); - Core::Logger::Info("[GameState] Created hit animation clip with {} frames (frame size: {}x{}, texture size: {}x{})", - frameCount, singleFrameWidth, frameHeight, textureSize.x, textureSize.y); - } - - if (m_animationModule) { - Animation::GridLayout waveLayout; - waveLayout.columns = 3; - waveLayout.rows = 1; - waveLayout.frameCount = 3; - waveLayout.frameWidth = 30.0f; - waveLayout.frameHeight = 40.0f; - waveLayout.defaultDuration = 0.3f; - - m_waveAttackClipId = m_animationModule->CreateClipFromGrid( - "wave_attack", - "assets/boss/attacks/boss 2/wave_attack.png", - waveLayout, - true); - Core::Logger::Info("[GameState] Created wave attack animation clip with {} frames", 3); - } - - if (m_animationModule) { - Animation::GridLayout secondLayout; - secondLayout.columns = 3; - secondLayout.rows = 1; - secondLayout.frameCount = 3; - secondLayout.frameWidth = 19.0f; - secondLayout.frameHeight = 29.0f; - secondLayout.defaultDuration = 0.5f; - - m_secondAttackClipId = m_animationModule->CreateClipFromGrid( - "second_attack", - "assets/boss/attacks/boss_2/second_attack_boss2.png", - secondLayout, - true); - Core::Logger::Info("[GameState] Created second attack animation clip with {} frames", 3); - } - - if (m_animationModule) { - Animation::GridLayout fireLayout; - fireLayout.columns = 2; - fireLayout.rows = 1; - fireLayout.frameCount = 2; - fireLayout.frameWidth = 25.0f; - fireLayout.frameHeight = 25.0f; - fireLayout.defaultDuration = 0.3f; - - m_fireBulletClipId = m_animationModule->CreateClipFromGrid( - "fire_bullet", - "assets/boss/attacks/boss_3/fire.png", - fireLayout, - true); - Core::Logger::Info("[GameState] Created fire bullet animation clip with {} frames", 4); - } - - if (m_animationModule) { - Animation::GridLayout mineLayout; - mineLayout.columns = 2; - mineLayout.rows = 1; - mineLayout.frameCount = 2; - mineLayout.frameWidth = 36.0f; - mineLayout.frameHeight = 42.0f; - mineLayout.defaultDuration = 0.3f; - - m_mineClipId = m_animationModule->CreateClipFromGrid( - "mine", - "assets/boss/attacks/boss_3/mine.png", - mineLayout, - true); - Core::Logger::Info("[GameState] Created mine animation clip with {} frames", 4); - } - - if (m_animationModule) { - Animation::GridLayout explosionLayout; - explosionLayout.columns = 5; - explosionLayout.rows = 1; - explosionLayout.frameCount = 5; - explosionLayout.frameWidth = 54.0f; - explosionLayout.frameHeight = 65.0f; - explosionLayout.defaultDuration = 0.1f; - - m_mineExplosionClipId = m_animationModule->CreateClipFromGrid( - "mine_explosion", - "assets/boss/attacks/boss_3/explosion.png", - explosionLayout, - false); - Core::Logger::Info("[GameState] Created mine explosion animation clip with {} frames", 7); - } - - m_animationSystem = std::make_unique(m_animationModule.get()); - - ECS::EffectConfig effectConfig; - effectConfig.explosionSmall = m_explosionClipId; - effectConfig.effectsTexture = m_explosionTexture; - effectConfig.effectsSprite = m_explosionSprite; - effectConfig.shootingAnimation = m_shootingClipId; - effectConfig.shootingTexture = m_shootingTexture; - effectConfig.shootingSprite = m_shootingSprite; - effectConfig.forcePodAnimation = m_forcePodClipId; - effectConfig.forcePodTexture = m_forcePodTexture; - effectConfig.forcePodSprite = m_forcePodSprite; - effectConfig.beamAnimation = m_beamClipId; - effectConfig.beamTexture = m_beamTexture; - effectConfig.beamSprite = m_beamSprite; - effectConfig.hitAnimation = m_hitClipId; - effectConfig.hitTexture = m_hitTexture; - effectConfig.hitSprite = m_hitSprite; - - if (m_animationModule) { - if (m_explosionClipId != Animation::INVALID_CLIP_ID) { - auto explosionFirstFrame = m_animationModule->GetFrameAtTime(m_explosionClipId, 0.0f, false); - effectConfig.explosionFirstFrameRegion = explosionFirstFrame.region; - } - - if (m_shootingClipId != Animation::INVALID_CLIP_ID) { - auto shootingFirstFrame = m_animationModule->GetFrameAtTime(m_shootingClipId, 0.0f, false); - effectConfig.shootingFirstFrameRegion = shootingFirstFrame.region; - } - - if (m_forcePodClipId != Animation::INVALID_CLIP_ID) { - auto forcePodFirstFrame = m_animationModule->GetFrameAtTime(m_forcePodClipId, 0.0f, true); - effectConfig.forcePodFirstFrameRegion = forcePodFirstFrame.region; - } - - if (m_beamClipId != Animation::INVALID_CLIP_ID) { - auto beamFirstFrame = m_animationModule->GetFrameAtTime(m_beamClipId, 0.0f, true); - effectConfig.beamFirstFrameRegion = beamFirstFrame.region; - } - - if (m_hitClipId != Animation::INVALID_CLIP_ID) { - auto hitFirstFrame = m_animationModule->GetFrameAtTime(m_hitClipId, 0.0f, false); - effectConfig.hitFirstFrameRegion = hitFirstFrame.region; - } - } - - m_effectFactory = std::make_unique(effectConfig); - - m_forcePodSystem = std::make_unique(); - m_shieldSystem = std::make_unique(); - - if (!m_isNetworkSession) { - m_powerUpSpawnSystem = std::make_unique( - m_renderer.get(), - m_levelData.config.screenWidth, - m_levelData.config.screenHeight); - m_powerUpSpawnSystem->SetSpawnInterval(m_levelData.config.powerUpSpawnInterval); - m_powerUpSpawnSystem->SetEffectFactory(m_effectFactory.get()); - m_powerUpCollisionSystem = std::make_unique(m_renderer.get()); - m_collisionDetectionSystem = std::make_unique(); - m_playerResponseSystem = std::make_unique(); - m_shootingSystem = std::make_unique(bulletSprite); - m_shootingSystem->SetEffectFactory(m_effectFactory.get()); - m_movementSystem = std::make_unique(); - m_inputSystem = std::make_unique(m_renderer.get()); - m_bulletResponseSystem = std::make_unique(m_effectFactory.get()); - m_obstacleResponseSystem = std::make_unique(); - m_healthSystem = std::make_unique(); - m_scoreSystem = std::make_unique(); - } else { - // In network sessions, we still need collision systems for visual effects - m_collisionDetectionSystem = std::make_unique(); - m_bulletResponseSystem = std::make_unique(m_effectFactory.get()); - - // These systems are server-authoritative, so we don't need them on client - m_powerUpSpawnSystem.reset(); - m_powerUpCollisionSystem.reset(); - m_playerResponseSystem.reset(); - m_shootingSystem.reset(); - m_movementSystem.reset(); - m_inputSystem.reset(); - m_obstacleResponseSystem.reset(); - m_healthSystem.reset(); - m_scoreSystem.reset(); - } - } - - void InGameState::initializePlayers() { - if (m_isNetworkSession) { - return; - } - - const uint8_t playerNumber = m_context.playerNumber == 0 ? 1 : m_context.playerNumber; - const uint64_t playerHash = m_context.playerHash; - - const auto& spawns = ECS::LevelLoader::GetPlayerSpawns(m_levelData); - uint8_t playerIndex = playerNumber - 1; - - Position spawnPos{100.0f, 360.0f}; - if (!spawns.empty()) { - if (playerIndex < spawns.size()) { - spawnPos = Position{spawns[playerIndex].x, spawns[playerIndex].y}; - Core::Logger::Info("[GameState] Player {} spawning at ({}, {}) from level data", playerNumber, spawnPos.x, spawnPos.y); - } else { - spawnPos = Position{spawns[0].x, spawns[0].y}; - Core::Logger::Warning("[GameState] Player {} index out of range, using first spawn", playerNumber); - } - } else { - Core::Logger::Warning("[GameState] No player spawns in level data, using default position"); - } - - const auto& playerConfig = m_levelData.config.playerDefaults; - - m_localPlayerEntity = ECS::PlayerFactory::CreatePlayer(m_registry, playerNumber, playerHash, spawnPos.x, spawnPos.y, m_renderer.get()); - - if (m_localPlayerEntity == ECS::NULL_ENTITY) { - Core::Logger::Error("[GameState] Failed to create local player entity"); - return; - } - - if (m_registry.HasComponent(m_localPlayerEntity)) { - auto& playerComp = m_registry.GetComponent(m_localPlayerEntity); - playerComp.isLocalPlayer = true; - } else { - m_registry.AddComponent(m_localPlayerEntity, Player{playerNumber, playerHash, true}); - } - - if (m_registry.HasComponent(m_localPlayerEntity)) { - auto& position = m_registry.GetComponent(m_localPlayerEntity); - position = spawnPos; - } else { - m_registry.AddComponent(m_localPlayerEntity, Position{spawnPos.x, spawnPos.y}); - } - - if (!m_registry.HasComponent(m_localPlayerEntity)) { - m_registry.AddComponent(m_localPlayerEntity, Velocity{0.0f, 0.0f}); - } - - if (m_registry.HasComponent(m_localPlayerEntity)) { - auto& controllable = m_registry.GetComponent(m_localPlayerEntity); - controllable.speed = playerConfig.movementSpeed; - } else { - m_registry.AddComponent(m_localPlayerEntity, Controllable{playerConfig.movementSpeed}); - } - - Shooter shooterConfig{ - playerConfig.fireRate, - playerConfig.bulletOffsetX, - playerConfig.bulletOffsetY}; - if (m_registry.HasComponent(m_localPlayerEntity)) { - auto& shooter = m_registry.GetComponent(m_localPlayerEntity); - shooter = shooterConfig; - } else { - m_registry.AddComponent(m_localPlayerEntity, Shooter{shooterConfig.fireRate, shooterConfig.offsetX, shooterConfig.offsetY}); - } - - if (m_registry.HasComponent(m_localPlayerEntity)) { - auto& shootCommand = m_registry.GetComponent(m_localPlayerEntity); - shootCommand = ShootCommand{}; - } else { - m_registry.AddComponent(m_localPlayerEntity, ShootCommand{}); - } - - if (m_registry.HasComponent(m_localPlayerEntity)) { - auto& health = m_registry.GetComponent(m_localPlayerEntity); - health.max = playerConfig.maxHealth; - if (health.current > playerConfig.maxHealth) { - health.current = playerConfig.maxHealth; - } - } else { - m_registry.AddComponent(m_localPlayerEntity, Health{playerConfig.maxHealth}); - } - - auto spriteIt = m_levelAssets.sprites.find("player_blue"); - if (spriteIt != m_levelAssets.sprites.end()) { - if (m_registry.HasComponent(m_localPlayerEntity)) { - auto& drawable = m_registry.GetComponent(m_localPlayerEntity); - drawable.spriteId = spriteIt->second; - drawable.layer = 10; - drawable.scale = {0.5f, 0.5f}; - } else { - auto& drawable = m_registry.AddComponent(m_localPlayerEntity, Drawable(spriteIt->second, 10)); - drawable.scale = {0.5f, 0.5f}; - } - } - } - - } -} diff --git a/client/src/game/GameStateNetwork.cpp b/client/src/game/GameStateNetwork.cpp deleted file mode 100644 index d443c3e..0000000 --- a/client/src/game/GameStateNetwork.cpp +++ /dev/null @@ -1,1059 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** GameState - Network state update functions -*/ - -#include "../../include/GameState.hpp" - -#include "ECS/Components/TextLabel.hpp" -#include "ECS/Component.hpp" -#include "Core/Logger.hpp" -#include "ECS/PowerUpFactory.hpp" -#include "Animation/AnimationModule.hpp" -#include - -using namespace RType::ECS; -using namespace Animation; - -namespace RType { - namespace Client { - - void InGameState::OnServerStateUpdate(uint32_t /*tick*/, const std::vector& entities, const std::vector& inputAcks) { - - if (m_context.networkClient) { - m_serverScrollOffset = m_context.networkClient->GetLastScrollOffset(); - } - - for (const auto& ack : inputAcks) { - if (ack.playerHash == m_context.playerHash) { - ReconcileWithServer(ack); - break; - } - } - - std::unordered_set receivedIds; - std::vector entitiesToRemove; - - { - auto localObstacles = m_registry.GetEntitiesWithComponent(); - for (auto obsEntity : localObstacles) { - if (!m_registry.IsEntityAlive(obsEntity)) { - continue; - } - bool contaminated = m_registry.HasComponent(obsEntity) || - m_registry.HasComponent(obsEntity) || - m_registry.HasComponent(obsEntity) || - m_registry.HasComponent(obsEntity); - if (contaminated) { - - for (auto itMap = m_networkEntityMap.begin(); itMap != m_networkEntityMap.end();) { - if (itMap->second == obsEntity) { - itMap = m_networkEntityMap.erase(itMap); - } else { - ++itMap; - } - } - for (auto itId = m_obstacleIdToCollider.begin(); itId != m_obstacleIdToCollider.end();) { - if (itId->second == obsEntity) { - itId = m_obstacleIdToCollider.erase(itId); - } else { - ++itId; - } - } - auto itColl = std::find(m_obstacleColliderEntities.begin(), m_obstacleColliderEntities.end(), obsEntity); - if (itColl != m_obstacleColliderEntities.end()) { - m_obstacleColliderEntities.erase(itColl); - } - if (m_registry.IsEntityAlive(obsEntity)) { - m_registry.DestroyEntity(obsEntity); - } - } - } - } - - for (const auto& entityState : entities) { - receivedIds.insert(entityState.entityId); - - network::EntityType type = static_cast(entityState.entityType); - - if (auto it = m_networkEntityMap.find(entityState.entityId); it == m_networkEntityMap.end()) { - if (type == network::EntityType::PLAYER) { - if (entityState.ownerHash == m_context.playerHash && m_localPlayerEntity != ECS::NULL_ENTITY) { - continue; - } - - Renderer::SpriteId playerSprite = Renderer::INVALID_SPRITE_ID; - size_t playerIndex = m_networkEntityMap.size() % 3; - - const char* spriteKeys[] = {"player_green", "player_blue", "player_red"}; - auto spriteIt = m_levelAssets.sprites.find(spriteKeys[playerIndex]); - if (spriteIt != m_levelAssets.sprites.end()) { - playerSprite = spriteIt->second; - } - - if (playerSprite == Renderer::INVALID_SPRITE_ID) { - continue; - } - - auto newEntity = m_registry.CreateEntity(); - CleanupInvalidComponents(newEntity, network::EntityType::PLAYER); - - m_registry.AddComponent(newEntity, Position{entityState.x, entityState.y}); - m_registry.AddComponent(newEntity, Velocity{entityState.vx, entityState.vy}); - m_registry.AddComponent(newEntity, Health{static_cast(entityState.health), 300}); - - m_registry.AddComponent(newEntity, Controllable{200.0f}); - m_registry.AddComponent(newEntity, Shooter{0.2f, 50.0f, 20.0f}); - m_registry.AddComponent(newEntity, BoxCollider{25.0f, 25.0f}); - m_registry.AddComponent(newEntity, CircleCollider{12.5f}); - m_registry.AddComponent(newEntity, CollisionLayer(CollisionLayers::PLAYER, CollisionLayers::ENEMY | CollisionLayers::ENEMY_BULLET | CollisionLayers::OBSTACLE)); - - auto& drawable = m_registry.AddComponent(newEntity, Drawable(playerSprite, 10)); - drawable.scale = {0.5f, 0.5f}; - drawable.origin = Math::Vector2(128.0f, 128.0f); - - auto [playerName, playerNum] = FindPlayerNameAndNumber(entityState.ownerHash, m_assignedPlayerNumbers); - uint8_t pNum = (playerNum > 0 && playerNum <= MAX_PLAYERS) ? playerNum : 1; - m_registry.AddComponent(newEntity, Player{pNum, entityState.ownerHash, false}); - if (playerNum > 0 && playerNum <= MAX_PLAYERS) { - m_assignedPlayerNumbers.insert(playerNum); - } - CreatePlayerNameLabel(newEntity, playerName, entityState.x, entityState.y); - - ApplyPowerUpStateToPlayer(newEntity, entityState); - - m_networkEntityMap[entityState.entityId] = newEntity; - - if (entityState.ownerHash == m_context.playerHash) { - m_localPlayerEntity = newEntity; - if (m_registry.HasComponent(newEntity)) { - auto& playerComp = m_registry.GetComponent(newEntity); - playerComp.isLocalPlayer = true; - } - if (!m_registry.HasComponent(newEntity)) { - m_registry.AddComponent(newEntity, ShootCommand{}); - } - if (m_context.playerNumber >= 1 && m_context.playerNumber <= MAX_PLAYERS) { - size_t localPlayerIndex = static_cast(m_context.playerNumber - 1); - m_playersHUD[localPlayerIndex].playerEntity = newEntity; - m_playersHUD[localPlayerIndex].active = true; - m_playersHUD[localPlayerIndex].score = entityState.score; - m_playersHUD[localPlayerIndex].health = static_cast(entityState.health); - m_playersHUD[localPlayerIndex].maxHealth = 300; - m_playerScore = entityState.score; - if (entityState.health > 0) { - m_playersHUD[localPlayerIndex].isDead = false; - } - } - } - if (playerNum > 0 && playerNum <= MAX_PLAYERS) { - size_t hudPlayerIndex = static_cast(playerNum - 1); - m_playersHUD[hudPlayerIndex].active = true; - m_playersHUD[hudPlayerIndex].playerEntity = newEntity; - m_playersHUD[hudPlayerIndex].score = entityState.score; - m_playersHUD[hudPlayerIndex].health = static_cast(entityState.health); - m_playersHUD[hudPlayerIndex].maxHealth = 300; - - if (entityState.health > 0) { - m_playersHUD[hudPlayerIndex].isDead = false; - } - } - } else if (type == network::EntityType::ENEMY) { - uint8_t enemyType = entityState.flags; - EnemySpriteConfig config = GetEnemySpriteConfig(enemyType); - Renderer::SpriteId enemySprite = config.sprite; - Math::Color enemyTint = config.tint; - - if (enemySprite == Renderer::INVALID_SPRITE_ID) { - enemySprite = m_enemyGreenSprite; - } - - if (enemySprite == Renderer::INVALID_SPRITE_ID) { - Core::Logger::Error("[GameState] Missing enemy sprite for type {}, skipping entity {}", static_cast(enemyType), entityState.entityId); - continue; - } - - auto newEntity = m_registry.CreateEntity(); - CleanupInvalidComponents(newEntity, network::EntityType::ENEMY); - - m_registry.AddComponent(newEntity, Position{entityState.x, entityState.y}); - m_registry.AddComponent(newEntity, Velocity{entityState.vx, entityState.vy}); - m_registry.AddComponent(newEntity, Health{static_cast(entityState.health), 100}); - m_registry.AddComponent(newEntity, Enemy{static_cast(enemyType)}); - - // Add collision components for hit effect detection - m_registry.AddComponent(newEntity, BoxCollider(64.0f, 64.0f)); - m_registry.AddComponent(newEntity, - CollisionLayer(CollisionLayers::ENEMY, CollisionLayers::PLAYER | CollisionLayers::PLAYER_BULLET)); - - auto& drawable = m_registry.AddComponent(newEntity, Drawable(enemySprite, 1)); - drawable.scale = {0.5f, 0.5f}; - drawable.origin = Math::Vector2(128.0f, 128.0f); - drawable.rotation = config.rotation; - drawable.tint = enemyTint; - - m_networkEntityMap[entityState.entityId] = newEntity; - } else if (type == network::EntityType::BOSS) { - Renderer::SpriteId bossSprite = Renderer::INVALID_SPRITE_ID; - - std::vector bossNames = {"boss_2", "boss_dragon", "boss", "boss_3"}; - for (const auto& bossName : bossNames) { - auto spriteIt = m_levelAssets.sprites.find(bossName); - if (spriteIt != m_levelAssets.sprites.end()) { - bossSprite = spriteIt->second; - Core::Logger::Info("[GameState] Using boss sprite: {}", bossName); - break; - } - } - - if (bossSprite == Renderer::INVALID_SPRITE_ID) { - Core::Logger::Warning("[GameState] No boss sprite found in level assets"); - continue; - } - - auto newEntity = m_registry.CreateEntity(); - CleanupInvalidComponents(newEntity, network::EntityType::BOSS); - - m_registry.AddComponent(newEntity, Position{entityState.x, entityState.y}); - m_registry.AddComponent(newEntity, Velocity{entityState.vx, entityState.vy}); - m_registry.AddComponent(newEntity, Health{static_cast(entityState.health), 1000}); - m_registry.AddComponent(newEntity, Boss{}); - - // Add collision components for hit effect detection - m_registry.AddComponent(newEntity, BoxCollider(200.0f, 200.0f)); - m_registry.AddComponent(newEntity, - CollisionLayer(CollisionLayers::ENEMY, CollisionLayers::PLAYER | CollisionLayers::PLAYER_BULLET)); - - auto& drawable = m_registry.AddComponent(newEntity, Drawable(bossSprite, 5)); - drawable.scale = {3.0f, 3.0f}; - drawable.origin = Math::Vector2(0.0f, 0.0f); - - m_networkEntityMap[entityState.entityId] = newEntity; - - m_bossHealthBar.currentHealth = static_cast(entityState.health); - m_bossHealthBar.maxHealth = 100; - m_bossHealthBar.bossNetworkId = entityState.entityId; - } else if (type == network::EntityType::BULLET) { - auto newEntity = m_registry.CreateEntity(); - CleanupInvalidComponents(newEntity, network::EntityType::BULLET); - - m_registry.AddComponent(newEntity, Position{entityState.x, entityState.y}); - m_registry.AddComponent(newEntity, Velocity{entityState.vx, entityState.vy}); - - if (entityState.flags == 15) { - auto thirdBulletSpriteIt = m_levelAssets.sprites.find("third_bullet"); - if (thirdBulletSpriteIt != m_levelAssets.sprites.end()) { - auto& d = m_registry.AddComponent(newEntity, Drawable(thirdBulletSpriteIt->second, 12)); - d.scale = {2.5f, 2.5f}; - d.origin = Math::Vector2(16.0f, 16.0f); - } else { - Core::Logger::Warning("[GameState] Missing third bullet sprite (entity {})", entityState.entityId); - m_registry.DestroyEntity(newEntity); - continue; - } - } else if (entityState.flags == 14) { - auto blackOrbSpriteIt = m_levelAssets.sprites.find("black_orb"); - if (blackOrbSpriteIt != m_levelAssets.sprites.end()) { - auto& d = m_registry.AddComponent(newEntity, Drawable(blackOrbSpriteIt->second, 12)); - d.scale = {2.0f, 2.0f}; - d.origin = Math::Vector2(20.0f, 20.0f); - } else { - Core::Logger::Warning("[GameState] Missing black orb sprite (entity {})", entityState.entityId); - m_registry.DestroyEntity(newEntity); - continue; - } - } else if (entityState.flags == 16) { - auto waveAttackSpriteIt = m_levelAssets.sprites.find("wave_attack"); - if (waveAttackSpriteIt != m_levelAssets.sprites.end() && - m_waveAttackClipId != Animation::INVALID_CLIP_ID) { - - auto& d = m_registry.AddComponent(newEntity, Drawable(waveAttackSpriteIt->second, 12)); - d.scale = {3.0f, 3.0f}; - d.origin = Math::Vector2(15.0f, 20.0f); - - if (m_animationModule) { - auto& anim = m_registry.AddComponent(newEntity, ECS::SpriteAnimation{m_waveAttackClipId, true, 1.0f}); - auto firstFrame = m_animationModule->GetFrameAtTime(m_waveAttackClipId, 0.0f, true); - anim.currentRegion = firstFrame.region; - anim.currentFrameIndex = 0; - - auto& animatedSprite = m_registry.AddComponent(newEntity); - animatedSprite.needsUpdate = true; - } - - m_registry.AddComponent(newEntity, Bullet(ECS::NULL_ENTITY)); - m_registry.AddComponent(newEntity, CircleCollider(45.0f)); - m_registry.AddComponent(newEntity, Damage(10)); - m_registry.AddComponent(newEntity, - CollisionLayer(CollisionLayers::ENEMY_BULLET, CollisionLayers::PLAYER)); - } else { - Core::Logger::Warning("[GameState] Missing wave_attack sprite or animation (entity {})", entityState.entityId); - m_registry.DestroyEntity(newEntity); - continue; - } - } else if (entityState.flags == 17) { - auto secondAttackSpriteIt = m_levelAssets.sprites.find("second_attack"); - if (secondAttackSpriteIt != m_levelAssets.sprites.end() && - m_secondAttackClipId != Animation::INVALID_CLIP_ID) { - - auto& d = m_registry.AddComponent(newEntity, Drawable(secondAttackSpriteIt->second, 12)); - d.scale = {2.8f, 2.8f}; - d.origin = Math::Vector2(11.0f, 11.0f); - - if (m_animationModule) { - auto& anim = m_registry.AddComponent(newEntity, ECS::SpriteAnimation{m_secondAttackClipId, true, 1.0f}); - auto firstFrame = m_animationModule->GetFrameAtTime(m_secondAttackClipId, 0.0f, true); - anim.currentRegion = firstFrame.region; - anim.currentFrameIndex = 0; - - auto& animatedSprite = m_registry.AddComponent(newEntity); - animatedSprite.needsUpdate = true; - } - - } else { - Core::Logger::Warning("[GameState] Missing second_attack sprite or animation (entity {})", entityState.entityId); - m_registry.DestroyEntity(newEntity); - continue; - } - } else if (entityState.flags == 18) { - auto fireBulletSpriteIt = m_levelAssets.sprites.find("fire_bullet"); - if (fireBulletSpriteIt != m_levelAssets.sprites.end() && - m_fireBulletClipId != Animation::INVALID_CLIP_ID) { - - auto& d = m_registry.AddComponent(newEntity, Drawable(fireBulletSpriteIt->second, 12)); - d.scale = {2.5f, 2.5f}; - d.origin = Math::Vector2(10.5f, 8.5f); - - if (m_animationModule) { - auto& anim = m_registry.AddComponent(newEntity, ECS::SpriteAnimation{m_fireBulletClipId, true, 1.0f}); - auto firstFrame = m_animationModule->GetFrameAtTime(m_fireBulletClipId, 0.0f, true); - anim.currentRegion = firstFrame.region; - anim.currentFrameIndex = 0; - - auto& animatedSprite = m_registry.AddComponent(newEntity); - animatedSprite.needsUpdate = true; - } - - m_registry.AddComponent(newEntity, ECS::CircleCollider{12.0f}); - m_registry.AddComponent(newEntity, - CollisionLayer(CollisionLayers::ENEMY_BULLET, CollisionLayers::PLAYER)); - } else { - Core::Logger::Warning("[GameState] Missing fire_bullet sprite or animation (entity {})", entityState.entityId); - m_registry.DestroyEntity(newEntity); - continue; - } - } else if (entityState.flags == 20) { - Core::Logger::Debug("[GameState] Received exploding mine entity {} at ({}, {})", entityState.entityId, entityState.x, entityState.y); - auto mineExplosionSpriteIt = m_levelAssets.sprites.find("mine_explosion"); - Core::Logger::Debug("[GameState] Mine explosion sprite found: {}, clipId valid: {}", - mineExplosionSpriteIt != m_levelAssets.sprites.end(), - m_mineExplosionClipId != Animation::INVALID_CLIP_ID); - if (mineExplosionSpriteIt != m_levelAssets.sprites.end() && - m_mineExplosionClipId != Animation::INVALID_CLIP_ID) { - - auto& d = m_registry.AddComponent(newEntity, Drawable(mineExplosionSpriteIt->second, 12)); - d.scale = {2.0f, 2.0f}; - d.origin = Math::Vector2(13.0f, 11.5f); - - if (m_animationModule) { - auto& anim = m_registry.AddComponent(newEntity, ECS::SpriteAnimation{m_mineExplosionClipId, false, 1.0f}); - auto firstFrame = m_animationModule->GetFrameAtTime(m_mineExplosionClipId, 0.0f, false); - anim.currentRegion = firstFrame.region; - anim.currentFrameIndex = 0; - - auto& animatedSprite = m_registry.AddComponent(newEntity); - animatedSprite.needsUpdate = true; - } - } else { - Core::Logger::Warning("[GameState] Missing mine_explosion sprite or animation (entity {})", entityState.entityId); - m_registry.DestroyEntity(newEntity); - continue; - } - } else if (entityState.flags == 19) { - Core::Logger::Debug("[GameState] Received mine entity {} at ({}, {})", entityState.entityId, entityState.x, entityState.y); - auto mineSpriteIt = m_levelAssets.sprites.find("mine"); - Core::Logger::Debug("[GameState] Mine sprite found: {}, clipId valid: {}", - mineSpriteIt != m_levelAssets.sprites.end(), - m_mineClipId != Animation::INVALID_CLIP_ID); - if (mineSpriteIt != m_levelAssets.sprites.end() && - m_mineClipId != Animation::INVALID_CLIP_ID) { - - auto& d = m_registry.AddComponent(newEntity, Drawable(mineSpriteIt->second, 12)); - d.scale = {2.5f, 2.5f}; - d.origin = Math::Vector2(9.5f, 22.5f); - - if (m_animationModule) { - auto& anim = m_registry.AddComponent(newEntity, ECS::SpriteAnimation{m_mineClipId, true, 1.0f}); - auto firstFrame = m_animationModule->GetFrameAtTime(m_mineClipId, 0.0f, true); - anim.currentRegion = firstFrame.region; - anim.currentFrameIndex = 0; - - auto& animatedSprite = m_registry.AddComponent(newEntity); - animatedSprite.needsUpdate = true; - } - - m_registry.AddComponent(newEntity, ECS::CircleCollider{20.0f}); - m_registry.AddComponent(newEntity, - CollisionLayer(CollisionLayers::OBSTACLE, CollisionLayers::PLAYER)); - } else { - Core::Logger::Warning("[GameState] Missing mine sprite or animation (entity {})", entityState.entityId); - m_registry.DestroyEntity(newEntity); - continue; - } - } else if (entityState.flags == 13) { - auto bossBulletSpriteIt = m_levelAssets.sprites.find("boss_bullet"); - if (bossBulletSpriteIt != m_levelAssets.sprites.end()) { - auto& d = m_registry.AddComponent(newEntity, Drawable(bossBulletSpriteIt->second, 12)); - d.scale = {2.0f, 2.0f}; - d.origin = Math::Vector2(8.0f, 4.0f); - } else { - Core::Logger::Warning("[GameState] Missing boss bullet sprite (entity {})", entityState.entityId); - m_registry.DestroyEntity(newEntity); - continue; - } - } else if (entityState.flags >= 10) { - uint8_t enemyType = entityState.flags - 10; - EnemyBulletSpriteConfig config = GetEnemyBulletSpriteConfig(enemyType); - Renderer::SpriteId bulletSprite = config.sprite; - Math::Color bulletTint = config.tint; - float scaleValue = config.scale; - - if (bulletSprite == Renderer::INVALID_SPRITE_ID) { - auto bulletSpriteIt = m_levelAssets.sprites.find("bullet"); - if (bulletSpriteIt != m_levelAssets.sprites.end()) { - bulletSprite = bulletSpriteIt->second; - } - } - - if (bulletSprite == Renderer::INVALID_SPRITE_ID) { - Core::Logger::Warning("[GameState] Missing enemy bullet sprite for type {} (entity {})", static_cast(enemyType), entityState.entityId); - m_registry.DestroyEntity(newEntity); - continue; - } - - auto& d = m_registry.AddComponent(newEntity, Drawable(bulletSprite, 12)); - d.scale = {scaleValue, scaleValue}; - d.origin = Math::Vector2(128.0f, 128.0f); - d.tint = bulletTint; - } else { - auto bulletSpriteIt = m_levelAssets.sprites.find("bullet"); - if (bulletSpriteIt != m_levelAssets.sprites.end()) { - auto& d = m_registry.AddComponent(newEntity, Drawable(bulletSpriteIt->second, 12)); - d.scale = {0.1f, 0.1f}; - d.origin = Math::Vector2(128.0f, 128.0f); - d.tint = {0.2f, 0.8f, 1.0f, 1.0f}; - } - } - - bool isPlayerBullet = (entityState.flags < 10); - - m_registry.AddComponent(newEntity, Bullet(m_localPlayerEntity)); - m_registry.AddComponent(newEntity, BoxCollider(20.0f, 10.0f)); - m_registry.AddComponent(newEntity, Damage(25)); - m_registry.AddComponent(newEntity, - CollisionLayer(CollisionLayers::PLAYER_BULLET, - CollisionLayers::ENEMY | CollisionLayers::OBSTACLE)); - - if (isPlayerBullet && m_localPlayerEntity != ECS::NULL_ENTITY && - m_registry.IsEntityAlive(m_localPlayerEntity) && m_effectFactory && - m_registry.HasComponent(m_localPlayerEntity)) { - const auto& playerPos = m_registry.GetComponent(m_localPlayerEntity); - float bulletX = entityState.x; - float bulletY = entityState.y; - float dx = bulletX - playerPos.x; - float dy = bulletY - playerPos.y; - float distance = std::sqrt(dx * dx + dy * dy); - - if (distance < 80.0f && dx > 0 && std::abs(dy) < 40.0f && entityState.vx > 400.0f) { - m_effectFactory->CreateShootingEffect(m_registry, playerPos.x, playerPos.y, m_localPlayerEntity); - } - } - - m_networkEntityMap[entityState.entityId] = newEntity; - m_bulletFlagsMap[entityState.entityId] = entityState.flags; - } else if (type == network::EntityType::POWERUP) { - uint8_t powerupType = entityState.flags; - ECS::PowerUpType puType = static_cast(powerupType); - - auto newEntity = m_registry.CreateEntity(); - CleanupInvalidComponents(newEntity, network::EntityType::POWERUP); - - m_registry.AddComponent(newEntity, Position{entityState.x, entityState.y}); - m_registry.AddComponent(newEntity, Velocity{entityState.vx, entityState.vy}); - - Math::Color powerupColor = ECS::PowerUpFactory::GetPowerUpColor(puType); - Renderer::SpriteId powerupSprite = Renderer::INVALID_SPRITE_ID; - - powerupSprite = GetPowerUpSprite(puType); - - if (powerupSprite == Renderer::INVALID_SPRITE_ID && puType == ECS::PowerUpType::FORCE_POD) { - auto textureIt = m_levelAssets.textures.find("powerup-force-pod"); - if (textureIt != m_levelAssets.textures.end()) { - powerupSprite = m_renderer->CreateSprite(textureIt->second, {}); - } else { - powerupSprite = GetPowerUpSprite(ECS::PowerUpType::LASER_BEAM); - } - } - - if (powerupSprite != Renderer::INVALID_SPRITE_ID) { - auto& d = m_registry.AddComponent(newEntity, Drawable(powerupSprite, 5)); - float scale = ECS::PowerUpFactory::GetPowerUpScale(puType); - d.scale = {scale, scale}; - d.tint = powerupColor; - - if (puType == ECS::PowerUpType::FORCE_POD && m_effectFactory) { - const auto& config = m_effectFactory->GetConfig(); - if (config.forcePodAnimation != Animation::INVALID_CLIP_ID) { - if (config.forcePodSprite != Renderer::INVALID_SPRITE_ID) { - d.spriteId = config.forcePodSprite; - } - - auto& anim = m_registry.AddComponent(newEntity, - ECS::SpriteAnimation(config.forcePodAnimation, true, 1.0f)); - anim.looping = true; - - if (config.forcePodFirstFrameRegion.size.x > 0.0f && config.forcePodFirstFrameRegion.size.y > 0.0f) { - anim.currentRegion = config.forcePodFirstFrameRegion; - anim.currentFrameIndex = 0; - } else if (m_animationModule) { - auto firstFrame = m_animationModule->GetFrameAtTime(config.forcePodAnimation, 0.0f, true); - anim.currentRegion = firstFrame.region; - anim.currentFrameIndex = m_animationModule->GetFrameIndexAtTime(config.forcePodAnimation, 0.0f, true); - } - auto& animatedSprite = m_registry.AddComponent(newEntity); - animatedSprite.needsUpdate = true; - } else { - auto& glow = m_registry.AddComponent(newEntity); - glow.baseScale = scale; - } - } else { - auto& glow = m_registry.AddComponent(newEntity); - glow.baseScale = scale; - } - } - - m_registry.AddComponent(newEntity, BoxCollider{32.0f, 32.0f}); - m_registry.AddComponent(newEntity, - CollisionLayer(CollisionLayers::POWERUP, - CollisionLayers::PLAYER)); - m_registry.AddComponent(newEntity, ECS::PowerUp(puType, entityState.entityId)); - - m_networkEntityMap[entityState.entityId] = newEntity; - } else if (type == network::EntityType::OBSTACLE) { - uint64_t obstacleId = entityState.ownerHash; - auto colliderIt = m_obstacleIdToCollider.find(obstacleId); - - if (colliderIt == m_obstacleIdToCollider.end()) { - continue; - } - - ECS::Entity colliderEntity = colliderIt->second; - - if (!m_registry.IsEntityAlive(colliderEntity)) { - continue; - } - - if (!m_registry.HasComponent(colliderEntity) || - !m_registry.HasComponent(colliderEntity)) { - continue; - } - - m_networkEntityMap[entityState.entityId] = colliderEntity; - - CleanupInvalidComponents(colliderEntity, network::EntityType::OBSTACLE); - } - } else { - auto ecsEntity = it->second; - - if (type == network::EntityType::BULLET) { - bool contaminated = false; - if (!m_registry.IsEntityAlive(ecsEntity)) { - contaminated = true; - } else if (m_registry.HasComponent(ecsEntity) || - m_registry.HasComponent(ecsEntity)) { - contaminated = true; - } - if (contaminated) { - if (m_registry.IsEntityAlive(ecsEntity)) { - m_registry.DestroyEntity(ecsEntity); - } - m_networkEntityMap.erase(it); - m_bulletFlagsMap.erase(entityState.entityId); - continue; - } - } - - if (type == network::EntityType::BULLET) { - auto flagsIt = m_bulletFlagsMap.find(entityState.entityId); - bool flagsChanged = false; - - if (flagsIt != m_bulletFlagsMap.end()) { - if (flagsIt->second != entityState.flags) { - flagsChanged = true; - } - } - - if (flagsChanged || !m_registry.IsEntityAlive(ecsEntity)) { - if (m_registry.IsEntityAlive(ecsEntity)) { - m_registry.DestroyEntity(ecsEntity); - } - m_networkEntityMap.erase(it); - m_bulletFlagsMap.erase(entityState.entityId); - continue; - } - } - - if (type == network::EntityType::OBSTACLE) { - uint64_t obstacleId = entityState.ownerHash; - auto colliderIt = m_obstacleIdToCollider.find(obstacleId); - - if (colliderIt == m_obstacleIdToCollider.end()) { - continue; - } - - ECS::Entity colliderEntity = colliderIt->second; - - if (!m_registry.IsEntityAlive(colliderEntity)) { - m_networkEntityMap.erase(it); - continue; - } - - if (!m_registry.HasComponent(colliderEntity)) { - CleanupInvalidComponents(colliderEntity, network::EntityType::OBSTACLE); - m_networkEntityMap.erase(it); - continue; - } - - if (!m_registry.HasComponent(colliderEntity)) { - m_networkEntityMap.erase(it); - continue; - } - - auto& colliderPos = m_registry.GetComponent(colliderEntity); - colliderPos.x = entityState.x; - colliderPos.y = entityState.y; - - if (m_registry.HasComponent(colliderEntity)) { - m_registry.RemoveComponent(colliderEntity); - } - - if (m_registry.HasComponent(colliderEntity)) { - const auto& metadata = m_registry.GetComponent(colliderEntity); - if (metadata.visualEntity != ECS::NULL_ENTITY && - m_registry.IsEntityAlive(metadata.visualEntity) && - m_registry.HasComponent(metadata.visualEntity)) { - auto& visualPos = m_registry.GetComponent(metadata.visualEntity); - visualPos.x = entityState.x - metadata.offsetX; - visualPos.y = entityState.y - metadata.offsetY; - - if (m_registry.HasComponent(metadata.visualEntity)) { - m_registry.RemoveComponent(metadata.visualEntity); - } - } - } - - continue; - } - - if (m_registry.HasComponent(ecsEntity)) { - if (type == network::EntityType::PLAYER && ecsEntity == m_localPlayerEntity) { - - } else { - auto& pos = m_registry.GetComponent(ecsEntity); - bool useInterpolation = (type == network::EntityType::PLAYER || - type == network::EntityType::ENEMY || - type == network::EntityType::BOSS); - if (useInterpolation && m_isNetworkSession) { - auto interpIt = m_interpolationStates.find(entityState.entityId); - if (interpIt == m_interpolationStates.end()) { - InterpolationState state; - state.prevX = pos.x; - state.prevY = pos.y; - state.targetX = entityState.x; - state.targetY = entityState.y; - state.interpTime = 0.0f; - state.interpDuration = 1.0f / 60.0f; - m_interpolationStates[entityState.entityId] = state; - } else { - interpIt->second.prevX = pos.x; - interpIt->second.prevY = pos.y; - interpIt->second.targetX = entityState.x; - interpIt->second.targetY = entityState.y; - interpIt->second.interpTime = 0.0f; - } - } else { - pos.x = entityState.x; - pos.y = entityState.y; - } - } - - if (type == network::EntityType::PLAYER) { - auto& pos = m_registry.GetComponent(ecsEntity); - UpdatePlayerNameLabelPosition(ecsEntity, pos.x, pos.y); - ApplyPowerUpStateToPlayer(ecsEntity, entityState); - } - } - - if (m_registry.HasComponent(ecsEntity)) { - auto& vel = m_registry.GetComponent(ecsEntity); - vel.dx = entityState.vx; - vel.dy = entityState.vy; - } - - if (type == network::EntityType::PLAYER) { - for (size_t i = 0; i < MAX_PLAYERS; i++) { - if (m_playersHUD[i].playerEntity == ecsEntity) { - m_playersHUD[i].score = entityState.score; - m_playersHUD[i].health = static_cast(entityState.health); - if (ecsEntity == m_localPlayerEntity) { - m_playerScore = entityState.score; - } - break; - } - } - } - - if (type == network::EntityType::POWERUP) { - if (entityState.health == 0 && m_registry.IsEntityAlive(ecsEntity)) { - m_registry.DestroyEntity(ecsEntity); - entitiesToRemove.push_back(entityState.entityId); - } - continue; - } - - if (m_registry.HasComponent(ecsEntity)) { - auto& health = m_registry.GetComponent(ecsEntity); - int newHealth = static_cast(entityState.health); - if (newHealth < 0) - newHealth = 0; - - bool playerIsDead = false; - size_t playerIndex = MAX_PLAYERS; - bool isPlayerEntity = (type == network::EntityType::PLAYER); - - for (size_t i = 0; i < MAX_PLAYERS; i++) { - if (m_playersHUD[i].playerEntity == ecsEntity) { - playerIsDead = m_playersHUD[i].isDead; - playerIndex = i; - break; - } - } - if (ecsEntity == m_localPlayerEntity && playerIndex == MAX_PLAYERS) { - size_t localPlayerIndex = static_cast(m_context.playerNumber - 1); - if (localPlayerIndex < MAX_PLAYERS) { - playerIsDead = m_playersHUD[localPlayerIndex].isDead; - playerIndex = localPlayerIndex; - } - } - - if (playerIsDead) { - health.current = 0; - if (playerIndex < MAX_PLAYERS) { - m_playersHUD[playerIndex].health = 0; - } - } else { - health.current = newHealth; - - if (isPlayerEntity && playerIndex < MAX_PLAYERS) { - // Player dies when health reaches 0 (all 300 HP gone) - if (newHealth <= 0) { - m_playersHUD[playerIndex].isDead = true; - m_playersHUD[playerIndex].health = 0; - health.current = 0; - - DestroyPlayerNameLabel(ecsEntity); - - for (const auto& p : m_context.allPlayers) { - if (m_playersHUD[playerIndex].playerEntity == ecsEntity) { - if (p.number > 0 && p.number <= MAX_PLAYERS) { - m_assignedPlayerNumbers.erase(p.number); - } - break; - } - } - - if (m_registry.IsEntityAlive(ecsEntity)) { - m_registry.DestroyEntity(ecsEntity); - } - m_playersHUD[playerIndex].playerEntity = NULL_ENTITY; - if (ecsEntity == m_localPlayerEntity) { - m_localPlayerEntity = NULL_ENTITY; - } - entitiesToRemove.push_back(entityState.entityId); - } else { - m_playersHUD[playerIndex].health = newHealth; - } - } - } - } - - if (type == network::EntityType::BOSS) { - if (m_registry.HasComponent(ecsEntity)) { - auto& drawable = m_registry.GetComponent(ecsEntity); - if (entityState.flags == 1) { - drawable.tint = {1.0f, 0.3f, 0.3f, 1.0f}; - } else { - drawable.tint = {1.0f, 1.0f, 1.0f, 1.0f}; - } - } - - if (!m_bossWarningTriggered && entityState.x < 2200.0f && entityState.x > -200.0f) { - Core::Logger::Info("[GameState] Boss warning TRIGGERED at x={}", entityState.x); - m_bossWarningActive = true; - m_bossWarningTriggered = true; - m_bossWarningTimer = 0.0f; - } - - if (!m_bossHealthBar.active && entityState.x < 1300.0f) { - initializeBossHealthBar(); - m_bossHealthBar.maxHealth = 100; - m_bossHealthBar.bossNetworkId = entityState.entityId; - } - - m_bossHealthBar.currentHealth = static_cast(entityState.health); - - if (m_bossHealthBar.active && entityState.health == 0) { - destroyBossHealthBar(); - } else { - updateBossHealthBar(); - } - } - - if (type == network::EntityType::PLAYER) { - ApplyPowerUpStateToPlayer(ecsEntity, entityState); - } - } - } - - - for (uint32_t entityId : entitiesToRemove) { - auto it = m_networkEntityMap.find(entityId); - if (it != m_networkEntityMap.end()) { - m_networkEntityMap.erase(it); - } - m_bulletFlagsMap.erase(entityId); - } - - for (auto it = m_networkEntityMap.begin(); it != m_networkEntityMap.end();) { - if (receivedIds.find(it->first) == receivedIds.end()) { - auto ecsEntity = it->second; - uint32_t networkId = it->first; - - if (m_effectFactory && m_registry.IsEntityAlive(ecsEntity) && - m_registry.HasComponent(ecsEntity) && - m_registry.HasComponent(ecsEntity)) { - const auto& pos = m_registry.GetComponent(ecsEntity); - auto explosionEntity = m_effectFactory->CreateExplosionSmall(m_registry, pos.x, pos.y); - - if (m_explosionSprite != Renderer::INVALID_SPRITE_ID && - m_registry.HasComponent(explosionEntity)) { - auto& drawable = m_registry.GetComponent(explosionEntity); - if (drawable.spriteId == Renderer::INVALID_SPRITE_ID) { - drawable.spriteId = m_explosionSprite; - } - } - - if (m_effectFactory && m_registry.HasComponent(explosionEntity)) { - const auto& config = m_effectFactory->GetConfig(); - auto& anim = m_registry.GetComponent(explosionEntity); - if (anim.clipId == config.explosionSmall && - config.explosionFirstFrameRegion.size.x > 0.0f && - config.explosionFirstFrameRegion.size.y > 0.0f) { - anim.currentRegion = config.explosionFirstFrameRegion; - anim.currentFrameIndex = 0; - if (m_registry.HasComponent(explosionEntity)) { - m_registry.GetComponent(explosionEntity).needsUpdate = true; - } - } - } - } - - // Create hit effect when player bullets are destroyed (server-side collision) - auto bulletFlagsIt = m_bulletFlagsMap.find(networkId); - if (bulletFlagsIt != m_bulletFlagsMap.end() && bulletFlagsIt->second < 10) { - // Player bullet was destroyed - create hit effect at last position - if (m_effectFactory && m_registry.IsEntityAlive(ecsEntity) && - m_registry.HasComponent(ecsEntity)) { - const auto& pos = m_registry.GetComponent(ecsEntity); - m_effectFactory->CreateHitEffect(m_registry, pos.x, pos.y); - Core::Logger::Info("[Network] Created hit effect for destroyed bullet at ({}, {})", pos.x, pos.y); - } - } - - DestroyPlayerNameLabel(ecsEntity); - - if (m_registry.IsEntityAlive(ecsEntity) && m_registry.HasComponent(ecsEntity)) { - for (auto obsIt = m_obstacleIdToCollider.begin(); obsIt != m_obstacleIdToCollider.end(); ) { - if (obsIt->second == ecsEntity) { - obsIt = m_obstacleIdToCollider.erase(obsIt); - } else { - ++obsIt; - } - } - - auto colliderIt = std::find(m_obstacleColliderEntities.begin(), m_obstacleColliderEntities.end(), ecsEntity); - if (colliderIt != m_obstacleColliderEntities.end()) { - m_obstacleColliderEntities.erase(colliderIt); - } - - if (m_registry.HasComponent(ecsEntity)) { - const auto& metadata = m_registry.GetComponent(ecsEntity); - if (metadata.visualEntity != ECS::NULL_ENTITY && - m_registry.IsEntityAlive(metadata.visualEntity)) { - auto spriteIt = std::find(m_obstacleSpriteEntities.begin(), m_obstacleSpriteEntities.end(), metadata.visualEntity); - if (spriteIt != m_obstacleSpriteEntities.end()) { - m_obstacleSpriteEntities.erase(spriteIt); - } - m_registry.DestroyEntity(metadata.visualEntity); - } - } - } - - for (size_t i = 0; i < MAX_PLAYERS; i++) { - if (m_playersHUD[i].playerEntity == ecsEntity) { - m_playersHUD[i].isDead = true; - m_playersHUD[i].health = 0; - m_playersHUD[i].playerEntity = NULL_ENTITY; - for (const auto& p : m_context.allPlayers) { - if (p.number > 0 && p.number <= MAX_PLAYERS && static_cast(p.number - 1) == i) { - m_assignedPlayerNumbers.erase(p.number); - break; - } - } - break; - } - } - if (ecsEntity == m_localPlayerEntity) { - size_t localPlayerIndex = static_cast(m_context.playerNumber - 1); - if (localPlayerIndex < MAX_PLAYERS) { - m_playersHUD[localPlayerIndex].isDead = true; - m_playersHUD[localPlayerIndex].health = 0; - m_playersHUD[localPlayerIndex].playerEntity = NULL_ENTITY; - } - m_localPlayerEntity = NULL_ENTITY; - } - - if (m_bossHealthBar.active && networkId == m_bossHealthBar.bossNetworkId) { - destroyBossHealthBar(); - if (m_context.audio && m_bossMusicPlaying) { - m_context.audio->StopMusic(m_bossMusic); - m_bossMusicPlaying = false; - - if (m_gameMusic != Audio::INVALID_MUSIC_ID && !m_isGameOver) { - Audio::PlaybackOptions opts; - opts.loop = true; - opts.volume = 0.5f; - m_context.audio->PlayMusic(m_gameMusic, opts); - m_gameMusicPlaying = true; - } - } - } - - if (m_registry.IsEntityAlive(ecsEntity)) { - m_registry.DestroyEntity(ecsEntity); - } - it = m_networkEntityMap.erase(it); - m_bulletFlagsMap.erase(networkId); - } else { - ++it; - } - } - } - - void InGameState::ReconcileWithServer(const network::InputAck& ack) { - if (m_localPlayerEntity == ECS::NULL_ENTITY || - !m_registry.IsEntityAlive(m_localPlayerEntity) || - !m_registry.HasComponent(m_localPlayerEntity)) { - return; - } - - if (ack.lastProcessedSeq > m_lastAckedSequence) { - m_lastAckedSequence = ack.lastProcessedSeq; - } - - while (!m_inputHistory.empty() && m_inputHistory.front().sequence <= ack.lastProcessedSeq) { - m_inputHistory.pop_front(); - } - - auto& pos = m_registry.GetComponent(m_localPlayerEntity); - - float serverX = ack.serverPosX; - float serverY = ack.serverPosY; - - constexpr float RECONCILE_THRESHOLD = 1.0f; - float dx = serverX - m_predictedX; - float dy = serverY - m_predictedY; - float errorSq = dx * dx + dy * dy; - - if (m_inputHistory.empty()) { - if (errorSq > RECONCILE_THRESHOLD * RECONCILE_THRESHOLD) { - pos.x = serverX; - pos.y = serverY; - m_predictedX = serverX; - m_predictedY = serverY; - } - return; - } - - constexpr float SNAP_THRESHOLD = 100.0f; - if (errorSq > SNAP_THRESHOLD * SNAP_THRESHOLD) { - pos.x = serverX; - pos.y = serverY; - m_predictedX = serverX; - m_predictedY = serverY; - - m_inputHistory.clear(); - return; - } - - if (errorSq > RECONCILE_THRESHOLD * RECONCILE_THRESHOLD) { - float replayX = serverX; - float replayY = serverY; - - for (const auto& input : m_inputHistory) { - if (input.inputs & network::InputFlags::UP) { - replayY -= PREDICTION_SPEED * input.deltaTime; - } - if (input.inputs & network::InputFlags::DOWN) { - replayY += PREDICTION_SPEED * input.deltaTime; - } - if (input.inputs & network::InputFlags::LEFT) { - replayX -= PREDICTION_SPEED * input.deltaTime; - } - if (input.inputs & network::InputFlags::RIGHT) { - replayX += PREDICTION_SPEED * input.deltaTime; - } - - replayX = std::max(0.0f, std::min(replayX, 1280.0f - 66.0f)); - replayY = std::max(0.0f, std::min(replayY, 720.0f - 32.0f)); - } - - pos.x = replayX; - pos.y = replayY; - m_predictedX = replayX; - m_predictedY = replayY; - } - - UpdatePlayerNameLabelPosition(m_localPlayerEntity, pos.x, pos.y); - } - - void InGameState::OnLevelComplete(uint8_t completedLevel, uint8_t nextLevel) { - Core::Logger::Info("[InGameState] Level {} complete! Starting transition to level {}", - static_cast(completedLevel), - static_cast(nextLevel)); - - m_levelProgress.transitionPhase = TransitionPhase::FADE_OUT; - m_levelProgress.transitionTimer = 0.0f; - m_levelProgress.fadeAlpha = 0.0f; - m_levelProgress.levelComplete = true; - m_levelProgress.bossDefeated = true; - m_levelProgress.currentLevelNumber = static_cast(completedLevel); - m_levelProgress.nextLevelNumber = static_cast(nextLevel); - - Core::Logger::Info("[InGameState] Transition started - staying in GameState, network stays active"); - } - - } -} diff --git a/client/src/game/GameStateTransition.cpp b/client/src/game/GameStateTransition.cpp deleted file mode 100644 index bb098ac..0000000 --- a/client/src/game/GameStateTransition.cpp +++ /dev/null @@ -1,303 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** GameState - Level transition logic -*/ - -#include "../../include/GameState.hpp" -#include "Core/Logger.hpp" -#include -#include - -namespace RType { - namespace Client { - - void InGameState::UpdateLevelTransition(float dt) { - if (m_levelProgress.transitionPhase == TransitionPhase::NONE) { - return; - } - - m_levelProgress.transitionTimer += dt; - - switch (m_levelProgress.transitionPhase) { - case TransitionPhase::FADE_OUT: { - const float FADE_DURATION = 1.0f; - m_levelProgress.fadeAlpha = std::min(1.0f, m_levelProgress.transitionTimer / FADE_DURATION); - - if (m_levelProgress.transitionTimer >= FADE_DURATION) { - m_levelProgress.transitionPhase = TransitionPhase::LOADING; - m_levelProgress.transitionTimer = 0.0f; - Core::Logger::Info("[Transition] FADE_OUT complete, entering LOADING"); - } - break; - } - - case TransitionPhase::LOADING: { - const float LOADING_DURATION = 2.0f; - - if (m_levelProgress.transitionTimer >= LOADING_DURATION) { - if (m_levelProgress.currentLevelNumber >= m_levelProgress.totalLevels) { - m_levelProgress.allLevelsComplete = true; - m_levelProgress.transitionPhase = TransitionPhase::NONE; - m_levelProgress.fadeAlpha = 0.0f; - Core::Logger::Info("[Transition] ALL LEVELS COMPLETE! Victory!"); - } else { - LoadNextLevel(); - - m_levelProgress.transitionPhase = TransitionPhase::FADE_IN; - m_levelProgress.transitionTimer = 0.0f; - Core::Logger::Info("[Transition] LOADING complete, entering FADE_IN"); - } - } - break; - } - - case TransitionPhase::FADE_IN: { - const float FADE_DURATION = 1.0f; - m_levelProgress.fadeAlpha = std::max(0.0f, 1.0f - (m_levelProgress.transitionTimer / FADE_DURATION)); - - if (m_levelProgress.transitionTimer >= FADE_DURATION) { - m_levelProgress.transitionPhase = TransitionPhase::NONE; - m_levelProgress.fadeAlpha = 0.0f; - m_levelProgress.levelComplete = false; - // currentLevelNumber already updated in LoadNextLevel() - Core::Logger::Info("[Transition] FADE_IN complete, transition finished"); - } - break; - } - - default: - break; - } - } - - void InGameState::LoadNextLevel() { - Core::Logger::Info("[Transition] Loading level {}...", m_levelProgress.nextLevelNumber); - - m_levelProgress.currentLevelNumber = m_levelProgress.nextLevelNumber; - - std::unordered_set hudEntities; - - if (m_hudPlayerEntity != ECS::NULL_ENTITY) { - hudEntities.insert(m_hudPlayerEntity); - } - if (m_hudScoreEntity != ECS::NULL_ENTITY) { - hudEntities.insert(m_hudScoreEntity); - } - if (m_hudLivesEntity != ECS::NULL_ENTITY) { - hudEntities.insert(m_hudLivesEntity); - } - if (m_hudScoreboardTitle != ECS::NULL_ENTITY) { - hudEntities.insert(m_hudScoreboardTitle); - } - - for (size_t i = 0; i < MAX_PLAYERS; i++) { - if (m_playersHUD[i].scoreEntity != ECS::NULL_ENTITY) { - hudEntities.insert(m_playersHUD[i].scoreEntity); - } - if (m_playersHUD[i].powerupSpreadEntity != ECS::NULL_ENTITY) { - hudEntities.insert(m_playersHUD[i].powerupSpreadEntity); - } - if (m_playersHUD[i].powerupLaserEntity != ECS::NULL_ENTITY) { - hudEntities.insert(m_playersHUD[i].powerupLaserEntity); - } - if (m_playersHUD[i].powerupSpeedEntity != ECS::NULL_ENTITY) { - hudEntities.insert(m_playersHUD[i].powerupSpeedEntity); - } - if (m_playersHUD[i].powerupShieldEntity != ECS::NULL_ENTITY) { - hudEntities.insert(m_playersHUD[i].powerupShieldEntity); - } - } - - for (const auto& pair : m_playerNameLabels) { - if (pair.second != ECS::NULL_ENTITY) { - hudEntities.insert(pair.second); - } - } - - if (m_gameOverTitleEntity != ECS::NULL_ENTITY) { - hudEntities.insert(m_gameOverTitleEntity); - } - if (m_gameOverScoreEntity != ECS::NULL_ENTITY) { - hudEntities.insert(m_gameOverScoreEntity); - } - if (m_gameOverHintEntity != ECS::NULL_ENTITY) { - hudEntities.insert(m_gameOverHintEntity); - } - - if (m_victoryTitleEntity != ECS::NULL_ENTITY) { - hudEntities.insert(m_victoryTitleEntity); - } - if (m_victoryScoreEntity != ECS::NULL_ENTITY) { - hudEntities.insert(m_victoryScoreEntity); - } - if (m_victoryHintEntity != ECS::NULL_ENTITY) { - hudEntities.insert(m_victoryHintEntity); - } - - std::vector toDestroy; - - auto positionEntities = m_registry.GetEntitiesWithComponent(); - for (auto entity : positionEntities) { - if (entity == m_localPlayerEntity) { - continue; - } - if (hudEntities.find(entity) != hudEntities.end()) { - continue; - } - toDestroy.push_back(entity); - } - - auto colliderEntities = m_registry.GetEntitiesWithComponent(); - for (auto entity : colliderEntities) { - if (entity == m_localPlayerEntity) { - continue; - } - if (hudEntities.find(entity) != hudEntities.end()) { - continue; - } - if (std::find(toDestroy.begin(), toDestroy.end(), entity) == toDestroy.end()) { - toDestroy.push_back(entity); - } - } - - auto drawableEntities = m_registry.GetEntitiesWithComponent(); - for (auto entity : drawableEntities) { - if (entity == m_localPlayerEntity) { - continue; - } - if (hudEntities.find(entity) != hudEntities.end()) { - continue; - } - if (std::find(toDestroy.begin(), toDestroy.end(), entity) == toDestroy.end()) { - toDestroy.push_back(entity); - } - } - - for (auto entity : toDestroy) { - if (m_registry.IsEntityAlive(entity)) { - m_registry.DestroyEntity(entity); - } - } - - Core::Logger::Info("[Transition] Destroyed {} entities (preserved player + {} HUD entities)", - toDestroy.size(), hudEntities.size()); - - std::vector networkIdsToRemove; - for (const auto& [networkId, localEntity] : m_networkEntityMap) { - if (!m_registry.IsEntityAlive(localEntity)) { - networkIdsToRemove.push_back(networkId); - } - } - for (uint32_t networkId : networkIdsToRemove) { - m_networkEntityMap.erase(networkId); - } - Core::Logger::Info("[Transition] Cleaned network map: removed {} dead entities, kept {} alive", - networkIdsToRemove.size(), m_networkEntityMap.size()); - - m_obstacleColliderEntities.clear(); - m_obstacleSpriteEntities.clear(); - m_obstacleIdToCollider.clear(); - m_backgroundEntities.clear(); - - m_serverScrollOffset = 0.0f; - m_localScrollOffset = 0.0f; - - if (m_bossHealthBar.titleEntity != ECS::NULL_ENTITY && m_registry.IsEntityAlive(m_bossHealthBar.titleEntity)) { - m_registry.DestroyEntity(m_bossHealthBar.titleEntity); - } - if (m_bossHealthBar.barBackgroundEntity != ECS::NULL_ENTITY && m_registry.IsEntityAlive(m_bossHealthBar.barBackgroundEntity)) { - m_registry.DestroyEntity(m_bossHealthBar.barBackgroundEntity); - } - if (m_bossHealthBar.barForegroundEntity != ECS::NULL_ENTITY && m_registry.IsEntityAlive(m_bossHealthBar.barForegroundEntity)) { - m_registry.DestroyEntity(m_bossHealthBar.barForegroundEntity); - } - - m_bossHealthBar.bossNetworkId = 0; - m_bossHealthBar.currentHealth = 0; - m_bossHealthBar.maxHealth = 0; - m_bossHealthBar.active = false; - m_bossHealthBar.titleEntity = ECS::NULL_ENTITY; - m_bossHealthBar.barBackgroundEntity = ECS::NULL_ENTITY; - m_bossHealthBar.barForegroundEntity = ECS::NULL_ENTITY; - - m_bossWarningActive = false; - m_bossWarningTriggered = false; - m_bossWarningTimer = 0.0f; - - std::string levelPath = "assets/levels/level" + std::to_string(m_levelProgress.nextLevelNumber) + ".json"; - m_currentLevelPath = levelPath; - - try { - m_levelData = ECS::LevelLoader::LoadFromFile(levelPath); - m_levelAssets = ECS::LevelLoader::LoadAssets(m_levelData, m_renderer.get()); - - m_levelEntities = ECS::LevelLoader::CreateEntities(m_registry, m_levelData, m_levelAssets, m_renderer.get()); - m_backgroundEntities = m_levelEntities.backgrounds; - m_obstacleSpriteEntities = m_levelEntities.obstacleVisuals; - m_obstacleColliderEntities = m_levelEntities.obstacleColliders; - - Core::Logger::Info("[Transition] Level {} loaded: {} textures, {} sprites", - m_levelProgress.nextLevelNumber, - m_levelAssets.textures.size(), - m_levelAssets.sprites.size()); - - for (auto collider : m_obstacleColliderEntities) { - if (!m_registry.IsEntityAlive(collider) || - !m_registry.HasComponent(collider)) { - continue; - } - const auto& metadata = m_registry.GetComponent(collider); - m_obstacleIdToCollider[metadata.uniqueId] = collider; - - if (metadata.visualEntity != ECS::NULL_ENTITY && - m_registry.IsEntityAlive(metadata.visualEntity) && - m_registry.HasComponent(metadata.visualEntity) && - m_registry.HasComponent(collider)) { - - const auto& visualPos = m_registry.GetComponent(metadata.visualEntity); - auto& colliderPos = m_registry.GetComponent(collider); - - colliderPos.x = visualPos.x + metadata.offsetX; - colliderPos.y = visualPos.y + metadata.offsetY; - } - } - - Core::Logger::Info("[Transition] Rebuilt obstacle mapping: {} obstacles tracked", - m_obstacleIdToCollider.size()); - - } catch (const std::exception& e) { - Core::Logger::Error("[Transition] Failed to load level {}: {}", m_levelProgress.nextLevelNumber, e.what()); - } - - if (m_context.audio) { - if (m_gameMusic != Audio::INVALID_MUSIC_ID) { - m_context.audio->StopMusic(m_gameMusic); - m_context.audio->UnloadMusic(m_gameMusic); - } - - std::string musicPath = "assets/sounds/stage1.flac"; - if (m_levelProgress.nextLevelNumber == 2) { - musicPath = "assets/sounds/stage2.flac"; - } else if (m_levelProgress.nextLevelNumber == 3) { - musicPath = "assets/sounds/stage3.flac"; - } - - m_gameMusic = m_context.audio->LoadMusic(musicPath); - if (m_gameMusic == Audio::INVALID_MUSIC_ID) { - m_gameMusic = m_context.audio->LoadMusic("../" + musicPath); - } - - if (m_gameMusic != Audio::INVALID_MUSIC_ID) { - Audio::PlaybackOptions opts; - opts.loop = true; - opts.volume = 0.35f; - m_context.audio->PlayMusic(m_gameMusic, opts); - m_gameMusicPlaying = true; - } - } - } - - } -} diff --git a/client/src/game/GameStateUI.cpp b/client/src/game/GameStateUI.cpp deleted file mode 100644 index e52d1d7..0000000 --- a/client/src/game/GameStateUI.cpp +++ /dev/null @@ -1,605 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** GameState - UI and HUD rendering functions -*/ - -#include "../../include/GameState.hpp" - -#include "ECS/Components/TextLabel.hpp" -#include "ECS/Component.hpp" -#include "Core/Logger.hpp" -#include -#include -#include - -using namespace RType::ECS; - -namespace RType { - namespace Client { - - void InGameState::initializeUI() { - m_hudFont = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 16); - if (m_hudFont == Renderer::INVALID_FONT_ID) { - m_hudFont = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 16); - } - - m_hudFontSmall = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 12); - if (m_hudFontSmall == Renderer::INVALID_FONT_ID) { - m_hudFontSmall = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 12); - } - - m_gameOverFontLarge = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 48); - if (m_gameOverFontLarge == Renderer::INVALID_FONT_ID) { - m_gameOverFontLarge = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 48); - } - - m_gameOverFontMedium = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 20); - if (m_gameOverFontMedium == Renderer::INVALID_FONT_ID) { - m_gameOverFontMedium = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 20); - } - - if (m_hudFont == Renderer::INVALID_FONT_ID) { - Core::Logger::Error("[GameState] Failed to load HUD font"); - return; - } - - m_hudPlayerEntity = m_registry.CreateEntity(); - m_registry.AddComponent(m_hudPlayerEntity, Position{20.0f, 680.0f}); - std::string playerText = "P" + std::to_string(m_context.playerNumber); - TextLabel playerLabel(playerText, m_hudFont, 16); - playerLabel.color = {0.0f, 1.0f, 1.0f, 1.0f}; - m_registry.AddComponent(m_hudPlayerEntity, std::move(playerLabel)); - - m_hudLivesEntity = m_registry.CreateEntity(); - m_registry.AddComponent(m_hudLivesEntity, Position{350.0f, 680.0f}); - TextLabel livesLabel("LIVES x3", m_hudFontSmall != Renderer::INVALID_FONT_ID ? m_hudFontSmall : m_hudFont, 12); - livesLabel.color = {1.0f, 0.8f, 0.0f, 1.0f}; - m_registry.AddComponent(m_hudLivesEntity, std::move(livesLabel)); - - m_hudScoreboardTitle = m_registry.CreateEntity(); - m_registry.AddComponent(m_hudScoreboardTitle, Position{1050.0f, 620.0f}); - TextLabel titleLabel("SCORES", m_hudFontSmall != Renderer::INVALID_FONT_ID ? m_hudFontSmall : m_hudFont, 12); - titleLabel.color = {0.0f, 1.0f, 1.0f, 1.0f}; - m_registry.AddComponent(m_hudScoreboardTitle, std::move(titleLabel)); - - for (size_t i = 0; i < MAX_PLAYERS; i++) { - m_playersHUD[i].active = false; - m_playersHUD[i].score = 0; - m_playersHUD[i].lives = 3; - m_playersHUD[i].health = 100; - m_playersHUD[i].maxHealth = 100; - m_playersHUD[i].playerEntity = NULL_ENTITY; - - m_playersHUD[i].scoreEntity = m_registry.CreateEntity(); - float yPos = 645.0f + (i * 20.0f); - m_registry.AddComponent(m_playersHUD[i].scoreEntity, Position{1050.0f, yPos}); - - std::string scoreText = "P" + std::to_string(i + 1) + " --------"; - TextLabel label(scoreText, m_hudFontSmall != Renderer::INVALID_FONT_ID ? m_hudFontSmall : m_hudFont, 12); - label.color = {0.4f, 0.4f, 0.4f, 0.6f}; - m_registry.AddComponent(m_playersHUD[i].scoreEntity, std::move(label)); - } - - if (m_context.playerNumber >= 1 && m_context.playerNumber <= MAX_PLAYERS) { - m_playersHUD[m_context.playerNumber - 1].active = true; - } - - } - - void InGameState::updateHUD() { - if (m_localPlayerEntity != ECS::NULL_ENTITY && - m_registry.IsEntityAlive(m_localPlayerEntity) && - m_registry.HasComponent(m_localPlayerEntity)) { - const auto& scoreComp = m_registry.GetComponent(m_localPlayerEntity); - m_playerScore = scoreComp.points; - } - - if (m_hudScoreEntity != NULL_ENTITY && m_registry.IsEntityAlive(m_hudScoreEntity) && m_registry.HasComponent(m_hudScoreEntity)) { - auto& scoreLabel = m_registry.GetComponent(m_hudScoreEntity); - std::ostringstream ss; - ss << std::setw(8) << std::setfill('0') << m_playerScore; - scoreLabel.text = ss.str(); - } - - if (m_hudLivesEntity != NULL_ENTITY && m_registry.IsEntityAlive(m_hudLivesEntity) && m_registry.HasComponent(m_hudLivesEntity)) { - auto& livesLabel = m_registry.GetComponent(m_hudLivesEntity); - - // Calculate visual lives from real health (0-300) - // 201-300 HP = 3 lives, 101-200 HP = 2 lives, 1-100 HP = 1 life, 0 HP = 0 lives - int realHealth = 0; - if (m_context.playerNumber >= 1 && m_context.playerNumber <= MAX_PLAYERS) { - realHealth = m_playersHUD[m_context.playerNumber - 1].health; - } - - int visualLives = 0; - if (realHealth > 200) { - visualLives = 3; - } else if (realHealth > 100) { - visualLives = 2; - } else if (realHealth > 0) { - visualLives = 1; - } - - livesLabel.text = "LIVES x" + std::to_string(visualLives); - - if (visualLives <= 1) { - livesLabel.color = {1.0f, 0.2f, 0.2f, 1.0f}; - } else if (visualLives <= 2) { - livesLabel.color = {1.0f, 0.6f, 0.0f, 1.0f}; - } else { - livesLabel.color = {1.0f, 0.8f, 0.0f, 1.0f}; - } - } - - if (m_context.playerNumber >= 1 && m_context.playerNumber <= MAX_PLAYERS) { - m_playersHUD[m_context.playerNumber - 1].score = m_playerScore; - m_playersHUD[m_context.playerNumber - 1].lives = m_playerLives; - } - - const Math::Color playerColors[MAX_PLAYERS] = { - {0.2f, 1.0f, 0.2f, 1.0f}, // P1 - green - {0.2f, 0.6f, 1.0f, 1.0f}, // P2 - blue - {1.0f, 0.3f, 0.3f, 1.0f}, // P3 - Red - {1.0f, 1.0f, 0.2f, 1.0f} // P4 - yellow - }; - - for (size_t i = 0; i < MAX_PLAYERS; i++) { - if (m_playersHUD[i].scoreEntity == NULL_ENTITY || - !m_registry.IsEntityAlive(m_playersHUD[i].scoreEntity) || - !m_registry.HasComponent(m_playersHUD[i].scoreEntity)) { - continue; - } - - auto& label = m_registry.GetComponent(m_playersHUD[i].scoreEntity); - - if (m_playersHUD[i].active) { - if (m_playersHUD[i].isDead) { - m_playersHUD[i].health = 0; - } else { - bool entityExists = false; - if (m_playersHUD[i].playerEntity != NULL_ENTITY && - m_registry.IsEntityAlive(m_playersHUD[i].playerEntity) && - m_registry.HasComponent(m_playersHUD[i].playerEntity)) { - const auto& health = m_registry.GetComponent(m_playersHUD[i].playerEntity); - if (health.current > 0) { - m_playersHUD[i].health = health.current; - m_playersHUD[i].maxHealth = health.max; - } else { - m_playersHUD[i].isDead = true; - m_playersHUD[i].health = 0; - } - if (m_registry.HasComponent(m_playersHUD[i].playerEntity)) { - const auto& scoreComp = m_registry.GetComponent(m_playersHUD[i].playerEntity); - m_playersHUD[i].score = scoreComp.points; - } - entityExists = true; - } else if (i == static_cast(m_context.playerNumber - 1) && m_localPlayerEntity != ECS::NULL_ENTITY && m_registry.IsEntityAlive(m_localPlayerEntity) && m_registry.HasComponent(m_localPlayerEntity)) { - const auto& health = m_registry.GetComponent(m_localPlayerEntity); - if (health.current > 0) { - m_playersHUD[i].health = health.current; - m_playersHUD[i].maxHealth = health.max; - } else { - m_playersHUD[i].isDead = true; - m_playersHUD[i].health = 0; - } - if (m_registry.HasComponent(m_localPlayerEntity)) { - const auto& scoreComp = m_registry.GetComponent(m_localPlayerEntity); - m_playersHUD[i].score = scoreComp.points; - } - entityExists = true; - - if (m_playersHUD[i].playerEntity == NULL_ENTITY) { - m_playersHUD[i].playerEntity = m_localPlayerEntity; - } - } - - if (!entityExists) { - if (!m_isNetworkSession) { - m_playersHUD[i].isDead = true; - m_playersHUD[i].health = 0; - } - } - } - - std::string playerDisplayName = "P" + std::to_string(i + 1); - uint8_t playerNum = static_cast(i + 1); - - for (const auto& p : m_context.allPlayers) { - if (p.number == playerNum && p.name[0] != '\0') { - playerDisplayName = std::string(p.name); - break; - } - } - - if (playerDisplayName == "P" + std::to_string(i + 1)) { - auto nameIt = m_playerNameMap.find(static_cast(playerNum)); - if (nameIt != m_playerNameMap.end() && !nameIt->second.empty()) { - playerDisplayName = nameIt->second; - } - } - - size_t originalLength = playerDisplayName.length(); - bool nameTooLong = originalLength > 16; - - if (nameTooLong) { - playerDisplayName = playerDisplayName.substr(0, 16); - } - - if (nameTooLong && m_registry.HasComponent(m_playersHUD[i].scoreEntity)) { - auto& pos = m_registry.GetComponent(m_playersHUD[i].scoreEntity); - float shiftAmount = static_cast(originalLength - 16) * 38.0f; - pos.x = 1050.0f - shiftAmount; - } else if (m_registry.HasComponent(m_playersHUD[i].scoreEntity)) { - auto& pos = m_registry.GetComponent(m_playersHUD[i].scoreEntity); - pos.x = 1050.0f; - } - - std::ostringstream ss; - ss << playerDisplayName << " " << std::setw(8) << std::setfill('0') << m_playersHUD[i].score; - label.text = ss.str(); - label.color = playerColors[i]; - - if (i == static_cast(m_context.playerNumber - 1)) { - label.color.a = 1.0f; - } else { - label.color.a = 0.85f; - } - } else { - label.text = "P" + std::to_string(i + 1) + " --------"; - label.color = {0.4f, 0.4f, 0.4f, 0.5f}; - } - } - - updatePowerUpIcons(); - } - - void InGameState::updatePowerUpIcons() { - if (m_context.playerNumber < 1 || m_context.playerNumber > MAX_PLAYERS) { - return; - } - - size_t playerIndex = static_cast(m_context.playerNumber - 1); - if (!m_playersHUD[playerIndex].active) { - return; - } - - ECS::Entity playerEntity = m_playersHUD[playerIndex].playerEntity; - if (playerEntity == ECS::NULL_ENTITY) { - playerEntity = m_localPlayerEntity; - } - - bool hasSpreadShot = false; - bool hasLaserBeam = false; - bool hasSpeedBoost = false; - bool hasShield = false; - - if (playerEntity != ECS::NULL_ENTITY && m_registry.IsEntityAlive(playerEntity) && - m_registry.HasComponent(playerEntity)) { - const auto& powerUps = m_registry.GetComponent(playerEntity); - hasSpreadShot = powerUps.hasSpreadShot; - hasLaserBeam = powerUps.hasLaserBeam; - hasSpeedBoost = powerUps.speedMultiplier > 1.0f; - hasShield = powerUps.hasShield; - } - - const float textSpacing = 25.0f; - const float columnSpacing = 85.0f; - const float startX = 800.0f; - const float startY = 675.0f; - float currentY = startY; - - updatePowerUpText(m_playersHUD[playerIndex].powerupSpreadEntity, "SPREAD", - hasSpreadShot, startX, currentY); - currentY += textSpacing; - - updatePowerUpText(m_playersHUD[playerIndex].powerupLaserEntity, "LASER", - hasLaserBeam, startX, currentY); - currentY += textSpacing; - - currentY = startY; - updatePowerUpText(m_playersHUD[playerIndex].powerupSpeedEntity, "SPEED", - hasSpeedBoost, startX + columnSpacing, currentY); - currentY += textSpacing; - - updatePowerUpText(m_playersHUD[playerIndex].powerupShieldEntity, "SHIELD", - hasShield, startX + columnSpacing, currentY); - } - - void InGameState::updatePowerUpText(ECS::Entity& textEntity, const std::string& text, - bool isActive, float x, float y) { - if (textEntity == ECS::NULL_ENTITY || !m_registry.IsEntityAlive(textEntity)) { - textEntity = m_registry.CreateEntity(); - m_registry.AddComponent(textEntity, Position{x, y}); - Renderer::FontId fontId = (m_hudFontSmall != Renderer::INVALID_FONT_ID) ? m_hudFontSmall : m_hudFont; - TextLabel label(text, fontId, 10); - label.color = isActive ? Math::Color{1.0f, 1.0f, 1.0f, 1.0f} : Math::Color{0.5f, 0.5f, 0.5f, 0.7f}; - m_registry.AddComponent(textEntity, std::move(label)); - } else { - if (m_registry.HasComponent(textEntity)) { - auto& pos = m_registry.GetComponent(textEntity); - pos.x = x; - pos.y = y; - } - if (m_registry.HasComponent(textEntity)) { - auto& label = m_registry.GetComponent(textEntity); - label.color = isActive ? Math::Color{1.0f, 1.0f, 1.0f, 1.0f} : Math::Color{0.5f, 0.5f, 0.5f, 0.7f}; - } - } - } - - void InGameState::Draw() { - m_renderingSystem->Update(m_registry, 0.0f); - m_textSystem->Update(m_registry, 0.0f); - - renderChargeBar(); - renderHealthBars(); - renderBossHealthBar(); - renderGameOverOverlay(); - renderVictoryOverlay(); - renderLevelTransition(); - renderBossWarning(); - } - - void InGameState::renderChargeBar() { - const float barX = 500.0f; - const float barY = 675.0f; - const float barWidth = 200.0f; - const float barHeight = 20.0f; - - Renderer::Rectangle bgRect; - bgRect.position = Renderer::Vector2(barX - 2, barY - 2); - bgRect.size = Renderer::Vector2(barWidth + 4, barHeight + 4); - m_renderer->DrawRectangle(bgRect, Renderer::Color(0.2f, 0.2f, 0.2f, 0.8f)); - - Renderer::Rectangle innerBgRect; - innerBgRect.position = Renderer::Vector2(barX, barY); - innerBgRect.size = Renderer::Vector2(barWidth, barHeight); - m_renderer->DrawRectangle(innerBgRect, Renderer::Color(0.1f, 0.1f, 0.1f, 0.9f)); - - float chargePercent = m_chargeTime / MAX_CHARGE_TIME; - float filledWidth = barWidth * chargePercent; - - Renderer::Color barColor; - if (chargePercent < 0.5f) { - float blend = chargePercent * 2.0f; - barColor = Renderer::Color(blend, 1.0f, 1.0f - blend, 1.0f); - } else { - float blend = (chargePercent - 0.5f) * 2.0f; - barColor = Renderer::Color(1.0f, 1.0f - blend * 0.5f, 0.0f, 1.0f); - } - - if (filledWidth > 0) { - Renderer::Rectangle fillRect; - fillRect.position = Renderer::Vector2(barX, barY); - fillRect.size = Renderer::Vector2(filledWidth, barHeight); - m_renderer->DrawRectangle(fillRect, barColor); - } - - if (m_isCharging && m_hudFontSmall != Renderer::INVALID_FONT_ID) { - Renderer::TextParams textParams; - textParams.position = Renderer::Vector2(barX + barWidth + 10, barY + 2); - textParams.color = Renderer::Color(0.0f, 1.0f, 1.0f, 1.0f); - textParams.scale = 1.0f; - - if (m_chargeTime >= MAX_CHARGE_TIME) { - float pulse = 0.7f + 0.3f * std::sin(m_chargeTime * 10.0f); - textParams.color = Renderer::Color(1.0f, pulse, 0.0f, 1.0f); - m_renderer->DrawText(m_hudFontSmall, "MAX!", textParams); - } else { - m_renderer->DrawText(m_hudFontSmall, "BEAM", textParams); - } - } - } - - void InGameState::renderHealthBars() { - if (m_context.playerNumber < 1 || m_context.playerNumber > MAX_PLAYERS) { - return; - } - - size_t playerIndex = static_cast(m_context.playerNumber - 1); - if (!m_playersHUD[playerIndex].active) { - return; - } - - const float barWidth = 150.0f; - const float barHeight = 12.0f; - const float barX = 180.0f; - const float barY = 680.0f; - - Renderer::Rectangle bgRect; - bgRect.position = Renderer::Vector2(barX - 2, barY - 2); - bgRect.size = Renderer::Vector2(barWidth + 4, barHeight + 4); - m_renderer->DrawRectangle(bgRect, Renderer::Color(0.1f, 0.1f, 0.1f, 0.9f)); - - Renderer::Rectangle innerBgRect; - innerBgRect.position = Renderer::Vector2(barX, barY); - innerBgRect.size = Renderer::Vector2(barWidth, barHeight); - m_renderer->DrawRectangle(innerBgRect, Renderer::Color(0.3f, 0.1f, 0.1f, 0.8f)); - - Renderer::TextParams textParams; - textParams.position = Renderer::Vector2(barX, barY - barHeight - 2); - textParams.color = Renderer::Color(0.0f, 1.0f, 1.0f, 1.0f); - textParams.scale = 1.0f; - m_renderer->DrawText(m_hudFontSmall, "HEALTH", textParams); - - // Calculate visual health (0-100) from real health (0-300) - // Each "life" represents 100 HP - int realHealth = m_playersHUD[playerIndex].health; - int visualHealth = 0; - - if (m_playersHUD[playerIndex].isDead || realHealth <= 0) { - visualHealth = 0; - } else if (realHealth > 200) { - visualHealth = realHealth - 200; // 201-300 -> 1-100 - } else if (realHealth > 100) { - visualHealth = realHealth - 100; // 101-200 -> 1-100 - } else { - visualHealth = realHealth; // 1-100 -> 1-100 - } - - float healthPercent = static_cast(visualHealth) / 100.0f; - healthPercent = std::max(0.0f, std::min(1.0f, healthPercent)); - - float filledWidth = barWidth * healthPercent; - - Renderer::Color healthColor; - if (healthPercent <= 0.3f) { - healthColor = Renderer::Color(1.0f, 0.2f, 0.2f, 1.0f); - } else if (healthPercent <= 0.6f) { - float blend = (healthPercent - 0.3f) / 0.3f; - healthColor = Renderer::Color(1.0f, 0.2f + blend * 0.8f, 0.2f, 1.0f); - } else { - healthColor = Renderer::Color(0.2f, 1.0f, 0.2f, 1.0f); - } - - if (filledWidth > 0) { - Renderer::Rectangle fillRect; - fillRect.position = Renderer::Vector2(barX, barY); - fillRect.size = Renderer::Vector2(filledWidth, barHeight); - m_renderer->DrawRectangle(fillRect, healthColor); - } - } - - void InGameState::renderGameOverOverlay() { - if (!m_isGameOver) { - return; - } - - Renderer::Rectangle rect; - rect.position = Renderer::Vector2(0.0f, 0.0f); - rect.size = Renderer::Vector2(1280.0f, 720.0f); - m_renderer->DrawRectangle(rect, Renderer::Color(0.0f, 0.0f, 0.0f, 0.45f)); - } - - void InGameState::renderVictoryOverlay() { - if (!m_levelProgress.allLevelsComplete) { - return; - } - - Renderer::Rectangle bgRect; - bgRect.position = Renderer::Vector2(0.0f, 0.0f); - bgRect.size = Renderer::Vector2(1280.0f, 720.0f); - m_renderer->DrawRectangle(bgRect, Renderer::Color(0.0f, 0.0f, 0.0f, 0.45f)); - } - - void InGameState::initializeBossHealthBar() { - if (m_bossHealthBar.active) { - return; - } - - m_bossHealthBar.active = true; - - m_bossHealthBar.titleEntity = m_registry.CreateEntity(); - m_registry.AddComponent(m_bossHealthBar.titleEntity, Position{640.0f - 80.0f, 10.0f}); - - std::string bossTitle = "BOSS - LEVEL " + std::to_string(m_levelProgress.currentLevelNumber); - m_registry.AddComponent(m_bossHealthBar.titleEntity, - TextLabel(bossTitle, m_hudFontSmall != Renderer::INVALID_FONT_ID ? m_hudFontSmall : m_hudFont, 12)); - - Core::Logger::Info("[GameState] Boss health bar initialized for level {}", m_levelProgress.currentLevelNumber); - } - - void InGameState::destroyBossHealthBar() { - if (!m_bossHealthBar.active) { - return; - } - - if (m_bossHealthBar.titleEntity != NULL_ENTITY && m_registry.IsEntityAlive(m_bossHealthBar.titleEntity)) { - m_registry.DestroyEntity(m_bossHealthBar.titleEntity); - } - - m_bossHealthBar.active = false; - m_bossHealthBar.titleEntity = NULL_ENTITY; - m_bossHealthBar.bossNetworkId = 0; - - Core::Logger::Info("[GameState] Boss health bar destroyed"); - } - - void InGameState::updateBossHealthBar() { - if (!m_bossHealthBar.active) { - return; - } - - float healthPercent = static_cast(m_bossHealthBar.currentHealth) / - static_cast(m_bossHealthBar.maxHealth); - if (healthPercent < 0.0f) healthPercent = 0.0f; - if (healthPercent > 1.0f) healthPercent = 1.0f; - - if (m_bossHealthBar.currentHealth <= 0) { - destroyBossHealthBar(); - } - } - - void InGameState::renderBossHealthBar() { - if (!m_bossHealthBar.active) { - return; - } - - const float barWidth = 400.0f; - const float barHeight = 15.0f; - const float barX = 440.0f; - const float barY = 35.0f; - - Renderer::Rectangle bgRect; - bgRect.position = Renderer::Vector2(barX, barY); - bgRect.size = Renderer::Vector2(barWidth, barHeight); - m_renderer->DrawRectangle(bgRect, Renderer::Color(0.3f, 0.3f, 0.3f, 1.0f)); - - float healthPercent = static_cast(m_bossHealthBar.currentHealth) / - static_cast(m_bossHealthBar.maxHealth); - if (healthPercent < 0.0f) healthPercent = 0.0f; - if (healthPercent > 1.0f) healthPercent = 1.0f; - - Renderer::Rectangle fgRect; - fgRect.position = Renderer::Vector2(barX, barY); - fgRect.size = Renderer::Vector2(barWidth * healthPercent, barHeight); - - float red = 1.0f - (healthPercent * 0.5f); - float green = healthPercent * 0.8f; - m_renderer->DrawRectangle(fgRect, Renderer::Color(red, green, 0.0f, 1.0f)); - } - - void InGameState::renderLevelTransition() { - if (m_levelProgress.transitionPhase == TransitionPhase::NONE) { - return; - } - - if (m_levelProgress.fadeAlpha > 0.0f) { - Renderer::Rectangle fadeOverlay; - fadeOverlay.position = Renderer::Vector2(0.0f, 0.0f); - fadeOverlay.size = Renderer::Vector2(1280.0f, 720.0f); - m_renderer->DrawRectangle(fadeOverlay, Renderer::Color(0.0f, 0.0f, 0.0f, m_levelProgress.fadeAlpha)); - } - - if (m_levelProgress.transitionPhase == TransitionPhase::LOADING) { - Renderer::Rectangle fullScreen; - fullScreen.position = Renderer::Vector2(0.0f, 0.0f); - fullScreen.size = Renderer::Vector2(1280.0f, 720.0f); - m_renderer->DrawRectangle(fullScreen, Renderer::Color(0.0f, 0.0f, 0.0f, 1.0f)); - } - } - - void InGameState::renderBossWarning() { - if (!m_bossWarningActive) { - return; - } - - if (!m_bossWarningFlashState) { - return; - } - - if (m_gameOverFontLarge != Renderer::INVALID_FONT_ID) { - Renderer::TextParams textParams; - textParams.position = Renderer::Vector2(440.0f, 300.0f); - textParams.color = Renderer::Color(1.0f, 0.0f, 0.0f, 1.0f); - textParams.scale = 1.0f; - m_renderer->DrawText(m_gameOverFontLarge, "WARNING !!", textParams); - } else { - Core::Logger::Error("[GameState] Cannot render boss warning - Invalid Font ID"); - } - } - - } -} diff --git a/client/src/game/GameStateUpdate.cpp b/client/src/game/GameStateUpdate.cpp deleted file mode 100644 index b432b55..0000000 --- a/client/src/game/GameStateUpdate.cpp +++ /dev/null @@ -1,800 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** GameState - Update and input handling functions -*/ - -#include "../../include/GameState.hpp" -#include "Core/InputMapping.hpp" -#include "ECS/Components/TextLabel.hpp" -#include "ECS/Component.hpp" -#include "Core/Logger.hpp" -#include "ResultsState.hpp" -#include "RoomListState.hpp" -#include "MenuState.hpp" -#include - -using namespace RType::ECS; - -namespace RType { - namespace Client { - - void InGameState::HandleInput() { - if (m_levelProgress.allLevelsComplete) { - if (m_renderer->IsKeyPressed(Renderer::Key::Enter) && !m_gameOverEnterPressed) { - m_gameOverEnterPressed = true; - enterResultsScreen(); - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Enter)) { - m_gameOverEnterPressed = false; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Escape) && !m_gameOverEscapePressed) { - m_gameOverEscapePressed = true; - if (m_context.networkClient) { - m_context.networkClient->Stop(); - m_context.networkClient.reset(); - } - m_machine.ChangeState(std::make_unique(m_machine, m_context)); - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Escape)) { - m_gameOverEscapePressed = false; - } - return; - } - - if (m_isGameOver) { - if (m_renderer->IsKeyPressed(Renderer::Key::Enter) && !m_gameOverEnterPressed) { - m_gameOverEnterPressed = true; - enterResultsScreen(); - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Enter)) { - m_gameOverEnterPressed = false; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Escape) && !m_gameOverEscapePressed) { - m_gameOverEscapePressed = true; - if (m_context.networkClient) { - m_context.networkClient->Stop(); - m_context.networkClient.reset(); - } - m_machine.ChangeState(std::make_unique(m_machine, m_context)); - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Escape)) { - m_gameOverEscapePressed = false; - } - return; - } - - m_currentInputs = 0; - - Renderer::Key moveUpKey = Core::InputMapping::GetKey("MOVE_UP"); - Renderer::Key moveDownKey = Core::InputMapping::GetKey("MOVE_DOWN"); - Renderer::Key moveLeftKey = Core::InputMapping::GetKey("MOVE_LEFT"); - Renderer::Key moveRightKey = Core::InputMapping::GetKey("MOVE_RIGHT"); - Renderer::Key shootKey = Core::InputMapping::GetKey("SHOOT"); - - if (moveUpKey == Renderer::Key::Unknown) moveUpKey = Renderer::Key::Up; - if (moveDownKey == Renderer::Key::Unknown) moveDownKey = Renderer::Key::Down; - if (moveLeftKey == Renderer::Key::Unknown) moveLeftKey = Renderer::Key::Left; - if (moveRightKey == Renderer::Key::Unknown) moveRightKey = Renderer::Key::Right; - if (shootKey == Renderer::Key::Unknown) shootKey = Renderer::Key::Space; - - if (m_renderer->IsKeyPressed(moveUpKey)) { - m_currentInputs |= network::InputFlags::UP; - } - if (m_renderer->IsKeyPressed(moveDownKey)) { - m_currentInputs |= network::InputFlags::DOWN; - } - if (m_renderer->IsKeyPressed(moveLeftKey)) { - m_currentInputs |= network::InputFlags::LEFT; - } - if (m_renderer->IsKeyPressed(moveRightKey)) { - m_currentInputs |= network::InputFlags::RIGHT; - } - static bool shootPressedLastFrame = false; - bool shootPressed = m_renderer->IsKeyPressed(shootKey); - - if (shootPressed) { - m_isCharging = true; - m_chargeTime += 0.016f; - if (m_chargeTime > 2.0f) { - m_chargeTime = 2.0f; - } - } else { - if (shootPressedLastFrame) { - float savedChargeTime = m_chargeTime; - - if (savedChargeTime >= 2.0f) { - if (m_localPlayerEntity != ECS::NULL_ENTITY && - m_registry.IsEntityAlive(m_localPlayerEntity) && - m_registry.HasComponent(m_localPlayerEntity) && - m_registry.HasComponent(m_localPlayerEntity)) { - createBeamEntity(); - m_beamDuration = 2.0f; - } - } else { - m_currentInputs |= network::InputFlags::SHOOT; - if (m_isNetworkSession) { - if (m_shootMusic != Audio::INVALID_MUSIC_ID) { - Audio::PlaybackOptions opts; - opts.volume = 1.0f; - opts.loop = false; - m_context.audio->StopMusic(m_shootMusic); - m_context.audio->PlayMusic(m_shootMusic, opts); - } else if (m_playerShootSound != Audio::INVALID_SOUND_ID) { - Audio::PlaybackOptions opts; - opts.volume = 1.0f; - m_context.audio->PlaySound(m_playerShootSound, opts); - } - } - } - } - m_isCharging = false; - m_chargeTime = 0.0f; - } - shootPressedLastFrame = shootPressed; - - - - if (!m_isNetworkSession && - m_localPlayerEntity != ECS::NULL_ENTITY && - m_registry.HasComponent(m_localPlayerEntity)) { - auto& shootCmd = m_registry.GetComponent(m_localPlayerEntity); - shootCmd.wantsToShoot = (m_currentInputs & network::InputFlags::SHOOT); - } - if (m_context.networkClient && m_currentInputs != m_previousInputs) { - auto now = std::chrono::steady_clock::now(); - auto ms = std::chrono::duration_cast(now - m_lastInputTime).count(); - - if (ms >= 30) { - m_context.networkClient->SendInput(static_cast(m_currentInputs)); - m_lastInputTime = now; - - } - m_previousInputs = m_currentInputs; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Escape) && !m_escapeKeyPressed) { - m_escapeKeyPressed = true; - m_machine.PopState(); - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Escape)) { - m_escapeKeyPressed = false; - } - } - - void InGameState::Update(float dt) { - UpdateLevelTransition(dt); - - if (m_context.networkClient) { - m_context.networkClient->ReceivePackets(); - } else { - m_scoreAccumulator += dt; - if (m_scoreAccumulator >= 10.0f) { - uint32_t intervals = static_cast(m_scoreAccumulator / 10.0f); - m_playerScore += intervals * 10; - m_scoreAccumulator -= static_cast(intervals) * 10.0f; - } - } - - if (m_shootSfxCooldown > 0.0f) { - m_shootSfxCooldown -= dt; - } - - updateBeam(dt); - - triggerGameOverIfNeeded(); - if (m_isGameOver) { - m_gameOverElapsed += dt; - updateHUD(); - return; - } - - triggerVictoryIfNeeded(); - if (m_levelProgress.allLevelsComplete) { - m_levelProgress.victoryElapsed += dt; - updateHUD(); - return; - } - if (m_inputSystem) { - m_inputSystem->Update(m_registry, dt); - } - if (m_movementSystem) { - m_movementSystem->Update(m_registry, dt); - } - - - if (m_isNetworkSession) { - // Scroll backgrounds - for (auto& bg : m_backgroundEntities) { - if (!m_registry.IsEntityAlive(bg)) - continue; - if (!m_registry.HasComponent(bg) || - !m_registry.HasComponent(bg)) - continue; - auto& pos = m_registry.GetComponent(bg); - const auto& scrollable = m_registry.GetComponent(bg); - pos.x += scrollable.speed * dt; - } - auto obstacleColliders = m_registry.GetEntitiesWithComponent(); - for (auto collider : obstacleColliders) { - if (!m_registry.HasComponent(collider)) { - continue; - } - const auto& metadata = m_registry.GetComponent(collider); - auto visual = metadata.visualEntity; - - if (visual == ECS::NULL_ENTITY || - !m_registry.IsEntityAlive(visual) || - !m_registry.HasComponent(visual) || - !m_registry.HasComponent(visual)) { - continue; - } - - auto& pos = m_registry.GetComponent(visual); - const auto& scrollable = m_registry.GetComponent(visual); - pos.x += scrollable.speed * dt; - } - auto obstacleCollidersForSync = m_registry.GetEntitiesWithComponent(); - for (auto collider : obstacleCollidersForSync) { - if (!m_registry.IsEntityAlive(collider) || - !m_registry.HasComponent(collider) || - !m_registry.HasComponent(collider)) - continue; - - if (!m_registry.HasComponent(collider)) - continue; - - const auto& metadata = m_registry.GetComponent(collider); - - if (metadata.visualEntity != ECS::NULL_ENTITY && - m_registry.IsEntityAlive(metadata.visualEntity) && - m_registry.HasComponent(metadata.visualEntity)) { - - const auto& visualPos = m_registry.GetComponent(metadata.visualEntity); - auto& colliderPos = m_registry.GetComponent(collider); - colliderPos.x = visualPos.x + metadata.offsetX; - colliderPos.y = visualPos.y + metadata.offsetY; - } else if (m_registry.HasComponent(collider)) { - auto& pos = m_registry.GetComponent(collider); - const auto& scrollable = m_registry.GetComponent(collider); - pos.x += scrollable.speed * dt; - } - } - } else if (m_scrollingSystem) { - m_scrollingSystem->Update(m_registry, dt); - } - - if (m_isNetworkSession && m_localPlayerEntity != ECS::NULL_ENTITY && - m_registry.IsEntityAlive(m_localPlayerEntity) && - m_registry.HasComponent(m_localPlayerEntity)) { - auto& pos = m_registry.GetComponent(m_localPlayerEntity); - - float newX = pos.x; - float newY = pos.y; - if (m_currentInputs & network::InputFlags::UP) { - newY -= PREDICTION_SPEED * dt; - } - if (m_currentInputs & network::InputFlags::DOWN) { - newY += PREDICTION_SPEED * dt; - } - if (m_currentInputs & network::InputFlags::LEFT) { - newX -= PREDICTION_SPEED * dt; - } - if (m_currentInputs & network::InputFlags::RIGHT) { - newX += PREDICTION_SPEED * dt; - } - - newX = std::max(0.0f, std::min(newX, 1280.0f - 66.0f)); - newY = std::max(0.0f, std::min(newY, 720.0f - 32.0f)); - - float playerW = 25.0f, playerH = 25.0f; - if (m_registry.HasComponent(m_localPlayerEntity)) { - const auto& box = m_registry.GetComponent(m_localPlayerEntity); - playerW = box.width; - playerH = box.height; - } - - bool blocked = false; - for (auto& collider : m_obstacleColliderEntities) { - if (!m_registry.IsEntityAlive(collider) || - !m_registry.HasComponent(collider) || - !m_registry.HasComponent(collider)) - continue; - const auto& obstPos = m_registry.GetComponent(collider); - const auto& obstBox = m_registry.GetComponent(collider); - - bool wouldCollide = - newX < obstPos.x + obstBox.width && - newX + playerW > obstPos.x && - newY < obstPos.y + obstBox.height && - newY + playerH > obstPos.y; - - if (wouldCollide) { - blocked = true; - float overlapLeft = (newX + playerW) - obstPos.x; - float overlapRight = (obstPos.x + obstBox.width) - newX; - float overlapTop = (newY + playerH) - obstPos.y; - float overlapBottom = (obstPos.y + obstBox.height) - newY; - - float minOverlapX = std::min(overlapLeft, overlapRight); - float minOverlapY = std::min(overlapTop, overlapBottom); - - if (minOverlapX < minOverlapY) { - if (overlapLeft < overlapRight) { - newX = obstPos.x - playerW - 0.5f; - } else { - newX = obstPos.x + obstBox.width + 0.5f; - } - } else { - if (overlapTop < overlapBottom) { - newY = obstPos.y - playerH - 0.5f; - } else { - newY = obstPos.y + obstBox.height + 0.5f; - } - } - break; - } - } - - newX = std::max(0.0f, std::min(newX, 1280.0f - 66.0f)); - newY = std::max(0.0f, std::min(newY, 720.0f - 32.0f)); - - pos.x = newX; - pos.y = newY; - - PredictedInput prediction; - prediction.sequence = m_inputSequence; - prediction.inputs = m_currentInputs; - prediction.predictedX = newX; - prediction.predictedY = newY; - prediction.deltaTime = dt; - m_inputHistory.push_back(prediction); - - while (m_inputHistory.size() > MAX_INPUT_HISTORY) { - m_inputHistory.pop_front(); - } - - m_predictedX = newX; - m_predictedY = newY; - - (void)blocked; - } - - auto entities = m_registry.GetEntitiesWithComponent(); - for (auto entity : entities) { - if (m_registry.HasComponent(entity)) { - auto& pos = m_registry.GetComponent(entity); - if (entity == m_localPlayerEntity && !m_isNetworkSession) { - pos.x = std::max(0.0f, std::min(pos.x, 1280.0f - 66.0f)); - pos.y = std::max(0.0f, std::min(pos.y, 720.0f - 32.0f)); - } - UpdatePlayerNameLabelPosition(entity, pos.x, pos.y); - } - } - - if (m_collisionDetectionSystem) { - m_collisionDetectionSystem->Update(m_registry, dt); - } - if (m_powerUpCollisionSystem) { - m_powerUpCollisionSystem->Update(m_registry, dt); - } - if (m_bulletResponseSystem) { - m_bulletResponseSystem->Update(m_registry, dt); - } - if (m_playerResponseSystem) { - m_playerResponseSystem->Update(m_registry, dt); - } - if (m_obstacleResponseSystem) { - m_obstacleResponseSystem->Update(m_registry, dt); - } - if (m_scoreSystem) { - m_scoreSystem->Update(m_registry, dt); - } - if (m_healthSystem) { - m_healthSystem->Update(m_registry, dt); - } - - m_localScrollOffset += -150.0f * dt; - - if (m_bossWarningActive) { - m_bossWarningTimer += dt; - if (m_bossWarningTimer >= BOSS_WARNING_DURATION) { - m_bossWarningActive = false; - } else { - int interval = static_cast(m_bossWarningTimer / 0.25f); - m_bossWarningFlashState = (interval % 2 == 0); - } - } - - if (m_shieldSystem) { - m_shieldSystem->Update(m_registry, dt); - } - if (m_forcePodSystem) { - m_forcePodSystem->Update(m_registry, dt); - } - - if (m_shootingSystem) { - m_shootingSystem->Update(m_registry, dt); - } - - if (m_bossHealthBar.active && !m_bossMusicPlaying && !m_isGameOver) { - auto it = m_networkEntityMap.find(m_bossHealthBar.bossNetworkId); - if (it != m_networkEntityMap.end()) { - auto bossEntity = it->second; - if (m_registry.IsEntityAlive(bossEntity) && m_registry.HasComponent(bossEntity)) { - auto& pos = m_registry.GetComponent(bossEntity); - if (pos.x < 1300.0f) { - if (m_context.audio) { - if (m_gameMusicPlaying) { - m_context.audio->StopMusic(m_gameMusic); - m_gameMusicPlaying = false; - } - if (m_bossMusic != Audio::INVALID_MUSIC_ID) { - Audio::PlaybackOptions opts; - opts.loop = true; - opts.volume = 0.6f; - m_context.audio->PlayMusic(m_bossMusic, opts); - m_bossMusicPlaying = true; - } - } - } - } - } - } - - if (m_audioSystem) { - if (!m_isGameOver && !m_bossMusicPlaying && m_gameMusic != Audio::INVALID_MUSIC_ID && !m_gameMusicPlaying) { - if (m_context.audio) { - Audio::PlaybackOptions opts; - opts.loop = true; - opts.volume = 0.35f; - m_context.audio->PlayMusic(m_gameMusic, opts); - m_gameMusicPlaying = true; - } - } - m_audioSystem->Update(m_registry, dt); - } - - if (m_animationSystem) { - m_animationSystem->Update(m_registry, dt); - } - - for (auto& bg : m_backgroundEntities) { - if (!m_registry.HasComponent(bg)) - continue; - auto& pos = m_registry.GetComponent(bg); - if (pos.x <= -1280.0f) { - pos.x = pos.x + 3 * 1280.0f; - } - } - - if (m_isNetworkSession) { - std::vector toRemove; - for (auto& [networkId, interpState] : m_interpolationStates) { - auto entityIt = m_networkEntityMap.find(networkId); - if (entityIt == m_networkEntityMap.end()) { - toRemove.push_back(networkId); - continue; - } - - ECS::Entity entity = entityIt->second; - if (!m_registry.IsEntityAlive(entity)) { - toRemove.push_back(networkId); - continue; - } - - if (!m_registry.HasComponent(entity)) { - continue; - } - - interpState.interpTime += dt; - float t = interpState.interpTime / interpState.interpDuration; - if (t > 1.0f) { - t = 1.0f; - } - - auto& pos = m_registry.GetComponent(entity); - pos.x = interpState.prevX + (interpState.targetX - interpState.prevX) * t; - pos.y = interpState.prevY + (interpState.targetY - interpState.prevY) * t; - - if (m_registry.HasComponent(entity)) { - UpdatePlayerNameLabelPosition(entity, pos.x, pos.y); - } - } - for (uint32_t id : toRemove) { - m_interpolationStates.erase(id); - } - - for (auto& [networkId, ecsEntity] : m_networkEntityMap) { - if (!m_registry.IsEntityAlive(ecsEntity)) continue; - if (!m_registry.HasComponent(ecsEntity)) continue; - if (!m_registry.HasComponent(ecsEntity)) continue; - if (m_interpolationStates.count(networkId) > 0) continue; - if (ecsEntity == m_localPlayerEntity) continue; - - auto& pos = m_registry.GetComponent(ecsEntity); - const auto& vel = m_registry.GetComponent(ecsEntity); - pos.x += vel.dx * dt; - pos.y += vel.dy * dt; - } - } - updateHUD(); - } - - void InGameState::triggerGameOverIfNeeded() { - if (m_isGameOver) { - return; - } - - bool dead = false; - if (m_isNetworkSession) { - if (m_context.playerNumber >= 1 && m_context.playerNumber <= MAX_PLAYERS) { - size_t idx = static_cast(m_context.playerNumber - 1); - dead = m_playersHUD[idx].active && m_playersHUD[idx].isDead; - } - } else { - if (m_localPlayerEntity == ECS::NULL_ENTITY || !m_registry.IsEntityAlive(m_localPlayerEntity)) { - dead = true; - } else if (m_registry.HasComponent(m_localPlayerEntity)) { - const auto& h = m_registry.GetComponent(m_localPlayerEntity); - dead = (h.current <= 0); - } - } - - if (!dead) { - return; - } - - m_isGameOver = true; - m_gameOverElapsed = 0.0f; - m_isCharging = false; - m_chargeTime = 0.0f; - - if (m_context.audio) { - if (m_gameMusic != Audio::INVALID_MUSIC_ID && m_gameMusicPlaying) { - m_context.audio->StopMusic(m_gameMusic); - m_gameMusicPlaying = false; - } - - if (m_bossMusic != Audio::INVALID_MUSIC_ID && m_bossMusicPlaying) { - m_context.audio->StopMusic(m_bossMusic); - m_bossMusicPlaying = false; - } - - if (m_gameOverMusic != Audio::INVALID_MUSIC_ID && !m_gameOverMusicPlaying) { - Audio::PlaybackOptions opts; - opts.loop = true; - opts.volume = 0.5f; - m_context.audio->PlayMusic(m_gameOverMusic, opts); - m_gameOverMusicPlaying = true; - } - } - - if (m_gameOverTitleEntity == NULL_ENTITY && m_gameOverFontLarge != Renderer::INVALID_FONT_ID) { - m_gameOverTitleEntity = m_registry.CreateEntity(); - m_registry.AddComponent(m_gameOverTitleEntity, Position{640.0f, 260.0f}); - TextLabel title("GAME OVER", m_gameOverFontLarge, 56); - title.centered = true; - title.color = {1.0f, 0.08f, 0.58f, 1.0f}; - m_registry.AddComponent(m_gameOverTitleEntity, std::move(title)); - } - - if (m_gameOverScoreEntity == NULL_ENTITY && m_gameOverFontMedium != Renderer::INVALID_FONT_ID) { - m_gameOverScoreEntity = m_registry.CreateEntity(); - m_registry.AddComponent(m_gameOverScoreEntity, Position{640.0f, 360.0f}); - TextLabel score("", m_gameOverFontMedium, 22); - score.centered = true; - score.color = {0.5f, 0.86f, 1.0f, 0.95f}; - m_registry.AddComponent(m_gameOverScoreEntity, std::move(score)); - } - - if (m_gameOverHintEntity == NULL_ENTITY && m_hudFontSmall != Renderer::INVALID_FONT_ID) { - m_gameOverHintEntity = m_registry.CreateEntity(); - m_registry.AddComponent(m_gameOverHintEntity, Position{640.0f, 430.0f}); - TextLabel hint("Press ENTER to view results | ESC to return to lobby", m_hudFontSmall, 14); - hint.centered = true; - hint.color = {0.5f, 0.86f, 1.0f, 0.85f}; - m_registry.AddComponent(m_gameOverHintEntity, std::move(hint)); - } - - if (m_gameOverScoreEntity != NULL_ENTITY && m_registry.IsEntityAlive(m_gameOverScoreEntity)) { - auto& label = m_registry.GetComponent(m_gameOverScoreEntity); - label.text = "SCORE " + std::to_string(m_playerScore); - } - } - - void InGameState::triggerVictoryIfNeeded() { - if (!m_levelProgress.allLevelsComplete) { - return; - } - - if (m_context.audio) { - if (m_gameMusic != Audio::INVALID_MUSIC_ID && m_gameMusicPlaying) { - m_context.audio->StopMusic(m_gameMusic); - m_gameMusicPlaying = false; - } - - if (m_bossMusic != Audio::INVALID_MUSIC_ID && m_bossMusicPlaying) { - m_context.audio->StopMusic(m_bossMusic); - m_bossMusicPlaying = false; - } - - if (m_victoryMusic != Audio::INVALID_MUSIC_ID && !m_victoryMusicPlaying) { - Audio::PlaybackOptions opts; - opts.loop = false; // Do not loop victory music - opts.volume = 0.5f; - m_context.audio->PlayMusic(m_victoryMusic, opts); - m_victoryMusicPlaying = true; - } - } - - if (m_victoryTitleEntity == NULL_ENTITY && m_gameOverFontLarge != Renderer::INVALID_FONT_ID) { - m_victoryTitleEntity = m_registry.CreateEntity(); - m_registry.AddComponent(m_victoryTitleEntity, Position{640.0f, 260.0f}); - TextLabel title("VICTORY!", m_gameOverFontLarge, 56); - title.centered = true; - title.color = {0.2f, 1.0f, 0.2f, 1.0f}; - m_registry.AddComponent(m_victoryTitleEntity, std::move(title)); - } - - if (m_victoryScoreEntity == NULL_ENTITY && m_gameOverFontMedium != Renderer::INVALID_FONT_ID) { - m_victoryScoreEntity = m_registry.CreateEntity(); - m_registry.AddComponent(m_victoryScoreEntity, Position{640.0f, 360.0f}); - TextLabel score("", m_gameOverFontMedium, 22); - score.centered = true; - score.color = {0.5f, 1.0f, 0.5f, 0.95f}; - m_registry.AddComponent(m_victoryScoreEntity, std::move(score)); - } - - if (m_victoryHintEntity == NULL_ENTITY && m_hudFontSmall != Renderer::INVALID_FONT_ID) { - m_victoryHintEntity = m_registry.CreateEntity(); - m_registry.AddComponent(m_victoryHintEntity, Position{640.0f, 430.0f}); - TextLabel hint("Press ENTER to view results | ESC to return to menu", m_hudFontSmall, 14); - hint.centered = true; - hint.color = {0.5f, 1.0f, 0.5f, 0.85f}; - m_registry.AddComponent(m_victoryHintEntity, std::move(hint)); - } - - if (m_victoryScoreEntity != NULL_ENTITY && m_registry.IsEntityAlive(m_victoryScoreEntity)) { - auto& label = m_registry.GetComponent(m_victoryScoreEntity); - label.text = "SCORE " + std::to_string(m_playerScore); - } - } - - void InGameState::Cleanup() { - - if (m_context.audio && m_shootMusic != Audio::INVALID_MUSIC_ID) { - m_context.audio->StopMusic(m_shootMusic); - m_context.audio->UnloadMusic(m_shootMusic); - } - - if (m_context.audio && m_powerUpMusic != Audio::INVALID_MUSIC_ID) { - m_context.audio->StopMusic(m_powerUpMusic); - m_context.audio->UnloadMusic(m_powerUpMusic); - } - - if (m_context.audio && m_gameMusic != Audio::INVALID_MUSIC_ID) { - m_context.audio->StopMusic(m_gameMusic); - m_context.audio->UnloadMusic(m_gameMusic); - m_gameMusic = Audio::INVALID_MUSIC_ID; - m_gameMusicPlaying = false; - } - - if (m_context.audio && m_bossMusic != Audio::INVALID_MUSIC_ID) { - m_context.audio->StopMusic(m_bossMusic); - m_context.audio->UnloadMusic(m_bossMusic); - m_bossMusic = Audio::INVALID_MUSIC_ID; - m_bossMusicPlaying = false; - } - - if (m_context.audio && m_gameOverMusic != Audio::INVALID_MUSIC_ID) { - m_context.audio->StopMusic(m_gameOverMusic); - m_context.audio->UnloadMusic(m_gameOverMusic); - m_gameOverMusic = Audio::INVALID_MUSIC_ID; - m_gameOverMusicPlaying = false; - } - - if (m_context.audio && m_victoryMusic != Audio::INVALID_MUSIC_ID) { - m_context.audio->StopMusic(m_victoryMusic); - m_context.audio->UnloadMusic(m_victoryMusic); - m_victoryMusic = Audio::INVALID_MUSIC_ID; - m_victoryMusicPlaying = false; - } - - for (auto& bg : m_backgroundEntities) { - if (m_registry.IsEntityAlive(bg)) { - m_registry.DestroyEntity(bg); - } - } - m_backgroundEntities.clear(); - - for (auto& obstacle : m_obstacleSpriteEntities) { - if (m_registry.IsEntityAlive(obstacle)) { - m_registry.DestroyEntity(obstacle); - } - } - m_obstacleSpriteEntities.clear(); - - for (auto& obstacle : m_obstacleColliderEntities) { - if (m_registry.IsEntityAlive(obstacle)) { - m_registry.DestroyEntity(obstacle); - } - } - m_obstacleColliderEntities.clear(); - m_obstacleIdToCollider.clear(); - - if (m_hudPlayerEntity != NULL_ENTITY && m_registry.IsEntityAlive(m_hudPlayerEntity)) { - m_registry.DestroyEntity(m_hudPlayerEntity); - } - if (m_hudScoreEntity != NULL_ENTITY && m_registry.IsEntityAlive(m_hudScoreEntity)) { - m_registry.DestroyEntity(m_hudScoreEntity); - } - if (m_hudLivesEntity != NULL_ENTITY && m_registry.IsEntityAlive(m_hudLivesEntity)) { - m_registry.DestroyEntity(m_hudLivesEntity); - } - if (m_hudScoreboardTitle != NULL_ENTITY && m_registry.IsEntityAlive(m_hudScoreboardTitle)) { - m_registry.DestroyEntity(m_hudScoreboardTitle); - } - for (auto& playerHUD : m_playersHUD) { - if (playerHUD.scoreEntity != NULL_ENTITY && m_registry.IsEntityAlive(playerHUD.scoreEntity)) { - m_registry.DestroyEntity(playerHUD.scoreEntity); - } - if (playerHUD.powerupSpreadEntity != ECS::NULL_ENTITY && m_registry.IsEntityAlive(playerHUD.powerupSpreadEntity)) { - m_registry.DestroyEntity(playerHUD.powerupSpreadEntity); - } - if (playerHUD.powerupLaserEntity != ECS::NULL_ENTITY && m_registry.IsEntityAlive(playerHUD.powerupLaserEntity)) { - m_registry.DestroyEntity(playerHUD.powerupLaserEntity); - } - if (playerHUD.powerupSpeedEntity != ECS::NULL_ENTITY && m_registry.IsEntityAlive(playerHUD.powerupSpeedEntity)) { - m_registry.DestroyEntity(playerHUD.powerupSpeedEntity); - } - if (playerHUD.powerupShieldEntity != ECS::NULL_ENTITY && m_registry.IsEntityAlive(playerHUD.powerupShieldEntity)) { - m_registry.DestroyEntity(playerHUD.powerupShieldEntity); - } - } - - for (auto& [playerEntity, labelEntity] : m_playerNameLabels) { - if (m_registry.IsEntityAlive(labelEntity)) { - m_registry.DestroyEntity(labelEntity); - } - } - m_playerNameLabels.clear(); - - if (m_beamEntity != ECS::NULL_ENTITY && m_registry.IsEntityAlive(m_beamEntity)) { - m_registry.DestroyEntity(m_beamEntity); - m_beamEntity = ECS::NULL_ENTITY; - } - } - - void InGameState::enterResultsScreen() { - std::vector> scores; - scores.reserve(MAX_PLAYERS); - - for (size_t i = 0; i < MAX_PLAYERS; i++) { - if (!m_playersHUD[i].active) { - continue; - } - uint8_t playerNum = static_cast(i + 1); - std::string name = "P" + std::to_string(playerNum); - for (const auto& p : m_context.allPlayers) { - if (p.number == playerNum && p.name[0] != '\0') { - name = std::string(p.name); - break; - } - } - scores.push_back({name, m_playersHUD[i].score}); - } - - if (!m_isNetworkSession && scores.empty()) { - std::string name = m_context.playerName.empty() ? "P1" : m_context.playerName; - scores.push_back({name, m_playerScore}); - } - - if (m_context.networkClient) { - m_context.networkClient->Stop(); - m_context.networkClient.reset(); - } - - m_machine.ChangeState(std::make_unique(m_machine, m_context, std::move(scores))); - } - - } -} diff --git a/client/src/lobby/LobbyStateInit.cpp b/client/src/lobby/LobbyStateInit.cpp deleted file mode 100644 index 2245d51..0000000 --- a/client/src/lobby/LobbyStateInit.cpp +++ /dev/null @@ -1,203 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** LobbyState - Initialization and cleanup functions -*/ - -#include "../../include/LobbyState.hpp" -#include "ECS/Components/TextLabel.hpp" -#include "ECS/Component.hpp" -#include "ECS/AudioSystem.hpp" -#include - -using namespace RType::ECS; - -namespace RType { - namespace Client { - - LobbyState::LobbyState(GameStateMachine& machine, GameContext& context) - : m_machine(machine), m_context(context), m_playerName(context.playerName) { - m_renderer = context.renderer; - m_renderingSystem = std::make_unique(m_renderer.get()); - m_textSystem = std::make_unique(m_renderer.get()); - } - - LobbyState::LobbyState(GameStateMachine& machine, GameContext& context, network::NetworkTcpSocket&& socket) - : m_machine(machine), m_context(context), m_playerName(context.playerName) { - m_renderer = context.renderer; - m_renderingSystem = std::make_unique(m_renderer.get()); - m_textSystem = std::make_unique(m_renderer.get()); - m_client = std::make_unique(std::move(socket)); - } - - void LobbyState::Init() { - std::cout << "[LobbyState] Connecting to " << m_context.serverIp << ":" << m_context.serverPort << " as " << m_playerName << std::endl; - - if (m_context.audio) { - m_audioSystem = std::make_unique(m_context.audio.get()); - m_lobbyMusic = m_context.audio->LoadMusic("assets/sounds/lobby.flac"); - if (m_lobbyMusic == Audio::INVALID_MUSIC_ID) { - m_lobbyMusic = m_context.audio->LoadMusic("../assets/sounds/lobby.flac"); - } - - if (m_lobbyMusic != Audio::INVALID_MUSIC_ID) { - auto cmd = m_registry.CreateEntity(); - auto& me = m_registry.AddComponent(cmd, MusicEffect(m_lobbyMusic)); - me.play = true; - me.stop = false; - me.loop = true; - me.volume = 0.35f; - me.pitch = 1.0f; - m_lobbyMusicPlaying = true; - } - } - - if (!m_client) { - if (!m_context.networkModule) { - std::cout << "[LobbyState] ERROR: networkModule is null (cannot create LobbyClient)" << std::endl; - m_errorMessage = "Internal error: network module missing"; - m_hasError = true; - return; - } - m_client = std::make_unique( - m_context.networkModule.get(), m_context.serverIp, m_context.serverPort); - } - - m_fontLarge = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 32); - if (m_fontLarge == Renderer::INVALID_FONT_ID) { - m_fontLarge = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 32); - } - - m_fontMedium = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 20); - if (m_fontMedium == Renderer::INVALID_FONT_ID) { - m_fontMedium = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 20); - } - - m_fontSmall = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 14); - if (m_fontSmall == Renderer::INVALID_FONT_ID) { - m_fontSmall = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 14); - } - - m_playerBlueTexture = m_renderer->LoadTexture("assets/spaceships/player_blue.png"); - if (m_playerBlueTexture == Renderer::INVALID_TEXTURE_ID) { - m_playerBlueTexture = m_renderer->LoadTexture("../assets/spaceships/player_blue.png"); - } - - m_playerGreenTexture = m_renderer->LoadTexture("assets/spaceships/player_green.png"); - if (m_playerGreenTexture == Renderer::INVALID_TEXTURE_ID) { - m_playerGreenTexture = m_renderer->LoadTexture("../assets/spaceships/player_green.png"); - } - - m_nave2BlueTexture = m_renderer->LoadTexture("assets/spaceships/nave2_blue.png"); - if (m_nave2BlueTexture == Renderer::INVALID_TEXTURE_ID) { - m_nave2BlueTexture = m_renderer->LoadTexture("../assets/spaceships/nave2_blue.png"); - } - - m_nave2Texture = m_renderer->LoadTexture("assets/spaceships/nave2.png"); - if (m_nave2Texture == Renderer::INVALID_TEXTURE_ID) { - m_nave2Texture = m_renderer->LoadTexture("../assets/spaceships/nave2.png"); - } - - m_bgTexture = m_renderer->LoadTexture("assets/backgrounds/1.jpg"); - if (m_bgTexture == Renderer::INVALID_TEXTURE_ID) { - m_bgTexture = m_renderer->LoadTexture("../assets/backgrounds/1.jpg"); - } - - m_client->onPlayerLeft([this](uint8_t playerNum) { - removePlayer(playerNum); - }); - - m_client->onConnectionError([this](const std::string& error) { - m_errorMessage = error; - m_hasError = true; - }); - - m_client->onCountdown([this](uint8_t seconds) { - m_countdownSeconds = seconds; - }); - - createUI(); - m_client->connect(m_playerName); - } - - void LobbyState::Cleanup() { - std::cout << "[LobbyState] Cleaning up..." << std::endl; - - if (m_context.audio && m_lobbyMusic != Audio::INVALID_MUSIC_ID) { - m_context.audio->StopMusic(m_lobbyMusic); - m_context.audio->UnloadMusic(m_lobbyMusic); - m_lobbyMusic = Audio::INVALID_MUSIC_ID; - m_lobbyMusicPlaying = false; - } - - if (m_client) { - m_client->disconnect(); - m_client.reset(); - } - - for (auto& [playerNum, entity] : m_playerEntities) { - if (m_registry.IsEntityAlive(entity)) { - m_registry.DestroyEntity(entity); - } - } - m_playerEntities.clear(); - - for (auto& [playerNum, sprite] : m_playerSprites) { - if (m_registry.IsEntityAlive(sprite)) { - m_registry.DestroyEntity(sprite); - } - } - m_playerSprites.clear(); - } - - void LobbyState::createUI() { - if (m_bgTexture != Renderer::INVALID_TEXTURE_ID) { - m_bgEntity = m_registry.CreateEntity(); - m_registry.AddComponent(m_bgEntity, Position{0.0f, 0.0f}); - - Renderer::SpriteId spriteId = m_renderer->CreateSprite(m_bgTexture, {}); - Drawable drawable(spriteId, -10); - - Renderer::Vector2 texSize = m_renderer->GetTextureSize(m_bgTexture); - if (texSize.x > 0 && texSize.y > 0) { - drawable.scale = {1280.0f / texSize.x, 720.0f / texSize.y}; - } - - m_registry.AddComponent(m_bgEntity, std::move(drawable)); - } - - if (m_fontLarge == Renderer::INVALID_FONT_ID) - return; - m_titleEntity = m_registry.CreateEntity(); - m_registry.AddComponent(m_titleEntity, Position{640.0f, 50.0f}); - TextLabel titleLabel("MULTIPLAYER LOBBY", m_fontLarge, 36); - titleLabel.color = {0.0f, 1.0f, 1.0f, 1.0f}; - titleLabel.centered = true; - m_registry.AddComponent(m_titleEntity, std::move(titleLabel)); - - m_instructionsEntity = m_registry.CreateEntity(); - m_registry.AddComponent(m_instructionsEntity, Position{640.0f, 650.0f}); - TextLabel instrLabel("[R] Toggle Ready | [ESC] Change Room", m_fontSmall, 14); - instrLabel.color = {0.5f, 0.86f, 1.0f, 0.9f}; - instrLabel.centered = true; - m_registry.AddComponent(m_instructionsEntity, std::move(instrLabel)); - } - - Renderer::TextureId LobbyState::getPlayerTexture(uint8_t playerNum) { - switch (playerNum) { - case 0: - return m_playerBlueTexture; - case 1: - return m_playerGreenTexture; - case 2: - return m_nave2BlueTexture; - case 3: - return m_nave2Texture; - default: - return m_playerBlueTexture; - } - } - - } -} diff --git a/client/src/lobby/LobbyStateUpdate.cpp b/client/src/lobby/LobbyStateUpdate.cpp deleted file mode 100644 index b7a2bc4..0000000 --- a/client/src/lobby/LobbyStateUpdate.cpp +++ /dev/null @@ -1,253 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** LobbyState - Update, input handling, and player management functions -*/ - -#include "../../include/LobbyState.hpp" -#include "../../include/RoomListState.hpp" -#include "ECS/Components/TextLabel.hpp" -#include "ECS/Component.hpp" -#include -#include -#include - -using namespace RType::ECS; - -namespace RType { - namespace Client { - - void LobbyState::HandleInput() { - if (m_renderer->IsKeyPressed(Renderer::Key::R) && !m_rKeyPressed) { - m_rKeyPressed = true; - if (m_client && m_client->isConnected()) { - m_client->ready(); - std::cout << "[LobbyState] Toggled ready!" << std::endl; - } - } else if (!m_renderer->IsKeyPressed(Renderer::Key::R)) { - m_rKeyPressed = false; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Escape) && !m_escapeKeyPressed) { - m_escapeKeyPressed = true; - std::cout << "[LobbyState] Returning to room selection..." << std::endl; - m_machine.ChangeState(std::make_unique(m_machine, m_context)); - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Escape)) { - m_escapeKeyPressed = false; - } - } - - void LobbyState::Update(float dt) { - if (m_hasError) { - if (m_errorEntity == NULL_ENTITY && m_fontLarge != Renderer::INVALID_FONT_ID) { - m_errorEntity = m_registry.CreateEntity(); - m_registry.AddComponent(m_errorEntity, Position{640.0f, 300.0f}); - TextLabel errorLabel(m_errorMessage, m_fontMedium, 24); - errorLabel.color = {1.0f, 0.08f, 0.58f, 1.0f}; - errorLabel.centered = true; - m_registry.AddComponent(m_errorEntity, std::move(errorLabel)); - - Entity backMsg = m_registry.CreateEntity(); - m_registry.AddComponent(backMsg, Position{640.0f, 380.0f}); - TextLabel backLabel("Press ESC to return to room selection", m_fontSmall, 14); - backLabel.color = {0.5f, 0.86f, 1.0f, 0.9f}; - backLabel.centered = true; - m_registry.AddComponent(backMsg, std::move(backLabel)); - } - m_errorTimer += dt; - if (m_errorTimer >= 3.0f) { - std::cout << "[LobbyState] Auto-returning to room selection after error..." << std::endl; - m_machine.ChangeState(std::make_unique(m_machine, m_context)); - } - return; - } - - if (m_client) { - m_client->update(); - } - updateLobbyState(); - - if (m_audioSystem && m_lobbyMusic != Audio::INVALID_MUSIC_ID && !m_lobbyMusicPlaying) { - auto cmd = m_registry.CreateEntity(); - auto& me = m_registry.AddComponent(cmd, MusicEffect(m_lobbyMusic)); - me.play = true; - me.stop = false; - me.loop = true; - me.volume = 0.35f; - me.pitch = 1.0f; - m_lobbyMusicPlaying = true; - } - - if (m_audioSystem) { - m_audioSystem->Update(m_registry, dt); - } - - if (m_countdownSeconds > 0) { - if (m_countdownEntity == NULL_ENTITY && m_fontLarge != Renderer::INVALID_FONT_ID) { - m_countdownEntity = m_registry.CreateEntity(); - m_registry.AddComponent(m_countdownEntity, Position{640.0f, 500.0f}); - TextLabel countdownLabel("", m_fontLarge, 48); - countdownLabel.color = {1.0f, 0.08f, 0.58f, 1.0f}; - countdownLabel.centered = true; - m_registry.AddComponent(m_countdownEntity, std::move(countdownLabel)); - } - - if (m_countdownEntity != NULL_ENTITY && m_registry.IsEntityAlive(m_countdownEntity)) { - auto& label = m_registry.GetComponent(m_countdownEntity); - label.text = "Starting in " + std::to_string((int)m_countdownSeconds) + "..."; - } - } else { - if (m_countdownEntity != NULL_ENTITY && m_registry.IsEntityAlive(m_countdownEntity)) { - m_registry.DestroyEntity(m_countdownEntity); - m_countdownEntity = NULL_ENTITY; - } - } - - if (m_client && m_client->isGameStarted()) { - m_countdownSeconds = 0; - - uint32_t seed = m_client->getGameSeed(); - std::string serverIp = m_context.serverIp; - uint16_t udpPort = m_context.serverPort; - - std::cout << "[LobbyState] Game started! Seed: " << seed << std::endl; - std::cout << "[LobbyState] Waiting for server to start UDP..." << std::endl; - std::this_thread::sleep_for(std::chrono::milliseconds(1500)); - - std::cout << "[LobbyState] Transitioning to GameState..." << std::endl; - - network::PlayerInfo localPlayer = m_client->getMyInfo(); - - m_context.playerHash = localPlayer.hash; - m_context.playerNumber = localPlayer.number; - m_context.allPlayers = m_client->getPlayers(); - - auto gameClient = std::make_shared( - m_context.networkModule.get(), serverIp, udpPort, localPlayer); - if (gameClient->ConnectToServer()) { - m_context.networkClient = gameClient; - m_machine.ChangeState(std::make_unique(m_machine, m_context, seed)); - } - return; - } - } - - void LobbyState::Draw() { - m_renderer->Clear({0.05f, 0.05f, 0.12f, 1.0f}); - - m_renderingSystem->Update(m_registry, 0.0f); - m_textSystem->Update(m_registry, 0.0f); - } - - void LobbyState::updateLobbyState() { - if (!m_client || !m_client->isConnected()) { - return; - } - - const auto& players = m_client->getPlayers(); - for (const auto& player : players) { - updateOrCreatePlayerEntity(player); - } - } - - void LobbyState::updateOrCreatePlayerEntity(const network::PlayerInfo& player) { - Entity entity; - auto it = m_playerEntities.find(player.number); - - if (it == m_playerEntities.end()) { - entity = m_registry.CreateEntity(); - m_playerEntities[player.number] = entity; - - float baseX = 240.0f; - float baseY = 180.0f; - float spacingX = 420.0f; - float spacingY = 220.0f; - - uint8_t index = player.number - 1; - float cardX = baseX + (index % 2) * spacingX; - float cardY = baseY + (index / 2) * spacingY; - - Entity spriteEntity = m_registry.CreateEntity(); - m_playerSprites[player.number] = spriteEntity; - - Renderer::TextureId texture = getPlayerTexture(index); - if (texture != Renderer::INVALID_TEXTURE_ID) { - Renderer::SpriteId spriteId = m_renderer->CreateSprite(texture, {}); - Drawable drawable(spriteId, 0); - drawable.scale = {1.0f, 1.0f}; - m_registry.AddComponent(spriteEntity, Position{cardX, cardY}); - m_registry.AddComponent(spriteEntity, std::move(drawable)); - } - - m_registry.AddComponent(entity, Position{cardX + 10.0f, cardY + 50.0f}); - NetworkPlayer netPlayer{player.number, player.hash, player.name, player.ready}; - m_registry.AddComponent(entity, std::move(netPlayer)); - - if (m_fontMedium != Renderer::INVALID_FONT_ID) { - TextLabel label("", m_fontMedium, 16); - m_registry.AddComponent(entity, std::move(label)); - } - - updatePlayerCardVisuals(player.number, player); - } else { - entity = it->second; - auto& netPlayer = m_registry.GetComponent(entity); - if (netPlayer.ready != player.ready) { - netPlayer.ready = player.ready; - updatePlayerCardVisuals(player.number, player); - } - } - } - - void LobbyState::updatePlayerCardVisuals(uint8_t playerNum, const network::PlayerInfo& player) { - auto it = m_playerEntities.find(playerNum); - if (it == m_playerEntities.end()) - return; - - Entity entity = it->second; - const auto& netPlayer = m_registry.GetComponent(entity); - - std::string playerNameStr = std::string(player.name); - if (playerNameStr.length() > 16) { - playerNameStr = playerNameStr.substr(0, 16); - } - std::string displayText = "P" + std::to_string(netPlayer.playerNumber) + " - " + playerNameStr; - if (netPlayer.ready) { - displayText += "\n[READY]"; - } else { - displayText += "\n[WAITING]"; - } - - if (m_fontMedium != Renderer::INVALID_FONT_ID) { - auto& label = m_registry.GetComponent(entity); - label.text = displayText; - - if (netPlayer.ready) { - label.color = {0.4f, 1.0f, 0.5f, 1.0f}; - } else { - label.color = {1.0f, 0.85f, 0.3f, 1.0f}; - } - } - } - - void LobbyState::removePlayer(uint8_t playerNum) { - auto it = m_playerEntities.find(playerNum); - if (it != m_playerEntities.end()) { - if (m_registry.IsEntityAlive(it->second)) { - m_registry.DestroyEntity(it->second); - } - m_playerEntities.erase(it); - } - - auto spriteIt = m_playerSprites.find(playerNum); - if (spriteIt != m_playerSprites.end()) { - if (m_registry.IsEntityAlive(spriteIt->second)) { - m_registry.DestroyEntity(spriteIt->second); - } - m_playerSprites.erase(spriteIt); - } - } - - } -} diff --git a/client/src/room/RoomListState.cpp b/client/src/room/RoomListState.cpp deleted file mode 100644 index d92bf7e..0000000 --- a/client/src/room/RoomListState.cpp +++ /dev/null @@ -1,358 +0,0 @@ -/* -** EPITECH PROJECT, 2025 -** R-Type -** File description: -** RoomListState - Room selection screen implementation -*/ - -#include "../../include/RoomListState.hpp" -#include "../../include/LobbyState.hpp" -#include "ECS/Components/TextLabel.hpp" -#include -#include - -using namespace RType::ECS; - -namespace RType { - namespace Client { - - RoomListState::RoomListState(GameStateMachine& machine, GameContext& context) - : m_machine(machine), m_context(context) { - m_renderer = context.renderer; - m_renderingSystem = std::make_unique(m_renderer.get()); - m_textSystem = std::make_unique(m_renderer.get()); - } - - void RoomListState::Init() { - std::cout << "[RoomListState] Initializing room selection..." << std::endl; - - m_fontLarge = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 32); - if (m_fontLarge == Renderer::INVALID_FONT_ID) { - m_fontLarge = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 32); - } - - m_fontMedium = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 18); - if (m_fontMedium == Renderer::INVALID_FONT_ID) { - m_fontMedium = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 18); - } - - m_fontSmall = m_renderer->LoadFont("assets/fonts/PressStart2P-Regular.ttf", 14); - if (m_fontSmall == Renderer::INVALID_FONT_ID) { - m_fontSmall = m_renderer->LoadFont("../assets/fonts/PressStart2P-Regular.ttf", 14); - } - - m_bgTexture = m_renderer->LoadTexture("assets/backgrounds/1.jpg"); - if (m_bgTexture == Renderer::INVALID_TEXTURE_ID) { - m_bgTexture = m_renderer->LoadTexture("../assets/backgrounds/1.jpg"); - } - - if (!m_context.networkModule) { - m_hasError = true; - m_errorMessage = "Internal error: network module missing"; - m_errorTimer = 3.0f; - createUI(); - return; - } - - m_roomClient = std::make_unique( - m_context.networkModule.get(), m_context.serverIp, m_context.serverPort); - - if (!m_roomClient->isConnected()) { - m_hasError = true; - m_errorMessage = "Failed to connect to server"; - } else { - m_roomClient->onRoomList([this](const std::vector& rooms) { - m_rooms = rooms; - createRoomEntities(); - }); - - m_roomClient->onRoomCreated([this](uint32_t roomId) { - std::cout << "[RoomListState] Room created! Joining room " << roomId << std::endl; - m_roomClient->joinRoom(roomId); - }); - - m_roomClient->onRoomJoined([this](network::JoinRoomStatus status) { - if (status == network::JoinRoomStatus::SUCCESS) { - std::cout << "[RoomListState] Joined room! Transitioning to lobby..." << std::endl; - network::NetworkTcpSocket socket = m_roomClient->releaseSocket(); - m_machine.ChangeState(std::make_unique(m_machine, m_context, std::move(socket))); - } else { - m_hasError = true; - switch (status) { - case network::JoinRoomStatus::ROOM_FULL: - m_errorMessage = "Room is full"; - break; - case network::JoinRoomStatus::ROOM_NOT_FOUND: - m_errorMessage = "Room not found"; - break; - case network::JoinRoomStatus::ROOM_IN_GAME: - m_errorMessage = "Game already in progress"; - break; - default: - m_errorMessage = "Failed to join room"; - break; - } - m_errorTimer = 3.0f; - } - }); - - m_roomClient->onRoomUpdate([this](const network::RoomInfo& room) { - bool found = false; - for (auto& r : m_rooms) { - if (r.id == room.id) { - r = room; - found = true; - break; - } - } - if (!found) { - m_rooms.push_back(room); - } - createRoomEntities(); - }); - - m_roomClient->onConnectionError([this](const std::string& error) { - m_hasError = true; - m_errorMessage = error; - m_errorTimer = 3.0f; - }); - - m_roomClient->requestRooms(); - } - - createUI(); - } - - void RoomListState::Cleanup() { - std::cout << "[RoomListState] Cleaning up..." << std::endl; - clearRoomEntities(); - m_roomClient.reset(); - } - - void RoomListState::createUI() { - if (m_bgTexture != Renderer::INVALID_TEXTURE_ID) { - m_bgEntity = m_registry.CreateEntity(); - m_registry.AddComponent(m_bgEntity, Position{0.0f, 0.0f}); - - Renderer::SpriteId spriteId = m_renderer->CreateSprite(m_bgTexture, {}); - Drawable drawable(spriteId, -10); - - Renderer::Vector2 texSize = m_renderer->GetTextureSize(m_bgTexture); - if (texSize.x > 0 && texSize.y > 0) { - drawable.scale = {1280.0f / texSize.x, 720.0f / texSize.y}; - } - - m_registry.AddComponent(m_bgEntity, std::move(drawable)); - } - - if (m_fontLarge == Renderer::INVALID_FONT_ID) - return; - - m_titleEntity = m_registry.CreateEntity(); - m_registry.AddComponent(m_titleEntity, Position{640.0f, 50.0f}); - TextLabel titleLabel("SELECT A ROOM", m_fontLarge, 32); - titleLabel.color = {0.0f, 1.0f, 1.0f, 1.0f}; - titleLabel.centered = true; - m_registry.AddComponent(m_titleEntity, std::move(titleLabel)); - - m_instructionsEntity = m_registry.CreateEntity(); - m_registry.AddComponent(m_instructionsEntity, Position{640.0f, 670.0f}); - TextLabel instrLabel("[ENTER] Join | [C] Create Room | [ESC] Back", m_fontSmall, 14); - instrLabel.color = {0.5f, 0.86f, 1.0f, 0.9f}; - instrLabel.centered = true; - m_registry.AddComponent(m_instructionsEntity, std::move(instrLabel)); - } - - void RoomListState::clearRoomEntities() { - for (Entity e : m_roomEntities) { - if (m_registry.IsEntityAlive(e)) { - m_registry.DestroyEntity(e); - } - } - m_roomEntities.clear(); - - if (m_noRoomsEntity != NULL_ENTITY && m_registry.IsEntityAlive(m_noRoomsEntity)) { - m_registry.DestroyEntity(m_noRoomsEntity); - m_noRoomsEntity = NULL_ENTITY; - } - } - - void RoomListState::createRoomEntities() { - clearRoomEntities(); - - if (m_rooms.empty()) { - m_noRoomsEntity = m_registry.CreateEntity(); - m_registry.AddComponent(m_noRoomsEntity, Position{640.0f, 300.0f}); - TextLabel noRoomsLabel("No rooms available\nPress [C] to create one!", m_fontMedium, 18); - noRoomsLabel.color = {0.7f, 0.7f, 0.7f, 1.0f}; - noRoomsLabel.centered = true; - m_registry.AddComponent(m_noRoomsEntity, std::move(noRoomsLabel)); - return; - } - - float startY = 140.0f; - float spacing = 60.0f; - - for (size_t i = 0; i < m_rooms.size(); ++i) { - const auto& room = m_rooms[i]; - - Entity roomEntity = m_registry.CreateEntity(); - m_roomEntities.push_back(roomEntity); - - float y = startY + i * spacing; - m_registry.AddComponent(roomEntity, Position{640.0f, y}); - - std::string prefix = (static_cast(i) == m_selectedIndex) ? "> " : " "; - std::string status = room.inGame ? "[IN GAME]" : "[WAITING]"; - std::string playerCount = std::to_string(room.playerCount) + "/" + std::to_string(room.maxPlayers); - - std::string roomName(room.name); - if (roomName.length() > 16) { - roomName = roomName.substr(0, 16); - } - while (roomName.length() < 16) { - roomName += " "; - } - - std::string text = prefix + roomName + " " + playerCount + " " + status; - - TextLabel roomLabel(text, m_fontMedium, 18); - - if (static_cast(i) == m_selectedIndex) { - if (room.inGame) { - roomLabel.color = {0.5f, 0.5f, 0.5f, 1.0f}; - } else { - roomLabel.color = {1.0f, 0.08f, 0.58f, 1.0f}; - } - } else { - if (room.inGame) { - roomLabel.color = {0.4f, 0.4f, 0.4f, 0.8f}; - } else { - roomLabel.color = {0.8f, 0.8f, 0.8f, 1.0f}; - } - } - - roomLabel.centered = true; - m_registry.AddComponent(roomEntity, std::move(roomLabel)); - } - } - - void RoomListState::handleCreateRoom() { - std::string roomName = m_context.playerName + "'s Room"; - std::cout << "[RoomListState] Creating room: " << roomName << std::endl; - m_roomClient->createRoom(roomName); - } - - void RoomListState::handleJoinRoom() { - if (m_rooms.empty() || m_selectedIndex < 0 || m_selectedIndex >= static_cast(m_rooms.size())) { - return; - } - - const auto& room = m_rooms[m_selectedIndex]; - - if (room.inGame) { - m_hasError = true; - m_errorMessage = "Cannot join - game in progress"; - m_errorTimer = 2.0f; - return; - } - - if (room.playerCount >= room.maxPlayers) { - m_hasError = true; - m_errorMessage = "Cannot join - room is full"; - m_errorTimer = 2.0f; - return; - } - - std::cout << "[RoomListState] Joining room " << room.id << " (" << room.name << ")" << std::endl; - m_roomClient->joinRoom(room.id); - } - - void RoomListState::HandleInput() { - if (m_renderer->IsKeyPressed(Renderer::Key::Up) && !m_upKeyPressed) { - m_upKeyPressed = true; - if (!m_rooms.empty()) { - m_selectedIndex = (m_selectedIndex - 1 + static_cast(m_rooms.size())) % static_cast(m_rooms.size()); - createRoomEntities(); - } - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Up)) { - m_upKeyPressed = false; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Down) && !m_downKeyPressed) { - m_downKeyPressed = true; - if (!m_rooms.empty()) { - m_selectedIndex = (m_selectedIndex + 1) % static_cast(m_rooms.size()); - createRoomEntities(); - } - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Down)) { - m_downKeyPressed = false; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Enter) && !m_enterKeyPressed) { - m_enterKeyPressed = true; - handleJoinRoom(); - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Enter)) { - m_enterKeyPressed = false; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::C) && !m_cKeyPressed) { - m_cKeyPressed = true; - handleCreateRoom(); - } else if (!m_renderer->IsKeyPressed(Renderer::Key::C)) { - m_cKeyPressed = false; - } - - if (m_renderer->IsKeyPressed(Renderer::Key::Escape) && !m_escapeKeyPressed) { - m_escapeKeyPressed = true; - std::cout << "[RoomListState] Returning to menu..." << std::endl; - m_machine.PopState(); - } else if (!m_renderer->IsKeyPressed(Renderer::Key::Escape)) { - m_escapeKeyPressed = false; - } - } - - void RoomListState::Update(float dt) { - if (m_roomClient) { - m_roomClient->update(); - } - - m_refreshTimer += dt; - if (m_refreshTimer >= REFRESH_INTERVAL) { - m_refreshTimer = 0.0f; - if (m_roomClient && m_roomClient->isConnected()) { - m_roomClient->requestRooms(); - } - } - - if (m_hasError) { - m_errorTimer -= dt; - if (m_errorTimer <= 0.0f) { - m_hasError = false; - m_errorMessage.clear(); - - if (m_errorEntity != NULL_ENTITY && m_registry.IsEntityAlive(m_errorEntity)) { - m_registry.DestroyEntity(m_errorEntity); - m_errorEntity = NULL_ENTITY; - } - } - } - - if (m_hasError && m_errorEntity == NULL_ENTITY) { - m_errorEntity = m_registry.CreateEntity(); - m_registry.AddComponent(m_errorEntity, Position{640.0f, 620.0f}); - TextLabel errorLabel(m_errorMessage, m_fontSmall, 14); - errorLabel.color = {1.0f, 0.3f, 0.3f, 1.0f}; // Red - errorLabel.centered = true; - m_registry.AddComponent(m_errorEntity, std::move(errorLabel)); - } - } - - void RoomListState::Draw() { - m_renderer->Clear({0.05f, 0.05f, 0.1f, 1.0f}); - m_renderingSystem->Update(m_registry, 0.0f); - m_textSystem->Update(m_registry, 0.0f); - } - - } -} From 3712617cb4429776997448cf3ac12b8a8077c039 Mon Sep 17 00:00:00 2001 From: erwan Date: Mon, 2 Feb 2026 00:59:45 +0100 Subject: [PATCH 7/7] feat: implemented new utils in the engine --- CMakeLists.txt | 11 ++ engine/include/Core/Engine.hpp | 15 ++ engine/include/Core/EventBus.hpp | 152 +++++++++++++++++++++ engine/include/Core/InputManager.hpp | 65 +++++++++ engine/include/Core/ResourceManager.hpp | 133 ++++++++++++++++++ engine/include/Core/SceneManager.hpp | 81 +++++++++++ engine/include/ECS/Camera2DSystem.hpp | 82 +++++++++++ engine/include/ECS/Components/Camera2D.hpp | 64 +++++++++ engine/include/ECS/Registry.hpp | 8 ++ engine/include/Renderer/IRenderer.hpp | 12 +- engine/include/Renderer/SFMLRenderer.hpp | 1 + engine/src/Core/CMakeLists.txt | 2 + engine/src/Core/Engine.cpp | 10 +- engine/src/Core/InputManager.cpp | 124 +++++++++++++++++ engine/src/Core/SceneManager.cpp | 109 +++++++++++++++ engine/src/ECS/CMakeLists.txt | 3 + engine/src/ECS/Camera2DSystem.cpp | 135 ++++++++++++++++++ engine/src/ECS/MenuSystem.cpp | 2 +- engine/src/ECS/Registry.cpp | 10 ++ engine/src/Renderer/SFMLRenderer.cpp | 8 ++ games/rtype/client/src/EditorState.cpp | 2 +- tests/test_camera_system.cpp | 108 +++++++++++++++ tests/test_event_bus.cpp | 69 ++++++++++ tests/test_scene_manager.cpp | 47 +++++++ 24 files changed, 1245 insertions(+), 8 deletions(-) create mode 100644 engine/include/Core/EventBus.hpp create mode 100644 engine/include/Core/InputManager.hpp create mode 100644 engine/include/Core/ResourceManager.hpp create mode 100644 engine/include/Core/SceneManager.hpp create mode 100644 engine/include/ECS/Camera2DSystem.hpp create mode 100644 engine/include/ECS/Components/Camera2D.hpp create mode 100644 engine/src/Core/InputManager.cpp create mode 100644 engine/src/Core/SceneManager.cpp create mode 100644 engine/src/ECS/Camera2DSystem.cpp create mode 100644 tests/test_camera_system.cpp create mode 100644 tests/test_event_bus.cpp create mode 100644 tests/test_scene_manager.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2414740..df78572 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -117,6 +117,8 @@ endif() # ============================================================================= # Tests # ============================================================================= +enable_testing() + add_executable(test_lobby tests/test_lobby.cpp) target_link_libraries(test_lobby PRIVATE rtype_network rtype_asio_network) @@ -130,6 +132,15 @@ message(STATUS "test_udp_protocol will be built (UDP game protocol test)") add_executable(test_tcp_framing tests/test_tcp_framing.cpp) target_link_libraries(test_tcp_framing PRIVATE rtype_asio_network) +add_executable(test_event_bus tests/test_event_bus.cpp) +target_link_libraries(test_event_bus PRIVATE rtype_core) + +add_executable(test_scene_manager tests/test_scene_manager.cpp) +target_link_libraries(test_scene_manager PRIVATE rtype_core rtype_ecs_core) + +add_executable(test_camera_system tests/test_camera_system.cpp) +target_link_libraries(test_camera_system PRIVATE rtype_core rtype_ecs_core) + if(TARGET rtype_sfml_renderer) add_executable(test_renderer tests/test_renderer.cpp) target_link_libraries(test_renderer PRIVATE diff --git a/engine/include/Core/Engine.hpp b/engine/include/Core/Engine.hpp index e01682f..b3e87be 100644 --- a/engine/include/Core/Engine.hpp +++ b/engine/include/Core/Engine.hpp @@ -3,6 +3,10 @@ #include "Module.hpp" #include "ModuleLoader.hpp" #include "Logger.hpp" +#include "SceneManager.hpp" +#include "ResourceManager.hpp" +#include "InputManager.hpp" +#include "EventBus.hpp" #include "../ECS/Registry.hpp" #include "../ECS/ISystem.hpp" #include @@ -48,6 +52,11 @@ namespace RType { template void RegisterSystem(std::unique_ptr system); void UpdateSystems(float deltaTime); + SceneManager& GetSceneManager() { return m_sceneManager; } + ResourceManager& GetResourceManager() { return m_resourceManager; } + InputManager& GetInputManager() { return m_inputManager; } + EventBus& GetEventBus() { return m_eventBus; } + private: void SortModulesByPriority(); bool InitializeModules(); @@ -61,6 +70,12 @@ namespace RType { std::vector m_sortedModules; ECS::Registry m_registry; std::vector> m_systems; + + SceneManager m_sceneManager; + ResourceManager m_resourceManager; + InputManager m_inputManager; + EventBus m_eventBus; + bool m_initialized{false}; }; diff --git a/engine/include/Core/EventBus.hpp b/engine/include/Core/EventBus.hpp new file mode 100644 index 0000000..6e7a13a --- /dev/null +++ b/engine/include/Core/EventBus.hpp @@ -0,0 +1,152 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type Engine +** File description: +** EventBus - Simple publish-subscribe event system for decoupled communication +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace RType { + + namespace Core { + + + class EventBus { + public: + EventBus() = default; + ~EventBus() = default; + + // Non-copyable but movable + EventBus(const EventBus&) = delete; + EventBus& operator=(const EventBus&) = delete; + EventBus(EventBus&&) = default; + EventBus& operator=(EventBus&&) = default; + + + template + size_t Subscribe(std::function handler) { + std::type_index typeId = std::type_index(typeid(EventT)); + + auto wrapper = [handler](const std::any& event) { + handler(std::any_cast(event)); + }; + + size_t id = m_nextId++; + m_handlers[typeId].push_back({id, std::move(wrapper)}); + return id; + } + + + template + void Unsubscribe(size_t subscriptionId) { + std::type_index typeId = std::type_index(typeid(EventT)); + + auto it = m_handlers.find(typeId); + if (it != m_handlers.end()) { + auto& handlers = it->second; + handlers.erase( + std::remove_if(handlers.begin(), handlers.end(), + [subscriptionId](const HandlerEntry& entry) { + return entry.id == subscriptionId; + }), + handlers.end() + ); + } + } + + + template + void Publish(const EventT& event) { + std::type_index typeId = std::type_index(typeid(EventT)); + + auto it = m_handlers.find(typeId); + if (it != m_handlers.end()) { + for (const auto& entry : it->second) { + entry.handler(event); + } + } + } + + + template + void ClearSubscribers() { + std::type_index typeId = std::type_index(typeid(EventT)); + m_handlers.erase(typeId); + } + + + void Clear() { + m_handlers.clear(); + } + + + template + size_t GetSubscriberCount() const { + std::type_index typeId = std::type_index(typeid(EventT)); + + auto it = m_handlers.find(typeId); + if (it != m_handlers.end()) { + return it->second.size(); + } + return 0; + } + + private: + struct HandlerEntry { + size_t id; + std::function handler; + }; + + std::unordered_map> m_handlers; + size_t m_nextId = 0; + }; + + + + namespace Events { + + /// Fired when an entity is created + struct EntityCreated { + uint32_t entity; + }; + + /// Fired when an entity is destroyed + struct EntityDestroyed { + uint32_t entity; + }; + + /// Fired when a collision occurs between two entities + struct Collision { + uint32_t entityA; + uint32_t entityB; + }; + + /// Fired when a scene is loaded + struct SceneLoaded { + std::string sceneName; + }; + + /// Fired when a scene is unloaded + struct SceneUnloaded { + std::string sceneName; + }; + + /// Fired when input action is triggered + struct ActionTriggered { + std::string action; + bool pressed; // true = pressed, false = released + }; + + } + + } + +} diff --git a/engine/include/Core/InputManager.hpp b/engine/include/Core/InputManager.hpp new file mode 100644 index 0000000..ab4adbf --- /dev/null +++ b/engine/include/Core/InputManager.hpp @@ -0,0 +1,65 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type Engine +** File description: +** InputManager - Action and axis-based input abstraction +*/ + +#pragma once + +#include "../Renderer/IRenderer.hpp" +#include +#include +#include + +namespace RType { + + namespace Core { + + class InputManager { + public: + InputManager() = default; + ~InputManager() = default; + + // Non-copyable + InputManager(const InputManager&) = delete; + InputManager& operator=(const InputManager&) = delete; + + void bindAction(const std::string& action, Renderer::Key key); + void bindActionMouse(const std::string& action, Renderer::MouseButton button); + void unbindAction(const std::string& action); + bool isActionPressed(const std::string& action) const; + bool isActionJustPressed(const std::string& action) const; + bool isActionJustReleased(const std::string& action) const; + void bindAxis(const std::string& axis, Renderer::Key negative, Renderer::Key positive); + void unbindAxis(const std::string& axis); + float getAxisValue(const std::string& axis) const; + + void update(Renderer::IRenderer* renderer); + + void clearBindings(); + + void loadDefaults(); + + private: + struct ActionBinding { + Renderer::Key key = Renderer::Key::Unknown; + Renderer::MouseButton mouseButton = Renderer::MouseButton::Left; + bool isMouseBinding = false; + bool currentlyPressed = false; + bool wasPressed = false; + }; + + struct AxisBinding { + Renderer::Key negative = Renderer::Key::Unknown; + Renderer::Key positive = Renderer::Key::Unknown; + }; + + std::unordered_map m_actions; + std::unordered_map m_axes; + Renderer::IRenderer* m_renderer = nullptr; + }; + + } + +} diff --git a/engine/include/Core/ResourceManager.hpp b/engine/include/Core/ResourceManager.hpp new file mode 100644 index 0000000..02d8c28 --- /dev/null +++ b/engine/include/Core/ResourceManager.hpp @@ -0,0 +1,133 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type Engine +** File description: +** ResourceManager - Centralized asset loading with caching +*/ + +#pragma once + +#include +#include +#include +#include +#include + +namespace RType { + + namespace Core { + + + class ResourceManager { + public: + ResourceManager() = default; + ~ResourceManager() = default; + + // Non-copyable + ResourceManager(const ResourceManager&) = delete; + ResourceManager& operator=(const ResourceManager&) = delete; + + + template + void RegisterLoader(std::function(const std::string&)> loader) { + std::type_index typeId = std::type_index(typeid(T)); + m_loaders[typeId] = [loader](const std::string& path) -> std::shared_ptr { + return loader(path); + }; + } + + + template + std::shared_ptr Load(const std::string& path) { + std::string cacheKey = GetCacheKey(path); + + // Check cache first + auto it = m_cache.find(cacheKey); + if (it != m_cache.end()) { + auto locked = it->second.lock(); + if (locked) { + return std::static_pointer_cast(locked); + } + // Expired, remove from cache + m_cache.erase(it); + } + + // Load using registered loader + std::type_index typeId = std::type_index(typeid(T)); + auto loaderIt = m_loaders.find(typeId); + if (loaderIt == m_loaders.end()) { + return nullptr; // No loader registered + } + + auto resource = loaderIt->second(path); + if (resource) { + m_cache[cacheKey] = resource; + return std::static_pointer_cast(resource); + } + return nullptr; + } + + + template + void Preload(const std::vector& paths) { + for (const auto& path : paths) { + Load(path); + } + } + + + template + bool IsCached(const std::string& path) const { + std::string cacheKey = GetCacheKey(path); + auto it = m_cache.find(cacheKey); + if (it != m_cache.end()) { + return !it->second.expired(); + } + return false; + } + + + void CleanupExpired() { + for (auto it = m_cache.begin(); it != m_cache.end(); ) { + if (it->second.expired()) { + it = m_cache.erase(it); + } else { + ++it; + } + } + } + + + void Clear() { + m_cache.clear(); + } + + + size_t GetCacheSize() const { + return m_cache.size(); + } + + + size_t GetActiveCacheCount() const { + size_t count = 0; + for (const auto& [key, weakPtr] : m_cache) { + if (!weakPtr.expired()) { + count++; + } + } + return count; + } + + private: + template + std::string GetCacheKey(const std::string& path) const { + return std::string(typeid(T).name()) + ":" + path; + } + + std::unordered_map> m_cache; + std::unordered_map(const std::string&)>> m_loaders; + }; + + } + +} diff --git a/engine/include/Core/SceneManager.hpp b/engine/include/Core/SceneManager.hpp new file mode 100644 index 0000000..ff8b8db --- /dev/null +++ b/engine/include/Core/SceneManager.hpp @@ -0,0 +1,81 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type Engine +** File description: +** SceneManager - Basic scene lifecycle management +*/ + +#pragma once + +#include "../ECS/Registry.hpp" +#include +#include +#include + +namespace RType { + + namespace Core { + + + class SceneManager { + public: + using SceneSetupFn = std::function; + using SceneTeardownFn = std::function; + + explicit SceneManager(ECS::Registry* registry = nullptr); + ~SceneManager() = default; + + // Non-copyable + SceneManager(const SceneManager&) = delete; + SceneManager& operator=(const SceneManager&) = delete; + + + void SetRegistry(ECS::Registry* registry) { m_registry = registry; } + + + void RegisterScene(const std::string& sceneName, + SceneSetupFn setup, + SceneTeardownFn teardown = nullptr); + + + bool LoadScene(const std::string& sceneName); + + + bool LoadScene(const std::string& sceneName, SceneSetupFn setup); + + + void UnloadCurrentScene(); + + + bool TransitionTo(const std::string& sceneName); + + + const std::string& GetCurrentSceneName() const { return m_currentScene; } + + + bool HasActiveScene() const { return !m_currentScene.empty(); } + + + bool IsSceneRegistered(const std::string& sceneName) const; + + + void UnregisterScene(const std::string& sceneName); + + + void ClearScenes(); + + private: + struct SceneData { + SceneSetupFn setup; + SceneTeardownFn teardown; + }; + + ECS::Registry* m_registry = nullptr; + std::string m_currentScene; + SceneTeardownFn m_currentTeardown; + std::unordered_map m_scenes; + }; + + } + +} diff --git a/engine/include/ECS/Camera2DSystem.hpp b/engine/include/ECS/Camera2DSystem.hpp new file mode 100644 index 0000000..fec902e --- /dev/null +++ b/engine/include/ECS/Camera2DSystem.hpp @@ -0,0 +1,82 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type Engine +** File description: +** Camera2DSystem - Updates camera position based on target following +*/ + +#pragma once + +#include "ISystem.hpp" +#include "../Renderer/IRenderer.hpp" +#include "../Math/Types.hpp" + +namespace RType { + + namespace ECS { + + /** + * @brief System that updates Camera2D components + * + * This system handles: + * - Smooth target following with configurable speed + * - World bounds clamping + * - Screen shake effects + * - Viewport updates on renderer + * + * The system should be registered early to ensure camera position + * is updated before rendering systems use it. + */ + class Camera2DSystem : public ISystem { + public: + explicit Camera2DSystem(Renderer::IRenderer* renderer = nullptr); + ~Camera2DSystem() override = default; + + void Update(Registry& registry, float deltaTime) override; + const char* GetName() const override { return "Camera2DSystem"; } + + /** + * @brief Set the renderer for viewport updates + * @param renderer The renderer to update + */ + void SetRenderer(Renderer::IRenderer* renderer) { m_renderer = renderer; } + + /** + * @brief Get the current active camera position + * @return Camera position in world coordinates + */ + Math::Vector2 GetCameraPosition() const { return m_cameraPosition; } + + /** + * @brief Convert screen coordinates to world coordinates + * @param screenPos Position in screen space + * @return Position in world space + */ + Math::Vector2 ScreenToWorld(const Math::Vector2& screenPos) const; + + /** + * @brief Convert world coordinates to screen coordinates + * @param worldPos Position in world space + * @return Position in screen space + */ + Math::Vector2 WorldToScreen(const Math::Vector2& worldPos) const; + + /** + * @brief Get the current camera zoom + */ + float GetZoom() const { return m_zoom; } + + private: + Renderer::IRenderer* m_renderer = nullptr; + Math::Vector2 m_cameraPosition{0.0f, 0.0f}; + Math::Vector2 m_screenSize{1920.0f, 1080.0f}; + float m_zoom = 1.0f; + + Math::Vector2 Lerp(const Math::Vector2& a, const Math::Vector2& b, float t) const; + Math::Vector2 Clamp(const Math::Vector2& value, const Math::Vector2& min, const Math::Vector2& max) const; + Math::Vector2 GetShakeOffset(float intensity) const; + }; + + } + +} diff --git a/engine/include/ECS/Components/Camera2D.hpp b/engine/include/ECS/Components/Camera2D.hpp new file mode 100644 index 0000000..867cc60 --- /dev/null +++ b/engine/include/ECS/Components/Camera2D.hpp @@ -0,0 +1,64 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type Engine +** File description: +** Camera2D - 2D camera component for target following +*/ + +#pragma once + +#include "IComponent.hpp" +#include "../Entity.hpp" +#include "../../Math/Types.hpp" + +namespace RType { + + namespace ECS { + + + struct Camera2D : public IComponent { + /// Entity to follow (NULL_ENTITY for static camera) + Entity target = NULL_ENTITY; + + /// Offset from target position + Math::Vector2 offset{0.0f, 0.0f}; + + /// Current camera position (updated by Camera2DSystem) + Math::Vector2 position{0.0f, 0.0f}; + + /// World bounds for camera clamping (0 = no bounds) + Math::Vector2 boundsMin{0.0f, 0.0f}; + Math::Vector2 boundsMax{0.0f, 0.0f}; + + /// Smooth interpolation speed (higher = snappier, 0 = instant) + float smoothSpeed = 5.0f; + + /// Camera zoom level (1.0 = default, 2.0 = 2x zoom in) + float zoom = 1.0f; + + /// Whether this camera is active + bool active = true; + + /// Screen shake intensity (0 = no shake) + float shakeIntensity = 0.0f; + + /// Screen shake duration remaining + float shakeDuration = 0.0f; + + Camera2D() = default; + + + void Shake(float intensity, float duration) { + shakeIntensity = intensity; + shakeDuration = duration; + } + + + bool HasBounds() const { + return boundsMax.x > boundsMin.x || boundsMax.y > boundsMin.y; + } + }; + + } + +} diff --git a/engine/include/ECS/Registry.hpp b/engine/include/ECS/Registry.hpp index 3f0e4e6..e4451df 100644 --- a/engine/include/ECS/Registry.hpp +++ b/engine/include/ECS/Registry.hpp @@ -22,6 +22,7 @@ namespace RType { virtual ~IComponentPool() = default; virtual bool Has(Entity entity) const = 0; virtual void Remove(Entity entity) = 0; + virtual void Clear() = 0; }; template @@ -32,6 +33,7 @@ namespace RType { const T& Get(Entity entity) const; bool Has(Entity entity) const override; void Remove(Entity entity) override; + void Clear() override; std::vector GetEntities() const; private: SparseArray m_components; @@ -64,6 +66,7 @@ namespace RType { template std::vector GetEntitiesWithComponent() const; size_t GetEntityCount() const { return m_entityCount; } + void Clear(); private: template ComponentPool* GetOrCreatePool(); @@ -119,6 +122,11 @@ namespace RType { m_components.erase(entity); } + template + void ComponentPool::Clear() { + m_components.clear(); + } + template std::vector ComponentPool::GetEntities() const { std::vector entities; diff --git a/engine/include/Renderer/IRenderer.hpp b/engine/include/Renderer/IRenderer.hpp index 6012dfb..e30759e 100644 --- a/engine/include/Renderer/IRenderer.hpp +++ b/engine/include/Renderer/IRenderer.hpp @@ -118,6 +118,12 @@ namespace Renderer { RAlt }; + enum class MouseButton { + Left, + Right, + Middle + }; + class IRenderer : public RType::Core::IModule { public: ~IRenderer() override = default; @@ -131,6 +137,7 @@ namespace Renderer { virtual bool CreateWindow(const WindowConfig& config) = 0; virtual void Destroy() = 0; virtual bool IsWindowOpen() const = 0; + virtual Vector2 GetWindowSize() const = 0; virtual void Resize(std::uint32_t width, std::uint32_t height) = 0; virtual void SetWindowTitle(const std::string& title) = 0; @@ -166,11 +173,6 @@ namespace Renderer { virtual bool IsKeyPressed(Key key) const = 0; - enum class MouseButton { - Left, - Right, - Middle - }; virtual bool IsMouseButtonPressed(MouseButton button) const = 0; virtual Vector2 GetMousePosition() const = 0; }; diff --git a/engine/include/Renderer/SFMLRenderer.hpp b/engine/include/Renderer/SFMLRenderer.hpp index 3d8edd1..f95e1f0 100644 --- a/engine/include/Renderer/SFMLRenderer.hpp +++ b/engine/include/Renderer/SFMLRenderer.hpp @@ -22,6 +22,7 @@ namespace Renderer { bool CreateWindow(const WindowConfig& config) override; void Destroy() override; bool IsWindowOpen() const override; + Vector2 GetWindowSize() const override; void Resize(std::uint32_t width, std::uint32_t height) override; void SetWindowTitle(const std::string& title) override; diff --git a/engine/src/Core/CMakeLists.txt b/engine/src/Core/CMakeLists.txt index da61b84..ac0db2b 100644 --- a/engine/src/Core/CMakeLists.txt +++ b/engine/src/Core/CMakeLists.txt @@ -3,6 +3,8 @@ set(CORE_SOURCES ModuleLoader.cpp ColorFilter.cpp InputMapping.cpp + InputManager.cpp + SceneManager.cpp ) set(CORE_HEADERS diff --git a/engine/src/Core/Engine.cpp b/engine/src/Core/Engine.cpp index 9c91b1d..6c5c0ea 100644 --- a/engine/src/Core/Engine.cpp +++ b/engine/src/Core/Engine.cpp @@ -1,4 +1,5 @@ #include "../../include/Core/Engine.hpp" +#include "../../include/Renderer/IRenderer.hpp" #include namespace RType { @@ -6,8 +7,9 @@ namespace RType { namespace Core { Engine::Engine(const EngineConfig& config) - : m_config(config) { + : m_config(config), m_sceneManager(&m_registry) { Logger::Info("Creating R-Type Engine"); + m_inputManager.loadDefaults(); } Engine::~Engine() { @@ -140,6 +142,12 @@ namespace RType { } void Engine::UpdateSystems(float deltaTime) { + // Update input manager first + auto renderer = GetModule(); + if (renderer) { + m_inputManager.update(renderer); + } + for (auto& system : m_systems) { system->Update(m_registry, deltaTime); } diff --git a/engine/src/Core/InputManager.cpp b/engine/src/Core/InputManager.cpp new file mode 100644 index 0000000..1e3ffe8 --- /dev/null +++ b/engine/src/Core/InputManager.cpp @@ -0,0 +1,124 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type Engine +** File description: +** InputManager implementation +*/ + +#include "Core/InputManager.hpp" + +namespace RType { + + namespace Core { + + void InputManager::bindAction(const std::string& action, Renderer::Key key) { + ActionBinding binding; + binding.key = key; + binding.isMouseBinding = false; + m_actions[action] = binding; + } + + void InputManager::bindActionMouse(const std::string& action, Renderer::MouseButton button) { + ActionBinding binding; + binding.mouseButton = button; + binding.isMouseBinding = true; + m_actions[action] = binding; + } + + void InputManager::unbindAction(const std::string& action) { + m_actions.erase(action); + } + + bool InputManager::isActionPressed(const std::string& action) const { + auto it = m_actions.find(action); + if (it == m_actions.end()) { + return false; + } + return it->second.currentlyPressed; + } + + bool InputManager::isActionJustPressed(const std::string& action) const { + auto it = m_actions.find(action); + if (it == m_actions.end()) { + return false; + } + return it->second.currentlyPressed && !it->second.wasPressed; + } + + bool InputManager::isActionJustReleased(const std::string& action) const { + auto it = m_actions.find(action); + if (it == m_actions.end()) { + return false; + } + return !it->second.currentlyPressed && it->second.wasPressed; + } + + void InputManager::bindAxis(const std::string& axis, Renderer::Key negative, Renderer::Key positive) { + AxisBinding binding; + binding.negative = negative; + binding.positive = positive; + m_axes[axis] = binding; + } + + void InputManager::unbindAxis(const std::string& axis) { + m_axes.erase(axis); + } + + float InputManager::getAxisValue(const std::string& axis) const { + auto it = m_axes.find(axis); + if (it == m_axes.end() || m_renderer == nullptr) { + return 0.0F; + } + + float value = 0.0F; + if (m_renderer->IsKeyPressed(it->second.negative)) { + value -= 1.0F; + } + if (m_renderer->IsKeyPressed(it->second.positive)) { + value += 1.0F; + } + return value; + } + + void InputManager::update(Renderer::IRenderer* renderer) { + m_renderer = renderer; + if (m_renderer == nullptr) { + return; + } + + for (auto& [name, binding] : m_actions) { + binding.wasPressed = binding.currentlyPressed; + + if (binding.isMouseBinding) { + binding.currentlyPressed = m_renderer->IsMouseButtonPressed(binding.mouseButton); + } else { + binding.currentlyPressed = m_renderer->IsKeyPressed(binding.key); + } + } + } + + void InputManager::clearBindings() { + m_actions.clear(); + m_axes.clear(); + } + + void InputManager::loadDefaults() { + // Movement + bindAction("MoveLeft", Renderer::Key::A); + bindAction("MoveRight", Renderer::Key::D); + bindAction("MoveUp", Renderer::Key::W); + bindAction("MoveDown", Renderer::Key::S); + + // Movement axes + bindAxis("MoveX", Renderer::Key::A, Renderer::Key::D); + bindAxis("MoveY", Renderer::Key::S, Renderer::Key::W); // S=up (positive Y), W=down + + // Actions + bindAction("Jump", Renderer::Key::Space); + bindActionMouse("Shoot", Renderer::MouseButton::Left); + bindAction("Pause", Renderer::Key::Escape); + } + + } + +} diff --git a/engine/src/Core/SceneManager.cpp b/engine/src/Core/SceneManager.cpp new file mode 100644 index 0000000..3dc4e00 --- /dev/null +++ b/engine/src/Core/SceneManager.cpp @@ -0,0 +1,109 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type Engine +** File description: +** SceneManager implementation +*/ + +#include "Core/SceneManager.hpp" +#include "Core/Logger.hpp" + +namespace RType { + + namespace Core { + + SceneManager::SceneManager(ECS::Registry* registry) + : m_registry(registry) {} + + void SceneManager::RegisterScene(const std::string& sceneName, + SceneSetupFn setup, + SceneTeardownFn teardown) { + if (!setup) { + Logger::Warning("SceneManager: Cannot register scene '{}' with null setup function", sceneName); + return; + } + + m_scenes[sceneName] = SceneData{std::move(setup), std::move(teardown)}; + Logger::Info("SceneManager: Registered scene '{}'", sceneName); + } + + bool SceneManager::LoadScene(const std::string& sceneName) { + auto it = m_scenes.find(sceneName); + if (it == m_scenes.end()) { + Logger::Error("SceneManager: Scene '{}' not registered", sceneName); + return false; + } + + return LoadScene(sceneName, it->second.setup); + } + + bool SceneManager::LoadScene(const std::string& sceneName, SceneSetupFn setup) { + if (!m_registry) { + Logger::Error("SceneManager: No registry set"); + return false; + } + + if (!setup) { + Logger::Error("SceneManager: Setup function is null"); + return false; + } + + // Unload current scene first + UnloadCurrentScene(); + + // Store teardown for later if scene is registered + auto it = m_scenes.find(sceneName); + if (it != m_scenes.end()) { + m_currentTeardown = it->second.teardown; + } else { + m_currentTeardown = nullptr; + } + + // Set current scene + m_currentScene = sceneName; + + // Call setup function + Logger::Info("SceneManager: Loading scene '{}'", sceneName); + setup(*m_registry); + + return true; + } + + void SceneManager::UnloadCurrentScene() { + if (m_currentScene.empty() || !m_registry) { + return; + } + + Logger::Info("SceneManager: Unloading scene '{}'", m_currentScene); + + // Call teardown if registered + if (m_currentTeardown) { + m_currentTeardown(*m_registry); + } + + // Clear the registry (destroy all entities) + m_registry->Clear(); + + m_currentScene.clear(); + m_currentTeardown = nullptr; + } + + bool SceneManager::TransitionTo(const std::string& sceneName) { + return LoadScene(sceneName); + } + + bool SceneManager::IsSceneRegistered(const std::string& sceneName) const { + return m_scenes.find(sceneName) != m_scenes.end(); + } + + void SceneManager::UnregisterScene(const std::string& sceneName) { + m_scenes.erase(sceneName); + } + + void SceneManager::ClearScenes() { + m_scenes.clear(); + } + + } + +} diff --git a/engine/src/ECS/CMakeLists.txt b/engine/src/ECS/CMakeLists.txt index 4cff039..1d63e97 100644 --- a/engine/src/ECS/CMakeLists.txt +++ b/engine/src/ECS/CMakeLists.txt @@ -19,6 +19,7 @@ set(ECS_CORE_SOURCES MenuSystem.cpp HealthSystem.cpp ScoreSystem.cpp + Camera2DSystem.cpp ) set(ECS_CORE_HEADERS @@ -47,6 +48,8 @@ set(ECS_CORE_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/MenuSystem.hpp ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/HealthSystem.hpp ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/ScoreSystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/Camera2DSystem.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../include/ECS/Components/Camera2D.hpp ) add_library(rtype_ecs_core STATIC diff --git a/engine/src/ECS/Camera2DSystem.cpp b/engine/src/ECS/Camera2DSystem.cpp new file mode 100644 index 0000000..30fc6b3 --- /dev/null +++ b/engine/src/ECS/Camera2DSystem.cpp @@ -0,0 +1,135 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type Engine +** File description: +** Camera2DSystem implementation +*/ + +#include "ECS/Camera2DSystem.hpp" +#include "ECS/Registry.hpp" +#include "ECS/Components/Camera2D.hpp" +#include "ECS/Components/Transform.hpp" +#include "Math/Types.hpp" +#include +#include +#include + +namespace RType { + + namespace ECS { + + Camera2DSystem::Camera2DSystem(Renderer::IRenderer* renderer) + : m_renderer(renderer) { + if (m_renderer) { + Math::Vector2 size = m_renderer->GetWindowSize(); + m_screenSize = size; + } + } + + void Camera2DSystem::Update(Registry& registry, float deltaTime) { + auto cameras = registry.GetEntitiesWithComponent(); + + for (Entity cameraEntity : cameras) { + auto& camera = registry.GetComponent(cameraEntity); + + if (!camera.active) { + continue; + } + + // Calculate target position + Math::Vector2 targetPos = camera.position; + + if (camera.target != NULL_ENTITY && registry.HasComponent(camera.target)) { + auto& targetTransform = registry.GetComponent(camera.target); + targetPos.x = targetTransform.x + camera.offset.x; + targetPos.y = targetTransform.y + camera.offset.y; + } + + // Smooth interpolation + if (camera.smoothSpeed > 0.0f) { + float t = 1.0f - std::exp(-camera.smoothSpeed * deltaTime); + camera.position = Lerp(camera.position, targetPos, t); + } else { + camera.position = targetPos; + } + + // Apply bounds clamping + if (camera.HasBounds()) { + camera.position = Clamp(camera.position, camera.boundsMin, camera.boundsMax); + } + + // Update screen shake + Math::Vector2 shakeOffset{0.0f, 0.0f}; + if (camera.shakeDuration > 0.0f) { + shakeOffset = GetShakeOffset(camera.shakeIntensity); + camera.shakeDuration -= deltaTime; + if (camera.shakeDuration <= 0.0f) { + camera.shakeIntensity = 0.0f; + } + } + + // Store final camera position (with shake) + m_cameraPosition = camera.position; + m_cameraPosition.x += shakeOffset.x; + m_cameraPosition.y += shakeOffset.y; + m_zoom = camera.zoom; + + // Update renderer viewport if available + if (m_renderer) { + m_screenSize = m_renderer->GetWindowSize(); + + Renderer::Camera2D renderCam; + renderCam.center = m_cameraPosition; + renderCam.size.x = m_screenSize.x / m_zoom; + renderCam.size.y = m_screenSize.y / m_zoom; + + m_renderer->SetCamera(renderCam); + } + + // Only process first active camera + break; + } + } + + Math::Vector2 Camera2DSystem::ScreenToWorld(const Math::Vector2& screenPos) const { + Math::Vector2 worldPos; + worldPos.x = (screenPos.x / m_zoom) + m_cameraPosition.x - (m_screenSize.x / (2.0f * m_zoom)); + worldPos.y = (screenPos.y / m_zoom) + m_cameraPosition.y - (m_screenSize.y / (2.0f * m_zoom)); + return worldPos; + } + + Math::Vector2 Camera2DSystem::WorldToScreen(const Math::Vector2& worldPos) const { + Math::Vector2 screenPos; + screenPos.x = (worldPos.x - m_cameraPosition.x + (m_screenSize.x / (2.0f * m_zoom))) * m_zoom; + screenPos.y = (worldPos.y - m_cameraPosition.y + (m_screenSize.y / (2.0f * m_zoom))) * m_zoom; + return screenPos; + } + + Math::Vector2 Camera2DSystem::Lerp(const Math::Vector2& a, const Math::Vector2& b, float t) const { + return Math::Vector2{ + a.x + (b.x - a.x) * t, + a.y + (b.y - a.y) * t + }; + } + + Math::Vector2 Camera2DSystem::Clamp(const Math::Vector2& value, const Math::Vector2& min, const Math::Vector2& max) const { + return Math::Vector2{ + std::clamp(value.x, min.x, max.x), + std::clamp(value.y, min.y, max.y) + }; + } + + Math::Vector2 Camera2DSystem::GetShakeOffset(float intensity) const { + static std::random_device rd; + static std::mt19937 gen(rd()); + std::uniform_real_distribution dist(-1.0f, 1.0f); + + return Math::Vector2{ + dist(gen) * intensity, + dist(gen) * intensity + }; + } + + } + +} diff --git a/engine/src/ECS/MenuSystem.cpp b/engine/src/ECS/MenuSystem.cpp index 9b3d9d1..6bade0a 100644 --- a/engine/src/ECS/MenuSystem.cpp +++ b/engine/src/ECS/MenuSystem.cpp @@ -14,7 +14,7 @@ namespace RType { return; Renderer::Vector2 mousePos = m_renderer->GetMousePosition(); - bool isMouseDown = m_renderer->IsMouseButtonPressed(Renderer::IRenderer::MouseButton::Left); + bool isMouseDown = m_renderer->IsMouseButtonPressed(Renderer::MouseButton::Left); auto entities = registry.GetEntitiesWithComponent(); diff --git a/engine/src/ECS/Registry.cpp b/engine/src/ECS/Registry.cpp index 96525e9..063b111 100644 --- a/engine/src/ECS/Registry.cpp +++ b/engine/src/ECS/Registry.cpp @@ -75,6 +75,16 @@ namespace RType { return m_aliveEntities.find(entity) != m_aliveEntities.end(); } + void Registry::Clear() { + for (auto& [id, pool] : m_componentPools) { + pool->Clear(); + } + m_aliveEntities.clear(); + m_freeEntityIds.clear(); + m_nextEntityID = 1; + m_entityCount = 0; + } + } } diff --git a/engine/src/Renderer/SFMLRenderer.cpp b/engine/src/Renderer/SFMLRenderer.cpp index c7b7fe9..0c53199 100644 --- a/engine/src/Renderer/SFMLRenderer.cpp +++ b/engine/src/Renderer/SFMLRenderer.cpp @@ -103,6 +103,14 @@ namespace Renderer { return m_window && m_window->isOpen(); } + Vector2 SFMLRenderer::GetWindowSize() const { + if (!m_window) { + return {0.0f, 0.0f}; + } + sf::Vector2u size = m_window->getSize(); + return {static_cast(size.x), static_cast(size.y)}; + } + void SFMLRenderer::Resize(std::uint32_t width, std::uint32_t height) { if (!m_window) { RType::Core::Logger::Warning("Cannot resize: window not created"); diff --git a/games/rtype/client/src/EditorState.cpp b/games/rtype/client/src/EditorState.cpp index 8eb5475..39c33c1 100644 --- a/games/rtype/client/src/EditorState.cpp +++ b/games/rtype/client/src/EditorState.cpp @@ -173,7 +173,7 @@ namespace RType { m_canvasManager->HandleCameraInput(); } - bool isLeftPressed = m_renderer->IsMouseButtonPressed(Renderer::IRenderer::MouseButton::Left); + bool isLeftPressed = m_renderer->IsMouseButtonPressed(Renderer::MouseButton::Left); if (isLeftPressed && !m_leftMousePressed) { bool consumedByUI = false; diff --git a/tests/test_camera_system.cpp b/tests/test_camera_system.cpp new file mode 100644 index 0000000..b02d73c --- /dev/null +++ b/tests/test_camera_system.cpp @@ -0,0 +1,108 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** Test Camera2DSystem +*/ + +#include "ECS/Components/Camera2D.hpp" +#include "ECS/Camera2DSystem.hpp" +#include "Renderer/IRenderer.hpp" +#include "ECS/Registry.hpp" +#include "ECS/Component.hpp" +#include +#include + +class MockRenderer : public Renderer::IRenderer { +public: + Renderer::Vector2 GetWindowSize() const override { + return {1280.0f, 720.0f}; + } + + // Minimal override stubs for abstract class (returning default values) + const char* GetName() const override { return "Mock"; } + RType::Core::ModulePriority GetPriority() const override { return RType::Core::ModulePriority::Normal; } + bool Initialize(RType::Core::Engine*) override { return true; } + void Shutdown() override {} + void Update(float) override {} + bool ShouldUpdateInRenderThread() const override { return false; } + bool CreateWindow(const Renderer::WindowConfig&) override { return true; } + void Destroy() override {} + bool IsWindowOpen() const override { return true; } + void Resize(std::uint32_t, std::uint32_t) override {} + void SetWindowTitle(const std::string&) override {} + void BeginFrame() override {} + void EndFrame() override {} + void Clear(const Renderer::Color&) override {} + Renderer::TextureId LoadTexture(const std::string&, const Renderer::TextureConfig&) override { return 0; } + void UnloadTexture(Renderer::TextureId) override {} + Renderer::Vector2 GetTextureSize(Renderer::TextureId) const override { return {0,0}; } + Renderer::SpriteId CreateSprite(Renderer::TextureId, const Renderer::Rectangle&) override { return 0; } + void DestroySprite(Renderer::SpriteId) override {} + void SetSpriteRegion(Renderer::SpriteId, const Renderer::Rectangle&) override {} + void DrawSprite(Renderer::SpriteId, const Renderer::Transform2D&, const Renderer::Color&) override {} + void DrawRectangle(const Renderer::Rectangle&, const Renderer::Color&) override {} + Renderer::FontId LoadFont(const std::string&, std::uint32_t) override { return 0; } + void UnloadFont(Renderer::FontId) override {} + void DrawText(Renderer::FontId, const std::string&, const Renderer::TextParams&) override {} + + void SetCamera(const Renderer::Camera2D& cam) override { + lastCamera = cam; + cameraSet = true; + } + void ResetCamera() override { + cameraSet = false; + } + Renderer::RenderStats GetRenderStats() const override { return {}; } + float GetDeltaTime() override { return 0.016f; } + bool IsKeyPressed(Renderer::Key) const override { return false; } + bool IsMouseButtonPressed(Renderer::MouseButton) const override { return false; } + Renderer::Vector2 GetMousePosition() const override { return {0,0}; } + + Renderer::Camera2D lastCamera; + bool cameraSet = false; +}; + +int main() { + MockRenderer renderer; + RType::ECS::Camera2DSystem cameraSystem(&renderer); + RType::ECS::Registry registry; + + // Create camera entity + auto camEntity = registry.CreateEntity(); + auto& cam = registry.AddComponent(camEntity); + cam.active = true; + cam.zoom = 1.0f; + cam.smoothSpeed = 100.0f; // High speed for instant snap test or multiple updates + + // Create target entity + auto targetEntity = registry.CreateEntity(); + registry.AddComponent(targetEntity, RType::ECS::Position{100.0f, 200.0f}); + + // Set target + cam.target = targetEntity; + + // Initial update + cameraSystem.Update(registry, 1.0f); + + // Implementation detail: Camera2DSystem centers camera + // Window size (1280, 720) -> Center is offset (640, 360) + // If target is at (100, 200), Camera should center on it. + // SFML View center should be (100, 200). + // Let's verify what SetCamera received. + + assert(renderer.cameraSet && "SetCamera should be called"); + // Depending on implementation, Camera2DSystem logic might vary. + // Typically: view.setCenter(targetPos + offset) + assert(renderer.lastCamera.center.x == 100.0f); + assert(renderer.lastCamera.center.y == 200.0f); + + // Test screen to world + Math::Vector2 screenPos{640.0f, 360.0f}; // Center of screen + Math::Vector2 worldPos = cameraSystem.ScreenToWorld(screenPos); + // Should be approx 100, 200 + std::cout << "Screen(640,360) -> World(" << worldPos.x << "," << worldPos.y << ")" << std::endl; + + std::cout << "Camera2DSystem tests passed!" << std::endl; + return 0; +} diff --git a/tests/test_event_bus.cpp b/tests/test_event_bus.cpp new file mode 100644 index 0000000..69da887 --- /dev/null +++ b/tests/test_event_bus.cpp @@ -0,0 +1,69 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** Test EventBus +*/ + +#include "Core/EventBus.hpp" +#include +#include + +struct TestEvent { + int value; +}; + +struct AnotherEvent { + std::string message; +}; + +int main() { + RType::Core::EventBus eventBus; + bool handlerCalled = false; + int receivedValue = 0; + + // Test Subscription + eventBus.Subscribe([&](const TestEvent& event) { + handlerCalled = true; + receivedValue = event.value; + }); + + // Test Publish + TestEvent event{42}; + eventBus.Publish(event); + + assert(handlerCalled && "Handler should be called"); + assert(receivedValue == 42 && "Value should be 42"); + + // Test multiple handlers + bool handler2Called = false; + eventBus.Subscribe([&](const TestEvent&) { + handler2Called = true; + }); + + handlerCalled = false; + handler2Called = false; + eventBus.Publish(TestEvent{100}); + + assert(handlerCalled && "Handler 1 should be called"); + assert(handler2Called && "Handler 2 should be called"); + + // Test different event type + bool stringHandlerCalled = false; + eventBus.Subscribe([&](const AnotherEvent& event) { + stringHandlerCalled = true; + assert(event.message == "Hello"); + }); + + eventBus.Publish(AnotherEvent{"Hello"}); + assert(stringHandlerCalled && "String handler should be called"); + + // Test Clear + eventBus.Clear(); + handlerCalled = false; + eventBus.Publish(TestEvent{1}); + assert(!handlerCalled && "Handler should NOT be called after Clear()"); + + std::cout << "EventBus tests passed!" << std::endl; + return 0; +} diff --git a/tests/test_scene_manager.cpp b/tests/test_scene_manager.cpp new file mode 100644 index 0000000..28c4b9b --- /dev/null +++ b/tests/test_scene_manager.cpp @@ -0,0 +1,47 @@ +/* +** EPITECH PROJECT, 2025 +** R-Type +** File description: +** Test SceneManager +*/ + +#include "Core/SceneManager.hpp" +#include "ECS/Registry.hpp" +#include +#include + +int main() { + RType::ECS::Registry registry; + RType::Core::SceneManager sceneManager(®istry); + + bool scene1Loaded = false; + bool scene2Loaded = false; + + // Test LoadScene + sceneManager.LoadScene("Scene1", [&](RType::ECS::Registry& r) { + scene1Loaded = true; + auto e = r.CreateEntity(); // Create one entity + }); + + assert(scene1Loaded && "Scene1 setup should be called"); + assert(sceneManager.GetCurrentSceneName() == "Scene1"); + assert(registry.GetEntityCount() == 1 && "Registry should have 1 entity"); + + // Test Transition (should clear previous scene) + sceneManager.RegisterScene("Scene2", [&](RType::ECS::Registry& r) { + scene2Loaded = true; + // Verify registry was cleared (Entity count reset? ID reset?) + // Note: Registry::Clear resets ID to 1. + auto e = r.CreateEntity(); + }); + sceneManager.TransitionTo("Scene2"); + + assert(scene2Loaded && "Scene2 setup should be called"); + assert(sceneManager.GetCurrentSceneName() == "Scene2"); + // Implementation detail: SceneManager::TransitionTo calls UnloadCurrentScene (which clears registry) then LoadScene. + // So entity count should be 1 (from Scene2), not 2. + assert(registry.GetEntityCount() == 1 && "Registry should be cleared and have 1 entity from Scene2"); + + std::cout << "SceneManager tests passed!" << std::endl; + return 0; +}