diff --git a/rwengine/src/engine/GameState.cpp b/rwengine/src/engine/GameState.cpp index b6ea93e96..9b3a816ab 100644 --- a/rwengine/src/engine/GameState.cpp +++ b/rwengine/src/engine/GameState.cpp @@ -93,6 +93,7 @@ GameStats::GameStats() GameState::GameState() : basic{} + , rampage(this) , gameTime(0.f) , currentProgress(0) , maxProgress(1) @@ -216,4 +217,80 @@ bool GameState::isFading() const { void GameState::setFadeColour(glm::i32vec3 colour) { fadeColour = colour; +} + +Rampage::Status Rampage::getStatus() const { + return status; +} + +void Rampage::init(const std::string& gxtEntry, const int32_t weaponID, + const int32_t time, const int32_t kills, + const std::array& modelsToKill, + const bool headshot) { + if (gxtEntry != "PAGE_00") { + special = true; + } + status = Ongoing; + weapon = weaponID; + endTime = state->world->getGameTime() + static_cast(time) / 1000.f; + killsRequired = kills; + modelIDsToKill = modelsToKill; + headshotOnly = headshot; +} + +void Rampage::tick(float dt) { + RW_UNUSED(dt); + if (getStatus() != Ongoing) { + return; + } + + if (endTime <= state->world->getGameTime()) { + status = Failed; + } + + if (killsRequired <= 0) { + if (!special) { + state->gameStats.rampagesPassed++; + } + + status = Passed; + } + +} + +float Rampage::getRemainingTime() const { + return endTime - state->world->getGameTime(); +} + +uint32_t Rampage::getKillsForThisModel(ModelID model) { + const auto& search = modelIDsKilled.find(model); + if (search != modelIDsKilled.end()) { + return search->second; + } + return 0; +} + +void Rampage::clearKills() { + modelIDsKilled.clear(); +} + +void Rampage::onCharacterDie(CharacterObject* victim, GameObject* killer) { + if (getStatus() != Ongoing) { + return; + } + + if (killer) { + if (!static_cast(killer)->isPlayer()) { + return; + } + } + + const auto modelID = victim->getModelInfo()->id(); + const auto isRightModel = std::find(std::begin(modelIDsToKill), + std::end(modelIDsToKill), modelID); + + if (isRightModel != std::end(modelIDsToKill)) { + modelIDsKilled[modelID]++; + killsRequired--; + } } \ No newline at end of file diff --git a/rwengine/src/engine/GameState.hpp b/rwengine/src/engine/GameState.hpp index 071642cef..b1e663695 100644 --- a/rwengine/src/engine/GameState.hpp +++ b/rwengine/src/engine/GameState.hpp @@ -16,6 +16,7 @@ #include #include +#include #include class GameWorld; @@ -23,6 +24,8 @@ class GameObject; class ScriptMachine; struct CutsceneData; +class CharacterObject; + struct SystemTime { uint16_t year; uint16_t month; @@ -290,6 +293,44 @@ struct ScriptContactData { uint32_t baseBrief; }; +class Rampage { + int32_t weapon = 0; + float endTime = 0.f; + uint32_t killsRequired = 0; + std::array modelIDsToKill{}; + bool special = false; + bool headshotOnly = false; + std::unordered_map modelIDsKilled{}; + +public: + enum Status { + None = 0, + Ongoing = 1, + Passed = 2, + Failed = 3, + } status = None; + + GameState* state; + + Status getStatus() const; + + void init(const std::string& gxtEntry, const int32_t weaponID, + const int32_t time, const int32_t kills, + const std::array& modelsToKill, const bool headshot); + + float getRemainingTime() const; + + uint32_t getKillsForThisModel(ModelID model); + void clearKills(); + + void tick(float dt); + + void onCharacterDie(CharacterObject* character, GameObject* killer); + + Rampage(GameState* s) : state(s) { + } +}; + /** * Game and Runtime state * @@ -315,6 +356,8 @@ class GameState { */ GameStats gameStats; + Rampage rampage; + /** * Second since game was started */ diff --git a/rwengine/src/objects/PickupObject.cpp b/rwengine/src/objects/PickupObject.cpp index 7aad45697..bd6d8e2a1 100644 --- a/rwengine/src/objects/PickupObject.cpp +++ b/rwengine/src/objects/PickupObject.cpp @@ -173,14 +173,12 @@ PickupObject::~PickupObject() { } void PickupObject::tick(float dt) { - if (isRampage()) { - if (engine->state->scriptOnMissionFlag != nullptr) { - if (*(engine->state->scriptOnMissionFlag) != 0 && isEnabled()) { - setEnabled(false); - } else if (*(engine->state->scriptOnMissionFlag) == 0 && - !isEnabled()) { - setEnabled(true); - } + if (isRampage() && engine->state->scriptOnMissionFlag != nullptr) { + if (*(engine->state->scriptOnMissionFlag) != 0 && isEnabled()) { + setEnabled(false); + } else if (*(engine->state->scriptOnMissionFlag) == 0 && !isEnabled() && + !isCollected()) { + setEnabled(true); } } @@ -204,7 +202,7 @@ void PickupObject::tick(float dt) { m_corona->particle.colour = glm::vec4(red / 255.f, green / 255.f, blue / 255.f, 1.f) * colourValue; - if (m_enabled) { + if (!isCollected()) { // Sort out interactions with things that may or may not be players. btManifoldArray manifoldArray; btBroadphasePairArray& pairArray = diff --git a/rwengine/src/script/modules/GTA3ModuleImpl.inl b/rwengine/src/script/modules/GTA3ModuleImpl.inl index d2f9e1600..9bb5fd937 100644 --- a/rwengine/src/script/modules/GTA3ModuleImpl.inl +++ b/rwengine/src/script/modules/GTA3ModuleImpl.inl @@ -5602,31 +5602,28 @@ void opcode_01f7(const ScriptArguments& args, const ScriptPlayer player, const S } /** - @brief init_rampage %1g% weapon %2d% time %3d% %4d% targets %5o% %6o% %7o% %8o% flag %9d% + @brief init_rampage %1g% weapon %2d% time %3d% %4d% targets %5o% %6o% %7o% + %8o% flag %9d% opcode 01f9 @arg gxtEntry GXT entry @arg weaponID Weapon ID @arg time Time (ms) - @arg arg4 + @arg arg4 @arg model0 Model ID @arg model1 Model ID @arg model2 Model ID @arg model3 Model ID @arg arg9 Boolean true/false */ -void opcode_01f9(const ScriptArguments& args, const ScriptString gxtEntry, const ScriptWeaponType weaponID, const ScriptInt time, const ScriptInt arg4, const ScriptModelID model0, const ScriptModelID model1, const ScriptModelID model2, const ScriptModelID model3, const ScriptBoolean arg9) { - RW_UNIMPLEMENTED_OPCODE(0x01f9); - RW_UNUSED(gxtEntry); - RW_UNUSED(weaponID); - RW_UNUSED(time); - RW_UNUSED(arg4); - RW_UNUSED(model0); - RW_UNUSED(model1); - RW_UNUSED(model2); - RW_UNUSED(model3); - RW_UNUSED(arg9); - RW_UNUSED(args); +void opcode_01f9(const ScriptArguments& args, const ScriptString gxtEntry, + const ScriptWeaponType weaponID, const ScriptInt time, + const ScriptInt kills, const ScriptModelID model0, + const ScriptModelID model1, const ScriptModelID model2, + const ScriptModelID model3, const ScriptBoolean unused) { + RW_UNUSED(unused); + args.getState()->rampage.init(gxtEntry, weaponID, time, kills, + {model0, model1, model2, model3}, false); } /** @@ -5635,10 +5632,8 @@ void opcode_01f9(const ScriptArguments& args, const ScriptString gxtEntry, const opcode 01fa @arg arg1 */ -void opcode_01fa(const ScriptArguments& args, ScriptInt& arg1) { - RW_UNIMPLEMENTED_OPCODE(0x01fa); - RW_UNUSED(arg1); - RW_UNUSED(args); +void opcode_01fa(const ScriptArguments& args, ScriptInt& status) { + status = args.getState()->rampage.getStatus(); } /** @@ -6001,12 +5996,15 @@ void opcode_0211(const ScriptArguments& args, const ScriptCharacter character, S opcode 0213 @arg model Model ID - @arg pickup0 + @arg pickup0 @arg coord Coordinates @arg pickup1 Pickup */ -void opcode_0213(const ScriptArguments& args, const ScriptModel model, const ScriptPickupType pickup0, ScriptVec3 coord, ScriptPickup& pickup1) { - pickup1 = args.getWorld()->createPickup(coord, script::getModel(args, model), pickup0); +void opcode_0213(const ScriptArguments& args, const ScriptModel model, + const ScriptPickupType pickupType, ScriptVec3 coord, + ScriptPickup& pickup) { + pickup = args.getWorld()->createPickup(coord, script::getModel(args, model), + pickupType); } /** @@ -6021,8 +6019,10 @@ bool opcode_0214(const ScriptArguments& args, const ScriptPickup pickup) { if (!pickup) { return false; } - bool collected = pickup->isCollected(); - pickup->setCollected(false); + const bool collected = pickup->isCollected(); + if (collected) { + pickup->setCollected(false); + } return collected; } @@ -6814,8 +6814,7 @@ void opcode_0296(const ScriptArguments& args, const ScriptInt arg1) { opcode 0297 */ void opcode_0297(const ScriptArguments& args) { - RW_UNIMPLEMENTED_OPCODE(0x0297); - RW_UNUSED(args); + args.getState()->rampage.clearKills(); } /** @@ -6825,11 +6824,9 @@ void opcode_0297(const ScriptArguments& args) { @arg model0 Player @arg model1 Model ID */ -void opcode_0298(const ScriptArguments& args, const ScriptModelID model0, ScriptInt& model1) { - RW_UNIMPLEMENTED_OPCODE(0x0298); - RW_UNUSED(model0); - RW_UNUSED(model1); - RW_UNUSED(args); +void opcode_0298(const ScriptArguments& args, const ScriptModelID model, + ScriptInt& kills) { + kills = args.getState()->rampage.getKillsForThisModel(model); } /** @@ -9670,31 +9667,28 @@ bool opcode_0366(const ScriptArguments& args, const ScriptObject object) { } /** - @brief init_headshot_rampage %1g% weapon %2d% time %3d% %4d% targets %5o% %6o% %7o% %8o% flag %9d% + @brief init_headshot_rampage %1g% weapon %2d% time %3d% %4d% targets %5o% + %6o% %7o% %8o% flag %9d% opcode 0367 @arg gxtEntry GXT entry - @arg arg2 - @arg arg3 - @arg arg4 + @arg arg2 + @arg arg3 + @arg arg4 @arg model0 Model ID @arg model1 Model ID @arg model2 Model ID @arg model3 Model ID @arg arg9 Boolean true/false */ -void opcode_0367(const ScriptArguments& args, const ScriptString gxtEntry, const ScriptWeaponType arg2, const ScriptInt arg3, const ScriptInt arg4, const ScriptModelID model0, const ScriptModelID model1, const ScriptModelID model2, const ScriptModelID model3, const ScriptBoolean arg9) { - RW_UNIMPLEMENTED_OPCODE(0x0367); - RW_UNUSED(gxtEntry); - RW_UNUSED(arg2); - RW_UNUSED(arg3); - RW_UNUSED(arg4); - RW_UNUSED(model0); - RW_UNUSED(model1); - RW_UNUSED(model2); - RW_UNUSED(model3); - RW_UNUSED(arg9); - RW_UNUSED(args); +void opcode_0367(const ScriptArguments& args, const ScriptString gxtEntry, + const ScriptWeaponType weaponID, const ScriptInt time, + const ScriptInt kills, const ScriptModelID model0, + const ScriptModelID model1, const ScriptModelID model2, + const ScriptModelID model3, const ScriptBoolean unused) { + RW_UNUSED(unused); + args.getState()->rampage.init(gxtEntry, weaponID, time, kills, + {model0, model1, model2, model3}, true); } /** diff --git a/rwgame/DrawUI.cpp b/rwgame/DrawUI.cpp index 11e48eda2..d7d766cc4 100644 --- a/rwgame/DrawUI.cpp +++ b/rwgame/DrawUI.cpp @@ -1,9 +1,9 @@ #include "DrawUI.hpp" #include -#include -#include #include #include +#include +#include #include #include @@ -24,17 +24,49 @@ const glm::u8vec3 ui_timeColour(196, 165, 119); const glm::u8vec3 ui_moneyColour(89, 113, 147); const glm::u8vec3 ui_healthColour(187, 102, 47); const glm::u8vec3 ui_armourColour(123, 136, 93); +const glm::u8vec3 ui_rampageTimerColour(123, 136, 93); const glm::u8vec3 ui_shadowColour(0, 0, 0); constexpr float ui_mapSize = 150.f; constexpr float ui_worldSizeMin = 200.f; constexpr float ui_worldSizeMax = 300.f; +void drawRampage(GameWorld* world, GameRenderer* render) { + float rampageTimerX = 0 + ui_infoMargin; + float rampageTimerY = 0.f + ui_outerMargin; + + TextRenderer::TextInfo ti; + ti.font = 1; + ti.size = ui_textSize; + ti.align = TextRenderer::TextInfo::Left; + + { + float remainingTime = world->state->rampage.getRemainingTime(); + int32_t seconds = static_cast(remainingTime); + + std::stringstream ss; + ss << std::setw(1) << std::setfill('0') + << seconds / 60 << std::setw(0) + << ":" << std::setw(2) + << seconds % 60; + + ti.text = GameStringUtil::fromString(ss.str()); + } + + ti.baseColour = ui_shadowColour; + ti.screenPosition = glm::vec2(rampageTimerX + 1.f, rampageTimerY + 1.f); + render->text.renderText(ti); + + ti.baseColour = ui_rampageTimerColour; + ti.screenPosition = glm::vec2(rampageTimerX, rampageTimerY); + render->text.renderText(ti); +} + void drawMap(ViewCamera& currentView, PlayerController* player, GameWorld* world, GameRenderer* render) { MapRenderer::MapInfo map; - if (world->state->hudFlash != HudFlash::FlashRadar - || std::fmod(world->getGameTime(), 0.5f) >= .25f) { + if (world->state->hudFlash != HudFlash::FlashRadar || + std::fmod(world->getGameTime(), 0.5f) >= .25f) { glm::quat camRot = currentView.rotation; map.rotation = glm::roll(camRot) - glm::half_pi(); @@ -108,9 +140,10 @@ void drawPlayerInfo(PlayerController* player, GameWorld* world, infoTextY += ui_textHeight; - if ((world->state->hudFlash != HudFlash::FlashHealth - && player->getCharacter()->getCurrentState().health > ui_lowHealth) - || std::fmod(world->getGameTime(), 0.5f) >= .25f) { // UI: Blinking health indicator if health is low + if ((world->state->hudFlash != HudFlash::FlashHealth && + player->getCharacter()->getCurrentState().health > ui_lowHealth) || + std::fmod(world->getGameTime(), 0.5f) >= + .25f) { // UI: Blinking health indicator if health is low std::stringstream ss; ss << std::setw(3) << std::setfill('0') << (int)player->getCharacter()->getCurrentState().health; @@ -201,8 +234,8 @@ void drawPlayerInfo(PlayerController* player, GameWorld* world, // The clip is actually there, but it holds just one shot/charge displayBulletsTotal += slotInfo.bulletsClip; - ti.text = GameStringUtil::fromString( - std::to_string(displayBulletsTotal)); + ti.text = + GameStringUtil::fromString(std::to_string(displayBulletsTotal)); } else { // Limit the maximal displayed length for the total bullet count if (slotInfo.bulletsTotal > 9999) { @@ -229,6 +262,7 @@ void drawHUD(ViewCamera& currentView, PlayerController* player, if (player && player->getCharacter()) { drawMap(currentView, player, world, render); drawPlayerInfo(player, world, render); + drawRampage(world, render); } } diff --git a/rwgame/RWGame.cpp b/rwgame/RWGame.cpp index 5ce89064f..d884b2a94 100644 --- a/rwgame/RWGame.cpp +++ b/rwgame/RWGame.cpp @@ -530,6 +530,7 @@ void RWGame::tick(float dt) { world->destroyQueuedObjects(); + state.rampage.tick(dt); state.text.tick(dt); if (vm) {