1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-11-06 09:09:40 +02:00

Implemented new unit actions panel in combat

Alternative actions submod from extras is now deprecated and will have
no effect.

As long as screen width allows, game will now display additional panel
with all possible unit actions.

Panel will also display spells that can be cast by unit, allowing small
version of unit spellbook (total limit of actions is 12, but some are
used for creature actions, so unit spells are limited to 7-9)
This commit is contained in:
Ivan Savenko
2025-06-19 19:29:01 +03:00
parent 14a3f5a004
commit 1ac8080cbf
28 changed files with 378 additions and 152 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 602 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1002 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 719 B

After

Width:  |  Height:  |  Size: 717 B

View File

@@ -32,6 +32,12 @@
"vcmi.adventureMap.dwelling3" : "{%s}\n\nWould you like to recruit %s, %s, or %s?",
"vcmi.artifact.charges" : "Charges",
"vcmi.battle.action.move" : "Move unit to specified location",
"vcmi.battle.action.info" : "Show unit information",
"vcmi.battle.action.shoot" : "Use ranged attack",
"vcmi.battle.action.attack" : "Use melee attack",
"vcmi.battle.action.return" : "Use melee attack and return",
"vcmi.bonusSource.artifact" : "Artifact",
"vcmi.bonusSource.creature" : "Ability",

View File

@@ -33,6 +33,7 @@ set(vcmiclientcommon_SRCS
battle/QuickSpellPanel.cpp
battle/StackInfoBasicPanel.cpp
battle/StackQueue.cpp
battle/UnitActionPanel.cpp
eventsSDL/NotificationHandler.cpp
eventsSDL/InputHandler.cpp
@@ -235,6 +236,7 @@ set(vcmiclientcommon_HEADERS
battle/QuickSpellPanel.h
battle/StackInfoBasicPanel.h
battle/StackQueue.h
battle/UnitActionPanel.h
eventsSDL/NotificationHandler.h
eventsSDL/InputHandler.h

View File

@@ -148,7 +148,10 @@ void BattleActionsController::endCastingSpell()
}
if(owner.stacksController->getActiveStack())
{
possibleActions = getPossibleActionsForStack(owner.stacksController->getActiveStack()); //restore actions after they were cleared
owner.windowObject->setPossibleActions(possibleActions);
}
selectedStack = nullptr;
ENGINE->fakeMouseMove();
@@ -624,7 +627,7 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, c
return (targetStack && targetStackOwned && targetStack->getMovementRange() > 0);
case PossiblePlayerBattleAction::CREATURE_INFO:
return (targetStack && targetStackOwned && targetStack->alive());
return (targetStack && targetStack->alive());
case PossiblePlayerBattleAction::HERO_INFO:
if (targetHex == BattleHex::HERO_ATTACKER)
@@ -1037,33 +1040,7 @@ void BattleActionsController::activateStack()
tryActivateStackSpellcasting(s);
possibleActions = getPossibleActionsForStack(s);
std::list<PossiblePlayerBattleAction> actionsToSelect;
if(!possibleActions.empty())
{
auto primaryAction = possibleActions.front().get();
if(primaryAction == PossiblePlayerBattleAction::SHOOT || primaryAction == PossiblePlayerBattleAction::AIMED_SPELL_CREATURE
|| primaryAction == PossiblePlayerBattleAction::ANY_LOCATION || primaryAction == PossiblePlayerBattleAction::ATTACK_AND_RETURN)
{
actionsToSelect.push_back(possibleActions.front());
auto shootActionPredicate = [](const PossiblePlayerBattleAction& action)
{
return action.get() == PossiblePlayerBattleAction::SHOOT;
};
bool hasShootSecondaryAction = std::any_of(possibleActions.begin() + 1, possibleActions.end(), shootActionPredicate);
if(hasShootSecondaryAction) //casters may have shooting capabilities, for example storm elementals
actionsToSelect.emplace_back(PossiblePlayerBattleAction::SHOOT);
/* TODO: maybe it would also make sense to check spellcast as non-top priority action ("NO_SPELLCAST_BY_DEFAULT" bonus)?
* it would require going beyond this "if" block for melee casters
* F button helps, but some mod creatures may have that bonus and more than 1 castable spell */
actionsToSelect.emplace_back(PossiblePlayerBattleAction::ATTACK); //always allow melee attack as last option
}
}
owner.windowObject->setAlternativeActions(actionsToSelect);
owner.windowObject->setPossibleActions(possibleActions);
}
}
@@ -1123,14 +1100,9 @@ const std::vector<PossiblePlayerBattleAction> & BattleActionsController::getPoss
return possibleActions;
}
void BattleActionsController::removePossibleAction(PossiblePlayerBattleAction action)
void BattleActionsController::setPriorityActions(const std::vector<PossiblePlayerBattleAction> & actions)
{
vstd::erase(possibleActions, action);
}
void BattleActionsController::pushFrontPossibleAction(PossiblePlayerBattleAction action)
{
possibleActions.insert(possibleActions.begin(), action);
possibleActions = actions;
}
void BattleActionsController::resetCurrentStackPossibleActions()

