diff --git a/assets/imgs/dm_cooldown.png b/assets/imgs/dm_cooldown.png new file mode 100644 index 00000000..4ccb4c0f Binary files /dev/null and b/assets/imgs/dm_cooldown.png differ diff --git a/assets/imgs/dm_cooldown/dm_cooldown_1.png b/assets/imgs/dm_cooldown/dm_cooldown_1.png new file mode 100644 index 00000000..7b5262ac Binary files /dev/null and b/assets/imgs/dm_cooldown/dm_cooldown_1.png differ diff --git a/assets/imgs/dm_cooldown/dm_cooldown_10.png b/assets/imgs/dm_cooldown/dm_cooldown_10.png new file mode 100644 index 00000000..4668e125 Binary files /dev/null and b/assets/imgs/dm_cooldown/dm_cooldown_10.png differ diff --git a/assets/imgs/dm_cooldown/dm_cooldown_2.png b/assets/imgs/dm_cooldown/dm_cooldown_2.png new file mode 100644 index 00000000..b1965838 Binary files /dev/null and b/assets/imgs/dm_cooldown/dm_cooldown_2.png differ diff --git a/assets/imgs/dm_cooldown/dm_cooldown_3.png b/assets/imgs/dm_cooldown/dm_cooldown_3.png new file mode 100644 index 00000000..72283620 Binary files /dev/null and b/assets/imgs/dm_cooldown/dm_cooldown_3.png differ diff --git a/assets/imgs/dm_cooldown/dm_cooldown_4.png b/assets/imgs/dm_cooldown/dm_cooldown_4.png new file mode 100644 index 00000000..eb35910e Binary files /dev/null and b/assets/imgs/dm_cooldown/dm_cooldown_4.png differ diff --git a/assets/imgs/dm_cooldown/dm_cooldown_5.png b/assets/imgs/dm_cooldown/dm_cooldown_5.png new file mode 100644 index 00000000..47523a4c Binary files /dev/null and b/assets/imgs/dm_cooldown/dm_cooldown_5.png differ diff --git a/assets/imgs/dm_cooldown/dm_cooldown_6.png b/assets/imgs/dm_cooldown/dm_cooldown_6.png new file mode 100644 index 00000000..41268c64 Binary files /dev/null and b/assets/imgs/dm_cooldown/dm_cooldown_6.png differ diff --git a/assets/imgs/dm_cooldown/dm_cooldown_7.png b/assets/imgs/dm_cooldown/dm_cooldown_7.png new file mode 100644 index 00000000..20573450 Binary files /dev/null and b/assets/imgs/dm_cooldown/dm_cooldown_7.png differ diff --git a/assets/imgs/dm_cooldown/dm_cooldown_8.png b/assets/imgs/dm_cooldown/dm_cooldown_8.png new file mode 100644 index 00000000..844547d4 Binary files /dev/null and b/assets/imgs/dm_cooldown/dm_cooldown_8.png differ diff --git a/assets/imgs/dm_cooldown/dm_cooldown_9.png b/assets/imgs/dm_cooldown/dm_cooldown_9.png new file mode 100644 index 00000000..acd4c929 Binary files /dev/null and b/assets/imgs/dm_cooldown/dm_cooldown_9.png differ diff --git a/assets/imgs/dm_cooldown/dm_selected_cooldown_1.png b/assets/imgs/dm_cooldown/dm_selected_cooldown_1.png new file mode 100644 index 00000000..275723bb Binary files /dev/null and b/assets/imgs/dm_cooldown/dm_selected_cooldown_1.png differ diff --git a/assets/imgs/dm_cooldown/dm_selected_cooldown_10.png b/assets/imgs/dm_cooldown/dm_selected_cooldown_10.png new file mode 100644 index 00000000..e327ff3f Binary files /dev/null and b/assets/imgs/dm_cooldown/dm_selected_cooldown_10.png differ diff --git a/assets/imgs/dm_cooldown/dm_selected_cooldown_2.png b/assets/imgs/dm_cooldown/dm_selected_cooldown_2.png new file mode 100644 index 00000000..a4afef18 Binary files /dev/null and b/assets/imgs/dm_cooldown/dm_selected_cooldown_2.png differ diff --git a/assets/imgs/dm_cooldown/dm_selected_cooldown_3.png b/assets/imgs/dm_cooldown/dm_selected_cooldown_3.png new file mode 100644 index 00000000..69463b70 Binary files /dev/null and b/assets/imgs/dm_cooldown/dm_selected_cooldown_3.png differ diff --git a/assets/imgs/dm_cooldown/dm_selected_cooldown_4.png b/assets/imgs/dm_cooldown/dm_selected_cooldown_4.png new file mode 100644 index 00000000..9c552481 Binary files /dev/null and b/assets/imgs/dm_cooldown/dm_selected_cooldown_4.png differ diff --git a/assets/imgs/dm_cooldown/dm_selected_cooldown_5.png b/assets/imgs/dm_cooldown/dm_selected_cooldown_5.png new file mode 100644 index 00000000..ed445947 Binary files /dev/null and b/assets/imgs/dm_cooldown/dm_selected_cooldown_5.png differ diff --git a/assets/imgs/dm_cooldown/dm_selected_cooldown_6.png b/assets/imgs/dm_cooldown/dm_selected_cooldown_6.png new file mode 100644 index 00000000..ff686bec Binary files /dev/null and b/assets/imgs/dm_cooldown/dm_selected_cooldown_6.png differ diff --git a/assets/imgs/dm_cooldown/dm_selected_cooldown_7.png b/assets/imgs/dm_cooldown/dm_selected_cooldown_7.png new file mode 100644 index 00000000..964553a6 Binary files /dev/null and b/assets/imgs/dm_cooldown/dm_selected_cooldown_7.png differ diff --git a/assets/imgs/dm_cooldown/dm_selected_cooldown_8.png b/assets/imgs/dm_cooldown/dm_selected_cooldown_8.png new file mode 100644 index 00000000..262a61fe Binary files /dev/null and b/assets/imgs/dm_cooldown/dm_selected_cooldown_8.png differ diff --git a/assets/imgs/dm_cooldown/dm_selected_cooldown_9.png b/assets/imgs/dm_cooldown/dm_selected_cooldown_9.png new file mode 100644 index 00000000..5e9cba2f Binary files /dev/null and b/assets/imgs/dm_cooldown/dm_selected_cooldown_9.png differ diff --git a/assets/sounds/client_music/maze.mp3 b/assets/sounds/client_music/maze.mp3 deleted file mode 100644 index 4294cff7..00000000 Binary files a/assets/sounds/client_music/maze.mp3 and /dev/null differ diff --git a/assets/sounds/client_music/maze_exploration_dm.flac b/assets/sounds/client_music/maze_exploration_dm.flac new file mode 100644 index 00000000..db8b4e1c Binary files /dev/null and b/assets/sounds/client_music/maze_exploration_dm.flac differ diff --git a/assets/sounds/client_music/maze_exploration_players.flac b/assets/sounds/client_music/maze_exploration_players.flac new file mode 100644 index 00000000..b4da5439 Binary files /dev/null and b/assets/sounds/client_music/maze_exploration_players.flac differ diff --git a/assets/sounds/client_music/menu.mp3 b/assets/sounds/client_music/menu.mp3 new file mode 100644 index 00000000..b7d321fb Binary files /dev/null and b/assets/sounds/client_music/menu.mp3 differ diff --git a/assets/sounds/client_music/mono-retrowave.mp3 b/assets/sounds/client_music/mono-retrowave.mp3 deleted file mode 100644 index 3b1fe995..00000000 Binary files a/assets/sounds/client_music/mono-retrowave.mp3 and /dev/null differ diff --git a/assets/sounds/client_music/piano.wav b/assets/sounds/client_music/piano.wav deleted file mode 100644 index 88544b02..00000000 Binary files a/assets/sounds/client_music/piano.wav and /dev/null differ diff --git a/assets/sounds/client_music/relay_race_players.mp3 b/assets/sounds/client_music/relay_race_players.mp3 new file mode 100644 index 00000000..124f26ce Binary files /dev/null and b/assets/sounds/client_music/relay_race_players.mp3 differ diff --git a/assets/sounds/client_sfx/victory_players.flac b/assets/sounds/client_sfx/victory_players.flac new file mode 100644 index 00000000..eda2a473 Binary files /dev/null and b/assets/sounds/client_sfx/victory_players.flac differ diff --git a/assets/sounds/client_sfx/vine-boom-mono.mp3 b/assets/sounds/client_sfx/vine-boom-mono.mp3 deleted file mode 100644 index e4a8e70b..00000000 Binary files a/assets/sounds/client_sfx/vine-boom-mono.mp3 and /dev/null differ diff --git a/assets/sounds/server_sfx/cutscene_gate_open.wav b/assets/sounds/server_sfx/cutscene_gate_open.wav new file mode 100644 index 00000000..e9762c23 Binary files /dev/null and b/assets/sounds/server_sfx/cutscene_gate_open.wav differ diff --git a/assets/sounds/server_sfx/electric_hum.wav b/assets/sounds/server_sfx/electric_hum.wav new file mode 100644 index 00000000..04510a73 Binary files /dev/null and b/assets/sounds/server_sfx/electric_hum.wav differ diff --git a/assets/sounds/server_sfx/mirror_shatter.mp3 b/assets/sounds/server_sfx/mirror_shatter.mp3 new file mode 100644 index 00000000..9bbf22b7 Binary files /dev/null and b/assets/sounds/server_sfx/mirror_shatter.mp3 differ diff --git a/assets/sounds/server_sfx/players_start_theme.mp3 b/assets/sounds/server_sfx/players_start_theme.mp3 new file mode 100644 index 00000000..e09e53a3 Binary files /dev/null and b/assets/sounds/server_sfx/players_start_theme.mp3 differ diff --git a/assets/sounds/server_sfx/wind.wav b/assets/sounds/server_sfx/wind.wav new file mode 100644 index 00000000..e588adf1 Binary files /dev/null and b/assets/sounds/server_sfx/wind.wav differ diff --git a/assets/sounds/server_sfx/zeus_start_theme.mp3 b/assets/sounds/server_sfx/zeus_start_theme.mp3 new file mode 100644 index 00000000..b1180b71 Binary files /dev/null and b/assets/sounds/server_sfx/zeus_start_theme.mp3 differ diff --git a/config.json b/config.json index 549d79c3..462d4ba9 100644 --- a/config.json +++ b/config.json @@ -4,7 +4,8 @@ "directory": "maps", "procedural": false, "maze_file": "test/itemRoom.maze" - } + }, + "disable_enemies": true }, "network": { "server_ip": "localhost", @@ -14,7 +15,8 @@ "lobby_name": "Hope you're doing well!1", "lobby_broadcast": true, "max_players": 1, - "disable_dm": true + "disable_dm": true, + "skip_intro": true }, "client": { "default_name": "Conan O'Brien", diff --git a/include/client/camera.hpp b/include/client/camera.hpp index b3f7f60f..a83d7e05 100644 --- a/include/client/camera.hpp +++ b/include/client/camera.hpp @@ -89,6 +89,8 @@ class Camera { glm::mat4 getView(); + void setPitch(float pitch); + protected: // Perspective controls float FOV; // Field of View Angle (degrees) diff --git a/include/client/client.hpp b/include/client/client.hpp index dd0bfd31..66969d53 100644 --- a/include/client/client.hpp +++ b/include/client/client.hpp @@ -117,6 +117,13 @@ class Client { */ void mouseCallback(GLFWwindow* window, double xposIn, double yposIn); + /** + * @brief Callback which handles scrolling movement. + * + * @param window The GLFWwindow being monitered. + * @param xposIn The current scroll up value. + * @param yposIn The current scroll down value. + */ void scrollCallback(GLFWwindow* window, double xposIn, double yposIn); /** @@ -166,11 +173,28 @@ class Client { */ bool connect(std::string ip_addr); + /** + * @brief get the reference to the Audio manager + */ AudioManager* getAudioManager(); + + /** + * @brief get the reference to the Animation manager + */ AnimationManager* getAnimManager() { return animManager; } + /** + * @brief the current position in the world the player is looking at + */ void setWorldPos(); + /** + * @brief Send down a trap event to the server + * + * @param hover boolean to indicate the DM is only hovering and has not placed yet + * @param place boolean to indicate the DM would like to place a type + * @param trapType ModelType to indicate the type of the trap the DM wants to place + */ void sendTrapEvent(bool hover, bool place, ModelType trapType); int curr_fps; @@ -221,6 +245,9 @@ class Client { /* Current game state */ SharedGameState gameState; + /* Stuff for the intro cutscene, contains a whole other SharedGameState */ + std::optional intro_cutscene; + /* Shader objects for various */ std::shared_ptr deferred_geometry_shader; std::shared_ptr deferred_lighting_shader; @@ -284,7 +311,11 @@ class Client { */ RadioButtonState roleSelection; + /** + * @brief Audio Manager to play Client sounds + */ AudioManager* audioManager; + AnimationManager* animManager; /* Camera object representing player's current position & orientation */ @@ -303,17 +334,22 @@ class Client { bool is_held_left = false; bool is_held_space = false; + /* DM zooming in and out flags */ bool is_held_i = false; bool is_held_o = false; - bool is_pressed_p = false; - bool is_left_mouse_down = false; + bool is_right_mouse_down = false; + + /* DM Trap Orientation Traps With Directions */ + int orientation = 0; /* Mouse position coordinates */ float mouse_xpos = 0.0f; float mouse_ypos = 0.0f; + double lastTime = 0.0; + GameConfig config; tcp::resolver resolver; tcp::socket socket; @@ -330,6 +366,9 @@ class Client { std::deque events_received; + /** + * @brief boolean to see if a phase change from LOBBY to GAME already happened + */ bool phase_change; // id of last known player which is holding the orb diff --git a/include/client/constants.hpp b/include/client/constants.hpp index 9e7d14cb..cb7d9ca9 100644 --- a/include/client/constants.hpp +++ b/include/client/constants.hpp @@ -8,7 +8,7 @@ #define UNIT_WINDOW_WIDTH 1920 #define UNIT_WINDOW_HEIGHT 1080 -#define PLAYER_EYE_LEVEL 2.35f +#define PLAYER_EYE_LEVEL 3.45f // distance threshold of which objects // to render diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp index 843839ef..e3aa78bd 100644 --- a/include/client/gui/gui.hpp +++ b/include/client/gui/gui.hpp @@ -42,6 +42,7 @@ enum class GUIState { TITLE_SCREEN, LOBBY_BROWSER, LOBBY, + INTRO_CUTSCENE, GAME_HUD, GAME_ESC_MENU, DEAD_SCREEN, @@ -129,8 +130,12 @@ class GUI { * is down. Note: this is a reference so that if a click is captured it can toggle the * mouse down flag to false, so that click doesn't get "double counted" in subsequent * frames. + * @param is_right_mouse_down reference to flag which stores whether or not the right mouse + * is down. Note: this is a reference so that if a click is captured it can toggle the + * mouse down flag to false, so that click doesn't get "double counted" in subsequent + * frames. */ - void handleInputs(float mouse_xpos, float mouse_ypos, bool& is_left_mouse_down); + void handleInputs(float mouse_xpos, float mouse_ypos, bool& is_left_mouse_down, bool& is_right_mouse_down); /** * Renders the current state of the GUI to the screen, based on all of the widgets * that have been added and any changes caused by inputs. diff --git a/include/client/gui/img/img.hpp b/include/client/gui/img/img.hpp index 8d8565c1..de6d6c47 100644 --- a/include/client/gui/img/img.hpp +++ b/include/client/gui/img/img.hpp @@ -23,6 +23,7 @@ enum class ImgID { FireSpell, HealSpell, Orb, + Mirror, Crosshair, Scroll, Dagger, @@ -37,6 +38,7 @@ enum class ImgID { DMRightHotbar, DMMiddleHotbar, DMMiddleSelected, + DMMiddleCooldown, HealthBar, HealthTickEmpty, HealthTickFull, @@ -58,11 +60,16 @@ enum class ImgID { Lightning, ArrowTrap, SpikeTrap, + DMCD_10, DMCD_9, DMCD_8, DMCD_7, DMCD_6, DMCD_5, DMCD_4, DMCD_3, DMCD_2, DMCD_1, + DMCD_Selected_10, DMCD_Selected_9, DMCD_Selected_8, DMCD_Selected_7, DMCD_Selected_6, + DMCD_Selected_5, DMCD_Selected_4, DMCD_Selected_3, DMCD_Selected_2, DMCD_Selected_1, Blank, }; + +// Sorry for whoever has to look at this :) - ted #define GET_ALL_IMG_IDS() \ - { ImgID::Yoshi, ImgID::AwesomeSauce, ImgID::HealthPotion, ImgID::UnknownPotion, \ - ImgID::InvisPotion, ImgID::FireSpell, ImgID::HealSpell, ImgID::Orb, \ + {ImgID::Yoshi, ImgID::AwesomeSauce, ImgID::HealthPotion, ImgID::UnknownPotion, \ + ImgID::InvisPotion, ImgID::FireSpell, ImgID::HealSpell, ImgID::Orb, ImgID::Mirror, \ ImgID::Crosshair, ImgID::Scroll, ImgID::Dagger, ImgID::Sword, ImgID::Hammer, \ ImgID::LeftHotbar, ImgID::RightHotbar, ImgID::MiddleHotbar, ImgID::Blank, ImgID::Title, \ ImgID::MiddleSelected, ImgID::HealthBar, ImgID::HealthTickEmpty, ImgID::HealthTickFull, \ @@ -71,9 +78,15 @@ enum class ImgID { ImgID::Compass0, ImgID::Compass30, ImgID::Compass60, ImgID::Compass90, \ ImgID::Compass120, ImgID::Compass150, ImgID::Compass180, ImgID::Compass210, \ ImgID::Compass240, ImgID::Compass270, ImgID::Compass300, ImgID::Compass330, \ - ImgID::DMLeftHotbar, ImgID::DMRightHotbar, ImgID::DMMiddleHotbar, ImgID::DMMiddleSelected, \ ImgID::FloorSpikeTrap, ImgID::Sungod, ImgID::Teleporter, ImgID::Lightning, \ ImgID::ArrowTrap, ImgID::SpikeTrap, \ + ImgID::DMTrapBG, ImgID::Needle, \ + ImgID::EventBG, ImgID::DMEventBG, \ + ImgID::DMLeftHotbar, ImgID::DMRightHotbar, ImgID::DMMiddleHotbar, ImgID::DMMiddleSelected, ImgID::DMMiddleCooldown, \ + ImgID::DMCD_10, ImgID::DMCD_9, ImgID::DMCD_8, ImgID::DMCD_7, ImgID::DMCD_6, \ + ImgID::DMCD_5, ImgID::DMCD_4, ImgID::DMCD_3, ImgID::DMCD_2, ImgID::DMCD_1, \ + ImgID::DMCD_Selected_10, ImgID::DMCD_Selected_9, ImgID::DMCD_Selected_8, ImgID::DMCD_Selected_7, ImgID::DMCD_Selected_6, \ + ImgID::DMCD_Selected_5, ImgID::DMCD_Selected_4, ImgID::DMCD_Selected_3, ImgID::DMCD_Selected_2, ImgID::DMCD_Selected_1, \ } /** diff --git a/include/server/game/constants.hpp b/include/server/game/constants.hpp index 8f767932..78482682 100644 --- a/include/server/game/constants.hpp +++ b/include/server/game/constants.hpp @@ -73,14 +73,19 @@ #define DM_MANA_REGEN 1 #define LIGHTNING_MANA 10 +/* Mirror Item */ +// Mirror use duration in seconds +#define MIRROR_USE_DURATION 30 + /* Game */ #define GRAVITY 0.03f #define PLAYER_SPEED 1.65f #define JUMP_SPEED 0.55f - /* DM Constants */ #define MAX_TRAPS 10 #define TRAP_INVENTORY_SIZE 10 #define TRAP_TIME 10 -#define TRAP_COOL_DOWN 5 \ No newline at end of file +#define TRAP_COOL_DOWN 5 +#define ITEM_SPAWN_PROB 0.1 +#define ITEM_SPAWN_BOUND 3 \ No newline at end of file diff --git a/include/server/game/dungeonmaster.hpp b/include/server/game/dungeonmaster.hpp index 89370c40..87b85388 100644 --- a/include/server/game/dungeonmaster.hpp +++ b/include/server/game/dungeonmaster.hpp @@ -26,8 +26,53 @@ class DungeonMaster : public Creature { // For lightning usage void useMana(); void manaRegen(); + + /** + * @brief Sets the whether the DungeonMaster is paralyzed. If isParalyzed + * is true, then this sets the paralysis duration and marks the timestamp for + * the paralysis start event. + * @param isParalyzed Whether the DungeonMaster should now be paralyzed. + * @param paralysis_duration How long the DungeonMaster should be paralyzed for + * (ignored if isParalyzed is false) + */ + void setParalysis(bool isParalyzed, double paralysis_duration); + + /** + * @brief Getter for whether the DungeonMaster is paralyzed. + * @return true if the DungeonMaster is paralyzed and false otherwise. + */ + bool isParalyzed() const; + + /** + * @brief Getter for the DungeonMaster's paralysis duration. + * (this value should be ignored if the DungeonMaster's paralyzed boolean + * is false) + * @return double representing the number of seconds that the DungeonMaster + * should be paralyzed since the paralysis_start_time timestamp. + */ + double getParalysisDuration() const; + + /** + * @brief Getter for the timestamp of the last time the DungeonMaster was + * paralyzed. + * @return std::chrono::time_point timestamp of + * the last time the DungeonMaster became paralyzed. + */ + std::chrono::time_point getParalysisStartTime() const; private: + /** + * @brief Duration, in seconds, of the Dungeon Master's current paralysis + * (this value should be ignored if paralyzed is false) + */ + double paralysisDuration; + + /** + * @brief Timestamp for the last time the DungeonMaster became paralyzed. + * Set by setParalysis(). + */ + std::chrono::time_point paralysis_start_time; + int placedTraps; std::chrono::system_clock::time_point mana_used; }; \ No newline at end of file diff --git a/include/server/game/introcutscene.hpp b/include/server/game/introcutscene.hpp new file mode 100644 index 00000000..45211a3c --- /dev/null +++ b/include/server/game/introcutscene.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "server/game/servergamestate.hpp" +#include "shared/utilities/serialize_macro.hpp" +#include "shared/game/sharedgamestate.hpp" + +#include + +/** + * @brief config to use for the servergamestate used for the cutscene, + * This mainly just needs to set the room file to load the cutscene room + * from. I dont think the other values will really matter. + */ +GameConfig getCutsceneConfig(); + +class IntroCutscene { +public: + IntroCutscene(); + + /** + * update to the next frame of the cutscene + * @returns true if the cutscene is over + */ + bool update(); + + LoadIntroCutsceneEvent toNetwork(); + + + // just making everything public bc lazy + ServerGameState state; + EntityID pov_eid; + EntityID dm_eid; + std::array, MAX_POINT_LIGHTS> lights; +}; diff --git a/include/server/game/mirror.hpp b/include/server/game/mirror.hpp new file mode 100644 index 00000000..78d23557 --- /dev/null +++ b/include/server/game/mirror.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include "server/game/item.hpp" +#include + +class Mirror : public Item { +public: + + /** + * @brief Mirror constructor + * @param corner Corner position of the Mirror + * @param dimensions Dimensions of the Mirror + */ + Mirror(glm::vec3 corner, glm::vec3 dimensions); + + /** + * @brief Using the mirror causes it to be held for a few seconds + * @param other Player object that uses the mirror + * @param state Reference to the server's ServerGameState instance + * @param itemSelected Mirror's item index in the player's inventory + */ + void useItem(Object* other, ServerGameState& state, int itemSelected) override; + + void dropItem(Object* other, ServerGameState& state, int itemSelected, float dropDistance) override; + + /** + * @brief Determines whether the mirror has been used for the mirror holding + * duration and updated the remaining time it will be used. + * @return true if the mirror has been used for the specified duration. + */ + bool timeOut(); + + /** + * @brief Stop holding the mirror. + * @note This should be called when the use duration times out OR when the player + * selects a different item in their inventory. + * @param state Reference to the server's ServerGameState instance + */ + void revertEffect(ServerGameState& state); +private: + /** + * @brief The time at which the mirror was last used + */ + std::chrono::time_point used_time; + + /** + * @brief The last Player that used this mirror object. + */ + Player* used_player; +}; \ No newline at end of file diff --git a/include/server/game/player.hpp b/include/server/game/player.hpp index 92943121..6502f254 100644 --- a/include/server/game/player.hpp +++ b/include/server/game/player.hpp @@ -25,5 +25,61 @@ class Player : public Creature { bool canBeTargetted() const; + /** + * @brief This sets the Player as invulnerable to lightning and marks + * the timestamp for this event if the value is set to true. + * @param isInvulnerable whether the Player should now be invulnerable to lightning + * @param duration how long the Player should be invulnerable to lightning (ignored + * if isInvulnerable is false) + */ + void setInvulnerableToLightning(bool isInvulnerable, double duration); + + /** + * @brief Getter for whether this Player is invulnerable to lightning. + * @return true if this Player is invulnerable to lightning and false + * otherwise. + */ + bool isInvulnerableToLightning() const; + + /** + * @brief Getter for this Player's lightning invulnerability duration + * (this value should be ignored if the Player's invulnerableToLightning + * boolean is false) + * @return double representing the number of seconds that this Player + * should be invulnerable to lightning since the + * lightning_invulnerability_start_time timestamp. + */ + double getLightningInvulnerabilityDuration() const; + + /** + * @brief Returns the timestamp for the last time that this Player became invulnerable + * to lightning. + * @return std::chrono::time_point timestamp of the last time + * this Player became invulnerable to lightning. + */ + std::chrono::time_point getLightningInvulnerabilityStartTime() const; + private: + /** + * @brief Whether or not this Player is currently invulnerable to lightning. + * This can occur after a Player used a Mirror to effectively reflect a + * lightning bolt (this is to ensure that the player isn't harmed by that + * lightning bolt in collisions in subsequent ticks and protects the player + * from other lightning bolts in the vicinity for a short time to avoid + * a double lightning bolt attack by the DM or something like this). + */ + bool invulnerableToLightning; + + /** + * @brief Duration, in seconds, of this Player's current lightning invulnerability + * (this value should be ignored if invulnerableToLightning is false) + */ + double lightningInvulnerabilityDuration; + + /** + * @brief Timestamp for the last time this Player gained an invulnerability + * to lightning. + * Set by setInvulnerableToLightning(). + */ + std::chrono::time_point lightning_invulnerability_start_time; }; \ No newline at end of file diff --git a/include/server/game/servergamestate.hpp b/include/server/game/servergamestate.hpp index 65f39f8b..d665425f 100644 --- a/include/server/game/servergamestate.hpp +++ b/include/server/game/servergamestate.hpp @@ -141,6 +141,19 @@ class ServerGameState { void deleteEntities(); + /** + * @brief Updates player's lightning invulnerability status + * (sets to false once the player's lightning invulnerability duration + * is past) + */ + void updatePlayerLightningInvulnerabilityStatus(); + + /** + * @brief Updates the DungeonMaster's paralysis status + * (sets to false once the DungeonMaster's paralysis duration is past) + */ + void updateDungeonMasterParalysis(); + /* SharedGameState generation */ // TODO: Modify this function to dynamically allocate a SharedGameState @@ -298,11 +311,11 @@ class ServerGameState { MatchPhase matchPhase; /** - * @brief Amount of time, in timesteps, left until the end of the match - * This value only becomes relevant when matchPhase is set to + * @brief epoch timestamp of when the match will end + * This value only becomes set when matchPhase is set to * MatchPhase::RelayRace */ - unsigned int timesteps_left; + time_t relay_finish_time; /** * @brief Player victory is by default false - only becomes true if a Player @@ -339,9 +352,9 @@ class ServerGameState { */ std::unordered_set, pair_hash> collidedObjects; - - std::unordered_map, std::vector, IntPairHash> solidSurfaceInGridCells; - + /** + * @brief Field that stores the current trap the DM is hovering (not placed yet) + */ Trap* currentGhostTrap; /** diff --git a/include/server/game/spawner.hpp b/include/server/game/spawner.hpp index 58df78fc..490a1976 100644 --- a/include/server/game/spawner.hpp +++ b/include/server/game/spawner.hpp @@ -20,6 +20,8 @@ class Spawner { public: Item* dummyItem; + Item* smallDummyItem; + Spawner(); /* @@ -54,6 +56,12 @@ class Spawner { void spawnDummy(ServerGameState& state); + /* + * Smaller dummy for empty check + */ + void spawnSmallDummy(ServerGameState& state); + + private: int enemyValueCap; int currentEnemyValue; diff --git a/include/server/server.hpp b/include/server/server.hpp index cd99ff2e..81c17659 100644 --- a/include/server/server.hpp +++ b/include/server/server.hpp @@ -10,6 +10,7 @@ #include #include "server/lobbybroadcaster.hpp" +#include "server/game/introcutscene.hpp" #include "shared/network/session.hpp" #include "shared/utilities/config.hpp" #include "shared/utilities/typedefs.hpp" @@ -41,6 +42,8 @@ class Server { void sendLightSourceUpdates(EntityID playerID); + void sendSoundCommands(); + private: /// @brief EID that is reserved for the Server / World itself. EntityID world_eid; @@ -76,4 +79,7 @@ class Server { /// @brief config GameConfig config; + + /// @brief game state used to render the intro cutscene + IntroCutscene intro_cutscene; }; diff --git a/include/shared/audio/constants.hpp b/include/shared/audio/constants.hpp index 4a6998c5..d1e3d787 100644 --- a/include/shared/audio/constants.hpp +++ b/include/shared/audio/constants.hpp @@ -16,4 +16,4 @@ // Sound values fitting for something that should be heard from very far #define FAR_ATTEN 1.0f -#define FAR_DIST 50.0f \ No newline at end of file +#define FAR_DIST 80.0f \ No newline at end of file diff --git a/include/shared/audio/soundtype.hpp b/include/shared/audio/soundtype.hpp index 28fe5ee5..72950535 100644 --- a/include/shared/audio/soundtype.hpp +++ b/include/shared/audio/soundtype.hpp @@ -9,23 +9,29 @@ using namespace std::chrono_literals; // Sounds that the client can decide to play on its own // (e.g. BGM, clicking on UI elements...) enum class ClientMusic { - TitleTheme, - GameTheme, + MenuTheme, + MazeExplorationPlayersTheme, + MazeExplorationDMTheme, + RelayRacePlayersTheme, + RelayRaceDMTheme // make sure to add to macro below! }; #define GET_CLIENT_MUSICS() { \ - ClientMusic::TitleTheme, ClientMusic::GameTheme \ + ClientMusic::MenuTheme, ClientMusic::MazeExplorationPlayersTheme, \ + ClientMusic::MazeExplorationDMTheme, ClientMusic::RelayRacePlayersTheme, \ + ClientMusic::RelayRaceDMTheme \ } enum class ClientSFX { // TODO: decide what these are - TEMP, + VictoryThemePlayers, + VictoryThemeDM // make sure to add to macro below! }; #define GET_CLIENT_SFXS() { \ - ClientSFX::TEMP, \ + ClientSFX::VictoryThemePlayers, ClientSFX::VictoryThemeDM, \ } // Sounds that correspond to something in the game world @@ -57,7 +63,13 @@ enum class ServerSFX { Spell, ItemPickUp, ItemDrop, + MirrorShatter, TEMP, + PlayersStartTheme, + ZeusStartTheme, + ElectricHum, + IntroGateOpen, + Wind, // make sure to add to server sfx len map! // make sure to add to macro below! }; @@ -76,20 +88,29 @@ const std::unordered_map SERVER_SFX_LENS = {ServerSFX::PlayerWalk3, 500ms}, {ServerSFX::PlayerWalk4, 500ms}, {ServerSFX::PlayerWalk5, 500ms}, - {ServerSFX::Dagger, 500ms}, - {ServerSFX::Sword, 500ms}, + {ServerSFX::Dagger, 500ms}, + {ServerSFX::Sword, 500ms}, {ServerSFX::Hammer, 1000ms}, {ServerSFX::Minotaur, 1000ms}, {ServerSFX::Python, 1000ms}, {ServerSFX::CeilingSpikeTrigger, 380ms}, {ServerSFX::CeilingSpikeImpact, 1180ms}, {ServerSFX::Thunder, 2500ms}, + + // used not for in game, but for the intro cutscene, so this is the duration of sound in intro cutscene + {ServerSFX::TorchLoop, 30000ms}, + {ServerSFX::Wind, 14000ms}, + + {ServerSFX::PlayersStartTheme, 8000ms}, + {ServerSFX::ElectricHum, 1500ms}, + {ServerSFX::IntroGateOpen, 9000ms}, + {ServerSFX::ZeusStartTheme, 12000ms}, {ServerSFX::Teleport, 500ms}, {ServerSFX::Potion, 500ms}, {ServerSFX::Spell, 500ms}, {ServerSFX::ItemPickUp, 500ms}, {ServerSFX::ItemDrop, 500ms}, - {ServerSFX::TorchLoop, 9999ms} // wont actually be used because it loops + {ServerSFX::MirrorShatter, 2000ms}, // dont forget macro below! }; @@ -99,7 +120,10 @@ const std::unordered_map SERVER_SFX_LENS = ServerSFX::PlayerWalk1, ServerSFX::PlayerWalk2, ServerSFX::PlayerWalk3, ServerSFX::PlayerWalk4, ServerSFX::PlayerWalk5, \ ServerSFX::CeilingSpikeTrigger, ServerSFX::CeilingSpikeImpact, ServerSFX::TorchLoop, ServerSFX::Thunder,\ ServerSFX::Dagger, ServerSFX::Sword, ServerSFX::Hammer, ServerSFX::Minotaur, ServerSFX::Python, \ - ServerSFX::Teleport, ServerSFX::Potion, ServerSFX::Spell, ServerSFX::ItemPickUp, ServerSFX::ItemDrop \ + ServerSFX::PlayersStartTheme, ServerSFX::ElectricHum, ServerSFX::IntroGateOpen, ServerSFX::ZeusStartTheme, \ + ServerSFX::Wind, \ + ServerSFX::Teleport, ServerSFX::Potion, ServerSFX::Spell, ServerSFX::ItemPickUp, ServerSFX::ItemDrop, \ + ServerSFX::MirrorShatter \ } // const std::unordered_map serverSoundTickLengths = { diff --git a/include/shared/game/celltype.hpp b/include/shared/game/celltype.hpp index 43e55447..6c71653b 100644 --- a/include/shared/game/celltype.hpp +++ b/include/shared/game/celltype.hpp @@ -41,5 +41,6 @@ enum class CellType { TeleporterTrap, Exit, Lightning, + Mirror, Unknown }; \ No newline at end of file diff --git a/include/shared/game/constants.hpp b/include/shared/game/constants.hpp index ef501d40..0a24cfa8 100644 --- a/include/shared/game/constants.hpp +++ b/include/shared/game/constants.hpp @@ -7,16 +7,19 @@ /* Game phase information */ // Time limit initially set to 5 minutes -#define TIME_LIMIT_MS std::chrono::milliseconds(300000) -//#define TIME_LIMIT_MS std::chrono::milliseconds(5 * 60 * 1000) - -/* Number of player deaths to update match state to MatchState::RelayRace */ -#define PLAYER_DEATHS_TO_RELAY_RACE 5 -//#define PLAYER_DEATHS_TO_RELAY_RACE 15 +#define TIME_LIMIT_S std::chrono::seconds(300) /* Default model sizes */ #define BEAR_DIMENSIONS glm::vec3(14.163582, 17.914591, 10.655818) -#define FIRE_PLAYER_DIMENSIONS glm::vec3(8.008834, 10.069769, 2.198592) +#define FIRE_PLAYER_DIMENSIONS glm::vec3(4.0f, 10.069769, 4.0f) +#define LIGHTNING_PLAYER_DIMENSIONS glm::vec3(4.0f, 10.069769, 4.0f) +#define WATER_PLAYER_DIMENSIONS glm::vec3(4.0f, 10.069769, 4.0f) #define SUNGOD_DIMENSIONS glm::vec3(3.281404, 9.543382, 7.974873) #define ARROW_DIMENSIONS glm::vec3(45.025101, 68.662003, 815.164001) #define ARROW_TRAP_DIMENSIONS glm::vec3(2.815553, 3.673665, 1.588817) + +#define PLAYER_BBOX_SCALE 0.35f +#define PLAYER_MODEL_SCALE 0.004f + +/* Number of player deaths to update match state to MatchState::RelayRace */ +#define PLAYER_DEATHS_TO_RELAY_RACE 3 diff --git a/include/shared/game/event.hpp b/include/shared/game/event.hpp index d6dc3b58..97c60d37 100644 --- a/include/shared/game/event.hpp +++ b/include/shared/game/event.hpp @@ -42,7 +42,8 @@ enum class EventType { UseItem, DropItem, UpdateLightSources, - TrapPlacement + TrapPlacement, + LoadIntroCutscene, }; enum class ActionType { @@ -298,6 +299,25 @@ struct UpdateLightSourcesEvent { } }; +struct LoadIntroCutsceneEvent { + LoadIntroCutsceneEvent() = default; + + explicit LoadIntroCutsceneEvent( + const SharedGameState& state, + EntityID pov_eid, + EntityID dm_eid, + const std::array, MAX_POINT_LIGHTS>& lights + ) : state(state), pov_eid(pov_eid), dm_eid(dm_eid), lights(lights) {} + + SharedGameState state; + EntityID pov_eid; + EntityID dm_eid; + std::array, MAX_POINT_LIGHTS> lights; + + DEF_SERIALIZE(Archive& ar, const unsigned int version) { + ar & state & lights & pov_eid & dm_eid; + } +}; /** * All of the different kinds of events in a tagged union, so we can @@ -317,7 +337,8 @@ using EventData = boost::variant< UseItemEvent, UpdateLightSourcesEvent, DropItemEvent, - TrapPlacementEvent + TrapPlacementEvent, + LoadIntroCutsceneEvent >; /** diff --git a/include/shared/game/sharedgamestate.hpp b/include/shared/game/sharedgamestate.hpp index e3d00f16..5c30d952 100644 --- a/include/shared/game/sharedgamestate.hpp +++ b/include/shared/game/sharedgamestate.hpp @@ -20,6 +20,7 @@ enum class PlayerRole; enum class GamePhase { TITLE_SCREEN, LOBBY, + INTRO_CUTSCENE, GAME, RESULTS }; @@ -174,7 +175,7 @@ struct SharedGameState { MatchPhase matchPhase; - unsigned int timesteps_left; + time_t relay_finish_time; bool playerVictory; @@ -187,7 +188,7 @@ struct SharedGameState { this->timestep = FIRST_TIMESTEP; this->lobby.max_players = MAX_PLAYERS; this->matchPhase = MatchPhase::MazeExploration; - this->timesteps_left = TIME_LIMIT_MS / TIMESTEP_LEN; + this->relay_finish_time = 0; this->playerVictory = false; this->numPlayerDeaths = 0; } @@ -200,14 +201,14 @@ struct SharedGameState { this->lobby.max_players = config.server.max_players; this->lobby.name = config.server.lobby_name; this->matchPhase = MatchPhase::MazeExploration; - this->timesteps_left = TIME_LIMIT_MS / TIMESTEP_LEN; + this->relay_finish_time = 0; this->playerVictory = false; this->numPlayerDeaths = 0; } DEF_SERIALIZE(Archive& ar, const unsigned int version) { ar & objects & timestep & lobby & phase & matchPhase - & timesteps_left & playerVictory & numPlayerDeaths; + & relay_finish_time & playerVictory & numPlayerDeaths; } /** diff --git a/include/shared/game/sharedmodel.hpp b/include/shared/game/sharedmodel.hpp index b83df766..87370f79 100644 --- a/include/shared/game/sharedmodel.hpp +++ b/include/shared/game/sharedmodel.hpp @@ -25,12 +25,17 @@ enum class ModelType { ArrowTrapLeft, ArrowTrapRight, SpikeTrap, - SunGod, TeleporterTrap, Dagger, Sword, Hammer, Lightning, Arrow, - ArrowTrap + ArrowTrap, + FireballTrapLeft, + FireballTrapRight, + FireballTrapUp, + FireballTrapDown, + SunGod, + Mirror }; \ No newline at end of file diff --git a/include/shared/game/sharedobject.hpp b/include/shared/game/sharedobject.hpp index 90d48543..e3cc83eb 100644 --- a/include/shared/game/sharedobject.hpp +++ b/include/shared/game/sharedobject.hpp @@ -40,6 +40,7 @@ enum class ObjectType { Orb, Weapon, WeaponCollider, + Mirror }; /** @@ -126,10 +127,11 @@ struct SharedTrapInventory { int inventory_size; std::vector inventory; std::unordered_map trapsInCooldown; + std::unordered_map trapsCooldown; int trapsPlaced; DEF_SERIALIZE(Archive& ar, const unsigned int version) { - ar& selected& inventory_size& inventory& trapsInCooldown& trapsPlaced; + ar& selected& inventory_size& inventory& trapsInCooldown& trapsPlaced & trapsCooldown; } }; @@ -217,9 +219,10 @@ struct SharedPlayerInfo { bool is_alive; time_t respawn_time; // unix timestamp in ms when the player will be respawned bool render; // for invis potion + bool used_mirror_to_reflect_lightning; // To tell the player that they successfully reflected lightning DEF_SERIALIZE(Archive& ar, const unsigned int version) { - ar & is_alive & respawn_time & render; + ar & is_alive & respawn_time & render & used_mirror_to_reflect_lightning; } }; @@ -264,11 +267,18 @@ enum class AnimState { }; struct SharedDMInfo { + /** + * @brief The Dungeon Master can become paralyzed if a player used a Mirror + * object to reflect a lightning bolt back at the Dungeon Master. + * When the Dungeon Master is paralyzed, all input events from the DM should + * be ignored for the duration of the paralysis. + */ + bool paralyzed; int mana_remaining; DEF_SERIALIZE(Archive& ar, const unsigned int version) { - ar& mana_remaining; + ar & paralyzed & mana_remaining; } }; diff --git a/include/shared/network/session.hpp b/include/shared/network/session.hpp index 9e2b18bd..8833f328 100644 --- a/include/shared/network/session.hpp +++ b/include/shared/network/session.hpp @@ -113,6 +113,8 @@ class Session : public std::enable_shared_from_this { */ const SessionInfo& getInfo() const; + void setDM(bool is_dm); + private: /// @brief true until there is a fatal error on the socket bool okay; diff --git a/include/shared/utilities/config.hpp b/include/shared/utilities/config.hpp index c3205149..861364f0 100644 --- a/include/shared/utilities/config.hpp +++ b/include/shared/utilities/config.hpp @@ -51,6 +51,8 @@ struct GameConfig { int max_players; /// @brief whether or not the server will spawn a DM bool disable_dm; + /// @brief whether or not to skip the intro cutscene + bool skip_intro; } server; /// @brief Config settings for the client struct { @@ -92,4 +94,4 @@ struct GameConfig { * Note: Not using a constructor as then aggregate initialization will not be * possible for GameConfig structs */ -GameConfig getDefaultConfig(); \ No newline at end of file +GameConfig getDefaultConfig(); diff --git a/include/shared/utilities/time.hpp b/include/shared/utilities/time.hpp index fefafbfa..3dd8cd1e 100644 --- a/include/shared/utilities/time.hpp +++ b/include/shared/utilities/time.hpp @@ -1,3 +1,5 @@ #pragma once -long long getMsSinceEpoch(); \ No newline at end of file +long long getMsSinceEpoch(); + +long long getSecSinceEpoch(); \ No newline at end of file diff --git a/maps/cutscene/intro.maze b/maps/cutscene/intro.maze new file mode 100644 index 00000000..2c26434c --- /dev/null +++ b/maps/cutscene/intro.maze @@ -0,0 +1,6 @@ +##########[#####[#####[#### +.........................!o +..........@..............!o +.........................!o +.........................!o +##########]#####]#####]#### \ No newline at end of file diff --git a/maps/rooms/10x10/basic1.exit b/maps/rooms/10x10/basic1.exit index dc2b1bff..52cb9297 100644 --- a/maps/rooms/10x10/basic1.exit +++ b/maps/rooms/10x10/basic1.exit @@ -1,10 +1,10 @@ ########## ########## ##!#[#!### -!.......!# +!.......o# ........o# ........o# -!.......!# +!.......o# ##!#]#!### ########## ########## \ No newline at end of file diff --git a/maps/rooms/10x10/basic2.exit b/maps/rooms/10x10/basic2.exit index 2e90a72a..adeb37d9 100644 --- a/maps/rooms/10x10/basic2.exit +++ b/maps/rooms/10x10/basic2.exit @@ -1,10 +1,10 @@ ########## ########## ###!#[#!## -#!.......! +#o.......! #o........ #o........ -#!.......! +#o.......! ###!#]#!## ########## ########## \ No newline at end of file diff --git a/maps/rooms/10x10/basic3.exit b/maps/rooms/10x10/basic3.exit index f5c6e530..bb666c33 100644 --- a/maps/rooms/10x10/basic3.exit +++ b/maps/rooms/10x10/basic3.exit @@ -1,5 +1,5 @@ ########## -###!oo!### +###oooo### ##......## #!......!# #{......}# diff --git a/maps/rooms/10x10/basic4.exit b/maps/rooms/10x10/basic4.exit index b5dcab25..1d321824 100644 --- a/maps/rooms/10x10/basic4.exit +++ b/maps/rooms/10x10/basic4.exit @@ -5,6 +5,6 @@ #{......}# #!......!# ##......## -###!..!### -####oo#### +##!....!## +###oooo### ########## \ No newline at end of file diff --git a/maps/rooms/20x20/crush_axis.hard b/maps/rooms/20x20/crush_axis.hard index 62efa7e2..ef4cad6f 100644 --- a/maps/rooms/20x20/crush_axis.hard +++ b/maps/rooms/20x20/crush_axis.hard @@ -4,14 +4,14 @@ #..![#########..!..# .......|XXXX|....... .......|XXXX|....... -#..!..########..!..# -#..#....|..|....#..# -#..#....|..|....#..# +#..!..###[####..!..# #..#....|..|....#..# #..#....|..|....#..# +#..{....|..|....#..# +#..#....|..|....}..# {..#....|..|....#..# #..#....|..|....#..# -#..!..########..!..# +#..!..####]###..!..# .......|XXXX|....... .......|XXXX|....... #..!##[##########..# diff --git a/maps/rooms/20x20/dark_halls_1.hard b/maps/rooms/20x20/dark_halls_1.hard index eca364ab..58a05c34 100644 --- a/maps/rooms/20x20/dark_halls_1.hard +++ b/maps/rooms/20x20/dark_halls_1.hard @@ -1,20 +1,20 @@ ####..########..#### ####..########..#### ####..########..#### -####..########..#### +##[#..########..#### .......X............ ............X....... ####..###..###..#### -####..###.X###.X#### +####..###.X##{.X#### ####X.###..###..#### ####..###..###..#### ####..###..###..#### -####..###X.###..#### +####..}##X.###..#### ####.X###..###..#### ####..###..###X.#### .......X............ .............X...... -####..########..#### +####..########..#]## ####..########..#### ####..########..#### ####..########..#### \ No newline at end of file diff --git a/maps/rooms/20x20/dark_halls_2.hard b/maps/rooms/20x20/dark_halls_2.hard index f071dfb6..c952bad6 100644 --- a/maps/rooms/20x20/dark_halls_2.hard +++ b/maps/rooms/20x20/dark_halls_2.hard @@ -1,20 +1,20 @@ ####..########..#### ####..########..#### ####..###vv###..#### -####..###..###..#### +#[##..###..###..##[# .................... .................... ####..###..###..#### ####..###..###..#### ##>..............<## -####..###..###..#### +####..#]#..###..#### ####..###..###..#### ##>..............<## -####..###..###..#### +####..###..#]#..#### ####..###..###..#### .................... .............X...... -####..###..###..#### +#]##..###..###..##]# ####..###^^###..#### ####..########..#### ####..########..#### \ No newline at end of file diff --git a/maps/rooms/20x20/dark_room_1.hard b/maps/rooms/20x20/dark_room_1.hard index 6d18d4fe..ad3ef7e4 100644 --- a/maps/rooms/20x20/dark_room_1.hard +++ b/maps/rooms/20x20/dark_room_1.hard @@ -6,10 +6,10 @@ .................... ####............#### ####.............<## -##>.............#### -####.............<## -##>.............#### -####.............<## +##>......]!.....#### +####....!##!.....<## +##>.....!##!....#### +####.....![......<## ##>.............#### ####............#### .................... diff --git a/maps/rooms/20x20/dark_room_2.hard b/maps/rooms/20x20/dark_room_2.hard index 24911320..549d7b34 100644 --- a/maps/rooms/20x20/dark_room_2.hard +++ b/maps/rooms/20x20/dark_room_2.hard @@ -2,18 +2,18 @@ ####..########..#### ####..########..#### ####..########..#### -....X....X.......... -..............X..... -####..X..X......#### -####........X...#### +....X....##......... +.........##...X..... +####..X..##.....#### +###{.....##.X...}### ####.X....X.....#### -####..........X.#### -####..X....X....F.p# +#######......####### +#######....X.##F..p# ####...X.....X..#### -####............#### -####..X....X....#### -.................... -..............X..... +###{.....##.....}### +####..X..##X....#### +.........##......... +.........##...X..... ####..########..#### ####..########..#### ####..########..#### diff --git a/maps/rooms/20x20/pillar_maze_bottom_1.orb b/maps/rooms/20x20/pillar_maze_bottom_1.orb index a63a1fc0..8908c97c 100644 --- a/maps/rooms/20x20/pillar_maze_bottom_1.orb +++ b/maps/rooms/20x20/pillar_maze_bottom_1.orb @@ -1,17 +1,17 @@ #################### #################### -#################### +###[############]### ##................## ##..!.!!!.!.!.!.!.## ##........!...!...<# -##..!.!*!.!.!.!.!.## +##..!.!*!.!.}.!.!.## ##....!...........## ##..!.!!!.!.!!!.!.## -#>........!.......## -##..!.!.!.!.!.!.!.## +#>........!.......}# +##..!.!.{.!.!.!.!.## ##............!...## ##..!!!.!.!.!.!.!.## -##................<# +#{................<# ##..!.!.!.!.!.!.!.## ##..........!.....## ####..########..#### diff --git a/maps/rooms/20x20/pillar_maze_bottom_2.orb b/maps/rooms/20x20/pillar_maze_bottom_2.orb index eb9ee7be..6290969c 100644 --- a/maps/rooms/20x20/pillar_maze_bottom_2.orb +++ b/maps/rooms/20x20/pillar_maze_bottom_2.orb @@ -4,13 +4,13 @@ ##w...............## ##!!!.!!!.!.!.!.!.## ##.p......!.s.!...## -##.!!.!.![!.!.!.!.## +##.!!.!.![!.!.[.!.## ##....!...........## ##!]!.!.!.!.!!.!!.## ##..!.....!...*...## ##..!.!.!.!.!!.!!.## ##............!...## -##.!!!!.!.!.!.!.!.## +##.!![!.!.!.!.!.!.## ##................## ##..!.!.!.!.!.].!.## ##....!.!...!....p## diff --git a/maps/rooms/20x20/pillar_maze_top_1.orb b/maps/rooms/20x20/pillar_maze_top_1.orb index 2f2fa4d9..398eca2b 100644 --- a/maps/rooms/20x20/pillar_maze_top_1.orb +++ b/maps/rooms/20x20/pillar_maze_top_1.orb @@ -4,14 +4,14 @@ ##................## ##..!.!!!.!.!.!.!.## #{........!.s.!...<# -##..!.!.!.!.!.!.!.## +##..!.!.[.!.!.!.!.## ##..p.!...........## ##..!.!.!.!.!]!.!.## -#>........!.......## +#>........!.......}# ##..!.!.!!!.!.!.!.## ##............!.w.## -##..![!.!!!.!.!.!.## -##......!*........<# +##..![!.!]!.!.!.!.## +##{.....!*........<# ##..!.!.!.!.!.}.!.## ##..p.......!.....## #################### diff --git a/maps/rooms/20x20/pillar_maze_top_2.orb b/maps/rooms/20x20/pillar_maze_top_2.orb index b04cc76f..f4bb2627 100644 --- a/maps/rooms/20x20/pillar_maze_top_2.orb +++ b/maps/rooms/20x20/pillar_maze_top_2.orb @@ -2,18 +2,18 @@ ###{..}######{..}### ####..########..#### ##................## -##..!.!!!.!.!.!.!.## +##..!.!!!.].!.!.!.## #{..p.....!...!...<# ##..!.![!.!.!.!.!.## ##....!.......!...## ##..!.!.!.!.!]!.!.## #>....!.s.!.......## -##..!.!.!.!.!.!.!.## -##..........!.!.w.## -##!!![!.!!!.!.!.!.## +##..!.!.!.!.!.!.!.}# +#{..........!.!.w.## +##!!![!.!!!.[.!.!.## ##..*!...!........<# ##.!!!!.!!!.!.}.!.## -##..........!...p.## +#{..........!...p.## #################### #################### #################### diff --git a/maps/rooms/20x20/teleporter_hall.hard b/maps/rooms/20x20/teleporter_hall.hard index d1366a58..a96b4e33 100644 --- a/maps/rooms/20x20/teleporter_hall.hard +++ b/maps/rooms/20x20/teleporter_hall.hard @@ -1,20 +1,20 @@ ####..########..#### ####..##[##[##..#### -####........T...#### -###!...T........!### -...........T........ -....T.........T..... -##....T..T..T.....## -##.................# +####.....##.....#### +######...##...###### +.........##T........ +....T....##...T..... +##....T..##.T.....## +##.......!!........! #{.T....T...T...T.1# -##....T...T...T....# -#..T..............## +##....T...T...T....! +!..T..............## #2....T....T..T...}# -#..............T..## -##.T..T...T......T## -.............T...... -........T........... -###!............!### -####...T...T....#### +!........!!....T..## +##.T..T..##......T## +.........##..T...... +........T##......... +######...##...###### +####.....##.....#### ####..##]##]##..#### ####..########..#### \ No newline at end of file diff --git a/maps/test/itemRoom.maze b/maps/test/itemRoom.maze index 40d729bb..78c8c4f2 100644 --- a/maps/test/itemRoom.maze +++ b/maps/test/itemRoom.maze @@ -12,7 +12,7 @@ {................} #...@...@....@...# #................# -{................} -#T..............o# -#>...............# +{......MM......XX} +#T.............Xo# +#>.............XX# ################## \ No newline at end of file diff --git a/maps/test/mirrorRoom.maze b/maps/test/mirrorRoom.maze new file mode 100644 index 00000000..9a376116 --- /dev/null +++ b/maps/test/mirrorRoom.maze @@ -0,0 +1,9 @@ +#######[############## +#...........@........# +#.M..................} +{...........*........# +#.........n..........# +{....................} +#........n...........# +#.............o......} +###################### \ No newline at end of file diff --git a/src/client/audio/audiomanager.cpp b/src/client/audio/audiomanager.cpp index b5322890..87371a45 100644 --- a/src/client/audio/audiomanager.cpp +++ b/src/client/audio/audiomanager.cpp @@ -103,7 +103,9 @@ void AudioManager::doTick(glm::vec3 player_pos, auto source = light_sources.at(i); if (source.has_value() && source->type == ObjectType::Torchlight) { this->serverLightSFXs.at(i)->setPosition(source->physics.corner.x, source->physics.corner.y, source->physics.corner.z); - this->serverLightSFXs.at(i)->play(); + if (this->serverLightSFXs.at(i)->getStatus() != sf::SoundSource::Status::Playing) { + this->serverLightSFXs.at(i)->play(); + } } else { this->serverLightSFXs.at(i)->stop(); } diff --git a/src/client/camera.cpp b/src/client/camera.cpp index 7a8b33a3..1eefe444 100644 --- a/src/client/camera.cpp +++ b/src/client/camera.cpp @@ -67,9 +67,9 @@ void Camera::update(float xpos, float ypos) { pitch = -70.0f; glm::vec3 front; - front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch)); + front.x = cos(glm::radians(yaw + 90.0f)) * cos(glm::radians(pitch)); front.y = sin(glm::radians(pitch)); - front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch)); + front.z = sin(glm::radians(yaw + 90.0f)) * cos(glm::radians(pitch)); cameraFront = glm::normalize(front); glm::vec3 oldCamUp = cameraUp; @@ -97,6 +97,10 @@ void Camera::updatePos(glm::vec3 pos) { cameraPos = pos; } +void Camera::setPitch(float pitch) { + this->pitch = pitch; +} + DungeonMasterCamera::DungeonMasterCamera() : Camera() { pitch = -89.0f; this->farClip = 500.0f; @@ -148,4 +152,5 @@ void DungeonMasterCamera::update(float xpos, float ypos) { this->view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp); viewProjMat = this->projection * this->view; -} \ No newline at end of file +} + diff --git a/src/client/client.cpp b/src/client/client.cpp index f6cfc2d4..85c18881 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -255,7 +255,6 @@ bool Client::init() { auto player_use_potion_path = graphics_assets_dir / "animations/drink.fbx"; this->player_model = std::make_unique(player_model_path.string(), false); - this->player_model->scaleAbsolute(0.0025); Animation* player_walk = new Animation(player_walk_path.string(), this->player_model.get()); Animation* player_jump = new Animation(player_jump_path.string(), this->player_model.get()); Animation* player_idle = new Animation(player_idle_path.string(), this->player_model.get()); @@ -303,7 +302,10 @@ void Client::displayCallback() { if (this->gameState.phase == GamePhase::TITLE_SCREEN) { glClearColor(0.0f, 0.0f, 0.0f, 1.0f); } else if (this->gameState.phase == GamePhase::LOBBY) { - glClearColor(0.8f, 0.8f, 0.8f, 1.0f); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + } else if (this->gameState.phase == GamePhase::INTRO_CUTSCENE) { + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + this->draw(); } else if (this->gameState.phase == GamePhase::GAME) { glClearColor(0.0f, 0.0f, 0.0f, 1.0f); this->draw(); @@ -317,7 +319,7 @@ void Client::displayCallback() { this->setWorldPos(); this->gui.layoutFrame(this->gui_state); - this->gui.handleInputs(mouse_xpos, mouse_ypos, is_left_mouse_down); + this->gui.handleInputs(mouse_xpos, mouse_ypos, is_left_mouse_down, is_right_mouse_down); this->gui.renderFrame(); /* Poll for and process events */ @@ -339,12 +341,36 @@ void Client::sendTrapEvent(bool hover, bool place, ModelType trapType) { case ModelType::FloorSpikeHorizontal: this->session->sendEvent(Event(eid, EventType::TrapPlacement, TrapPlacementEvent(eid, this->world_pos, CellType::FloorSpikeHorizontal, hover, place))); break; - case ModelType::SunGod: + case ModelType::FireballTrapUp: this->session->sendEvent(Event(eid, EventType::TrapPlacement, TrapPlacementEvent(eid, this->world_pos, CellType::FireballTrapUp, hover, place))); break; + case ModelType::FireballTrapDown: + this->session->sendEvent(Event(eid, EventType::TrapPlacement, TrapPlacementEvent(eid, this->world_pos, CellType::FireballTrapDown, hover, place))); + break; + case ModelType::FireballTrapLeft: + this->session->sendEvent(Event(eid, EventType::TrapPlacement, TrapPlacementEvent(eid, this->world_pos, CellType::FireballTrapLeft, hover, place))); + break; + case ModelType::FireballTrapRight: + this->session->sendEvent(Event(eid, EventType::TrapPlacement, TrapPlacementEvent(eid, this->world_pos, CellType::FireballTrapRight, hover, place))); + break; + case ModelType::ArrowTrapUp: + this->session->sendEvent(Event(eid, EventType::TrapPlacement, TrapPlacementEvent(eid, this->world_pos, CellType::ArrowTrapUp, hover, place))); + break; + case ModelType::ArrowTrapDown: + this->session->sendEvent(Event(eid, EventType::TrapPlacement, TrapPlacementEvent(eid, this->world_pos, CellType::ArrowTrapDown, hover, place))); + break; + case ModelType::ArrowTrapLeft: + this->session->sendEvent(Event(eid, EventType::TrapPlacement, TrapPlacementEvent(eid, this->world_pos, CellType::ArrowTrapLeft, hover, place))); + break; + case ModelType::ArrowTrapRight: + this->session->sendEvent(Event(eid, EventType::TrapPlacement, TrapPlacementEvent(eid, this->world_pos, CellType::ArrowTrapRight, hover, place))); + break; case ModelType::SpikeTrap: this->session->sendEvent(Event(eid, EventType::TrapPlacement, TrapPlacementEvent(eid, this->world_pos, CellType::SpikeTrap, hover, place))); break; + case ModelType::TeleporterTrap: + this->session->sendEvent(Event(eid, EventType::TrapPlacement, TrapPlacementEvent(eid, this->world_pos, CellType::TeleporterTrap, hover, place))); + break; case ModelType::Lightning: this->session->sendEvent(Event(eid, EventType::TrapPlacement, TrapPlacementEvent(eid, this->world_pos, CellType::Lightning, hover, place))); break; @@ -419,21 +445,62 @@ void Client::idleCallback() { this->session->sendEvent(Event(eid, EventType::StartAction, StartActionEvent(eid, glm::vec3(0.0f, 1.0f, 0.0f), ActionType::Jump))); } - if (this->session->getInfo().is_dungeon_master.value()) { + // DM not placing + if (this->session->getInfo().is_dungeon_master.value() && !is_left_mouse_down) { + // zoom in if (is_held_i) { this->session->sendEvent(Event(eid, EventType::StartAction, StartActionEvent(eid, glm::vec3(0.0f, -1.0f, 0.0f), ActionType::Zoom))); } + + // zoom out if (is_held_o) { this->session->sendEvent(Event(eid, EventType::StartAction, StartActionEvent(eid, glm::vec3(0.0f, 1.0f, 0.0f), ActionType::Zoom))); } + + auto obj = this->gameState.objects.at(eid); + + auto model = obj->trapInventoryInfo->inventory[obj->trapInventoryInfo->selected - 1]; + + // udpate orientation if right clicked + if (is_right_mouse_down) { + this->orientation = (this->orientation + 1) % 4; // 4 possible directions + } - // send one event - if ((is_held_down || is_held_i || is_held_left || is_held_right || is_held_up || is_held_o) && is_pressed_p) - sendTrapEvent(true, false, (this->gameState.objects.at(eid))->trapInventoryInfo->inventory[(this->gameState.objects.at(eid))->trapInventoryInfo->selected-1]); + if (model == ModelType::SunGod) { + const ModelType sunGodCellType[] = { ModelType::FireballTrapUp, ModelType::FireballTrapRight, ModelType::FireballTrapDown, ModelType::FireballTrapLeft }; + + model = sunGodCellType[this->orientation]; + } + + if (model == ModelType::ArrowTrap) { + const ModelType arrowTrapCellType[] = { ModelType::ArrowTrapUp, ModelType::ArrowTrapRight, ModelType::ArrowTrapDown, ModelType::ArrowTrapLeft }; + + model = arrowTrapCellType[this->orientation]; + } + + // send a trap event regardless if DM + sendTrapEvent(true, false, model); } - if (this->session->getInfo().is_dungeon_master.value() && is_pressed_p && is_left_mouse_down) { - sendTrapEvent(false, true, (this->gameState.objects.at(eid))->trapInventoryInfo->inventory[(this->gameState.objects.at(eid))->trapInventoryInfo->selected-1]); + // DM actually placing now + if (this->session->getInfo().is_dungeon_master.value() && is_left_mouse_down) { + auto obj = this->gameState.objects.at(eid); + + auto model = obj->trapInventoryInfo->inventory[obj->trapInventoryInfo->selected - 1]; + + if (model == ModelType::SunGod) { + const ModelType sunGodCellType[] = { ModelType::FireballTrapUp, ModelType::FireballTrapRight, ModelType::FireballTrapDown, ModelType::FireballTrapLeft }; + + model = sunGodCellType[this->orientation]; + } + + if (model == ModelType::ArrowTrap) { + const ModelType arrowTrapCellType[] = { ModelType::ArrowTrapUp, ModelType::ArrowTrapRight, ModelType::ArrowTrapDown, ModelType::ArrowTrapLeft }; + + model = arrowTrapCellType[this->orientation]; + } + + sendTrapEvent(false, true, model); } // If movement 0, send stopevent @@ -484,19 +551,87 @@ void Client::processServerInput(bool allow_defer) { else { if (phase_change || (old_phase != GamePhase::GAME && this->gameState.phase == GamePhase::GAME)) { std::cout << "game phase change!" << std::endl; + + // Stop play menu theme music + audioManager->stopMusic(ClientMusic::MenuTheme); + // set to Dungeon Master POV if DM if (this->session->getInfo().is_dungeon_master.value()) { - std::cout << "dungeon master cam!" << std::endl; this->cam = std::make_unique(); + + // Play Maze Exploration theme (DM) + audioManager->playMusic(ClientMusic::MazeExplorationDMTheme); + // TODO: fix race condition where this doesn't get received in time when reconnecting because the server is doing way more stuff and is delayed } + else { + // If player, play Maze Exploration theme (players) + audioManager->playMusic(ClientMusic::MazeExplorationPlayersTheme); + } this->gui_state = GUIState::GAME_HUD; - - audioManager->stopMusic(ClientMusic::TitleTheme); - audioManager->playMusic(ClientMusic::GameTheme); + phase_change = false; } + else if (this->gameState.phase == GamePhase::GAME || this->gameState.phase == GamePhase::RESULTS) { + // TODO: Keep track of the match phase and who won the match to play the correct client + // music + + if (this->gameState.phase == GamePhase::GAME) { + // Keep track of the match phase from the previous tick (though initialize to current tick + // for initialization) + static MatchPhase previousMatchPhase = this->gameState.matchPhase; + + if (previousMatchPhase != this->gameState.matchPhase) { + std::cout << "Match phase change!" << std::endl; + + previousMatchPhase = this->gameState.matchPhase; + + // Play relay race theme + if (this->session->getInfo().is_dungeon_master.value()) { + std::cout << "Relay Race - DM!" << std::endl; + + // Play DM relay race theme + audioManager->stopMusic(ClientMusic::MazeExplorationDMTheme); + + audioManager->playMusic(ClientMusic::RelayRaceDMTheme); + + } + else { + // Player in Relay Race + std::cout << "Relay Race - Player!" << std::endl; + + // Play Player relay race theme + audioManager->stopMusic(ClientMusic::MazeExplorationPlayersTheme); + + audioManager->playMusic(ClientMusic::RelayRacePlayersTheme); + } + } + } + else { + // Game has ended - GamePhase::Results + std::cout << "Game has ended! Playing victory music." << std::endl; + + // Play victory theme of the team that won + if (this->session->getInfo().is_dungeon_master.value()) { + // Stop relay race music of the DM + audioManager->stopMusic(ClientMusic::RelayRaceDMTheme); + } + else { + // Stop relay race music of the Players + audioManager->stopMusic(ClientMusic::RelayRacePlayersTheme); + } + + // Play victory theme for team that won + if (this->gameState.playerVictory) { + audioManager->playSFX(ClientSFX::VictoryThemePlayers); + } + else { + audioManager->playSFX(ClientSFX::VictoryThemeDM); + } + } + + } } } else if (event.type == EventType::LoadSoundCommands) { auto self_eid = this->session->getInfo().client_eid; @@ -518,6 +653,11 @@ void Client::processServerInput(bool allow_defer) { // update intensity with incoming intensity this->closest_light_sources[i]->pointLightInfo->intensity = updated_light_source.lightSources[i]->intensity; } + } else if (event.type == EventType::LoadIntroCutscene) { + const auto& data = boost::get(event.data); + this->intro_cutscene = data; + this->gui_state = GUIState::INTRO_CUTSCENE; + this->audioManager->stopMusic(ClientMusic::MenuTheme); } this->events_received.pop_front(); @@ -605,12 +745,40 @@ void Client::geometryPass() { this->deferred_geometry_shader->setMat4("viewProj", viewProj); // this->deferred_geometry_shader->setMat4("finalBonesMatrices[0]", transforms[i]); - auto eid = this->session->getInfo().client_eid.value(); + if (!this->session->getInfo().client_eid.has_value()) { + std::cerr << "WARNING: trying to do geometry loop without knowing self eid\n"; + return; + } + + EntityID self_eid = this->session->getInfo().client_eid.value(); bool is_dm = this->session->getInfo().is_dungeon_master.value(); - glm::vec3 my_pos = this->gameState.objects[eid]->physics.corner; + std::unordered_map>* objects = &this->gameState.objects; + if (this->gameState.phase == GamePhase::INTRO_CUTSCENE) { + if (!this->intro_cutscene.has_value()) { + return; // haven't received cutscene packet yet + } + + // use different values for the intro cutscene + if (is_dm) { + self_eid = this->intro_cutscene->dm_eid; + // look down + this->cam->update(WINDOW_WIDTH / 2, WINDOW_HEIGHT - 5); + } else { + self_eid = this->intro_cutscene->pov_eid; + // look straight ahead + this->cam->update(WINDOW_WIDTH / 2, WINDOW_HEIGHT / 2); + } + objects = &this->intro_cutscene->state.objects; + } + + glm::vec3 my_pos = (*objects)[self_eid]->physics.corner; + + double currentTime = glfwGetTime(); + double timeElapsed = currentTime - lastTime; + lastTime = currentTime; // draw all objects to g-buffer - for (auto& [id, sharedObject] : this->gameState.objects) { + for (auto& [id, sharedObject] : *objects) { if (!sharedObject.has_value()) { continue; } @@ -637,10 +805,16 @@ void Client::geometryPass() { } // don't render yourself - if (this->session->getInfo().client_eid.has_value() && sharedObject->globalID == this->session->getInfo().client_eid.value()) { + if (sharedObject->globalID == self_eid) { // TODO: Update the player eye level to an acceptable level + // glm::vec3 pos = sharedObject->physics.getCenterPosition(); + // pos.y -= (sharedObject->physics.dimensions.y / 2.0f); + // pos.y += PLAYER_EYE_LEVEL; + // cam->updatePos(pos); + glm::vec3 pos = sharedObject->physics.getCenterPosition(); + pos.y -= sharedObject->physics.dimensions.y / 2.0f; pos.y += PLAYER_EYE_LEVEL; cam->updatePos(pos); @@ -662,7 +836,7 @@ void Client::geometryPass() { animManager->setAnimation(sharedObject->globalID, sharedObject->type, sharedObject->animState); /* Update model animation */ - animManager->updateAnimation(0.025f); + animManager->updateAnimation(timeElapsed); auto transforms = animManager->getFinalBoneMatrices(); for (int i = 0; i < (transforms.size() < 100 ? transforms.size() : 100); ++i) { @@ -672,6 +846,8 @@ void Client::geometryPass() { if (!sharedObject->playerInfo->render) { break; } // dont render while invisible auto player_pos = sharedObject->physics.getCenterPosition(); + player_pos.y -= (sharedObject->physics.dimensions.y / 2.0f); + auto player_dir = sharedObject->physics.facing; if (player_dir == glm::vec3(0.0f)) { @@ -680,23 +856,25 @@ void Client::geometryPass() { player_dir.y = 0.0f; this->player_model->rotateAbsolute(glm::normalize(player_dir), true); this->player_model->translateAbsolute(player_pos); + // this->player_model->setDimensions(sharedObject->physics.dimensions); + this->player_model->scaleAbsolute(PLAYER_MODEL_SCALE); this->player_model->draw( this->deferred_geometry_shader.get(), this->cam->getPos(), true); + break; } // CHANGE THIS case ObjectType::DungeonMaster: { // don't render yourself - if (this->session->getInfo().client_eid.has_value() && sharedObject->globalID == this->session->getInfo().client_eid.value()) { + if (sharedObject->globalID == self_eid) { // TODO: Update the player eye level to an acceptable level glm::vec3 pos = sharedObject->physics.getCenterPosition(); pos.y += PLAYER_EYE_LEVEL; cam->updatePos(pos); break; } - auto lightPos = glm::vec3(0.0f, 10.0f, 0.0f); auto player_pos = sharedObject->physics.corner; auto player_dir = sharedObject->physics.facing; @@ -948,14 +1126,17 @@ void Client::geometryPass() { break; } case ObjectType::WeaponCollider: { - /* if (sharedObject->weaponInfo->lightning) { if (!sharedObject->weaponInfo->attacked) { - this->item_model->setDimensions(sharedObject->physics.dimensions); - this->item_model->translateAbsolute(sharedObject->physics.getCenterPosition()); + glm::vec3 preview_dims = sharedObject->physics.dimensions; + preview_dims.y = 0.01f; + this->item_model->setDimensions(preview_dims); + glm::vec3 preview_pos = sharedObject->physics.getCenterPosition(); + preview_pos.y = 0.0f; + this->item_model->translateAbsolute(preview_pos); this->item_model->draw(this->deferred_geometry_shader.get(), this->cam->getPos(), - false); + true); } else { this->item_model->setDimensions(sharedObject->physics.dimensions); this->item_model->translateAbsolute(sharedObject->physics.getCenterPosition()); @@ -964,22 +1145,19 @@ void Client::geometryPass() { true); } } - else { - if (sharedObject->weaponInfo->attacked) { - this->item_model->setDimensions(sharedObject->physics.dimensions); - this->item_model->translateAbsolute(sharedObject->physics.getCenterPosition()); - this->item_model->draw(this->deferred_geometry_shader.get(), - this->cam->getPos(), - false); - } - }*/ + // dont render weapon colliders + break; + } + case ObjectType::Mirror: { + if (!sharedObject->iteminfo->held && !sharedObject->iteminfo->used) { + this->item_model->setDimensions(sharedObject->physics.dimensions); + this->item_model->translateAbsolute(sharedObject->physics.getCenterPosition()); + this->item_model->draw(this->deferred_geometry_shader.get(), + this->cam->getPos(), + true); + } break; } - // case ObjectType::Torchlight: { - // this->torchlight_model->setDimensions(2.0f * sharedObject->physics.dimensions); - // this->torchlight_model->translateAbsolute(sharedObject->physics.getCenterPosition()); - // this->torchlight_model->draw(this->deferred_geometry_shader.get(), this->cam->getPos(), true); - // } default: break; } @@ -997,9 +1175,26 @@ void Client::lightingPass() { glActiveTexture(GL_TEXTURE2); glBindTexture(GL_TEXTURE_2D, gAlbedoSpec); - bool is_dm = this->session->getInfo().is_dungeon_master.value(); + auto* closest_lights = &this->closest_light_sources; + auto* gamestate_objects = &this->gameState.objects; auto eid = this->session->getInfo().client_eid.value(); - glm::vec3 my_pos = this->gameState.objects[eid]->physics.corner; + bool is_dm = this->session->getInfo().is_dungeon_master.value(); + if (this->gameState.phase == GamePhase::INTRO_CUTSCENE) { + if (!this->intro_cutscene.has_value()) { + return; // don't have cutscene info yet + } + + // replace with intro cutscene state + closest_lights = &this->intro_cutscene->lights; + gamestate_objects = &this->intro_cutscene->state.objects; + if (is_dm) { + eid = this->intro_cutscene->dm_eid; + } else { + eid = this->intro_cutscene->pov_eid; + } + } + + glm::vec3 my_pos = gamestate_objects->at(eid)->physics.corner; std::shared_ptr lighting_shader = is_dm ? dm_deferred_lighting_shader: deferred_lighting_shader; @@ -1026,8 +1221,9 @@ void Client::lightingPass() { lighting_shader->setVec3("dirLights[" + i_s + "].specular_color", specular); } } - for (int i = 0; i < this->closest_light_sources.size(); i++) { - boost::optional& curr_source = this->closest_light_sources.at(i); + + for (int i = 0; i < closest_lights->size(); i++) { + boost::optional& curr_source = closest_lights->at(i); if (!curr_source.has_value()) { continue; } @@ -1086,7 +1282,7 @@ void Client::lightingPass() { this->deferred_light_box_shader->use(); glm::mat4 viewProj = this->cam->getViewProj(); this->deferred_light_box_shader->setMat4("viewProj", viewProj); - for (auto& [id, sharedObject] : this->gameState.objects) { + for (auto& [id, sharedObject] : *gamestate_objects) { if (!sharedObject.has_value()) { continue; } @@ -1106,6 +1302,8 @@ void Client::lightingPass() { } SharedPointLightInfo& properties = sharedObject->pointLightInfo.value(); + glm::vec3 v(1.0f); + this->deferred_light_box_shader->use(); this->deferred_light_box_shader->setVec3("lightColor", properties.diffuse_color); this->torchlight_model->setDimensions(2.0f * sharedObject->physics.dimensions); @@ -1125,16 +1323,17 @@ void Client::drawBbox(boost::optional object) { item_model->setDimensions(object->physics.dimensions); item_model->translateAbsolute(bbox_pos); + item_model->rotateAbsolute(glm::normalize(object->physics.facing), true); item_model->draw(this->deferred_geometry_shader.get(), this->cam->getPos(), - false); + true); } } // callbacks - for Interaction void Client::keyCallback(GLFWwindow *window, int key, int scancode, int action, int mods) { if (this->gui_state == GUIState::INITIAL_LOAD) { - this->audioManager->playMusic(ClientMusic::TitleTheme); + this->audioManager->playMusic(ClientMusic::MenuTheme); this->gui_state = GUIState::TITLE_SCREEN; return; } @@ -1159,6 +1358,10 @@ void Client::keyCallback(GLFWwindow *window, int key, int scancode, int action, if (this->gameState.phase == GamePhase::GAME) { if (this->gui_state == GUIState::GAME_ESC_MENU) { this->gui_state = GUIState::GAME_HUD; + + if (is_dm.has_value() && is_dm.value()) { + this->cam->setPitch(-89.0); + } } else if (this->gui_state == GUIState::GAME_HUD) { this->gui_state = GUIState::GAME_ESC_MENU; @@ -1255,19 +1458,6 @@ void Client::keyCallback(GLFWwindow *window, int key, int scancode, int action, case GLFW_KEY_O: // zoom out is_held_o = true; break; - case GLFW_KEY_P: // to place or not to place - is_pressed_p = !is_pressed_p; - if (is_pressed_p) { - // unhighlight hover - if (eid.has_value()) { - // nothing being placed, so the CellType we pass shouldn't matter! - sendTrapEvent(true, false, (this->gameState.objects.at(eid.value()))->trapInventoryInfo->inventory[(this->gameState.objects.at(eid.value()))->trapInventoryInfo->selected-1]); - } - } - else { - sendTrapEvent(false, false, (this->gameState.objects.at(eid.value()))->trapInventoryInfo->inventory[(this->gameState.objects.at(eid.value()))->trapInventoryInfo->selected-1]); - } - break; /* Send an event to start 'shift' movement (i.e. sprint) */ case GLFW_KEY_LEFT_SHIFT: if (eid.has_value() && !this->session->getInfo().is_dungeon_master.value()) { @@ -1291,29 +1481,29 @@ void Client::keyCallback(GLFWwindow *window, int key, int scancode, int action, switch (key) { case GLFW_KEY_S: is_held_down = false; - if (eid.has_value() && this->session->getInfo().is_dungeon_master.value() && is_pressed_p) { - sendTrapEvent(true, false, (this->gameState.objects.at(eid.value()))->trapInventoryInfo->inventory[(this->gameState.objects.at(eid.value()))->trapInventoryInfo->selected-1]); + if (eid.has_value() && this->session->getInfo().is_dungeon_master.value()) { + //sendTrapEvent(true, false, (this->gameState.objects.at(eid.value()))->trapInventoryInfo->inventory[(this->gameState.objects.at(eid.value()))->trapInventoryInfo->selected-1]); } break; case GLFW_KEY_W: is_held_up = false; - if (eid.has_value() && this->session->getInfo().is_dungeon_master.value() && is_pressed_p) { - sendTrapEvent(true, false, (this->gameState.objects.at(eid.value()))->trapInventoryInfo->inventory[(this->gameState.objects.at(eid.value()))->trapInventoryInfo->selected-1]); + if (eid.has_value() && this->session->getInfo().is_dungeon_master.value()) { + //sendTrapEvent(true, false, (this->gameState.objects.at(eid.value()))->trapInventoryInfo->inventory[(this->gameState.objects.at(eid.value()))->trapInventoryInfo->selected-1]); } break; case GLFW_KEY_A: is_held_left = false; - if (eid.has_value() && this->session->getInfo().is_dungeon_master.value() && is_pressed_p) { - sendTrapEvent(true, false, (this->gameState.objects.at(eid.value()))->trapInventoryInfo->inventory[(this->gameState.objects.at(eid.value()))->trapInventoryInfo->selected-1]); + if (eid.has_value() && this->session->getInfo().is_dungeon_master.value()) { + //sendTrapEvent(true, false, (this->gameState.objects.at(eid.value()))->trapInventoryInfo->inventory[(this->gameState.objects.at(eid.value()))->trapInventoryInfo->selected-1]); } break; case GLFW_KEY_D: is_held_right = false; - if (eid.has_value() && this->session->getInfo().is_dungeon_master.value() && is_pressed_p) { - sendTrapEvent(true, false, (this->gameState.objects.at(eid.value()))->trapInventoryInfo->inventory[(this->gameState.objects.at(eid.value()))->trapInventoryInfo->selected-1]); + if (eid.has_value() && this->session->getInfo().is_dungeon_master.value()) { + //sendTrapEvent(true, false, (this->gameState.objects.at(eid.value()))->trapInventoryInfo->inventory[(this->gameState.objects.at(eid.value()))->trapInventoryInfo->selected-1]); } break; @@ -1331,13 +1521,13 @@ void Client::keyCallback(GLFWwindow *window, int key, int scancode, int action, case GLFW_KEY_O: // zoom out is_held_o = false; - if (eid.has_value() && this->session->getInfo().is_dungeon_master.value() && is_pressed_p) { - sendTrapEvent(true, false, (this->gameState.objects.at(eid.value()))->trapInventoryInfo->inventory[(this->gameState.objects.at(eid.value()))->trapInventoryInfo->selected-1]); + if (eid.has_value() && this->session->getInfo().is_dungeon_master.value()) { + //sendTrapEvent(true, false, (this->gameState.objects.at(eid.value()))->trapInventoryInfo->inventory[(this->gameState.objects.at(eid.value()))->trapInventoryInfo->selected-1]); } break; case GLFW_KEY_I: // zoom out - if (eid.has_value() && this->session->getInfo().is_dungeon_master.value() && is_pressed_p) { - sendTrapEvent(true, false, (this->gameState.objects.at(eid.value()))->trapInventoryInfo->inventory[(this->gameState.objects.at(eid.value()))->trapInventoryInfo->selected-1]); + if (eid.has_value() && this->session->getInfo().is_dungeon_master.value()) { + //sendTrapEvent(true, false, (this->gameState.objects.at(eid.value()))->trapInventoryInfo->inventory[(this->gameState.objects.at(eid.value()))->trapInventoryInfo->selected-1]); } is_held_i = false; break; @@ -1366,47 +1556,47 @@ void Client::keyCallback(GLFWwindow *window, int key, int scancode, int action, void Client::scrollCallback(GLFWwindow* window, double xoffset, double yoffset) { std::optional eid; - - if (this->session != nullptr && this->session->getInfo().client_eid.has_value()) { - eid = this->session->getInfo().client_eid.value(); - } - std::optional is_dm; - if (this->session != nullptr && this->session->getInfo().is_dungeon_master.has_value()) { - is_dm = this->session->getInfo().is_dungeon_master.value(); + if (this->session != nullptr) { + eid = this->session->getInfo().client_eid; + is_dm = this->session->getInfo().is_dungeon_master; } + if (!eid.has_value()) + return; + auto self = this->gameState.objects.at(eid.value()); if (yoffset >= 1) { - if (eid.has_value()) { - this->session->sendEvent(Event(eid.value(), EventType::SelectItem, SelectItemEvent(eid.value(), -1))); - } + this->session->sendEvent(Event(eid.value(), EventType::SelectItem, SelectItemEvent(eid.value(), -1))); - if (is_dm.has_value() && is_dm.value() && is_pressed_p) { - // optimistic update on scroll, otherwise will lag + if (is_dm.has_value() && is_dm.value()) { + // optimistic update on scroll, otherwise might lag int idx = self->trapInventoryInfo->selected; - if (self->trapInventoryInfo->selected - 1 == 0) + + if (idx - 1 == 0) idx = TRAP_INVENTORY_SIZE; + idx -= 1; + sendTrapEvent(true, false, self->trapInventoryInfo->inventory[idx - 1]); } } if (yoffset <= -1) { - if (eid.has_value()) { - this->session->sendEvent(Event(eid.value(), EventType::SelectItem, SelectItemEvent(eid.value(), 1))); - } + this->session->sendEvent(Event(eid.value(), EventType::SelectItem, SelectItemEvent(eid.value(), 1))); - if (is_dm.has_value() && is_dm.value() && is_pressed_p) { - // optimistic update on scroll, otherwise will lag + if (is_dm.has_value() && is_dm.value()) { + // optimistic update on scroll, otherwise might lag int idx = self->trapInventoryInfo->selected; - if (self->trapInventoryInfo->selected + 1 > TRAP_INVENTORY_SIZE) + if (idx + 1 > TRAP_INVENTORY_SIZE) idx = 1; + idx += 1; + sendTrapEvent(true, false, self->trapInventoryInfo->inventory[idx - 1]); } } @@ -1422,11 +1612,14 @@ void Client::mouseCallback(GLFWwindow* window, double xposIn, double yposIn) { / mouse_xpos = new_mouse_xpos; mouse_ypos = new_mouse_ypos; - if (is_pressed_p) { - auto eid = this->session->getInfo().client_eid.value(); + if (this->gameState.phase == GamePhase::GAME && this->gui_state == GUIState::GAME_HUD) { + if (this->session->getInfo().is_dungeon_master.has_value() && + this->session->getInfo().is_dungeon_master.value()) { + auto eid = this->session->getInfo().client_eid; + auto obj = this->gameState.objects.at(eid.value()); - // the actual trap doesn't matter, this is just for highlighting purposes - sendTrapEvent(true, false, (this->gameState.objects.at(eid))->trapInventoryInfo->inventory[(this->gameState.objects.at(eid))->trapInventoryInfo->selected-1]); + //sendTrapEvent(true, false, obj->trapInventoryInfo->inventory[obj->trapInventoryInfo->selected - 1]); + } } } @@ -1457,6 +1650,15 @@ void Client::mouseButtonCallback(GLFWwindow* window, int button, int action, int } } + if (button == GLFW_MOUSE_BUTTON_RIGHT) { + if (action == GLFW_PRESS) { + is_right_mouse_down = true; + } + else { + is_right_mouse_down = false; // always set to false + } + } + std::optional eid; if (this->session != nullptr && this->session->getInfo().client_eid.has_value()) { diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp index 2470301f..5681a1fd 100644 --- a/src/client/gui/gui.cpp +++ b/src/client/gui/gui.cpp @@ -78,13 +78,16 @@ void GUI::renderFrame() { glDisable(GL_BLEND); } -void GUI::handleInputs(float mouse_xpos, float mouse_ypos, bool& is_left_mouse_down) { +void GUI::handleInputs(float mouse_xpos, float mouse_ypos, bool& is_left_mouse_down, bool& is_right_mouse_down) { // convert to gui coords, where (0,0) is bottome left mouse_ypos = WINDOW_HEIGHT - mouse_ypos; if (is_left_mouse_down) { this->_handleClick(mouse_xpos, mouse_ypos); is_left_mouse_down = false; } + if (is_right_mouse_down) { + is_right_mouse_down = false; + } this->_handleHover(mouse_xpos, mouse_ypos); } @@ -160,6 +163,10 @@ void GUI::layoutFrame(GUIState state) { glfwSetInputMode(client->window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); this->_layoutLobbyBrowser(); break; + case GUIState::INTRO_CUTSCENE: + glfwSetInputMode(client->window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); + // draw nothing + break; case GUIState::GAME_HUD: glfwSetInputMode(client->window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); @@ -377,7 +384,7 @@ void GUI::_layoutLobby() { this->client->gameState.lobby.name, font::Font::MENU, font::Size::LARGE, - font::Color::BLACK, + font::Color::WHITE, this->fonts, lobby_title_height ); @@ -389,9 +396,9 @@ void GUI::_layoutLobby() { // Define table column widths //glm::vec3 columnWidths(400.0f, 850.0f, 400.0f); - glm::vec3 columnWidths(font::getRelativePixels(400.0f), - font::getRelativePixels(900.0f), - font::getRelativePixels(400.0f)); + glm::vec3 columnWidths(font::getRelativePixelsHorizontal(400.0f), + font::getRelativePixelsHorizontal(900.0f), + font::getRelativePixelsHorizontal(400.0f)); float rowHeight = font::getFontSizePx(font::Size::LARGE); @@ -422,19 +429,17 @@ void GUI::_layoutLobby() { widget::DynText::Options( font::Font::TEXT, font::Size::MEDIUM, - font::Color::BLACK + font::Color::WHITE ) ); // Display differently based on whether all players in the lobby are ready bool allReady = true; - std::cout << "players size: " << std::to_string(this->client->gameState.lobby.players.size()) << std::endl; for (boost::optional player : this->client->gameState.lobby.players) { if (!player.has_value() || !player.get().ready) { // Either there aren't enough players in the lobby or at least // one player isn't ready - std::cout << "Not all players are ready" << std::endl; allReady = false; break; } @@ -463,7 +468,6 @@ void GUI::_layoutLobby() { } // Create flexbox to contain dynamic text - std::cout << "lobby max players: " << std::to_string(this->client->gameState.lobby.max_players) << std::endl; auto start_game_flex = widget::Flexbox::make( glm::vec2(0, 200), glm::vec2(WINDOW_WIDTH, 0), @@ -549,7 +553,7 @@ gui::widget::Flexbox::Ptr GUI::_createPlayerStatusRow( widget::DynText::Options( font::Font::TEXT, font::Size::MEDIUM, - font::Color::BLACK + font::Color::WHITE ) ); @@ -627,7 +631,7 @@ gui::widget::Flexbox::Ptr GUI::_createPlayerStatusRow( widget::DynText::Options( font::Font::TEXT, font::Size::MEDIUM, - font::Color::BLACK + font::Color::WHITE ) ); @@ -650,7 +654,7 @@ gui::widget::Flexbox::Ptr GUI::_createPlayerStatusRow( widget::DynText::Options( font::Font::TEXT, font::Size::MEDIUM, - font::Color::BLACK + font::Color::WHITE ) ); @@ -669,7 +673,7 @@ gui::widget::Flexbox::Ptr GUI::_createPlayerStatusRow( widget::DynText::Options( font::Font::TEXT, font::Size::MEDIUM, - font::Color::BLACK + font::Color::WHITE ) ); @@ -680,7 +684,7 @@ gui::widget::Flexbox::Ptr GUI::_createPlayerStatusRow( widget::DynText::Options( font::Font::TEXT, font::Size::MEDIUM, - font::Color::BLACK + font::Color::WHITE ) ); @@ -798,7 +802,7 @@ gui::widget::Flexbox::Ptr GUI::_createPlayerStatusRow( widget::DynText::Options( font::Font::TEXT, font::Size::MEDIUM, - font::Color::BLACK + font::Color::WHITE ) ); @@ -859,7 +863,7 @@ gui::widget::Flexbox::Ptr GUI::_createPlayerStatusRow( widget::DynText::Options( font::Font::TEXT, font::Size::MEDIUM, - font::Color::BLACK + font::Color::WHITE ) ); @@ -960,6 +964,7 @@ void GUI::_sharedGameHUD() { controls.push_back({ "Spacebar:", "Zoom Out" }); controls.push_back({ "Left Control:", "Boost" }); controls.push_back({ "Left Click:", "Place Trap" }); + controls.push_back({ "Right Click:", "Rotate Trap" }); controls.push_back({ "Mouse Wheel:", "Select Trap" }); controls.push_back({ "ESC:", "Menu" }); controls.push_back({ "H:", "Controls" }); @@ -1033,6 +1038,10 @@ void GUI::_sharedGameHUD() { itemString = "Hammer"; break; } + case ModelType::Mirror: { + itemString = "Mirror"; + break; + } } } } else { // DM hotbar @@ -1066,7 +1075,10 @@ void GUI::_sharedGameHUD() { case ModelType::SunGod: { itemString = "Fireball Trap"; - if (self->trapInventoryInfo->trapsInCooldown.find(CellType::FireballTrapUp) != self->trapInventoryInfo->trapsInCooldown.end()) { + if (self->trapInventoryInfo->trapsInCooldown.find(CellType::FireballTrapUp) != self->trapInventoryInfo->trapsInCooldown.end() + || self->trapInventoryInfo->trapsInCooldown.find(CellType::FireballTrapLeft) != self->trapInventoryInfo->trapsInCooldown.end() + || self->trapInventoryInfo->trapsInCooldown.find(CellType::FireballTrapRight) != self->trapInventoryInfo->trapsInCooldown.end() + || self->trapInventoryInfo->trapsInCooldown.find(CellType::FireballTrapDown) != self->trapInventoryInfo->trapsInCooldown.end()) { itemString += " (IN COOLDOWN)"; } break; @@ -1082,6 +1094,28 @@ void GUI::_sharedGameHUD() { case ModelType::Lightning: { itemString = "Lightning Bolt (10)"; + break; + } + case ModelType::TeleporterTrap: { + itemString = "Teleporter Trap"; + + if (self->trapInventoryInfo->trapsInCooldown.find(CellType::TeleporterTrap) != self->trapInventoryInfo->trapsInCooldown.end()) { + itemString += " (IN COOLDOWN)"; + } + + + break; + } + case ModelType::ArrowTrap: { + itemString = "Arrow Trap"; + + if (self->trapInventoryInfo->trapsInCooldown.find(CellType::ArrowTrapUp) != self->trapInventoryInfo->trapsInCooldown.end() + || self->trapInventoryInfo->trapsInCooldown.find(CellType::ArrowTrapLeft) != self->trapInventoryInfo->trapsInCooldown.end() + || self->trapInventoryInfo->trapsInCooldown.find(CellType::ArrowTrapDown) != self->trapInventoryInfo->trapsInCooldown.end() + || self->trapInventoryInfo->trapsInCooldown.find(CellType::ArrowTrapRight) != self->trapInventoryInfo->trapsInCooldown.end()) { + itemString += " (IN COOLDOWN)"; + } + break; } } @@ -1148,13 +1182,151 @@ void GUI::_sharedGameHUD() { frameflex->push(widget::StaticImg::make(glm::vec2(0.0f), images.getImg(img::ImgID::RightHotbar), 2)); } else { frameflex->push(widget::StaticImg::make(glm::vec2(0.0f), images.getImg(img::ImgID::DMLeftHotbar), 2)); + for (int i = 0; i < inventory_size; i++) { - if (selected == i) { - frameflex->push(widget::StaticImg::make(glm::vec2(0.0f), images.getImg(img::ImgID::DMMiddleSelected), 2)); + bool idxInCooldown = false; + int cdRemaining = 0; + + if (self->trapInventoryInfo->inventory[i] != ModelType::Frame) { + switch (self->trapInventoryInfo->inventory[i]) { + case ModelType::FloorSpikeFull: { + if (self->trapInventoryInfo->trapsInCooldown.find(CellType::FloorSpikeFull) != self->trapInventoryInfo->trapsInCooldown.end()) { + cdRemaining = self->trapInventoryInfo->trapsCooldown.at(CellType::FloorSpikeFull); + idxInCooldown = true; + } + break; + } + case ModelType::FloorSpikeVertical: { + if (self->trapInventoryInfo->trapsInCooldown.find(CellType::FloorSpikeVertical) != self->trapInventoryInfo->trapsInCooldown.end()) { + cdRemaining = self->trapInventoryInfo->trapsCooldown.at(CellType::FloorSpikeVertical); + idxInCooldown = true; + } + break; + } + case ModelType::FloorSpikeHorizontal: { + if (self->trapInventoryInfo->trapsInCooldown.find(CellType::FloorSpikeHorizontal) != self->trapInventoryInfo->trapsInCooldown.end()) { + cdRemaining = self->trapInventoryInfo->trapsCooldown.at(CellType::FloorSpikeHorizontal); + idxInCooldown = true; + } + break; + } + case ModelType::SunGod: { + itemString = "Fireball Trap"; + + if (self->trapInventoryInfo->trapsInCooldown.find(CellType::FireballTrapUp) != self->trapInventoryInfo->trapsInCooldown.end() + || self->trapInventoryInfo->trapsInCooldown.find(CellType::FireballTrapLeft) != self->trapInventoryInfo->trapsInCooldown.end() + || self->trapInventoryInfo->trapsInCooldown.find(CellType::FireballTrapRight) != self->trapInventoryInfo->trapsInCooldown.end() + || self->trapInventoryInfo->trapsInCooldown.find(CellType::FireballTrapDown) != self->trapInventoryInfo->trapsInCooldown.end()) + { + if (self->trapInventoryInfo->trapsInCooldown.find(CellType::FireballTrapUp) != self->trapInventoryInfo->trapsInCooldown.end()) { + cdRemaining = self->trapInventoryInfo->trapsCooldown.at(CellType::FireballTrapUp); + } + else if (self->trapInventoryInfo->trapsInCooldown.find(CellType::FireballTrapLeft) != self->trapInventoryInfo->trapsInCooldown.end()) { + cdRemaining = self->trapInventoryInfo->trapsCooldown.at(CellType::FireballTrapLeft); + } + else if (self->trapInventoryInfo->trapsInCooldown.find(CellType::FireballTrapRight) != self->trapInventoryInfo->trapsInCooldown.end()) { + cdRemaining = self->trapInventoryInfo->trapsCooldown.at(CellType::FireballTrapRight); + } + else { + cdRemaining = self->trapInventoryInfo->trapsCooldown.at(CellType::FireballTrapDown); + } + + idxInCooldown = true; + } + break; + } + case ModelType::SpikeTrap: { + itemString = "Ceiling Spike Trap"; + + if (self->trapInventoryInfo->trapsInCooldown.find(CellType::SpikeTrap) != self->trapInventoryInfo->trapsInCooldown.end()) { + cdRemaining = self->trapInventoryInfo->trapsCooldown.at(CellType::SpikeTrap); + idxInCooldown = true; + } + break; + } + case ModelType::Lightning: { + itemString = "Lightning Bolt (10)"; + + break; + } + case ModelType::TeleporterTrap: { + itemString = "Teleporter Trap"; + + if (self->trapInventoryInfo->trapsInCooldown.find(CellType::TeleporterTrap) != self->trapInventoryInfo->trapsInCooldown.end()) { + cdRemaining = self->trapInventoryInfo->trapsCooldown.at(CellType::TeleporterTrap); + idxInCooldown = true; + } + break; + } + case ModelType::ArrowTrap: { + itemString = "Arrow Trap"; + + if (self->trapInventoryInfo->trapsInCooldown.find(CellType::ArrowTrapUp) != self->trapInventoryInfo->trapsInCooldown.end() + || self->trapInventoryInfo->trapsInCooldown.find(CellType::ArrowTrapLeft) != self->trapInventoryInfo->trapsInCooldown.end() + || self->trapInventoryInfo->trapsInCooldown.find(CellType::ArrowTrapDown) != self->trapInventoryInfo->trapsInCooldown.end() + || self->trapInventoryInfo->trapsInCooldown.find(CellType::ArrowTrapRight) != self->trapInventoryInfo->trapsInCooldown.end()) + { + if (self->trapInventoryInfo->trapsInCooldown.find(CellType::ArrowTrapUp) != self->trapInventoryInfo->trapsInCooldown.end()) { + cdRemaining = self->trapInventoryInfo->trapsCooldown.at(CellType::ArrowTrapUp); + } + else if (self->trapInventoryInfo->trapsInCooldown.find(CellType::ArrowTrapLeft) != self->trapInventoryInfo->trapsInCooldown.end()) { + cdRemaining = self->trapInventoryInfo->trapsCooldown.at(CellType::ArrowTrapLeft); + } + else if (self->trapInventoryInfo->trapsInCooldown.find(CellType::ArrowTrapDown) != self->trapInventoryInfo->trapsInCooldown.end()) { + cdRemaining = self->trapInventoryInfo->trapsCooldown.at(CellType::ArrowTrapDown); + } + else { + cdRemaining = self->trapInventoryInfo->trapsCooldown.at(CellType::ArrowTrapRight); + } + idxInCooldown = true; + } + + break; + } + } + } + + if (!idxInCooldown) { + if (selected == i) { + frameflex->push(widget::StaticImg::make(glm::vec2(0.0f), images.getImg(img::ImgID::DMMiddleSelected), 2)); + + } + else { + frameflex->push(widget::StaticImg::make(glm::vec2(0.0f), images.getImg(img::ImgID::DMMiddleHotbar), 2)); + } } else { - frameflex->push(widget::StaticImg::make(glm::vec2(0.0f), images.getImg(img::ImgID::DMMiddleHotbar), 2)); + auto imgSelect = img::ImgID::DMCD_10; + if (cdRemaining > 4500) { + imgSelect = (selected != i) ? img::ImgID::DMCD_10 : img::ImgID::DMCD_Selected_10; + } + else if (cdRemaining > 4000) { + imgSelect = (selected != i) ? img::ImgID::DMCD_9 : img::ImgID::DMCD_Selected_9; + } + else if (cdRemaining > 3500) { + imgSelect = (selected != i) ? img::ImgID::DMCD_8 : img::ImgID::DMCD_Selected_8; + } + else if (cdRemaining > 3000) { + imgSelect = (selected != i) ? img::ImgID::DMCD_7 : img::ImgID::DMCD_Selected_7; + } + else if (cdRemaining > 2500) { + imgSelect = (selected != i) ? img::ImgID::DMCD_6 : img::ImgID::DMCD_Selected_6; + } + else if (cdRemaining > 2000) { + imgSelect = (selected != i) ? img::ImgID::DMCD_5 : img::ImgID::DMCD_Selected_5; + } + else if (cdRemaining > 1500) { + imgSelect = (selected != i) ? img::ImgID::DMCD_4 : img::ImgID::DMCD_Selected_4; + } + else if (cdRemaining > 1000) { + imgSelect = (selected != i) ? img::ImgID::DMCD_3 : img::ImgID::DMCD_Selected_3; + } + else { + // Doesn't use DMCD_1 b/c it looks better without it + imgSelect = (selected != i) ? img::ImgID::DMCD_2 : img::ImgID::DMCD_Selected_2; + } + frameflex->push(widget::StaticImg::make(glm::vec2(0.0f), images.getImg(imgSelect), 2)); } } frameflex->push(widget::StaticImg::make(glm::vec2(0.0f), images.getImg(img::ImgID::DMRightHotbar), 2)); @@ -1213,6 +1385,10 @@ void GUI::_sharedGameHUD() { itemflex->push(widget::StaticImg::make(glm::vec2(0.0f), images.getImg(img::ImgID::Hammer), 2)); break; } + case ModelType::Mirror: { + // TODO: Replace with an img of a mirror + itemflex->push(widget::StaticImg::make(glm::vec2(0.0f), images.getImg(img::ImgID::Mirror), 2)); + } } } else { itemflex->push(widget::StaticImg::make(glm::vec2(0.0f), images.getImg(img::ImgID::Blank), 2)); @@ -1244,6 +1420,14 @@ void GUI::_sharedGameHUD() { itemflex->push(widget::StaticImg::make(glm::vec2(0.0f), images.getImg(img::ImgID::Lightning), 2)); break; } + case ModelType::ArrowTrap: { + itemflex->push(widget::StaticImg::make(glm::vec2(0.0f), images.getImg(img::ImgID::ArrowTrap), 2)); + break; + } + case ModelType::TeleporterTrap: { + itemflex->push(widget::StaticImg::make(glm::vec2(0.0f), images.getImg(img::ImgID::Teleporter), 2)); + break; + } } } else { @@ -1471,7 +1655,7 @@ void GUI::_layoutGameHUD() { // Add timer string if (client->gameState.matchPhase == MatchPhase::RelayRace) { std::string timerString = "Time Left: "; - int timerSeconds = client->gameState.timesteps_left * ((float)TIMESTEP_LEN.count()) / 1000; + auto timerSeconds = client->gameState.relay_finish_time - getSecSinceEpoch(); timerString += std::to_string(timerSeconds); timerString += (timerSeconds > 1) ? " seconds" : " second"; @@ -1522,6 +1706,22 @@ void GUI::_layoutGameHUD() { font::getRelativePixels(13) ); this->addWidget(std::move(mana_txt)); + + // Show a large splash text at the center of the screen for the DM if the DM is paralyzed + if (self.get().DMInfo.get().paralyzed) { + std::cout << "DM is paralyzed!" << std::endl; + auto paralyzedSplashText = widget::CenterText::make( + "PARALYZED!", + gui::font::Font::TITLE, + gui::font::Size::LARGE, + gui::font::Color::RED, + this->fonts, + WINDOW_HEIGHT / 2 + ); + + this->addWidget(std::move(paralyzedSplashText)); + } + return; } @@ -1557,6 +1757,9 @@ void GUI::_layoutGameHUD() { else if (type == ModelType::NauseaPotion) { name = "Nauseous: "; } + else if (type == ModelType::Mirror) { + name = "Holding Mirror: "; + } durationFlex->push(widget::DynText::make( name + std::to_string((int)self->inventoryInfo->usedItems[id].second), @@ -1641,6 +1844,22 @@ void GUI::_layoutGameHUD() { ); } this->addWidget(std::move(needleFlex)); + + // Show a large splash text at the center of the screen for the player if the player + // successfully reflected a lightning bolt + if (self.get().playerInfo.get().used_mirror_to_reflect_lightning) { + std::cout << "Player used a mirror to reflect a lightning bolt!" << std::endl; + auto reflectedLightningSplashText = widget::CenterText::make( + "Reflected Lightning using Mirror!", + gui::font::Font::TITLE, + gui::font::Size::MEDIUM, + gui::font::Color::WHITE, + this->fonts, + WINDOW_HEIGHT / 2 + ); + + this->addWidget(std::move(reflectedLightningSplashText)); + } } void GUI::_layoutGameEscMenu() { diff --git a/src/client/gui/imgs/img.cpp b/src/client/gui/imgs/img.cpp index e17d412c..a4f30cf1 100644 --- a/src/client/gui/imgs/img.cpp +++ b/src/client/gui/imgs/img.cpp @@ -17,6 +17,8 @@ std::string getImgFilepath(ImgID img) { case ImgID::FireSpell: return (img_root / "fire_wand.png").string(); case ImgID::HealSpell: return (img_root / "heal_wand.png").string(); case ImgID::Orb: return (img_root / "orb.png").string(); + // TODO: Replace mirror image with an image of an actual mirror + case ImgID::Mirror: return (img_root / "orb.png").string(); case ImgID::Scroll: return (img_root / "scroll.png").string(); case ImgID::Crosshair: return (img_root / "crosshair046.png").string(); case ImgID::Dagger: return (img_root / "weapon_dagger.png").string(); @@ -30,6 +32,7 @@ std::string getImgFilepath(ImgID img) { case ImgID::DMRightHotbar: return (img_root / "dm_right.png").string(); case ImgID::DMMiddleHotbar: return (img_root / "dm_middle.png").string(); case ImgID::DMMiddleSelected: return (img_root / "dm_selected.png").string(); + case ImgID::DMMiddleCooldown: return (img_root / "dm_cooldown.png").string(); case ImgID::Blank: return (img_root / "blank.png").string(); case ImgID::HealthBar: return (img_root / "healthbar.png").string(); case ImgID::HealthTickEmpty: return (img_root / "healthtick_empty.png").string(); @@ -52,6 +55,26 @@ std::string getImgFilepath(ImgID img) { case ImgID::Compass210: return (img_root / "compasses/compass_210.png").string(); case ImgID::Compass240: return (img_root / "compasses/compass_240.png").string(); case ImgID::Compass270: return (img_root / "compasses/compass_270.png").string(); + case ImgID::DMCD_10: return (img_root / "dm_cooldown/dm_cooldown_10.png").string(); + case ImgID::DMCD_9: return (img_root / "dm_cooldown/dm_cooldown_9.png").string(); + case ImgID::DMCD_8: return (img_root / "dm_cooldown/dm_cooldown_8.png").string(); + case ImgID::DMCD_7: return (img_root / "dm_cooldown/dm_cooldown_7.png").string(); + case ImgID::DMCD_6: return (img_root / "dm_cooldown/dm_cooldown_6.png").string(); + case ImgID::DMCD_5: return (img_root / "dm_cooldown/dm_cooldown_5.png").string(); + case ImgID::DMCD_4: return (img_root / "dm_cooldown/dm_cooldown_4.png").string(); + case ImgID::DMCD_3: return (img_root / "dm_cooldown/dm_cooldown_3.png").string(); + case ImgID::DMCD_2: return (img_root / "dm_cooldown/dm_cooldown_2.png").string(); + case ImgID::DMCD_1: return (img_root / "dm_cooldown/dm_cooldown_1.png").string(); + case ImgID::DMCD_Selected_10: return (img_root / "dm_cooldown/dm_selected_cooldown_10.png").string(); + case ImgID::DMCD_Selected_9: return (img_root / "dm_cooldown/dm_selected_cooldown_9.png").string(); + case ImgID::DMCD_Selected_8: return (img_root / "dm_cooldown/dm_selected_cooldown_8.png").string(); + case ImgID::DMCD_Selected_7: return (img_root / "dm_cooldown/dm_selected_cooldown_7.png").string(); + case ImgID::DMCD_Selected_6: return (img_root / "dm_cooldown/dm_selected_cooldown_6.png").string(); + case ImgID::DMCD_Selected_5: return (img_root / "dm_cooldown/dm_selected_cooldown_5.png").string(); + case ImgID::DMCD_Selected_4: return (img_root / "dm_cooldown/dm_selected_cooldown_4.png").string(); + case ImgID::DMCD_Selected_3: return (img_root / "dm_cooldown/dm_selected_cooldown_3.png").string(); + case ImgID::DMCD_Selected_2: return (img_root / "dm_cooldown/dm_selected_cooldown_2.png").string(); + case ImgID::DMCD_Selected_1: return (img_root / "dm_cooldown/dm_selected_cooldown_1.png").string(); case ImgID::Compass300: return (img_root / "compasses/compass_300.png").string(); case ImgID::Compass330: return (img_root / "compasses/compass_330.png").string(); case ImgID::Sungod: return (img_root / "sungod.png").string(); @@ -62,5 +85,4 @@ std::string getImgFilepath(ImgID img) { case ImgID::SpikeTrap: return (img_root / "spiketrap.png").string(); } } - } diff --git a/src/client/model.cpp b/src/client/model.cpp index ff3f295b..a552966b 100644 --- a/src/client/model.cpp +++ b/src/client/model.cpp @@ -179,7 +179,7 @@ void Model::translateAbsolute(const glm::vec3& new_pos) { void Model::translateRelative(const glm::vec3& delta) { Renderable::translateRelative(delta); for(Mesh& mesh : this->meshes) { - mesh.translateAbsolute(delta); + mesh.translateRelative(delta); } } diff --git a/src/server/CMakeLists.txt b/src/server/CMakeLists.txt index 536d9202..62f491ec 100644 --- a/src/server/CMakeLists.txt +++ b/src/server/CMakeLists.txt @@ -36,6 +36,8 @@ set(FILES game/spawner.cpp game/minotaur.cpp game/python.cpp + game/introcutscene.cpp + game/mirror.cpp audio/soundtable.cpp ) diff --git a/src/server/audio/soundtable.cpp b/src/server/audio/soundtable.cpp index 37c27555..76a941ed 100644 --- a/src/server/audio/soundtable.cpp +++ b/src/server/audio/soundtable.cpp @@ -21,6 +21,15 @@ std::unordered_map> SoundTable::getCommandsP for (const auto& obj : players) { for (const SoundCommand& command : commands) { + // hack to prevent the dm and players from hearing the wrong sounds in intro cutscene... + if ((command.source.sfx == ServerSFX::ZeusStartTheme || + command.source.sfx == ServerSFX::Wind) && obj->type ==ObjectType::Player) { + continue; + } + if (command.source.sfx == ServerSFX::PlayersStartTheme && obj->type == ObjectType::DungeonMaster) { + continue; + } + if (command.source.canBeHeardFrom(obj->physics.shared.getCenterPosition())) { commands_per_player[obj->globalID].push_back(command); } diff --git a/src/server/game/arrowtrap.cpp b/src/server/game/arrowtrap.cpp index dd88a721..484f7325 100644 --- a/src/server/game/arrowtrap.cpp +++ b/src/server/game/arrowtrap.cpp @@ -50,9 +50,9 @@ bool ArrowTrap::shouldTrigger(ServerGameState& state) { if (!state.getGrid().getCell(curr_grid_pos.x, curr_grid_pos.y)) { break; } - if (state.getGrid().getCell(curr_grid_pos.x, curr_grid_pos.y)->type == CellType::Wall) { - return false; // didnt find a player before a wall - } + //if (state.getGrid().getCell(curr_grid_pos.x, curr_grid_pos.y)->type == CellType::Wall) { + // return false; // didnt find a player before a wall + //} for (const auto& curr_player_pos : player_grid_positions) { if (curr_grid_pos == curr_player_pos) { // cppcheck-suppress useStlAlgorithm diff --git a/src/server/game/dungeonmaster.cpp b/src/server/game/dungeonmaster.cpp index 8cdfd7a3..986af5f9 100644 --- a/src/server/game/dungeonmaster.cpp +++ b/src/server/game/dungeonmaster.cpp @@ -16,7 +16,7 @@ DungeonMaster::DungeonMaster(glm::vec3 corner, glm::vec3 facing) : Stat(0, 10, 5) )), sharedTrapInventory(SharedTrapInventory{ .selected = 1, .inventory_size = TRAP_INVENTORY_SIZE, \ .inventory = std::vector(TRAP_INVENTORY_SIZE, ModelType::Frame) }), \ - dmInfo(SharedDMInfo{ .mana_remaining = 15 }) + dmInfo(SharedDMInfo{ .paralyzed = false, .mana_remaining = 15 }) { this->physics.feels_gravity = false; this->physics.velocityMultiplier = glm::vec3(3.0f, 1.0f, 3.0f); @@ -25,12 +25,25 @@ DungeonMaster::DungeonMaster(glm::vec3 corner, glm::vec3 facing) : this->placedTraps = 0; // TODO: fill in rest of traps - this->sharedTrapInventory.inventory[0] = ModelType::FloorSpikeFull; - this->sharedTrapInventory.inventory[1] = ModelType::FloorSpikeHorizontal; - this->sharedTrapInventory.inventory[2] = ModelType::FloorSpikeVertical; + this->sharedTrapInventory.inventory[0] = ModelType::Lightning; + this->sharedTrapInventory.inventory[1] = ModelType::TeleporterTrap; + this->sharedTrapInventory.inventory[2] = ModelType::ArrowTrap; this->sharedTrapInventory.inventory[3] = ModelType::SunGod; this->sharedTrapInventory.inventory[4] = ModelType::SpikeTrap; - this->sharedTrapInventory.inventory[5] = ModelType::Lightning; + this->sharedTrapInventory.inventory[5] = ModelType::FloorSpikeFull; + + // DungeonMaster paralysis (relevant when the DM is paralyzed by a Player + // reflecting a lightning bolt back at the DM using a Mirror) + + // Initially, the DM isn't paralyzed + + // This will be overwritten when the DM is actually paralyzed using + // setParalysis() + this->paralysisDuration = -1; + + // This will be overwritten when the DM is actually paralyzed using + // setParalysis() + this->paralysis_start_time = std::chrono::system_clock::now(); } int DungeonMaster::getPlacedTraps() { @@ -63,4 +76,27 @@ void DungeonMaster::manaRegen() { DungeonMaster::~DungeonMaster() { +} + +void DungeonMaster::setParalysis(bool isParalyzed, double paralysis_duration) { + if (isParalyzed) { + // DM is now paralyzed - set duration and mark start timestamp + std::cout << "Paralyzing the DM!" << std::endl; + this->paralysisDuration = paralysis_duration; + this->paralysis_start_time = std::chrono::system_clock::now(); + } + + this->dmInfo.paralyzed = isParalyzed; +} + +bool DungeonMaster::isParalyzed() const { + return this->dmInfo.paralyzed; +} + +double DungeonMaster::getParalysisDuration() const { + return this->paralysisDuration; +} + +std::chrono::time_point DungeonMaster::getParalysisStartTime() const { + return this->paralysis_start_time; } \ No newline at end of file diff --git a/src/server/game/gridcell.cpp b/src/server/game/gridcell.cpp index 3c7917fa..4d065b32 100644 --- a/src/server/game/gridcell.cpp +++ b/src/server/game/gridcell.cpp @@ -85,6 +85,8 @@ CellType charToCellType(char c) { return CellType::TeleporterTrap; case 'o': return CellType::Exit; + case 'M': + return CellType::Mirror; default: std::cerr << "Unknown cell type: " << c << "\n"; return CellType::Unknown; @@ -171,6 +173,8 @@ char cellTypeToChar(CellType type) { return 'T'; case CellType::Exit: return 'o'; + case CellType::Mirror: + return 'M'; default: return '?'; } diff --git a/src/server/game/introcutscene.cpp b/src/server/game/introcutscene.cpp new file mode 100644 index 00000000..0d205fc4 --- /dev/null +++ b/src/server/game/introcutscene.cpp @@ -0,0 +1,216 @@ +#include "server/game/introcutscene.hpp" +#include "shared/game/sharedgamestate.hpp" +#include "shared/utilities/config.hpp" +#include "server/game/weaponcollider.hpp" +#include "shared/audio/constants.hpp" +#include "shared/utilities/rng.hpp" + +#include + + +GameConfig getCutsceneConfig() { + GameConfig config = getDefaultConfig(); + config.game.maze.maze_file = "cutscene/intro.maze"; + config.game.maze.procedural = false; + return config; +} + +IntroCutscene::IntroCutscene(): + state(GamePhase::INTRO_CUTSCENE, getCutsceneConfig()) +{ + // state has loaded in the maze file we use for this cutscene, + + // hard code direction to face right based on the intro cutscene maze orientation + Player* player = new Player(this->state.getGrid().getRandomSpawnPoint(), directionToFacing(Direction::RIGHT)); + Player* player_left = new Player(this->state.getGrid().getRandomSpawnPoint(), directionToFacing(Direction::RIGHT)); + Player* player_right = new Player(this->state.getGrid().getRandomSpawnPoint(), directionToFacing(Direction::RIGHT)); + + this->state.objects.createObject(player); + this->state.objects.createObject(player_left); + this->state.objects.createObject(player_right); + + this->pov_eid = player->globalID; + + const glm::vec3 VELOCITY = glm::normalize(player->physics.shared.facing) * 0.10f; + + player->physics.velocity = VELOCITY; + player_left->physics.velocity = VELOCITY; + player_right->physics.velocity = VELOCITY; + + player->animState = AnimState::WalkAnim; + player_left->animState = AnimState::WalkAnim; + player_right->animState = AnimState::WalkAnim; + + // move half a grid cell down to be in center of 4 wide hall + player->physics.shared.corner += directionToFacing(Direction::DOWN) * (Grid::grid_cell_width / 2.0f); + + // adjust left and up a bit, and a random offset forward/back + player_left->physics.shared.corner = player->physics.shared.corner; + player_left->physics.shared.corner.z += Grid::grid_cell_width; + player_left->physics.shared.corner.x += Grid::grid_cell_width; + + // adjust right and down a bit, and a random offset forward/back + player_right->physics.shared.corner = player->physics.shared.corner; + player_right->physics.shared.corner.z -= Grid::grid_cell_width; + player_right->physics.shared.corner.x += Grid::grid_cell_width; + + DungeonMaster* dm = new DungeonMaster(player->physics.shared.corner + glm::vec3(-20.0f, 10.0f, 0), directionToFacing(Direction::RIGHT)); + dm->physics.velocity = player->physics.velocity; + dm->physics.velocityMultiplier = player->physics.velocityMultiplier; + this->state.objects.createObject(dm); + this->dm_eid = dm->globalID; + + // load this->lights before sending down so the torch tick stuff works right +} + +bool IntroCutscene::update() { + this->state.updateMovement(); + this->state.doTorchlightTicks(); + this->state.updateItems(); + this->state.deleteEntities(); + this->state.updateAttacks(); + + static int ticks = 0; + ticks++; + + Player* player = this->state.objects.getPlayer(0); + Player* player_left = this->state.objects.getPlayer(1); + Player* player_right = this->state.objects.getPlayer(2); + DungeonMaster* dm = this->state.objects.getDM(); + + const int START_TICK = 1; + const int STOP_MOVING_TICK = START_TICK + 250; + const int GATE_RAISE_TICK = STOP_MOVING_TICK + 30; + const int GATE_STOP_RAISE_TICK = GATE_RAISE_TICK + 200; + const int LIGHTNING_1_TICK = GATE_STOP_RAISE_TICK + 80; + const int LIGHTNING_2_TICK = LIGHTNING_1_TICK + 50; + const int LIGHTNING_3_TICK = LIGHTNING_2_TICK + 40; + const int START_PLAYER_THEME_TICK = LIGHTNING_3_TICK + 110; + const int EXIT_CUTSCENE_TICK = START_PLAYER_THEME_TICK + 240; + + if (ticks == START_TICK) { + this->state.soundTable().addNewSoundSource(SoundSource( + ServerSFX::TorchLoop, + player->physics.shared.getCenterPosition(), + FULL_VOLUME, + MEDIUM_DIST, + MEDIUM_ATTEN + )); + this->state.soundTable().addNewSoundSource(SoundSource( + ServerSFX::Wind, + dm->physics.shared.getCenterPosition(), + FULL_VOLUME, + MEDIUM_DIST, + MEDIUM_ATTEN + )); + } + + if (ticks == STOP_MOVING_TICK) { + player->physics.velocity = glm::vec3(0.0f); + player_left->physics.velocity = glm::vec3(0.0f); + player_right->physics.velocity = glm::vec3(0.0f); + dm->physics.velocity = glm::vec3(0.0f); + + player->animState = AnimState::IdleAnim; + player_left->animState = AnimState::IdleAnim; + player_right->animState = AnimState::IdleAnim; + } + + if (ticks >= GATE_RAISE_TICK && ticks <= GATE_STOP_RAISE_TICK) { + bool played_sound = false; + + auto walls = this->state.objects.getSolidSurfaces(); + for (int i = 0; i < walls.size(); i++) { + auto wall = walls.get(i); + if (wall == nullptr || wall->shared.surfaceType != SurfaceType::Pillar) continue; + + wall->physics.shared.corner.y += 0.042f; + + if (!played_sound && ticks == GATE_RAISE_TICK) { + played_sound = true; + this->state.soundTable().addNewSoundSource(SoundSource( + ServerSFX::IntroGateOpen, + wall->physics.shared.getCenterPosition(), + QUIET_VOLUME, // the sound effect is already so fucking loud geez + FAR_DIST, + FAR_ATTEN + )); + } + } + } + + glm::vec3 lightning_pos1 = player->physics.shared.corner + glm::normalize(player->physics.shared.facing) * 10.0f; + + if (ticks == LIGHTNING_1_TICK) { + this->state.soundTable().addNewSoundSource(SoundSource( + ServerSFX::ZeusStartTheme, + player->physics.shared.getCenterPosition(), + FULL_VOLUME, + FAR_DIST, + FAR_ATTEN + )); + this->state.objects.createObject(new Lightning(lightning_pos1, player->physics.shared.facing)); + } + + if (ticks == LIGHTNING_2_TICK) { + glm::vec3 lightning_pos2 = lightning_pos1 + glm::vec3(3.0f, 0.0f, -3.0f); + this->state.objects.createObject(new Lightning(lightning_pos2, player->physics.shared.facing)); + } + + if (ticks == LIGHTNING_3_TICK) { + glm::vec3 lightning_pos3 = player->physics.shared.getCenterPosition() + directionToFacing(Direction::RIGHT) * Grid::grid_cell_width * 1.5f; + this->state.objects.createObject(new Lightning(lightning_pos3, player->physics.shared.facing)); + } + + if (ticks == START_PLAYER_THEME_TICK) { + this->state.soundTable().addNewSoundSource(SoundSource( + ServerSFX::PlayersStartTheme, + player->physics.shared.getCenterPosition(), + FULL_VOLUME, + FAR_DIST, + FAR_ATTEN + )); + } + + if (ticks == EXIT_CUTSCENE_TICK) { + return true; + } + + return false; +} + +LoadIntroCutsceneEvent IntroCutscene::toNetwork() { + std::vector updates = this->state.generateSharedGameState(true); + if (updates.size() != 1) { + // just put them all in one packet and pray it works... + SharedGameState& main = updates.at(0); + for (int i = 1; i < updates.size(); i++) { + for (const auto& [eid, obj] : updates.at(i).objects) { + main.objects.insert({eid, obj}); + } + } + } + + std::size_t lights_idx = 0; + for (int i = 0; i < this->state.objects.getObjects().size(); i++) { + auto object = this->state.objects.getObject(i); + if (object == nullptr) continue; + + if (lights_idx < MAX_POINT_LIGHTS && object->type == ObjectType::Torchlight) { + this->lights[lights_idx] = object->toShared(); + lights_idx++; + } + if (lights_idx > MAX_POINT_LIGHTS) { + std::cerr << "WARNING: can't fit all lights in intro cutscene world.\n"; + } + } + + for (int i = lights_idx; i < MAX_POINT_LIGHTS; i++) { + this->lights[i] = {}; // make sure this is initialized, idk if this is needed but just in case + // because i dont trust c++ to do anything ever + } + + auto evt = LoadIntroCutsceneEvent(updates.at(0), this->pov_eid, this->dm_eid, this->lights); + + return evt; +} diff --git a/src/server/game/item.cpp b/src/server/game/item.cpp index 9c09394f..3686c70a 100644 --- a/src/server/game/item.cpp +++ b/src/server/game/item.cpp @@ -67,6 +67,11 @@ void Item::doCollision(Object* other, ServerGameState& state) { SHORT_DIST, SHORT_ATTEN )); + + // update cell type in game state to empty + GridCell* cell = state.getGrid().getCell(this->physics.shared.corner.x / Grid::grid_cell_width, this->physics.shared.corner.z / Grid::grid_cell_width); + + cell->type = CellType::Empty; } } diff --git a/src/server/game/mirror.cpp b/src/server/game/mirror.cpp new file mode 100644 index 00000000..b0e3a2f4 --- /dev/null +++ b/src/server/game/mirror.cpp @@ -0,0 +1,78 @@ +#include "server/game/mirror.hpp" +#include "server/game/constants.hpp" + +Mirror::Mirror(glm::vec3 corner, glm::vec3 dimensions) + : Item(ObjectType::Mirror, false, corner, ModelType::Cube, dimensions) { + this->modelType = ModelType::Mirror; + +} + +void Mirror::useItem(Object* other, ServerGameState& state, int itemSelected) { + // Get Player object that used the mirror + this->used_player = state.objects.getPlayer(other->typeID); + + // Get use time + this->used_time = std::chrono::system_clock::now(); + + auto now = std::chrono::system_clock::now(); + std::chrono::duration elapsed_seconds{ this->used_time - now }; + this->iteminfo.remaining_time = (double)(MIRROR_USE_DURATION) - elapsed_seconds.count(); + + // Set used to true (since mirror is being used) + this->iteminfo.used = true; + + // Add mirror to used items map + this->used_player->sharedInventory.usedItems.insert( + { this->typeID, + { + ModelType::Mirror, + this->iteminfo.remaining_time + } + }); + + // Don't call Item::useItem() to avoid consuming the mirror +} + +void Mirror::dropItem(Object* other, ServerGameState& state, int itemSelected, float dropDistance) { + // Stop mirror's effect (returns immediately if mirror wasn't used) + this->revertEffect(state); + + Item::dropItem(other, state, itemSelected, dropDistance); +} + +bool Mirror::timeOut() { + // Determine whether mirror use has timed out + auto now = std::chrono::system_clock::now(); + + std::chrono::duration elapsed_seconds{ now - this->used_time }; + + // Update remaining time + this->iteminfo.remaining_time = + (double)MIRROR_USE_DURATION - elapsed_seconds.count(); + + // Update used items' remaining time + if (this->used_player->sharedInventory.usedItems.find(this->typeID) + != this->used_player->sharedInventory.usedItems.end()) { + this->used_player->sharedInventory.usedItems[this->typeID].second + = this->iteminfo.remaining_time; + } + + // Return true if duration has elapsed + return elapsed_seconds.count() > MIRROR_USE_DURATION; +} + +void Mirror::revertEffect(ServerGameState& state) { + if (!this->iteminfo.used) { + return; + } + + // Mark as no longer used (but still held) + this->iteminfo.used = false; + + // Update used items list to mark mirror as no longer used + auto it = this->used_player->sharedInventory.usedItems.find(this->typeID); + + if (it != this->used_player->sharedInventory.usedItems.end()) { + it = this->used_player->sharedInventory.usedItems.erase(it); + } +} \ No newline at end of file diff --git a/src/server/game/object.cpp b/src/server/game/object.cpp index 308531c3..e251de77 100644 --- a/src/server/game/object.cpp +++ b/src/server/game/object.cpp @@ -40,7 +40,8 @@ std::unordered_map Object::models ({ // can't move around anywhere. we should eventually solve this // by tucking in the player's arms since right now they're // spread out in the model - {ModelType::Player, (FIRE_PLAYER_DIMENSIONS / 4.0f)}, + {ModelType::Player, {FIRE_PLAYER_DIMENSIONS * PLAYER_BBOX_SCALE}}, // for bbox, rendering is done separately in geometryPass b/c weird + // discrepencies between the model size in world and reported dimensions {ModelType::WarrenBear, (BEAR_DIMENSIONS / 4.0f)}, {ModelType::Torchlight, glm::vec3(1.0f)}, {ModelType::SunGod, (SUNGOD_DIMENSIONS / 2.0f)}, diff --git a/src/server/game/objectmanager.cpp b/src/server/game/objectmanager.cpp index 627bdc46..b1ac654c 100644 --- a/src/server/game/objectmanager.cpp +++ b/src/server/game/objectmanager.cpp @@ -10,6 +10,7 @@ #include "server/game/spell.hpp" #include "server/game/weaponcollider.hpp" #include "server/game/weapon.hpp" +#include "server/game/mirror.hpp" #include "shared/utilities/rng.hpp" #include @@ -42,17 +43,16 @@ SpecificID ObjectManager::_createObject(Object* object, boost::optionalobjects.set(object, globalID); - } - else { + } else { globalID = this->objects.push(object); } object->globalID = globalID; object->gridCellPositions = this->objectGridCells(object); + for (auto pos : object->gridCellPositions) { if (!this->cellToObjects.contains(pos)) { this->cellToObjects.insert({pos, std::vector()}); @@ -98,6 +98,7 @@ SpecificID ObjectManager::_createObject(Object* object, boost::optionaltypeID = this->players.push(dynamic_cast(object)); + std::cout << "INSERTING A PLAYER of TYPEID " << object->typeID << std::endl; break; case ObjectType::Python: case ObjectType::Minotaur: @@ -110,6 +111,9 @@ SpecificID ObjectManager::_createObject(Object* object, boost::optionaltypeID = this->items.push(dynamic_cast(object)); break; + case ObjectType::Mirror: + object->typeID = this->items.push(dynamic_cast(object)); + break; default: std::cerr << "FATAL: invalid object type being created: " << static_cast(object->type) << "\nDid you remember to add a new switch statement to ObjectManager::createObject?\n"; @@ -178,6 +182,7 @@ bool ObjectManager::removeObject(EntityID globalID) { case ObjectType::Spell: case ObjectType::Potion: case ObjectType::Orb: + case ObjectType::Mirror: this->items.remove(object->typeID); break; case ObjectType::Exit: diff --git a/src/server/game/player.cpp b/src/server/game/player.cpp index 635b4743..b1d3afec 100644 --- a/src/server/game/player.cpp +++ b/src/server/game/player.cpp @@ -26,9 +26,21 @@ Player::Player(glm::vec3 corner, glm::vec3 facing): this->info.is_alive = true; this->info.respawn_time = NULL; this->info.render = true; + this->info.used_mirror_to_reflect_lightning = false; // initialize inventory as empty this->inventory = std::vector(INVENTORY_SIZE, -1); + + // Player lightning invulnerability is initially false + this->invulnerableToLightning = false; + + // Set invulnerability duration to -1 (this will be overwritten when the player + // actually gains lightning invulnerability using setInvulnerableToLightning()) + this->lightningInvulnerabilityDuration = -1; + + // Set invulnerability time to right now (this will be overwritten when the player + // actually gains lightning invulnerability using setInvulnerableToLightning()) + this->lightning_invulnerability_start_time = std::chrono::system_clock::now(); } Player::~Player() { @@ -38,4 +50,28 @@ Player::~Player() { bool Player::canBeTargetted() const { // cannot be seen / targetted if the player is dead or invisible return this->info.is_alive && this->info.render; +} + +void Player::setInvulnerableToLightning(bool isInvulnerable, double duration) { + if (isInvulnerable) { + // Player is now invulnerable to lightning - remember timestamp and set + // lightning invulnerability duration + this->lightning_invulnerability_start_time = std::chrono::system_clock::now(); + + this->lightningInvulnerabilityDuration = duration; + } + + this->invulnerableToLightning = isInvulnerable; +} + +bool Player::isInvulnerableToLightning() const { + return this->invulnerableToLightning; +} + +double Player::getLightningInvulnerabilityDuration() const { + return this->lightningInvulnerabilityDuration; +} + +std::chrono::time_point Player::getLightningInvulnerabilityStartTime() const { + return this->lightning_invulnerability_start_time; } \ No newline at end of file diff --git a/src/server/game/servergamestate.cpp b/src/server/game/servergamestate.cpp index c1dab666..50a20c1e 100644 --- a/src/server/game/servergamestate.cpp +++ b/src/server/game/servergamestate.cpp @@ -17,6 +17,7 @@ #include "server/game/orb.hpp" #include "server/game/weapon.hpp" #include "server/game/weaponcollider.hpp" +#include "server/game/mirror.hpp" #include "server/game/spawner.hpp" #include "shared/game/celltype.hpp" @@ -52,7 +53,7 @@ ServerGameState::ServerGameState(GameConfig config) : config(config) { // Initialize game instance match phase data // Match begins in MazeExploration phase (no timer) this->matchPhase = MatchPhase::MazeExploration; - this->timesteps_left = TIME_LIMIT_MS / TIMESTEP_LEN; + this->relay_finish_time = 0; // Player victory is by default false (need to collide with an open exit // while holding the Orb to win, whereas DM wins on time limit expiration) this->playerVictory = false; @@ -63,6 +64,7 @@ ServerGameState::ServerGameState(GameConfig config) : config(config) { this->currentGhostTrap = nullptr; this->spawner = std::make_unique(); this->spawner->spawnDummy(*this); + this->spawner->spawnSmallDummy(*this); MazeGenerator generator(config); @@ -115,7 +117,7 @@ std::vector ServerGameState::generateSharedGameState(bool send_ curr_update.lobby = this->lobby; curr_update.phase = this->phase; curr_update.matchPhase = this->matchPhase; - curr_update.timesteps_left = this->timesteps_left; + curr_update.relay_finish_time = this->relay_finish_time; curr_update.playerVictory = this->playerVictory; curr_update.numPlayerDeaths = this->numPlayerDeaths; return curr_update; @@ -158,7 +160,9 @@ std::vector ServerGameState::generateSharedGameState(bool send_ // Make sure that SharedGameState updates are sent while the server is in the // Lobby phase (to ensure players can see other players lobby status updates) - if (num_in_curr_update > 0 || this->getPhase() == GamePhase::LOBBY) { + if (num_in_curr_update > 0 || + this->getPhase() == GamePhase::LOBBY || + this->getPhase() == GamePhase::INTRO_CUTSCENE) { partial_updates.push_back(curr_update); } @@ -167,13 +171,6 @@ std::vector ServerGameState::generateSharedGameState(bool send_ std::swap(this->updated_entities, empty); } - // DEBUG - /*if (partial_updates.size() > 0) { - std::cout << "Number of partial updates: " << std::to_string(partial_updates.size()) << std::endl; - std::cout << "Partial update's lobby (in server):" << std::endl; - std::cout << partial_updates[0].lobby.to_string() << std::endl; - }*/ - return partial_updates; } @@ -198,6 +195,11 @@ void ServerGameState::update(const EventList& events) { case EventType::ChangeFacing: { auto changeFacingEvent = boost::get(event.data); obj = this->objects.getObject(changeFacingEvent.entity_to_change_face); + + // If the object is the DM and the DM is paralyzed, ignore the event + if (obj->type == ObjectType::DungeonMaster + && dynamic_cast(obj)->isParalyzed()) break; + obj->physics.shared.facing = changeFacingEvent.facing; this->updated_entities.insert({ obj->globalID }); break; @@ -206,6 +208,11 @@ void ServerGameState::update(const EventList& events) { case EventType::StartAction: { auto startAction = boost::get(event.data); obj = this->objects.getObject(startAction.entity_to_act); + + // If the object is the DM and the DM is paralyzed, ignore the event + if (obj->type == ObjectType::DungeonMaster + && dynamic_cast(obj)->isParalyzed()) break; + this->updated_entities.insert({ obj->globalID }); //switch case for action (currently using keys) @@ -242,7 +249,7 @@ void ServerGameState::update(const EventList& events) { case ActionType::Zoom: { // only for DM DungeonMaster * dm = this->objects.getDM(); - if ((dm->physics.shared.corner.y + startAction.movement.y >= 10.0f) && (dm->physics.shared.corner.y + startAction.movement.y <= 100.0f)) + if (dm != nullptr && (dm->physics.shared.corner.y + startAction.movement.y >= 10.0f) && (dm->physics.shared.corner.y + startAction.movement.y <= 100.0f)) dm->physics.shared.corner += startAction.movement; break; @@ -255,6 +262,11 @@ void ServerGameState::update(const EventList& events) { case EventType::StopAction: { auto stopAction = boost::get(event.data); obj = this->objects.getObject(stopAction.entity_to_act); + + // If the object is the DM and the DM is paralyzed, ignore the event + if (obj->type == ObjectType::DungeonMaster + && dynamic_cast(obj)->isParalyzed()) break; + this->updated_entities.insert({ obj->globalID }); //switch case for action (currently using keys) switch (stopAction.action) { @@ -283,6 +295,11 @@ void ServerGameState::update(const EventList& events) { //currently just sets the velocity to given auto moveRelativeEvent = boost::get(event.data); obj = this->objects.getObject(moveRelativeEvent.entity_to_move); + + // If the object is the DM and the DM is paralyzed, ignore the event + if (obj->type == ObjectType::DungeonMaster + && dynamic_cast(obj)->isParalyzed()) break; + obj->physics.velocity += moveRelativeEvent.movement; this->updated_entities.insert(obj->globalID); break; @@ -301,6 +318,9 @@ void ServerGameState::update(const EventList& events) { if (obj->type == ObjectType::DungeonMaster) { DungeonMaster* dm = this->objects.getDM(); + // If the dungeon master is paralyzed, do nothing + if (dm->isParalyzed()) break; + if (dm->sharedTrapInventory.selected + selectItemEvent.itemNum == 0) dm->sharedTrapInventory.selected = TRAP_INVENTORY_SIZE; else if (dm->sharedTrapInventory.selected + selectItemEvent.itemNum == TRAP_INVENTORY_SIZE + 1) @@ -311,6 +331,20 @@ void ServerGameState::update(const EventList& events) { this->updated_entities.insert(dm->globalID); } else { + // If the current selected item is a Mirror and it is used, set it to not be used + SpecificID currentItemSelected = player->inventory[player->sharedInventory.selected - 1]; + + if (currentItemSelected != -1) { + Item* selectedItem = this->objects.getItem(currentItemSelected); + + if (selectedItem->type == ObjectType::Mirror && selectedItem->iteminfo.used) { + // Set mirror to be unused + Mirror* mirror = dynamic_cast(selectedItem); + + mirror->revertEffect(*this); + } + } + if (player->sharedInventory.selected + selectItemEvent.itemNum == 0) player->sharedInventory.selected = INVENTORY_SIZE; else if (player->sharedInventory.selected + selectItemEvent.itemNum == INVENTORY_SIZE + 1) @@ -358,13 +392,20 @@ void ServerGameState::update(const EventList& events) { } case EventType::TrapPlacement: { + DungeonMaster* dm = this->objects.getDM(); + + // exit if DM is null? + if (dm == nullptr) + break; + auto trapPlacementEvent = boost::get(event.data); Grid& currGrid = this->getGrid(); float cellWidth = currGrid.grid_cell_width; - DungeonMaster* dm = this->objects.getDM(); + // If the DM is paralyzed, do nothing + if (dm->isParalyzed()) break; glm::vec3 dir = glm::normalize(trapPlacementEvent.world_pos-dm->physics.shared.corner); @@ -386,7 +427,7 @@ void ServerGameState::update(const EventList& events) { } if (trapPlacementEvent.hover) { - // only for traps, not lightning + // only hover for traps, not lightning if (trapPlacementEvent.cell == CellType::Lightning) { break; } @@ -418,6 +459,7 @@ void ServerGameState::update(const EventList& events) { lightning->useLightning(dm, *this, corner); dm->useMana(); } + break; } @@ -440,6 +482,9 @@ void ServerGameState::update(const EventList& events) { break; } + // change cell type + cell->type = trapPlacementEvent.cell; + trap->setIsDMTrap(true); trap->setExpiration(curr_time + std::chrono::seconds(10)); @@ -447,19 +492,273 @@ void ServerGameState::update(const EventList& events) { dm->sharedTrapInventory.trapsInCooldown[trapPlacementEvent.cell] = std::chrono::system_clock::to_time_t(curr_time); + // Store remaining CD in milliseconds + dm->sharedTrapInventory.trapsCooldown[trapPlacementEvent.cell] = TRAP_COOL_DOWN * 1000; + dm->setPlacedTraps(trapsPlaced + 1); dm->sharedTrapInventory.trapsPlaced = trapsPlaced + 1; + + // SPAWN AN ITEM FOR EACH PLAYER + float randFloat = randomDouble(0.0, 1.0); + + if (randFloat <= ITEM_SPAWN_PROB) { + auto players = this->objects.getPlayers(); + + for (int p = 0; p < players.size(); p++) { + auto _player = players.get(p); + + if (_player == nullptr) + continue; + + GridCell* _cell = this->getGrid().getCell(_player->physics.shared.corner.x / Grid::grid_cell_width, _player->physics.shared.corner.z / Grid::grid_cell_width); + + int randomC = randomInt(std::max(_cell->x - ITEM_SPAWN_BOUND, 0), std::min(this->grid.getColumns() - 1, _cell->x + ITEM_SPAWN_BOUND)); + int randomR = randomInt(std::max(_cell->y - ITEM_SPAWN_BOUND, 0), std::min(this->grid.getRows() - 1, _cell->y + ITEM_SPAWN_BOUND)); + + GridCell* cell = grid.getCell(randomC, randomR); + CellType celltype = cell->type; + + int counter = 0; + + // keep finding that cell! + while ((randomC == _cell->x && randomR == _cell->y) || celltype != CellType::Empty) { + randomC = randomInt(std::max(_cell->x - ITEM_SPAWN_BOUND, 0), std::min(this->grid.getColumns() - 1, _cell->x + ITEM_SPAWN_BOUND)); + randomR = randomInt(std::max(_cell->y - ITEM_SPAWN_BOUND, 0), std::min(this->grid.getRows() - 1, _cell->y + ITEM_SPAWN_BOUND)); + + GridCell* cell = grid.getCell(randomC, randomR); + CellType celltype = cell->type; + + counter += 1; + + // this allows us to break out in case infinite loop + if (counter >= ((ITEM_SPAWN_BOUND + 1) * (ITEM_SPAWN_BOUND + 1))) { + break; + } + } + + // early exit, just couldn't place anything sadly + if ((randomC == _cell->x && randomR == _cell->y) || celltype != CellType::Empty) { + std::cout << "COULDNT PLACE ANY ITEMS!" << std::endl; + break; + } + + if (!this->hasObjectCollided(this->spawner->smallDummyItem, + glm::vec3(randomC * Grid::grid_cell_width + 0.01f, 0, randomR * Grid::grid_cell_width + 0.01f))) + { + this->objects.moveObject(this->spawner->smallDummyItem, glm::vec3(-1, 0, -1)); + + glm::vec3 dimensions(1.0f); + + glm::vec3 corner( + cell->x * Grid::grid_cell_width + 1, + 0, + cell->y * Grid::grid_cell_width + 1 + ); + + int randomCellType = randomInt(1, 3); + + if (randomCellType == 1) { + int r = randomInt(1, 3); + if (r == 1) { + cell->type = CellType::HealthPotion; + } + else if (r == 2) { + cell->type = CellType::InvisibilityPotion; + } + else { + cell->type = CellType::InvincibilityPotion; + } + } + else if (randomCellType == 2) { + int r = randomInt(1, 3); + if (r == 1) { + cell->type = CellType::FireSpell; + } + else if (r == 2) { + cell->type = CellType::HealSpell; + } + else { + cell->type = CellType::TeleportSpell; + } + } + else { + int r = randomInt(1, 4); + if (r == 1) { + cell->type = CellType::Dagger; + } + else if (r == 2) { + cell->type = CellType::Sword; + } + else if (r == 3) { + cell->type = CellType::Mirror; + } + else { + cell->type = CellType::Hammer; + } + } + + switch (cell->type) { + case CellType::Dagger: { + glm::vec3 dimensions(1.0f); + + glm::vec3 corner( + cell->x * Grid::grid_cell_width + 1, + 0, + cell->y * Grid::grid_cell_width + 1); + + Weapon* weapon = new Weapon(corner, dimensions, WeaponType::Dagger); + + this->objects.createObject(weapon); + + this->updated_entities.insert(weapon->globalID); + break; + } + case CellType::Sword: { + glm::vec3 dimensions(1.0f); + + glm::vec3 corner( + cell->x * Grid::grid_cell_width + 1, + 0, + cell->y * Grid::grid_cell_width + 1); + + + Weapon* weapon = new Weapon(corner, dimensions, WeaponType::Sword); + + this->objects.createObject(weapon); + + this->updated_entities.insert(weapon->globalID); + + break; + } + case CellType::Hammer: { + glm::vec3 dimensions(1.0f); + + glm::vec3 corner( + cell->x * Grid::grid_cell_width + 1, + 0, + cell->y * Grid::grid_cell_width + 1); + + + Weapon* weapon = new Weapon(corner, dimensions, WeaponType::Hammer); + + this->objects.createObject(weapon); + + this->updated_entities.insert(weapon->globalID); + + break; + } + case CellType::TeleportSpell: { + glm::vec3 dimensions(1.0f); + + glm::vec3 corner( + cell->x * Grid::grid_cell_width + 1, + 0, + cell->y * Grid::grid_cell_width + 1); + + Spell* spell = new Spell(corner, dimensions, SpellType::Teleport); + + this->objects.createObject(spell); + + this->updated_entities.insert(spell->globalID); + + + break; + } + case CellType::FireSpell: { + glm::vec3 dimensions(1.0f); + + glm::vec3 corner( + cell->x * Grid::grid_cell_width + 1, + 0, + cell->y * Grid::grid_cell_width + 1); + + Spell* spell = new Spell(corner, dimensions, SpellType::Fireball); + + this->objects.createObject(spell); + + this->updated_entities.insert(spell->globalID); + + + break; + } + case CellType::HealSpell: { + glm::vec3 dimensions(1.0f); + + glm::vec3 corner( + cell->x * Grid::grid_cell_width + 1, + 0, + cell->y * Grid::grid_cell_width + 1); + + + Spell* spell = new Spell(corner, dimensions, SpellType::HealOrb); + + this->objects.createObject(spell); + + this->updated_entities.insert(spell->globalID); + + break; + } + case CellType::HealthPotion: { + glm::vec3 dimensions(1.0f); + + glm::vec3 corner(cell->x * Grid::grid_cell_width + 1, + 0, + cell->y * Grid::grid_cell_width + 1); + + Potion* potion = new Potion(corner, dimensions, PotionType::Health); + + this->objects.createObject(potion); + + this->updated_entities.insert(potion->globalID); + + break; + } + case CellType::InvisibilityPotion: { + glm::vec3 dimensions(1.0f); + + glm::vec3 corner(cell->x * Grid::grid_cell_width + 1, + 0, + cell->y * Grid::grid_cell_width + 1); + + + Potion* potion = new Potion(corner, dimensions, PotionType::Invisibility); + + this->objects.createObject(potion); + + this->updated_entities.insert(potion->globalID); + + break; + } + case CellType::InvincibilityPotion: { + glm::vec3 dimensions(1.0f); + + glm::vec3 corner(cell->x * Grid::grid_cell_width + 1, + 0, + cell->y * Grid::grid_cell_width + 1); + + Potion* potion = new Potion(corner, dimensions, PotionType::Invincibility); + + this->objects.createObject(potion); + + this->updated_entities.insert(potion->globalID); + + break; + } + default: + std::cout << "what item is this??" << std::endl; + } + } + } + } + } + break; } - - // default: - // std::cerr << "Unimplemented EventType (" << event.type << ") received" << std::endl; } } - // TODO: fill update() method with updating object movement doProjectileTicks(); doTorchlightTicks(); @@ -478,6 +777,11 @@ void ServerGameState::update(const EventList& events) { handleDM(); tickStatuses(); updateCompass(); + updatePlayerLightningInvulnerabilityStatus(); + + // Only do this if the DM exists + if (this->objects.getDM() != nullptr) + updateDungeonMasterParalysis(); // Increment timestep this->timestep++; @@ -485,9 +789,7 @@ void ServerGameState::update(const EventList& events) { // Countdown timer if the Orb has been picked up by a Player and the match // phase is now RelayRace if (this->matchPhase == MatchPhase::RelayRace) { - this->timesteps_left--; - - if (this->timesteps_left == 0) { + if (std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()) > this->relay_finish_time) { // Dungeon Master won on time limit expiration this->phase = GamePhase::RESULTS; } @@ -656,6 +958,15 @@ void ServerGameState::updateMovement() { object->physics.shared.corner.y = spike_low_y; object->physics.feels_gravity = false; object->physics.velocity.y = 0; + if (starting_corner_pos.y != 3.0f) { + this->sound_table.addNewSoundSource(SoundSource( + ServerSFX::CeilingSpikeImpact, + object->physics.shared.corner, + FULL_VOLUME, + FAR_DIST, + FAR_ATTEN + )); + } } // Vertical movement @@ -680,15 +991,7 @@ void ServerGameState::updateMovement() { MEDIUM_DIST, MEDIUM_ATTEN )); - } else if (object->type == ObjectType::SpikeTrap) { - this->sound_table.addNewSoundSource(SoundSource( - ServerSFX::CeilingSpikeImpact, - object->physics.shared.corner, - FULL_VOLUME, - FAR_DIST, - FAR_ATTEN - )); - } + } } } @@ -828,6 +1131,16 @@ void ServerGameState::updateItems() { } } + if (item->type == ObjectType::Mirror) { + Mirror* mirror = dynamic_cast(item); + if (mirror->iteminfo.used) { + if (mirror->timeOut()) { + mirror->revertEffect(*this); + this->updated_entities.insert(mirror->globalID); + } + } + } + if (item->type == ObjectType::Weapon) { Weapon* weapon = dynamic_cast(item); weapon->reset(*this); @@ -895,34 +1208,45 @@ void ServerGameState::updateTraps() { DungeonMaster* dm = this->objects.getDM(); // update DM trap cooldown - if (this->objects.getDM() != nullptr) { + if (dm != nullptr) { auto& coolDownMap = dm->sharedTrapInventory.trapsInCooldown; - //std::cout << coolDownMap << " cooldown map size" << std::endl; - for (auto it = dm->sharedTrapInventory.trapsInCooldown.cbegin(); it != dm->sharedTrapInventory.trapsInCooldown.cend();) { + auto key = it->first; + auto passedTime = std::chrono::round(current_time - std::chrono::system_clock::from_time_t(it->second)); + auto diff = std::chrono::milliseconds(TRAP_COOL_DOWN * 1000) - passedTime; + + // update remaining time + dm->sharedTrapInventory.trapsCooldown[key] = diff.count(); + if (std::chrono::round(current_time - std::chrono::system_clock::from_time_t(it->second)) >= std::chrono::seconds(TRAP_COOL_DOWN)) { + dm->sharedTrapInventory.trapsCooldown.erase(key); it = dm->sharedTrapInventory.trapsInCooldown.erase(it); } else { it++; } } + + this->updated_entities.insert(dm->globalID); } - // This object moved, so we should check to see if a trap should trigger because of it auto traps = this->objects.getTraps(); for (int i = 0; i < traps.size(); i++) { auto trap = traps.get(i); - if (trap == nullptr) { continue; } // unsure if i need this? - if (trap->getIsDMTrap()) { - + if (trap == nullptr) { continue; } + if (trap->getIsDMTrap() && dm != nullptr) { if (current_time >= trap->getExpiration()) { - int trapsPlaced = dm->getPlacedTraps(); - this->markForDeletion(trap->globalID); dm->setPlacedTraps(trapsPlaced - 1); + dm->sharedTrapInventory.trapsPlaced = trapsPlaced - 1; + + // change cell type to empty + GridCell* _cell = this->getGrid().getCell(trap->physics.shared.corner.x / Grid::grid_cell_width, trap->physics.shared.corner.z / Grid::grid_cell_width); + + _cell->type = CellType::Empty; + continue; } } @@ -932,10 +1256,10 @@ void ServerGameState::updateTraps() { trap->trigger(*this); this->updated_entities.insert(trap->globalID); } - if (trap->shouldReset(*this)) { - trap->reset(*this); + if (trap->shouldReset(*this)) { + trap->reset(*this); this->updated_entities.insert(trap->globalID); - } + } } } @@ -1188,6 +1512,49 @@ void ServerGameState::handleTickVelocity() { } } +void ServerGameState::updatePlayerLightningInvulnerabilityStatus() { + // Iterate through all players. If one of the players has a + // lightning invulernability, check whether it should be turned + // off, and if so, turn it off. + for (int i = 0; i < this->objects.getPlayers().size(); i++) { + Player* player = this->objects.getPlayers().get(i); + + if (player == nullptr) + continue; + + if (player->isInvulnerableToLightning()) { + // Player is invulnerable to lightning - check whether timeout + // has occurred and if so, set as vulnerable to lightning again + auto now = std::chrono::system_clock::now(); + std::chrono::duration elapsed_seconds{ now - player->getLightningInvulnerabilityStartTime()}; + + if (elapsed_seconds.count() > player->getLightningInvulnerabilityDuration()) { + std::cout << "Removing a player's lightning invulnerability." << std::endl; + player->setInvulnerableToLightning(false, -1); + + // If the player gained invulnerability due to reflecting a + // lightning bolt with a mirror, undo that boolean + player->info.used_mirror_to_reflect_lightning = false; + } + } + } +} + +void ServerGameState::updateDungeonMasterParalysis() { + // Check whether the DM is paralyzed + DungeonMaster* dm = this->objects.getDM(); + if (dm->isParalyzed()) { + // Check whether timeout has occurred and if so, set as not paralyzed + auto now = std::chrono::system_clock::now(); + std::chrono::duration elapsed_seconds{ now - dm->getParalysisStartTime() }; + + if (elapsed_seconds.count() > dm->getParalysisDuration()) { + std::cout << "Ending DM's paralysis" << std::endl; + dm->setParalysis(false, -1); + } + } +} + unsigned int ServerGameState::getTimestep() const { return this->timestep; @@ -1212,6 +1579,7 @@ void ServerGameState::transitionToRelayRace() { this->matchPhase = MatchPhase::RelayRace; + this->relay_finish_time = getSecSinceEpoch() + TIME_LIMIT_S.count(); // Open all exits! for (int i = 0; i < this->objects.getExits().size(); i++) { Exit* exit = this->objects.getExits().get(i); @@ -1303,7 +1671,39 @@ Trap* ServerGameState::placeTrapInCell(GridCell* cell, CellType type) { case CellType::FireballTrapDown: { if (cell->type != CellType::Empty) return nullptr; - return spawnFireballTrap(cell); + + glm::vec3 dimensions = Object::models.at(ModelType::SunGod); + glm::vec3 corner( + (cell->x * Grid::grid_cell_width), + 0.0f, + (cell->y * Grid::grid_cell_width) + ); + Direction dir; + switch (type) { + case CellType::FireballTrapLeft: + dir = Direction::LEFT; + // corner.z -= (dimensions.z / 2.0f); + break; + case CellType::FireballTrapRight: + dir = Direction::RIGHT; + // corner.z -= (dimensions.z / 2.0f); + break; + case CellType::FireballTrapUp: + dir = Direction::UP; + corner.x += (dimensions.x / 2.0f); + break; + case CellType::FireballTrapDown: + corner.x += (dimensions.x / 2.0f); + dir = Direction::DOWN; + break; + default: + dir = Direction::LEFT; + break; + } + + FireballTrap* fireBallTrap = new FireballTrap(corner, dir); + this->objects.createObject(fireBallTrap); + return fireBallTrap; } case CellType::SpikeTrap: { if (cell->type != CellType::Empty) @@ -1374,42 +1774,40 @@ Trap* ServerGameState::placeTrapInCell(GridCell* cell, CellType type) { this->objects.createObject(floorSpike); return floorSpike; } - /* - TODO: ADD BACK ARROWS! - case CellType::ArrowTrapDown: case CellType::ArrowTrapLeft: case CellType::ArrowTrapRight: case CellType::ArrowTrapUp: { - ArrowTrap::Direction dir; + if (cell->type != CellType::Empty) { + return nullptr; + } + + Direction dir; if (cell->type == CellType::ArrowTrapDown) { - dir = ArrowTrap::Direction::DOWN; + dir = Direction::DOWN; } else if (cell->type == CellType::ArrowTrapUp) { - + dir = Direction::UP; } else if (cell->type == CellType::ArrowTrapLeft) { - dir = ArrowTrap::Direction::LEFT; + dir = Direction::LEFT; } else { - dir = ArrowTrap::Direction::RIGHT; + dir = Direction::RIGHT; } - glm::vec3 dimensions( - Grid::grid_cell_width, - MAZE_CEILING_HEIGHT, - Grid::grid_cell_width - ); glm::vec3 corner( - cell->x * Grid::grid_cell_width, + (cell->x* Grid::grid_cell_width), 0.0f, - cell->y * Grid::grid_cell_width + (cell->y* Grid::grid_cell_width) ); - this->objects.createObject(new ArrowTrap(corner, dimensions, dir)); - break; - }*/ + ArrowTrap* arrowTrap = new ArrowTrap(corner, dir); + this->objects.createObject(arrowTrap); + + return arrowTrap; + } case CellType::TeleporterTrap: { if (cell->type != CellType::Empty) return nullptr; @@ -1472,9 +1870,6 @@ void ServerGameState::loadMaze(const Grid& grid) { } } - // Step 5: Add floor and ceiling SolidSurfaces. - std::vector> freeSpots(grid.getRows(), std::vector(grid.getColumns(), false)); - // create multiple floor and ceiling objects to populate the size of the entire maze. // currently doing this to stretch out the floor texture by the desired factor. here // in the maze generation we can have a lot of control over how frequently the floor texture @@ -1503,9 +1898,6 @@ void ServerGameState::loadMaze(const Grid& grid) { Grid::grid_cell_width * GRIDS_PER_FLOOR_OBJECT) ); this->objects.createObject(ceiling); - - if(freeSpots[r][c]) - solidSurfaceInGridCells.insert({{c, r}, {floor}}); } } @@ -1537,13 +1929,16 @@ void ServerGameState::loadMaze(const Grid& grid) { cell->type = CellType::TeleportSpell; } } else if (cell->type == CellType::RandomWeapon) { - int r = randomInt(1, 3); + int r = randomInt(1, 4); if (r == 1) { cell->type = CellType::Dagger; } else if (r == 2) { cell->type = CellType::Sword; } + else if (r == 3) { + cell->type = CellType::Mirror; + } else { cell->type = CellType::Hammer; } @@ -1784,9 +2179,9 @@ void ServerGameState::loadMaze(const Grid& grid) { } case CellType::Exit: { glm::vec3 corner( - cell->x* Grid::grid_cell_width, + cell->x * Grid::grid_cell_width, 0.0f, - cell->y* Grid::grid_cell_width + cell->y * Grid::grid_cell_width ); glm::vec3 dimensions( @@ -1809,13 +2204,22 @@ void ServerGameState::loadMaze(const Grid& grid) { this->objects.createObject(new Exit(false, corner, dimensions, lightProperties)); break; } + case CellType::Mirror: { + glm::vec3 dimensions(1.0f); + + glm::vec3 corner(cell->x* Grid::grid_cell_width + 1, + 0, + cell->y* Grid::grid_cell_width + 1); + + this->objects.createObject(new Mirror(corner, dimensions)); + break; + } default: { - freeSpots[row][col] = true; + } } } } - } void ServerGameState::spawnWall(GridCell* cell, int col, int row, bool is_internal) { @@ -1843,10 +2247,6 @@ void ServerGameState::spawnWall(GridCell* cell, int col, int row, bool is_intern SolidSurface* wall = new SolidSurface(false, Collider::Box, surface_type, corner, dimensions); wall->shared.is_internal = is_internal; this->objects.createObject(wall); - if (cell->type == CellType::Wall || cell->type == CellType::Pillar) { - // don't let the DM select walls with torches - solidSurfaceInGridCells.insert({{col, row}, { wall }}); - } } } diff --git a/src/server/game/spawner.cpp b/src/server/game/spawner.cpp index fff9e252..000f1c1f 100644 --- a/src/server/game/spawner.cpp +++ b/src/server/game/spawner.cpp @@ -16,6 +16,7 @@ Spawner::Spawner() { this->enemyValueCap = MAX_ENEMY_VALUE; this->currentEnemyValue = 0; this->dummyItem = nullptr; + this->smallDummyItem = nullptr; this->valueMap.push_back(20); // 0: Big slime (size = 4) this->valueMap.push_back(10); // 1: Medium slime (size = 3) @@ -136,8 +137,6 @@ glm::vec3 Spawner::findEmptyPosition(ServerGameState& state) { // add small offset for spawn if (!state.hasObjectCollided(this->dummyItem, glm::vec3(random_cell.x * Grid::grid_cell_width + 0.01f, 0, random_cell.y * Grid::grid_cell_width + 0.01f))) { - for (glm::ivec2 cellPos : this->dummyItem->gridCellPositions) { - } state.objects.moveObject(this->dummyItem, glm::vec3(-1, 0, -1)); break; } @@ -156,6 +155,16 @@ void Spawner::spawnDummy(ServerGameState& state) { dummy->iteminfo.held = true; } +void Spawner::spawnSmallDummy(ServerGameState& state) { + // Set dummy item with the biggest possible enemy size + // Used for checking spawnable tile + SpecificID itemID = state.objects.createObject(new Item(ObjectType::Item, true, glm::vec3(-1, 0, -1), ModelType::Cube, glm::vec3(1))); + auto dummy = state.objects.getItem(itemID); + dummy->physics.shared.dimensions = glm::vec3(1.0f, 1.0f, 1.0f); + this->smallDummyItem = dummy; + dummy->iteminfo.held = true; +} + void Spawner::addEnemy(ServerGameState& state, SpecificID id) { auto enemy = state.objects.getEnemy(id); auto type = enemy->type; diff --git a/src/server/game/spell.cpp b/src/server/game/spell.cpp index 959a0d17..9495d258 100644 --- a/src/server/game/spell.cpp +++ b/src/server/game/spell.cpp @@ -55,18 +55,22 @@ void Spell::useItem(Object* other, ServerGameState& state, int itemSelected) { } case SpellType::Teleport: { sound = ServerSFX::Teleport; - auto players = state.objects.getPlayers(); - Player* rand_player; - while (true) { - rand_player = players.get(randomInt(0, players.size() - 1)); - if (players.size() == 1) { - break; - } - if (player->typeID != rand_player->typeID) { - break; + + std::vector valid_players; + + auto all_players = state.objects.getPlayers(); + for (int i = 0; i < all_players.size(); i++) { + if (all_players.get(i) != nullptr && all_players.get(i)->typeID != player->typeID) { + valid_players.push_back(all_players.get(i)); } } + if (valid_players.size() < 1) { + return; + } + + Player* rand_player = valid_players.at(randomInt(0, valid_players.size() - 1)); + auto& grid = state.getGrid(); int r_col = 0; int r_row = 0; diff --git a/src/server/game/weaponcollider.cpp b/src/server/game/weaponcollider.cpp index 72bb187b..370ee619 100644 --- a/src/server/game/weaponcollider.cpp +++ b/src/server/game/weaponcollider.cpp @@ -80,11 +80,11 @@ bool WeaponCollider::readyTime(ServerGameState& state) { if (!this->playSound && this->info.lightning) { state.soundTable().addNewSoundSource(SoundSource( - ServerSFX::Thunder, + ServerSFX::ElectricHum, this->physics.shared.getCenterPosition(), DEFAULT_VOLUME, - FAR_DIST, - FAR_ATTEN + MEDIUM_DIST, + MEDIUM_ATTEN )); this->playSound = true; } @@ -105,6 +105,16 @@ bool WeaponCollider::readyTime(ServerGameState& state) { this->info.attacked = true; this->attacked_time = now; this->physics.collider = Collider::Box; + if (this->info.lightning) { + state.soundTable().addNewSoundSource(SoundSource( + ServerSFX::Thunder, + this->physics.shared.getCenterPosition(), + DEFAULT_VOLUME, + FAR_DIST, + FAR_ATTEN + )); + } + this->playSound = true; return true; } return false; diff --git a/src/server/server.cpp b/src/server/server.cpp index 37c66f87..49458ef2 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -204,14 +204,14 @@ std::chrono::milliseconds Server::doTick() { // Handle ready and start game events EventList clientEvents = getAllClientEvents(); + for (const auto& [src_eid, event] : clientEvents) { // Skip non-lobby action events + // std::cout << event << "\n"; if (event.type != EventType::LobbyAction) { continue; } - std::cout << "Received a LobbyAction event!" << std::endl; - LobbyActionEvent lobbyEvent = boost::get(event.data); switch (lobbyEvent.action) { @@ -288,19 +288,24 @@ std::chrono::milliseconds Server::doTick() { lightning->physics.collider = Collider::None; dm->lightning = lightning; - // Spawn player in random spawn point - - // TODO: Possibly replace this random spawn point with player assignments? - // I.e., assign each player a spawn point to avoid multiple players getting - // the same spawn point? - auto& by_id = this->sessions.get(); auto session_entry = by_id.find(dm->globalID); if (session_entry != by_id.end()) { - std::weak_ptr session_ref = session_entry->session; - std::shared_ptr session = session_ref.lock(); + auto& session = session_entry->session; + by_id.modify(session_entry, [](SessionEntry& entry) { entry.is_dungeon_master = true;}); + session_entry->session->setDM(true); if (session != nullptr) { + auto& by_ip = this->sessions.get(); + + auto addr = session_entry->ip; + + auto old_session = by_ip.find(addr); + + by_ip.modify(old_session, [](SessionEntry& entry) { + entry.is_dungeon_master = true; + }); + session->sendPacket(PackagedPacket::make_shared(PacketType::ServerAssignEID, ServerAssignEIDPacket{ .eid = dm->globalID, .is_dungeon_master = true })); } @@ -322,12 +327,13 @@ std::chrono::milliseconds Server::doTick() { std::cout << "Assigned player " + std::to_string(index) + " to be the DM" << std::endl; std::cout << "Starting game!" << std::endl; - // TODO: more permanent way to wait until DM has received their is_dungeon_master value - // from the assign eid packet - std::this_thread::sleep_for(2s); } - this->state.setPhase(GamePhase::GAME); + if (this->config.server.skip_intro) { + this->state.setPhase(GamePhase::GAME); + } else { + this->state.setPhase(GamePhase::INTRO_CUTSCENE); + } } break; @@ -341,27 +347,18 @@ std::chrono::milliseconds Server::doTick() { break; } - - /* - // Go through sessions and update GameState lobby info - // TODO: move this into updateGameState or something else - for (const auto& [eid, is_dm, ip, session]: this->sessions) { - if (session->isOkay()) { - this->state.addPlayerToLobby(eid, session->getInfo().client_name.value_or("UNKNOWN NAME")); - } else { - this->state.removePlayerFromLobby(eid); - } - } - - if (this->state.getLobby().players.size() >= this->state.getLobby().max_players) { + case GamePhase::INTRO_CUTSCENE: { + bool finished = this->intro_cutscene.update(); + if (finished) { this->state.setPhase(GamePhase::GAME); - // TODO: figure out how to selectively broadcast to only the players that were already in the lobby - // this->lobby_broadcaster.stopBroadcasting(); + } else { + LoadIntroCutsceneEvent update = this->intro_cutscene.toNetwork(); + sendUpdateToAllClients(Event(this->world_eid, EventType::LoadIntroCutscene, update)); } - this->lobby_broadcaster.setLobbyInfo(this->state.getLobby()); break; - */ + } + case GamePhase::GAME: { EventList allClientEvents = getAllClientEvents(); @@ -387,36 +384,7 @@ std::chrono::milliseconds Server::doTick() { std::exit(1); } - - // TODO: send sound effects to DM? - std::vector players; // hold players and DM - for (int i = 0; i < this->state.objects.getPlayers().size(); i++) { - auto player = this->state.objects.getPlayer(i); - if (player != nullptr) { - players.push_back(player); - } - } - if (this->state.objects.getDM() != nullptr) { - players.push_back(this->state.objects.getDM()); - } - auto audio_commands_per_player = this->state.soundTable().getCommandsPerPlayer(players); - - for (auto& session_entry : this->sessions) { - if (!audio_commands_per_player.contains(session_entry.id)) { - continue; // no sounds to send to that player - } - - auto session = session_entry.session; - if (!session->isOkay()) { - continue; // lost connection with this session, so can't send audio updates to it - } - - session->sendEvent(Event(this->world_eid, EventType::LoadSoundCommands, LoadSoundCommandsEvent( - audio_commands_per_player.at(session_entry.id) - ))); - } - - this->state.soundTable().tickSounds(); + this->sendSoundCommands(); // send partial updates to the clients // ALSO where the packets actually get sent @@ -424,7 +392,6 @@ std::chrono::milliseconds Server::doTick() { sendUpdateToAllClients(Event(this->world_eid, EventType::LoadGameState, LoadGameStateEvent(partial_update))); } - // Calculate how long we need to wait until the next tick auto stop = std::chrono::high_resolution_clock::now(); auto wait = std::chrono::duration_cast( @@ -474,6 +441,9 @@ std::shared_ptr Server::_handleNewSession(boost::asio::ip::address addr auto new_session = std::make_shared(std::move(this->socket), SessionInfo({}, old_id, old_session->is_dungeon_master)); + + std::cout << "OLD ID: " << old_id << " OLD IS DM: " << old_session->is_dungeon_master << std::endl; + by_ip.replace(old_session, SessionEntry(old_id, old_session->is_dungeon_master, addr, new_session)); std::cout << "Reestablished connection with " << addr @@ -505,4 +475,56 @@ std::shared_ptr Server::_handleNewSession(boost::asio::ip::address addr << player->globalID << std::endl; return session; +} + +void Server::sendSoundCommands() { + bool is_intro_cutscene = this->state.getPhase() == GamePhase::INTRO_CUTSCENE; + + ServerGameState& curr_state = (is_intro_cutscene) ? this->intro_cutscene.state : this->state; + + // TODO: send sound effects to DM? + std::vector players; // hold players and DM + for (int i = 0; i < curr_state.objects.getPlayers().size(); i++) { + auto player = curr_state.objects.getPlayer(i); + if (player != nullptr) { + players.push_back(player); + } + } + if (curr_state.objects.getDM() != nullptr) { + players.push_back(curr_state.objects.getDM()); + } + + auto audio_commands_per_player = curr_state.soundTable().getCommandsPerPlayer(players); + + for (auto& session_entry : this->sessions) { + EntityID eid; + if (is_intro_cutscene) { + if (!session_entry.session->getInfo().is_dungeon_master.has_value()){ + continue; + } + + if (session_entry.session->getInfo().is_dungeon_master.value()) { + eid = this->intro_cutscene.dm_eid; + } else { + eid = this->intro_cutscene.pov_eid; + } + } else { + eid = session_entry.id; + } + + if (!audio_commands_per_player.contains(eid)) { + continue; // no sounds to send to that player + } + + auto session = session_entry.session; + if (!session->isOkay()) { + continue; // lost connection with this session, so can't send audio updates to it + } + + session->sendEvent(Event(this->world_eid, EventType::LoadSoundCommands, LoadSoundCommandsEvent( + audio_commands_per_player.at(eid) + ))); + } + + curr_state.soundTable().tickSounds(); } \ No newline at end of file diff --git a/src/shared/audio/soundtype.cpp b/src/shared/audio/soundtype.cpp index 3cc96747..32277df3 100644 --- a/src/shared/audio/soundtype.cpp +++ b/src/shared/audio/soundtype.cpp @@ -9,8 +9,11 @@ static auto audio_dir = getRepoRoot() / "assets" / "sounds"; std::string getAudioPath(ClientSFX sound) { static auto dir = audio_dir / "client_sfx"; switch (sound) { - case ClientSFX::TEMP: - return (dir / "vine-boom-mono.mp3").string(); + case ClientSFX::VictoryThemePlayers: + return (dir / "victory_players.flac").string(); + case ClientSFX::VictoryThemeDM: + // TODO: Replace with DM Victory theme! + return (dir / "victory_players.flac").string(); default: std::cerr << "FATAL: no known path for ClientSFX " << static_cast(sound) << std::endl; std::exit(1); @@ -64,6 +67,16 @@ std::string getAudioPath(ServerSFX sfx) { return (dir / "thunder.wav").string(); case ServerSFX::TorchLoop: return (dir / "torch_loop_mono.wav").string(); + case ServerSFX::PlayersStartTheme: + return (dir / "players_start_theme.mp3").string(); + case ServerSFX::ElectricHum: + return (dir / "electric_hum.wav").string(); + case ServerSFX::IntroGateOpen: + return (dir / "cutscene_gate_open.wav").string(); + case ServerSFX::ZeusStartTheme: + return (dir / "zeus_start_theme.mp3").string(); + case ServerSFX::Wind: + return (dir / "wind.wav").string(); case ServerSFX::Teleport: return (dir / "teleport.wav").string(); case ServerSFX::Potion: @@ -74,6 +87,8 @@ std::string getAudioPath(ServerSFX sfx) { return (dir / "itempickup.wav").string(); case ServerSFX::ItemDrop: return (dir / "itemdrop.wav").string(); + case ServerSFX::MirrorShatter: + return (dir / "mirror_shatter.mp3").string(); default: std::cerr << "FATAL: no known path for ServerSFX " << static_cast(sfx) << std::endl; @@ -85,10 +100,18 @@ std::string getAudioPath(ClientMusic music) { static auto dir = audio_dir / "client_music"; switch (music) { - case ClientMusic::TitleTheme: - return (dir / "piano.wav").string(); - case ClientMusic::GameTheme: - return (dir / "maze.mp3").string(); + case ClientMusic::MenuTheme: + // TODO: Replace with menu theme! + return (dir / "menu.mp3").string(); + case ClientMusic::MazeExplorationPlayersTheme: + return (dir / "maze_exploration_players.flac").string(); + case ClientMusic::MazeExplorationDMTheme: + return (dir / "maze_exploration_dm.flac").string(); + case ClientMusic::RelayRacePlayersTheme: + return (dir / "relay_race_players.mp3").string(); + case ClientMusic::RelayRaceDMTheme: + // TODO: Replace with DM Relay Race theme! + return (dir / "maze_exploration_dm.flac").string(); default: std::cerr << "FATAL: no known path for ClientMusic " << static_cast(music) << std::endl; std::exit(1); diff --git a/src/shared/game/sharedgamestate.cpp b/src/shared/game/sharedgamestate.cpp index 1881a525..b00acbab 100644 --- a/src/shared/game/sharedgamestate.cpp +++ b/src/shared/game/sharedgamestate.cpp @@ -5,16 +5,10 @@ void SharedGameState::update(const SharedGameState& other) { // copy over static data that is sent every update this->lobby = other.lobby; - // DEBUG - /* - std::cout << "SharedGameState::update()" << std::endl; - std::cout << other.lobby.to_string() << std::endl; - */ - this->phase = other.phase; this->timestep = other.timestep; this->matchPhase = other.matchPhase; - this->timesteps_left = other.timesteps_left; + this->relay_finish_time = other.relay_finish_time; this->playerVictory = other.playerVictory; this->numPlayerDeaths = other.numPlayerDeaths; diff --git a/src/shared/network/session.cpp b/src/shared/network/session.cpp index 1dbe2433..b9602ccb 100644 --- a/src/shared/network/session.cpp +++ b/src/shared/network/session.cpp @@ -165,4 +165,8 @@ bool Session::socketHasEnoughBytes(std::size_t bytes) { std::size_t bytes_readable = command.get(); return bytes_readable >= bytes; +} + +void Session::setDM(bool is_dm) { + this->info.is_dungeon_master = is_dm; } \ No newline at end of file diff --git a/src/shared/utilities/config.cpp b/src/shared/utilities/config.cpp index d27062db..05f49c59 100644 --- a/src/shared/utilities/config.cpp +++ b/src/shared/utilities/config.cpp @@ -52,7 +52,8 @@ GameConfig GameConfig::parse(int argc, char** argv) { // cppcheck-suppress const .lobby_name = json.at("server").at("lobby_name"), .lobby_broadcast = json.at("server").at("lobby_broadcast"), .max_players = json.at("server").at("max_players"), - .disable_dm = json.at("server").at("disable_dm") + .disable_dm = json.at("server").at("disable_dm"), + .skip_intro = json.at("server").at("skip_intro") }, .client = { .default_name = json.at("client").at("default_name"), @@ -86,7 +87,8 @@ GameConfig getDefaultConfig() { .lobby_name = "My Test Lobby", .lobby_broadcast = false, .max_players = 1, - .disable_dm = false + .disable_dm = false, + .skip_intro = false }, .client = { .default_name = "Player", diff --git a/src/shared/utilities/time.cpp b/src/shared/utilities/time.cpp index 729cb54f..c6c33936 100644 --- a/src/shared/utilities/time.cpp +++ b/src/shared/utilities/time.cpp @@ -7,3 +7,9 @@ long long getMsSinceEpoch() { std::chrono::system_clock::now().time_since_epoch() ).count(); } + +long long getSecSinceEpoch() { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch() + ).count(); +}