From 04cea53ecdb361225c7176671150bd37c5c9c659 Mon Sep 17 00:00:00 2001 From: Vsevolod Kremianskii Date: Wed, 19 May 2021 11:39:05 +0700 Subject: [PATCH] Add feat-based actions to the action bar Actions are grouped into slots. Actions within each slot can be scrolled using the mouse wheel. --- src/engine/game/gui/selectoverlay.cpp | 235 +++++++++++++++++--------- src/engine/game/gui/selectoverlay.h | 14 +- src/engine/game/object/module.cpp | 29 +++- src/engine/game/object/module.h | 3 +- src/engine/game/types.h | 8 +- 5 files changed, 203 insertions(+), 86 deletions(-) diff --git a/src/engine/game/gui/selectoverlay.cpp b/src/engine/game/gui/selectoverlay.cpp index cd701245..354350ea 100644 --- a/src/engine/game/gui/selectoverlay.cpp +++ b/src/engine/game/gui/selectoverlay.cpp @@ -29,6 +29,7 @@ #include "../../graphics/window.h" #include "../../resource/resources.h" +#include "../d20/feats.h" #include "../game.h" #include "../objectconverter.h" #include "../reputes.h" @@ -48,7 +49,7 @@ static constexpr int kOffsetToReticle = 8; static constexpr int kTitleBarWidth = 250; static constexpr int kTitleBarPadding = 6; static constexpr int kHealthBarHeight = 6; -static constexpr int kActionCount = 3; +static constexpr int kNumActionSlots = 3; static constexpr int kActionBarMargin = 3; static constexpr int kActionBarPadding = 3; static constexpr int kActionWidth = 35; @@ -58,6 +59,7 @@ SelectionOverlay::SelectionOverlay(Game *game) : _game(game) { if (!game) { throw invalid_argument("game must not be null"); } + _actionSlots.resize(kNumActionSlots); } void SelectionOverlay::load() { @@ -73,6 +75,14 @@ void SelectionOverlay::load() { addTextureByAction(ContextualAction::Unlock, "isk_security"); addTextureByAction(ContextualAction::Attack, "i_attack"); + + // TODO: different icons per feat level + _textureByAction.insert(make_pair(ContextualAction::PowerAttack, Feats::instance().get(FeatType::PowerAttack)->icon)); + _textureByAction.insert(make_pair(ContextualAction::CriticalStrike, Feats::instance().get(FeatType::CriticalStrike)->icon)); + _textureByAction.insert(make_pair(ContextualAction::Flurry, Feats::instance().get(FeatType::Flurry)->icon)); + _textureByAction.insert(make_pair(ContextualAction::PowerShot, Feats::instance().get(FeatType::PowerBlast)->icon)); + _textureByAction.insert(make_pair(ContextualAction::SniperShot, Feats::instance().get(FeatType::SniperShot)->icon)); + _textureByAction.insert(make_pair(ContextualAction::RapidShot, Feats::instance().get(FeatType::RapidShot)->icon)); } void SelectionOverlay::addTextureByAction(ContextualAction action, const string &resRef) { @@ -85,21 +95,23 @@ bool SelectionOverlay::handle(const SDL_Event &event) { return handleMouseMotion(event.motion); case SDL_MOUSEBUTTONDOWN: return handleMouseButtonDown(event.button); + case SDL_MOUSEWHEEL: + return handleMouseWheel(event.wheel); default: return false; } } bool SelectionOverlay::handleMouseMotion(const SDL_MouseMotionEvent &event) { - _selectedActionIdx = -1; + _selectedActionSlot = -1; if (!_selectedObject) return false; - for (int i = 0; i < kActionCount; ++i) { + for (int i = 0; i < kNumActionSlots; ++i) { float x, y; getActionScreenCoords(i, x, y); if (event.x >= x && event.y >= y && event.x < x + kActionWidth && event.y < y + kActionHeight) { - _selectedActionIdx = i; + _selectedActionSlot = i; return true; } } @@ -108,27 +120,34 @@ bool SelectionOverlay::handleMouseMotion(const SDL_MouseMotionEvent &event) { } bool SelectionOverlay::handleMouseButtonDown(const SDL_MouseButtonEvent &event) { - if (event.button != SDL_BUTTON_LEFT || - _selectedActionIdx == -1 || _selectedActionIdx >= _actions.size()) return false; + if (event.button != SDL_BUTTON_LEFT) return false; + if (_selectedActionSlot == -1 || _selectedActionSlot >= _actionSlots.size()) return false; + + shared_ptr leader(_game->party().getLeader()); + if (!leader) return false; shared_ptr area(_game->module()->area()); - auto selectedObject = area->selectedObject(); if (!selectedObject) return false; - switch (_actions[_selectedActionIdx]) { + const ActionSlot &slot = _actionSlots[_selectedActionSlot]; + if (slot.indexSelected >= slot.actions.size()) return false; + + switch (slot.actions[slot.indexSelected]) { case ContextualAction::Unlock: { - shared_ptr partyLeader(_game->party().getLeader()); - partyLeader->addAction(make_unique(ActionType::OpenLock, selectedObject)); + leader->addAction(make_unique(ActionType::OpenLock, selectedObject)); break; } - - case ContextualAction::Attack: { - shared_ptr partyLeader(_game->party().getLeader()); - partyLeader->addAction(make_unique(static_pointer_cast(selectedObject), partyLeader->getAttackRange(), true)); + case ContextualAction::Attack: + case ContextualAction::PowerAttack: + case ContextualAction::CriticalStrike: + case ContextualAction::Flurry: + case ContextualAction::PowerShot: + case ContextualAction::SniperShot: + case ContextualAction::RapidShot: { + leader->addAction(make_unique(static_pointer_cast(selectedObject), leader->getAttackRange(), true)); break; } - default: break; } @@ -136,7 +155,28 @@ bool SelectionOverlay::handleMouseButtonDown(const SDL_MouseButtonEvent &event) return true; } +bool SelectionOverlay::handleMouseWheel(const SDL_MouseWheelEvent &event) { + if (_selectedActionSlot == -1 || _selectedActionSlot >= _actionSlots.size()) return false; + + ActionSlot &slot = _actionSlots[_selectedActionSlot]; + size_t numSlotActions = slot.actions.size(); + + if (event.y > 0) { + if (slot.indexSelected-- == 0) { + slot.indexSelected = numSlotActions - 1; + } + } else { + if (++slot.indexSelected == numSlotActions) { + slot.indexSelected = 0; + } + } + + return true; +} + void SelectionOverlay::update() { + // TODO: update on selection change only + _hilightedObject.reset(); _hilightedHostile = false; @@ -170,7 +210,43 @@ void SelectionOverlay::update() { if (_selectedScreenCoords.z < 1.0f) { _selectedObject = selectedObject; - _actions = module->getContextualActions(selectedObject); + + for (int i = 0; i < kNumActionSlots; ++i) { + _actionSlots[i].actions.clear(); + } + set actions(module->getContextualActions(selectedObject)); + _hasActions = !actions.empty(); + if (_hasActions) { + if (actions.count(ContextualAction::Attack) > 0) { + _actionSlots[0].actions.push_back(ContextualAction::Attack); + } + if (actions.count(ContextualAction::PowerAttack) > 0) { + _actionSlots[0].actions.push_back(ContextualAction::PowerAttack); + } + if (actions.count(ContextualAction::CriticalStrike) > 0) { + _actionSlots[0].actions.push_back(ContextualAction::CriticalStrike); + } + if (actions.count(ContextualAction::Flurry) > 0) { + _actionSlots[0].actions.push_back(ContextualAction::Flurry); + } + if (actions.count(ContextualAction::PowerShot) > 0) { + _actionSlots[0].actions.push_back(ContextualAction::PowerShot); + } + if (actions.count(ContextualAction::SniperShot) > 0) { + _actionSlots[0].actions.push_back(ContextualAction::SniperShot); + } + if (actions.count(ContextualAction::RapidShot) > 0) { + _actionSlots[0].actions.push_back(ContextualAction::RapidShot); + } + if (actions.count(ContextualAction::Unlock) > 0) { + _actionSlots[1].actions.push_back(ContextualAction::Unlock); + } + } + for (int i = 0; i < kNumActionSlots; ++i) { + if (_actionSlots[i].indexSelected >= _actionSlots[i].actions.size()) { + _actionSlots[i].indexSelected = 0; + } + } auto selectedCreature = ObjectConverter::toCreature(selectedObject); if (selectedCreature) { @@ -186,9 +262,7 @@ void SelectionOverlay::draw() { } if (_selectedObject) { drawReticle(_selectedHostile ? *_hostileReticle2 : *_friendlyReticle2, _selectedScreenCoords); - if (!_actions.empty()) { - drawActionBar(); - } + drawActionBar(); drawTitleBar(); drawHealthBar(); } @@ -223,7 +297,7 @@ void SelectionOverlay::drawTitleBar() { float x = opts.width * _selectedScreenCoords.x - kTitleBarWidth / 2; float y = opts.height * (1.0f - _selectedScreenCoords.y) - _reticleHeight / 2 - barHeight - kOffsetToReticle - kHealthBarHeight - 1.0f; - if (!_actions.empty()) { + if (_hasActions) { y -= kActionHeight + 2 * kActionBarMargin; } glm::mat4 transform(1.0f); @@ -242,7 +316,7 @@ void SelectionOverlay::drawTitleBar() { { float x = opts.width * _selectedScreenCoords.x; float y = opts.height * (1.0f - _selectedScreenCoords.y) - (_reticleHeight + barHeight) / 2 - kOffsetToReticle - kHealthBarHeight - 1.0f; - if (!_actions.empty()) { + if (_hasActions) { y -= kActionHeight + 2 * kActionBarMargin; } glm::vec3 position(x, y, 0.0f); @@ -256,7 +330,7 @@ void SelectionOverlay::drawHealthBar() { float y = opts.height * (1.0f - _selectedScreenCoords.y) - _reticleHeight / 2 - kHealthBarHeight - kOffsetToReticle; float w = glm::clamp(_selectedObject->currentHitPoints() / static_cast(_selectedObject->hitPoints()), 0.0f, 1.0f) * kTitleBarWidth; - if (!_actions.empty()) { + if (_hasActions) { y -= kActionHeight + 2 * kActionBarMargin; } glm::mat4 transform(1.0f); @@ -273,59 +347,39 @@ void SelectionOverlay::drawHealthBar() { } void SelectionOverlay::drawActionBar() { - const GraphicsOptions &opts = _game->options().graphics; - - for (int i = 0; i < kActionCount; ++i) { - shared_ptr frameTexture; - if (i == _selectedActionIdx) { - frameTexture = _hilightedScroll; - } else if (_selectedHostile) { - frameTexture = _hostileScroll; - } else { - frameTexture = _friendlyScroll; - } - setActiveTextureUnit(TextureUnits::diffuseMap); - frameTexture->bind(); - - float frameX, frameY; - getActionScreenCoords(i, frameX, frameY); - - glm::mat4 transform(1.0f); - transform = glm::translate(transform, glm::vec3(frameX, frameY, 0.0f)); - transform = glm::scale(transform, glm::vec3(kActionWidth, kActionHeight, 1.0f)); - - ShaderUniforms uniforms; - uniforms.combined.general.projection = Window::instance().getOrthoProjection(); - uniforms.combined.general.model = move(transform); - - Shaders::instance().activate(ShaderProgram::SimpleGUI, uniforms); - Meshes::instance().getQuad()->draw(); - - if (i < static_cast(_actions.size())) { - ContextualAction action = _actions[i]; - - shared_ptr texture(_textureByAction.find(action)->second); - if (texture) { - setActiveTextureUnit(TextureUnits::diffuseMap); - texture->bind(); - - float y = opts.height * (1.0f - _selectedScreenCoords.y) - (_reticleHeight + kActionHeight + kActionWidth) / 2.0f - kOffsetToReticle - kActionBarMargin; - - transform = glm::mat4(1.0f); - transform = glm::translate(transform, glm::vec3(frameX, y, 0.0f)); - transform = glm::scale(transform, glm::vec3(kActionWidth, kActionWidth, 1.0f)); - - ShaderUniforms uniforms; - uniforms.combined.general.projection = Window::instance().getOrthoProjection(); - uniforms.combined.general.model = move(transform); - - Shaders::instance().activate(ShaderProgram::SimpleGUI, uniforms); - Meshes::instance().getQuad()->draw(); - } - } + for (int i = 0; i < kNumActionSlots; ++i) { + drawActionFrame(i); + drawActionIcon(i); } } +void SelectionOverlay::drawActionFrame(int index) { + shared_ptr frameTexture; + if (index == _selectedActionSlot) { + frameTexture = _hilightedScroll; + } else if (_selectedHostile) { + frameTexture = _hostileScroll; + } else { + frameTexture = _friendlyScroll; + } + setActiveTextureUnit(TextureUnits::diffuseMap); + frameTexture->bind(); + + float frameX, frameY; + getActionScreenCoords(index, frameX, frameY); + + glm::mat4 transform(1.0f); + transform = glm::translate(transform, glm::vec3(frameX, frameY, 0.0f)); + transform = glm::scale(transform, glm::vec3(kActionWidth, kActionHeight, 1.0f)); + + ShaderUniforms uniforms; + uniforms.combined.general.projection = Window::instance().getOrthoProjection(); + uniforms.combined.general.model = move(transform); + + Shaders::instance().activate(ShaderProgram::SimpleGUI, uniforms); + Meshes::instance().getQuad()->draw(); +} + bool SelectionOverlay::getActionScreenCoords(int index, float &x, float &y) const { if (!_selectedObject) return false; @@ -336,14 +390,41 @@ bool SelectionOverlay::getActionScreenCoords(int index, float &x, float &y) cons return true; } +void SelectionOverlay::drawActionIcon(int index) { + const ActionSlot &slot = _actionSlots[index]; + if (slot.indexSelected >= slot.actions.size()) return; + + ContextualAction action = slot.actions[slot.indexSelected]; + shared_ptr texture(_textureByAction.find(action)->second); + if (!texture) return; + + setActiveTextureUnit(TextureUnits::diffuseMap); + texture->bind(); + + float frameX, frameY; + getActionScreenCoords(index, frameX, frameY); + + const GraphicsOptions &opts = _game->options().graphics; + float y = opts.height * (1.0f - _selectedScreenCoords.y) - (_reticleHeight + kActionHeight + kActionWidth) / 2.0f - kOffsetToReticle - kActionBarMargin; + + glm::mat4 transform(1.0f); + transform = glm::translate(transform, glm::vec3(frameX, y, 0.0f)); + transform = glm::scale(transform, glm::vec3(kActionWidth, kActionWidth, 1.0f)); + + ShaderUniforms uniforms; + uniforms.combined.general.projection = Window::instance().getOrthoProjection(); + uniforms.combined.general.model = move(transform); + + Shaders::instance().activate(ShaderProgram::SimpleGUI, uniforms); + Meshes::instance().getQuad()->draw(); +} + glm::vec3 SelectionOverlay::getColorFromSelectedObject() const { static glm::vec3 red(1.0f, 0.0f, 0.0f); - if (_selectedObject && _selectedHostile) { - return red; - } - - return getBaseColor(_game->gameId()); + return (_selectedObject && _selectedHostile) ? + red : + getBaseColor(_game->gameId()); } } // namespace game diff --git a/src/engine/game/gui/selectoverlay.h b/src/engine/game/gui/selectoverlay.h index 24027975..bb521c94 100644 --- a/src/engine/game/gui/selectoverlay.h +++ b/src/engine/game/gui/selectoverlay.h @@ -53,6 +53,11 @@ public: void draw(); private: + struct ActionSlot { + std::vector actions; + uint32_t indexSelected { 0 }; + }; + Game *_game { nullptr }; std::shared_ptr _font; std::shared_ptr _friendlyReticle; @@ -65,24 +70,29 @@ private: std::unordered_map> _textureByAction; std::shared_ptr _hilightedObject; std::shared_ptr _selectedObject; - std::vector _actions; + std::vector _actionSlots; glm::vec3 _hilightedScreenCoords { 0.0f }; glm::vec3 _selectedScreenCoords { 0.0f }; int _reticleHeight { 0 }; - int _selectedActionIdx { -1 }; + int _selectedActionSlot { -1 }; bool _hilightedHostile { false }; bool _selectedHostile { false }; + bool _hasActions { false }; void addTextureByAction(ContextualAction action, const std::string &resRef); bool handleMouseMotion(const SDL_MouseMotionEvent &event); bool handleMouseButtonDown(const SDL_MouseButtonEvent &event); + bool handleMouseWheel(const SDL_MouseWheelEvent &event); void drawReticle(graphics::Texture &texture, const glm::vec3 &screenCoords); void drawTitleBar(); void drawHealthBar(); void drawActionBar(); + void drawActionFrame(int index); + void drawActionIcon(int index); + bool getActionScreenCoords(int index, float &x, float &y) const; glm::vec3 getColorFromSelectedObject() const; }; diff --git a/src/engine/game/object/module.cpp b/src/engine/game/object/module.cpp index 51577e56..1409cdb7 100644 --- a/src/engine/game/object/module.cpp +++ b/src/engine/game/object/module.cpp @@ -267,21 +267,40 @@ void Module::update(float dt) { _area->update(dt); } -vector Module::getContextualActions(const shared_ptr &object) const { - vector actions; +set Module::getContextualActions(const shared_ptr &object) const { + set actions; switch (object->type()) { case ObjectType::Creature: { + auto leader = _game->party().getLeader(); auto creature = static_pointer_cast(object); - if (!creature->isDead() && Reputes::instance().getIsEnemy(*(_game->party().getLeader()), *creature)) { - actions.push_back(ContextualAction::Attack); + if (!creature->isDead() && Reputes::instance().getIsEnemy(*leader, *creature)) { + actions.insert(ContextualAction::Attack); + auto weapon = leader->getEquippedItem(InventorySlot::rightWeapon); + if (weapon && weapon->isRanged()) { + if (leader->attributes().hasFeat(FeatType::PowerBlast)) { + actions.insert(ContextualAction::PowerShot); + } else if (leader->attributes().hasFeat(FeatType::SniperShot)) { + actions.insert(ContextualAction::SniperShot); + } else if (leader->attributes().hasFeat(FeatType::RapidShot)) { + actions.insert(ContextualAction::RapidShot); + } + } else { + if (leader->attributes().hasFeat(FeatType::PowerAttack)) { + actions.insert(ContextualAction::PowerAttack); + } else if (leader->attributes().hasFeat(FeatType::CriticalStrike)) { + actions.insert(ContextualAction::CriticalStrike); + } else if (leader->attributes().hasFeat(FeatType::Flurry)) { + actions.insert(ContextualAction::Flurry); + } + } } break; } case ObjectType::Door: { auto door = static_pointer_cast(object); if (door->isLocked() && !door->isKeyRequired() && _game->party().getLeader()->attributes().hasSkill(Skill::Security)) { - actions.push_back(ContextualAction::Unlock); + actions.insert(ContextualAction::Unlock); } break; } diff --git a/src/engine/game/object/module.h b/src/engine/game/object/module.h index d48066f8..3e140bfd 100644 --- a/src/engine/game/object/module.h +++ b/src/engine/game/object/module.h @@ -18,6 +18,7 @@ #pragma once #include +#include #include "glm/vec3.hpp" @@ -74,7 +75,7 @@ public: bool handle(const SDL_Event &event); void update(float dt); - std::vector getContextualActions(const std::shared_ptr &object) const; + std::set getContextualActions(const std::shared_ptr &object) const; const std::string &name() const { return _name; } const ModuleInfo &info() const { return _info; } diff --git a/src/engine/game/types.h b/src/engine/game/types.h index 29baa35b..2290fe82 100644 --- a/src/engine/game/types.h +++ b/src/engine/game/types.h @@ -61,7 +61,13 @@ enum class CursorType { enum class ContextualAction { None, Unlock, - Attack + Attack, + PowerAttack, + CriticalStrike, + Flurry, + PowerShot, + SniperShot, + RapidShot }; enum class WeaponType {