View File

@@ -120,10 +120,9 @@ public:
/// methods to work with array of possible actions, needed to control special creatures abilities
const std::vector<PossiblePlayerBattleAction> & getPossibleActions() const;
void removePossibleAction(PossiblePlayerBattleAction);
/// inserts possible action in the beginning in order to prioritize it
void pushFrontPossibleAction(PossiblePlayerBattleAction);
/// sets list of high-priority actions that should be selected before any other actions
void setPriorityActions(const std::vector<PossiblePlayerBattleAction> &);
/// resets possible actions to original state
void resetCurrentStackPossibleActions();

View File

@@ -19,6 +19,7 @@
#include "QuickSpellPanel.h"
#include "StackInfoBasicPanel.h"
#include "StackQueue.h"
#include "UnitActionPanel.h"
#include "../CPlayerInterface.h"
#include "../GameEngine.h"
@@ -53,9 +54,8 @@
#include "../../lib/mapObjects/CGHeroInstance.h"
#include "../../lib/texts/CGeneralTextHandler.h"
BattleWindow::BattleWindow(BattleInterface & Owner):
owner(Owner),
lastAlternativeAction(PossiblePlayerBattleAction::INVALID)
BattleWindow::BattleWindow(BattleInterface & Owner)
: owner(Owner)
{
OBJECT_CONSTRUCTION;
pos.w = 800;
@@ -98,7 +98,6 @@ BattleWindow::BattleWindow(BattleInterface & Owner):
addShortcut(EShortcut::BATTLE_CONSOLE_DOWN, std::bind(&BattleWindow::bConsoleDownf, this));
addShortcut(EShortcut::BATTLE_TACTICS_NEXT, std::bind(&BattleWindow::bTacticNextStack, this));
addShortcut(EShortcut::BATTLE_TACTICS_END, std::bind(&BattleWindow::bTacticPhaseEnd, this));
addShortcut(EShortcut::BATTLE_SELECT_ACTION, std::bind(&BattleWindow::bSwitchActionf, this));
addShortcut(EShortcut::BATTLE_OPEN_ACTIVE_UNIT, std::bind(&BattleWindow::bOpenActiveUnit, this));
addShortcut(EShortcut::BATTLE_OPEN_HOVERED_UNIT, std::bind(&BattleWindow::bOpenHoveredUnit, this));
@@ -194,6 +193,9 @@ void BattleWindow::createQuickSpellWindow()
quickSpellWindow = std::make_shared<QuickSpellPanel>(owner);
quickSpellWindow->moveTo(Point(pos.x - 67, pos.y));
unitActionWindow = std::make_shared<UnitActionPanel>(owner);
unitActionWindow->moveTo(Point(pos.x + pos.w + 15, pos.y));
if(settings["battle"]["enableQuickSpellPanel"].Bool())
showStickyQuickSpellWindow();
else
@@ -214,6 +216,7 @@ void BattleWindow::hideStickyQuickSpellWindow()
showStickyQuickSpellWindow->Bool() = false;
quickSpellWindow->disable();
unitActionWindow->disable();
setPositionInfoWindow();
createTimerInfoWindows();
@@ -227,10 +230,11 @@ void BattleWindow::showStickyQuickSpellWindow()
auto hero = owner.getBattle()->battleGetMyHero();
if(ENGINE->screenDimensions().x >= 1050 && hero != nullptr && hero->hasSpellbook())
quickSpellWindow->enable();
else
quickSpellWindow->disable();
bool quickSpellWindowVisible = ENGINE->screenDimensions().x >= 1050 && hero != nullptr && hero->hasSpellbook();
bool unitActionWindowVisible = ENGINE->screenDimensions().x >= 1050;
quickSpellWindow->setEnabled(quickSpellWindowVisible);
unitActionWindow->setEnabled(unitActionWindowVisible);
setPositionInfoWindow();
createTimerInfoWindows();
@@ -242,6 +246,7 @@ void BattleWindow::createTimerInfoWindows()
OBJECT_CONSTRUCTION;
int xOffsetAttacker = quickSpellWindow->isDisabled() ? 0 : -53;
int xOffsetDefender = unitActionWindow->isDisabled() ? 0 : 53;
if(GAME->interface()->cb->getStartInfo()->turnTimerInfo.battleTimer != 0 || GAME->interface()->cb->getStartInfo()->turnTimerInfo.unitTimer != 0)
{
@@ -259,7 +264,7 @@ void BattleWindow::createTimerInfoWindows()
if (defender.isValidPlayer())
{
if (ENGINE->screenDimensions().x >= 1000)
defenderTimerWidget = std::make_shared<TurnTimerWidget>(Point(pos.w + 16, 1), defender);
defenderTimerWidget = std::make_shared<TurnTimerWidget>(Point(pos.w + 16 + xOffsetDefender, 1), defender);
else
defenderTimerWidget = std::make_shared<TurnTimerWidget>(Point(pos.w - 78, 135), defender);
}
@@ -379,10 +384,12 @@ void BattleWindow::updateQueue()
void BattleWindow::setPositionInfoWindow()
{
int xOffsetAttacker = quickSpellWindow->isDisabled() ? 0 : -53;
int xOffsetDefender = unitActionWindow->isDisabled() ? 0 : 53;
if(defenderHeroWindow)
{
Point position = (ENGINE->screenDimensions().x >= 1000)
? Point(pos.x + pos.w + 15, pos.y + 60)
? Point(pos.x + pos.w + 15 + xOffsetDefender, pos.y + 60)
: Point(pos.x + pos.w -79, pos.y + 195);
defenderHeroWindow->moveTo(position);
}
@@ -396,7 +403,7 @@ void BattleWindow::setPositionInfoWindow()
if(defenderStackWindow)
{
Point position = (ENGINE->screenDimensions().x >= 1000)
? Point(pos.x + pos.w + 15, defenderHeroWindow ? defenderHeroWindow->pos.y + 210 : pos.y + 60)
? Point(pos.x + pos.w + 15 + xOffsetDefender, defenderHeroWindow ? defenderHeroWindow->pos.y + 210 : pos.y + 60)
: Point(pos.x + pos.w -79, defenderHeroWindow ? defenderHeroWindow->pos.y : pos.y + 195);
defenderStackWindow->moveTo(position);
}
@@ -502,12 +509,9 @@ void BattleWindow::tacticPhaseStarted()
auto menuTactics = widget<CIntObject>("menuTactics");
auto tacticNext = widget<CIntObject>("tacticNext");
auto tacticEnd = widget<CIntObject>("tacticEnd");
auto alternativeAction = widget<CIntObject>("alternativeAction");
menuBattle->disable();
console->disable();
if (alternativeAction)
alternativeAction->disable();
menuTactics->enable();
tacticNext->enable();
@@ -523,12 +527,9 @@ void BattleWindow::tacticPhaseEnded()
auto menuTactics = widget<CIntObject>("menuTactics");
auto tacticNext = widget<CIntObject>("tacticNext");
auto tacticEnd = widget<CIntObject>("tacticEnd");
auto alternativeAction = widget<CIntObject>("alternativeAction");
menuBattle->enable();
console->enable();
if (alternativeAction)
alternativeAction->enable();
menuTactics->disable();
tacticNext->disable();
@@ -615,63 +616,9 @@ void BattleWindow::reallySurrender()
}
}
void BattleWindow::showAlternativeActionIcon(PossiblePlayerBattleAction action)
void BattleWindow::setPossibleActions(const std::vector<PossiblePlayerBattleAction> & actions)
{
auto w = widget<CButton>("alternativeAction");
if(!w)
return;
AnimationPath iconName = AnimationPath::fromJson(variables["actionIconDefault"]);
switch(action.get())
{
case PossiblePlayerBattleAction::ATTACK:
iconName = AnimationPath::fromJson(variables["actionIconAttack"]);
break;
case PossiblePlayerBattleAction::SHOOT:
iconName = AnimationPath::fromJson(variables["actionIconShoot"]);
break;
case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
iconName = AnimationPath::fromJson(variables["actionIconSpell"]);
break;
case PossiblePlayerBattleAction::ANY_LOCATION:
iconName = AnimationPath::fromJson(variables["actionIconSpell"]);
break;
//TODO: figure out purpose of this icon
//case PossiblePlayerBattleAction::???:
//iconName = variables["actionIconWalk"].String();
//break;
case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
iconName = AnimationPath::fromJson(variables["actionIconReturn"]);
break;
case PossiblePlayerBattleAction::WALK_AND_ATTACK:
iconName = AnimationPath::fromJson(variables["actionIconNoReturn"]);
break;
}
w->setImage(iconName);
w->redraw();
}
void BattleWindow::setAlternativeActions(const std::list<PossiblePlayerBattleAction> & actions)
{
assert(actions.size() != 1);
alternativeActions = actions;
lastAlternativeAction = PossiblePlayerBattleAction::INVALID;
if(alternativeActions.size() > 1)
{
lastAlternativeAction = alternativeActions.back();
showAlternativeActionIcon(alternativeActions.front());
}
else
showAlternativeActionIcon(PossiblePlayerBattleAction::INVALID);
unitActionWindow->setPossibleActions(actions);
}
void BattleWindow::bAutofightf()
@@ -761,28 +708,6 @@ void BattleWindow::bSpellf()
}
}
void BattleWindow::bSwitchActionf()
{
if(alternativeActions.empty())
return;
auto actions = owner.actionsController->getPossibleActions();
if(!actions.empty() && actions.front() != lastAlternativeAction)
{
owner.actionsController->removePossibleAction(alternativeActions.front());
showAlternativeActionIcon(*std::next(alternativeActions.begin()));
}
else
{
owner.actionsController->resetCurrentStackPossibleActions();
showAlternativeActionIcon(owner.actionsController->getPossibleActions().front());
}
alternativeActions.push_back(alternativeActions.front());
alternativeActions.pop_front();
}
void BattleWindow::bWaitf()
{
if (owner.actionsController->heroSpellcastingModeActive())
@@ -850,7 +775,6 @@ void BattleWindow::blockUI(bool on)
setShortcutBlocked(EShortcut::BATTLE_CAST_SPELL, on || owner.tacticsMode || !canCastSpells);
setShortcutBlocked(EShortcut::BATTLE_WAIT, on || owner.tacticsMode || !canWait);
setShortcutBlocked(EShortcut::BATTLE_DEFEND, on || owner.tacticsMode);
setShortcutBlocked(EShortcut::BATTLE_SELECT_ACTION, on || owner.tacticsMode);
setShortcutBlocked(EShortcut::BATTLE_AUTOCOMBAT, (settings["battle"]["endWithAutocombat"].Bool() && onlyOnePlayerHuman) ? on || owner.tacticsMode || owner.actionsController->heroSpellcastingModeActive() : owner.actionsController->heroSpellcastingModeActive());
setShortcutBlocked(EShortcut::BATTLE_END_WITH_AUTOCOMBAT, on || !onlyOnePlayerHuman || owner.actionsController->heroSpellcastingModeActive());
setShortcutBlocked(EShortcut::BATTLE_TACTICS_END, on || !owner.tacticsMode);
@@ -859,6 +783,7 @@ void BattleWindow::blockUI(bool on)
setShortcutBlocked(EShortcut::BATTLE_CONSOLE_UP, on && !owner.tacticsMode);
quickSpellWindow->setInputEnabled(!on);
unitActionWindow->setInputEnabled(!on);
}
void BattleWindow::bOpenActiveUnit()

View File

@@ -28,6 +28,7 @@ class TurnTimerWidget;
class HeroInfoBasicPanel;
class StackInfoBasicPanel;
class QuickSpellPanel;
class UnitActionPanel;
/// GUI object that handles functionality of panel at the bottom of combat screen
class BattleWindow : public InterfaceObjectConfigurable
@@ -42,6 +43,7 @@ class BattleWindow : public InterfaceObjectConfigurable
std::shared_ptr<StackInfoBasicPanel> defenderStackWindow;
std::shared_ptr<QuickSpellPanel> quickSpellWindow;
std::shared_ptr<UnitActionPanel> unitActionWindow;
std::shared_ptr<TurnTimerWidget> attackerTimerWidget;
std::shared_ptr<TurnTimerWidget> defenderTimerWidget;
@@ -53,7 +55,6 @@ class BattleWindow : public InterfaceObjectConfigurable
void bAutofightf();
void bSpellf();
void bWaitf();
void bSwitchActionf();
void bDefencef();
void bConsoleUpf();
void bConsoleDownf();
@@ -66,11 +67,6 @@ class BattleWindow : public InterfaceObjectConfigurable
void reallyFlee();
void reallySurrender();
/// management of alternative actions
std::list<PossiblePlayerBattleAction> alternativeActions;
PossiblePlayerBattleAction lastAlternativeAction;
void showAlternativeActionIcon(PossiblePlayerBattleAction);
void useSpellIfPossible(int slot);
/// flip battle queue visibility to opposite
@@ -140,8 +136,8 @@ public:
/// Toggle UI to displaying battle log in place of tactics UI
void tacticPhaseEnded();
/// Set possible alternative options. If more than 1 - the last will be considered as default option
void setAlternativeActions(const std::list<PossiblePlayerBattleAction> &);
/// Set possible alternative options to fill unit actions panel
void setPossibleActions(const std::vector<PossiblePlayerBattleAction> & allActions);
/// ends battle with autocombat
void endWithAutocombat();

View File

@@ -0,0 +1,152 @@
/*
* UnitActionPanel.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "UnitActionPanel.h"
#include "BattleInterface.h"
#include "BattleActionsController.h"
#include "../GameEngine.h"
#include "../eventsSDL/InputHandler.h"
#include "../gui/WindowHandler.h"
#include "../widgets/Buttons.h"
#include "../widgets/GraphicalPrimitiveCanvas.h"
#include "../widgets/Images.h"
#include "../widgets/TextControls.h"
#include "../windows/CSpellWindow.h"
#include "../../lib/CConfigHandler.h"
#include "../../lib/GameLibrary.h"
#include "../../lib/battle/CPlayerBattleCallback.h"
#include "../../lib/json/JsonUtils.h"
#include "../../lib/mapObjects/CGHeroInstance.h"
#include "../../lib/spells/CSpellHandler.h"
UnitActionPanel::UnitActionPanel(BattleInterface & owner)
: CIntObject(0)
, owner(owner)
{
OBJECT_CONSTRUCTION;
addUsedEvents(LCLICK | SHOW_POPUP | MOVE | INPUT_MODE_CHANGE);
pos = Rect(0, 0, 52, 600);
background = std::make_shared<CFilledTexture>(ImagePath::builtin("DIBOXBCK"), pos);
rect = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w + 1, pos.h + 1), ColorRGBA(0, 0, 0, 0), ColorRGBA(241, 216, 120, 255));
}
void UnitActionPanel::restoreAllActions()
{
owner.actionsController->resetCurrentStackPossibleActions();
}
void UnitActionPanel::setActions(int buttonIndex, const std::vector<PossiblePlayerBattleAction> & filteredActions)
{
for (const auto & button : buttons)
if (button != buttons.at(buttonIndex))
button->setSelectedSilent(false);
owner.actionsController->setPriorityActions(filteredActions);
if (filteredActions.front().spellcast())
owner.actionsController->enterCreatureCastingMode();
owner.actionsController->setPriorityActions(filteredActions);
}
void UnitActionPanel::testAndAddAction(const std::vector<PossiblePlayerBattleAction> & allActions, const std::vector<PossiblePlayerBattleAction::Actions> & actionFilter, const ImagePath & iconPath, const std::string & descriptionTextID)
{
std::vector<PossiblePlayerBattleAction> filteredActions;
for (const auto & action : allActions)
if (vstd::contains(actionFilter, action.get()))
filteredActions.push_back(action);
if (filteredActions.empty())
return;
int index = buttons.size();
const auto & callback = [this, filteredActions, index](bool isSelected){ if (isSelected) setActions(index, filteredActions); else restoreAllActions(); };
MetaString tooltip;
tooltip.appendTextID(descriptionTextID);
auto button = std::make_shared<CToggleButton>(Point(2, 7 + 50 * index), AnimationPath::builtin("battleUnitAction"), CButton::tooltip(tooltip.toString()), callback);
button->setOverlay(std::make_shared<CPicture>(iconPath));
button->setHighlightedBorderColor(Colors::WHITE);
button->setAllowDeselection(true);
buttons.push_back(button);
}
void UnitActionPanel::testAndAddSpell(const std::vector<PossiblePlayerBattleAction> & allActions, const SpellID & spellFilter)
{
std::vector<PossiblePlayerBattleAction> filteredActions;
for (const auto & action : allActions)
if (action.spellcast() && action.spell() == spellFilter)
filteredActions.push_back(action);
if (filteredActions.empty())
return;
int index = buttons.size();
const auto & callback = [this, filteredActions, index](bool isSelected){ if (isSelected) setActions(index, filteredActions); else restoreAllActions();};
MetaString tooltip;
tooltip.appendTextID("core.genrltxt.26");
tooltip.replaceName(spellFilter);
std::string hoverText = tooltip.toString();
std::string description = spellFilter.toSpell()->getDescriptionTranslated(0);
auto button = std::make_shared<CToggleButton>(Point(2, 7 + 50 * index), AnimationPath::builtin("battleUnitAction"), CButton::tooltip(hoverText, description), callback);
button->setOverlay(std::make_shared<CAnimImage>(AnimationPath::builtin("spellint"), spellFilter.getNum() + 1));
button->setHighlightedBorderColor(Colors::WHITE);
buttons.push_back(button);
}
void UnitActionPanel::setPossibleActions(const std::vector<PossiblePlayerBattleAction> & newActions)
{
OBJECT_CONSTRUCTION;
buttons.clear();
static const std::vector actionsMove = { PossiblePlayerBattleAction::MOVE_STACK };
static const std::vector actionsInfo = { PossiblePlayerBattleAction::CREATURE_INFO, PossiblePlayerBattleAction::HERO_INFO };
static const std::vector actionsShoot = { PossiblePlayerBattleAction::SHOOT };
static const std::vector actionsAttack = { PossiblePlayerBattleAction::ATTACK, PossiblePlayerBattleAction::WALK_AND_ATTACK };
static const std::vector actionsReturn = { PossiblePlayerBattleAction::ATTACK_AND_RETURN };
testAndAddAction(newActions, actionsMove, ImagePath::builtin("battle/actionMove"), "vcmi.battle.action.move");
testAndAddAction(newActions, actionsReturn, ImagePath::builtin("battle/actionReturn"), "vcmi.battle.action.return");
testAndAddAction(newActions, actionsAttack, ImagePath::builtin("battle/actionAttack"), "vcmi.battle.action.attack");
testAndAddAction(newActions, actionsShoot, ImagePath::builtin("battle/actionShoot"), "vcmi.battle.action.shoot");
std::vector<SpellID> spells;
for (const auto & action : newActions)
if (action.spellcast())
spells.push_back(action.spell());
for (const auto & spell : spells)
testAndAddSpell(newActions, spell);
// Not really a unit action, so place it at the end
testAndAddAction(newActions, actionsInfo, ImagePath::builtin("battle/actionInfo"), "vcmi.battle.action.info");
redraw();
}
void UnitActionPanel::show(Canvas & to)
{
showAll(to);
CIntObject::show(to);
}

View File

@@ -0,0 +1,46 @@
/*
* UnitActionPanel.h, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#pragma once
#include "../gui/CIntObject.h"
#include "../../lib/battle/PossiblePlayerBattleAction.h"
#include "../../lib/filesystem/ResourcePath.h"
class CFilledTexture;
class TransparentFilledRectangle;
class CToggleButton;
class CLabel;
class BattleInterface;
class UnitActionPanel : public CIntObject
{
private:
std::shared_ptr<CFilledTexture> background;
std::shared_ptr<TransparentFilledRectangle> rect;
std::vector<std::shared_ptr<CToggleButton>> buttons;
BattleInterface & owner;
void testAndAddAction(const std::vector<PossiblePlayerBattleAction> & allActions, const std::vector<PossiblePlayerBattleAction::Actions> & actionFilter, const ImagePath & iconPath, const std::string & descriptionTextID );
void testAndAddSpell(const std::vector<PossiblePlayerBattleAction> & allActions, const SpellID & spellFilter );
void restoreAllActions();
void setActions(int buttonIndex, const std::vector<PossiblePlayerBattleAction> & newActions);
public:
static constexpr int ACTION_SLOTS = 12;
UnitActionPanel(BattleInterface & owner);
void setPossibleActions(const std::vector<PossiblePlayerBattleAction> & actions);
std::vector<std::tuple<SpellID, bool>> getSpells() const;
void show(Canvas & to) override;
};

View File

@@ -192,7 +192,6 @@ enum class EShortcut
BATTLE_CONSOLE_DOWN,
BATTLE_TACTICS_NEXT,
BATTLE_TACTICS_END,
BATTLE_SELECT_ACTION, // Alternative actions toggle
BATTLE_TOGGLE_HEROES_STATS,
BATTLE_OPEN_ACTIVE_UNIT,
BATTLE_OPEN_HOVERED_UNIT,

View File

@@ -229,7 +229,6 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
{"battleConsoleDown", EShortcut::BATTLE_CONSOLE_DOWN },
{"battleTacticsNext", EShortcut::BATTLE_TACTICS_NEXT },
{"battleTacticsEnd", EShortcut::BATTLE_TACTICS_END },
{"battleSelectAction", EShortcut::BATTLE_SELECT_ACTION },
{"battleToggleQuickSpell", EShortcut::BATTLE_TOGGLE_QUICKSPELL },
{"battleSpellShortcut0", EShortcut::BATTLE_SPELL_SHORTCUT_0 },
{"battleSpellShortcut1", EShortcut::BATTLE_SPELL_SHORTCUT_1 },

View File

@@ -62,6 +62,11 @@ void CButton::setBorderColor(std::optional<ColorRGBA> newBorderColor)
borderColor = newBorderColor;
}
void CButton::setHighlightedBorderColor(std::optional<ColorRGBA> newBorderColor)
{
highlightedBorderColor = newBorderColor;
}
void CButton::addCallback(const std::function<void()> & callback)
{
this->callback += callback;
@@ -378,6 +383,12 @@ void CButton::showAll(Canvas & to)
if (borderColor)
to.drawBorder(Rect::createAround(pos, 1), *borderColor);
if (highlightedBorderColor && isHighlighted())
{
to.drawBorder(pos, *highlightedBorderColor);
to.drawBorder(Rect(pos.topLeft() + Point(1,1), pos.dimensions() - Point(1,1)), *highlightedBorderColor);
}
}
std::pair<std::string, std::string> CButton::tooltip()

View File

@@ -73,6 +73,7 @@ class CButton : public ButtonBase
std::array<std::string, 4> hoverTexts; //texts for statusbar, if empty - first entry will be used
std::optional<ColorRGBA> borderColor; // mapping of button state to border color
std::optional<ColorRGBA> highlightedBorderColor; // mapping of button state to border color
std::string helpBox; //for right-click help
bool actOnDown; //runs when mouse is pressed down over it, not when up
@@ -88,6 +89,7 @@ protected:
public:
// sets the same border color for all button states.
void setBorderColor(std::optional<ColorRGBA> borderColor);
void setHighlightedBorderColor(std::optional<ColorRGBA> borderColor);
/// adds one more callback to on-click actions
void addCallback(const std::function<void()> & callback);

View File

@@ -67,7 +67,6 @@
"battleOpenActiveUnit": "I",
"battleOpenHoveredUnit": "V",
"battleRetreat": "R",
"battleSelectAction": "S",
"battleToggleQuickSpell": "T",
"battleSpellShortcut0": "1",
"battleSpellShortcut1": "2",

View File

@@ -0,0 +1,114 @@
{
"normal" : {
"width": 48,
"height": 36,
"items" : [
{
"type": "texture",
"image": "DiBoxBck",
"rect": {"x": 0, "y": 0, "w": 48, "h": 36}
},
{
"type": "graphicalPrimitive",
"rect": {"x": 0, "y": 0, "w": 48, "h": 36},
"primitives" : [
{ "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 80 ] },
{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] },
{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] },
{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] },
{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] },
{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
{ "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
{ "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
]
}
]
},
"pressed" : {
"width": 48,
"height": 36,
"items" : [
{
"type": "texture",
"image": "DiBoxBck",
"rect": {"x": 1, "y": 1, "w": 47, "h": 35}
},
{
"type": "graphicalPrimitive",
"rect": {"x": 0, "y": 0, "w": 48, "h": 36},
"primitives" : [
{ "type" : "filledBox", "a" : { "x" : 3, "y" : 3}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 96 ] },
{ "type" : "rectangle", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 48 ] },
{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 96 ] },
{ "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : 2, "y" : -3}, "color" : [ 255, 255, 255, 64 ] },
{ "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : 2}, "color" : [ 255, 255, 255, 128 ] },
]
}
]
},
"blocked" : {
"width": 48,
"height": 36,
"items" : [
{
"type": "texture",
"image": "DiBoxBck",
"rect": {"x": 0, "y": 0, "w": 48, "h": 36}
},
{
"type": "graphicalPrimitive",
"rect": {"x": 0, "y": 0, "w": 48, "h": 36},
"primitives" : [
{ "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 80 ] },
{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] },
{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] },
{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] },
{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] },
{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
{ "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
{ "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
]
}
]
},
"highlighted" : {
"width": 48,
"height": 36,
"items" : [
{
"type": "texture",
"image": "DiBoxBck",
"rect": {"x": 0, "y": 0, "w": 48, "h": 36}
},
{
"type": "graphicalPrimitive",
"rect": {"x": 0, "y": 0, "w": 48, "h": 36},
"primitives" : [
{ "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 80 ] },
{ "type" : "rectangle", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 255, 255, 255, 255 ] },
{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 255 ] },
{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 255 ] },
{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 160 ] },
{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] },
]
}
]
},
}

View File

@@ -100,13 +100,17 @@
"primitives" : [
{ "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 80 ] },
{ "type" : "rectangle", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 255, 255, 255, 255 ] },
{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 255 ] },
{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 255 ] },
{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 160 ] },
{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] },
{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] },
{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] },
{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] },
{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
{ "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
{ "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
]
}
]

View File

@@ -1901,7 +1901,7 @@ SpellID CBattleInfoCallback::getRandomCastedSpell(vstd::RNG & rand,const CStack
if (!bl->size())
return SpellID::NONE;
if(bl->size() == 1)
if(bl->size() == 1 && bl->front()->additionalInfo[0] > 0) // there is one random spell -> select it
return bl->front()->subtype.as<SpellID>();
int totalWeight = 0;