1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-20 20:23:03 +02:00

Merge branch 'vcmi/master' into 'vcmi/develop'

This commit is contained in:
Ivan Savenko 2024-08-05 10:36:10 +00:00
commit b7391f49f6
32 changed files with 209 additions and 93 deletions

View File

@ -31,6 +31,35 @@
* Added support for multiple music tracks for terrains on adventure map
* Fixed several cases where vcmi will report errors in json without specifying filename of invalid file
# 1.5.5 -> 1.5.6
### Stability
* Fixed possible crash on transferring hero to next campaign scenario if hero has combined artifact some components of which can be transferred
* Fixed possible crash on transferring hero to next campaign scenario that has creature with faction limiter in his army
* Fixed possible crash on application shutdown due to incorrect destruction order of UI entities
### Multiplayer
* Mod compatibility issues when joining a lobby room now use color coding to make them less easy to miss.
* Incompatible mods are now placed before compatible mods when joining lobby room.
* Fixed text overflow in online lobby interface
* Fixed jittering simultaneous turns slider after moving it twice over short period
* Fixed non-functioning slider in invite to game room dialog
### Interface
* Fixed some shortcuts that were not active during the enemy's turn, such as Thieves' Guild.
* Game now correctly uses melee damage calculation when forcing a melee attack with a shooter.
* Game will now close all open dialogs on start of our turn, to avoid bugs like locked right-click popups
### Map Objects
* Spells the hero can't learn are no longer hidden when received from a rewardable object, such as the Pandora Box
* Spells that cannot be learned are now displayed with gray text in the name of the spell.
* Configurable objects with scouted state such as Witch Hut in HotA now correctly show their reward on right click after vising them but refusing to accept reward
* Right-click tooltip on map dwelling now always shows produced creatures. Player that owns the dwelling can also see number of creatures available for recruit
### Modding
* Fixed possible crash on invalid SPELL_LIKE_ATTACK bonus
* Added compatibility check when loading maps with old names for boats
# 1.5.4 -> 1.5.5
* Fixed crash when advancing to the next scenario in campaigns when the hero not transferring has a combination artefact that can be transferred to the next scenario.

View File

@ -171,40 +171,39 @@ void CPlayerInterface::initGameInterface(std::shared_ptr<Environment> ENV, std::
adventureInt.reset(new AdventureMapInterface());
}
void CPlayerInterface::closeAllDialogs()
{
// remove all active dialogs that do not expect query answer
for (;;)
{
auto adventureWindow = GH.windows().topWindow<AdventureMapInterface>();
auto infoWindow = GH.windows().topWindow<CInfoWindow>();
if(adventureWindow != nullptr)
break;
if(infoWindow && infoWindow->ID != QueryID::NONE)
break;
if (infoWindow)
infoWindow->close();
else
GH.windows().popWindows(1);
}
if(castleInt)
castleInt->close();
castleInt = nullptr;
}
void CPlayerInterface::playerEndsTurn(PlayerColor player)
{
EVENT_HANDLER_CALLED_BY_CLIENT;
if (player == playerID)
{
makingTurn = false;
// remove all active dialogs that do not expect query answer
for (;;)
{
auto adventureWindow = GH.windows().topWindow<AdventureMapInterface>();
auto infoWindow = GH.windows().topWindow<CInfoWindow>();
if(adventureWindow != nullptr)
break;
if(infoWindow && infoWindow->ID != QueryID::NONE)
break;
if (infoWindow)
infoWindow->close();
else
GH.windows().popWindows(1);
}
if(castleInt)
castleInt->close();
castleInt = nullptr;
// remove all pending dialogs that do not expect query answer
vstd::erase_if(dialogs, [](const std::shared_ptr<CInfoWindow> & window){
return window->ID == QueryID::NONE;
});
closeAllDialogs();
}
}
@ -286,6 +285,7 @@ void CPlayerInterface::gamePause(bool pause)
void CPlayerInterface::yourTurn(QueryID queryID)
{
closeAllDialogs();
CTutorialWindow::openWindowFirstTime(TutorialMode::TOUCH_ADVENTUREMAP);
EVENT_HANDLER_CALLED_BY_CLIENT;
@ -1477,7 +1477,7 @@ void CPlayerInterface::update()
return;
//if there are any waiting dialogs, show them
if ((CSH->howManyPlayerInterfaces() <= 1 || makingTurn) && !dialogs.empty() && !showingDialog->isBusy())
if (makingTurn && !dialogs.empty() && !showingDialog->isBusy())
{
showingDialog->setBusy();
GH.windows().pushWindow(dialogs.front());

View File

@ -197,6 +197,7 @@ public: // public interface for use by client via LOCPLINT access
void performAutosave();
void gamePause(bool pause);
void endNetwork();
void closeAllDialogs();
///returns true if all events are processed internally
bool capturedAllEvents();

View File

@ -375,7 +375,7 @@ void AdventureMapInterface::onEnemyTurnStarted(PlayerColor playerID, bool isHuma
mapAudio->onEnemyTurnStarted();
widget->getMinimap()->setAIRadar(!isHuman);
widget->getInfoBar()->startEnemyTurn(playerID);
setState(isHuman ? EAdventureState::OTHER_HUMAN_PLAYER_TURN : EAdventureState::AI_PLAYER_TURN);
setState(isHuman ? EAdventureState::MAKING_TURN : EAdventureState::AI_PLAYER_TURN);
}
void AdventureMapInterface::setState(EAdventureState state)

View File

@ -532,7 +532,7 @@ bool AdventureMapShortcuts::optionCanVisitObject()
auto * hero = LOCPLINT->localState->getCurrentHero();
auto objects = LOCPLINT->cb->getVisitableObjs(hero->visitablePos());
assert(vstd::contains(objects,hero));
//assert(vstd::contains(objects,hero));
return objects.size() > 1; // there is object other than our hero
}
@ -577,16 +577,15 @@ bool AdventureMapShortcuts::optionInWorldView()
bool AdventureMapShortcuts::optionSidePanelActive()
{
return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::OTHER_HUMAN_PLAYER_TURN;
return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW;
}
bool AdventureMapShortcuts::optionMapScrollingActive()
{
return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::OTHER_HUMAN_PLAYER_TURN;
return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW;
}
bool AdventureMapShortcuts::optionMapViewActive()
{
return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::CASTING_SPELL
|| state == EAdventureState::OTHER_HUMAN_PLAYER_TURN;
return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::CASTING_SPELL;
}

View File

@ -15,7 +15,6 @@ enum class EAdventureState
HOTSEAT_WAIT,
MAKING_TURN,
AI_PLAYER_TURN,
OTHER_HUMAN_PLAYER_TURN,
CASTING_SPELL,
WORLD_VIEW
};

View File

@ -499,9 +499,12 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle
case PossiblePlayerBattleAction::WALK_AND_ATTACK:
case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return
{
const auto * attacker = owner.stacksController->getActiveStack();
BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex);
int distance = attacker->position.isValid() ? owner.getBattle()->battleGetDistances(attacker, attacker->getPosition())[attackFromHex] : 0;
DamageEstimation retaliation;
DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(owner.stacksController->getActiveStack(), targetStack, attackFromHex, &retaliation);
BattleAttackInfo attackInfo(attacker, targetStack, distance, false );
DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(attackInfo, &retaliation);
estimation.kills.max = std::min<int64_t>(estimation.kills.max, targetStack->getCount());
estimation.kills.min = std::min<int64_t>(estimation.kills.min, targetStack->getCount());
bool enemyMayBeKilled = estimation.kills.max == targetStack->getCount();
@ -514,7 +517,8 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle
const auto * shooter = owner.stacksController->getActiveStack();
DamageEstimation retaliation;
DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(shooter, targetStack, shooter->getPosition(), &retaliation);
BattleAttackInfo attackInfo(shooter, targetStack, 0, true );
DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(attackInfo, &retaliation);
estimation.kills.max = std::min<int64_t>(estimation.kills.max, targetStack->getCount());
estimation.kills.min = std::min<int64_t>(estimation.kills.min, targetStack->getCount());
return formatRangedAttack(estimation, targetStack->getName(), shooter->shots.available());

View File

@ -94,7 +94,7 @@ GlobalLobbyInviteWindow::GlobalLobbyInviteWindow()
};
listBackground = std::make_shared<TransparentFilledRectangle>(Rect(8, 48, 220, 324), ColorRGBA(0, 0, 0, 64), ColorRGBA(64, 80, 128, 255), 1);
accountList = std::make_shared<CListBox>(createAccountCardCallback, Point(10, 50), Point(0, 40), 8, 0, 0, 1 | 4, Rect(200, 0, 320, 320));
accountList = std::make_shared<CListBox>(createAccountCardCallback, Point(10, 50), Point(0, 40), 8, CSH->getGlobalLobby().getActiveAccounts().size(), 0, 1 | 4, Rect(200, 0, 320, 320));
buttonClose = std::make_shared<CButton>(Point(86, 384), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this]() { close(); }, EShortcut::GLOBAL_RETURN );

View File

@ -43,7 +43,7 @@ GlobalLobbyLoginWindow::GlobalLobbyLoginWindow()
filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
labelTitle = std::make_shared<CLabel>( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.login.title"));
labelUsernameTitle = std::make_shared<CLabel>( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("vcmi.lobby.login.username"));
labelUsername = std::make_shared<CLabel>( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, loginAs.toString());
labelUsername = std::make_shared<CLabel>( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, loginAs.toString(), 265);
backgroundUsername = std::make_shared<TransparentFilledRectangle>(Rect(10, 90, 264, 20), ColorRGBA(0,0,0,128), ColorRGBA(64,64,64,64));
inputUsername = std::make_shared<CTextInput>(Rect(15, 93, 260, 16), FONT_SMALL, ETextAlignment::CENTERLEFT, true);
buttonLogin = std::make_shared<CButton>(Point(10, 180), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onLogin(); }, EShortcut::GLOBAL_ACCEPT);

View File

@ -37,7 +37,7 @@ GlobalLobbyRoomAccountCard::GlobalLobbyRoomAccountCard(const GlobalLobbyAccount
pos.w = 130;
pos.h = 40;
backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1);
labelName = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, accountDescription.displayName);
labelName = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, accountDescription.displayName, 120);
labelStatus = std::make_shared<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, accountDescription.status);
}
@ -56,9 +56,14 @@ GlobalLobbyRoomModCard::GlobalLobbyRoomModCard(const GlobalLobbyRoomModInfo & mo
pos.h = 40;
backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1);
labelName = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, modInfo.modName);
labelName = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, modInfo.modName, 190);
labelVersion = std::make_shared<CLabel>(195, 30, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::YELLOW, modInfo.version);
labelStatus = std::make_shared<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.mod.state." + statusToString.at(modInfo.status)));
auto statusColor = Colors::RED;
if(modInfo.status == ModVerificationStatus::FULL_MATCH)
statusColor = ColorRGBA(128, 128, 128);
else if(modInfo.status == ModVerificationStatus::VERSION_MISMATCH)
statusColor = Colors::YELLOW;
labelStatus = std::make_shared<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, statusColor, CGI->generaltexth->translate("vcmi.lobby.mod.state." + statusToString.at(modInfo.status)));
}
static std::string getJoinRoomErrorMessage(const GlobalLobbyRoom & roomDescription, const std::vector<GlobalLobbyRoomModInfo> & modVerificationList)
@ -134,6 +139,13 @@ GlobalLobbyRoomWindow::GlobalLobbyRoomWindow(GlobalLobbyWindow * window, const s
modVerificationList.push_back(modInfo);
}
std::sort(modVerificationList.begin(), modVerificationList.end(), [](const GlobalLobbyRoomModInfo &a, const GlobalLobbyRoomModInfo &b)
{
if(a.status == b.status)
return a.modName < b.modName;
return a.status < b.status;
});
MetaString subtitleText;
subtitleText.appendTextID("vcmi.lobby.preview.subtitle");
@ -142,7 +154,7 @@ GlobalLobbyRoomWindow::GlobalLobbyRoomWindow(GlobalLobbyWindow * window, const s
filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
labelTitle = std::make_shared<CLabel>( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.preview.title").toString());
labelSubtitle = std::make_shared<CLabel>( pos.w / 2, 40, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, subtitleText.toString());
labelSubtitle = std::make_shared<CLabel>( pos.w / 2, 40, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, subtitleText.toString(), 400);
labelVersionTitle = std::make_shared<CLabel>( 10, 60, FONT_MEDIUM, ETextAlignment::CENTERLEFT, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.preview.version").toString());
labelVersionValue = std::make_shared<CLabel>( 10, 80, FONT_MEDIUM, ETextAlignment::CENTERLEFT, Colors::WHITE, roomDescription.gameVersion);

View File

@ -207,7 +207,7 @@ GlobalLobbyAccountCard::GlobalLobbyAccountCard(GlobalLobbyWindow * window, const
: GlobalLobbyChannelCardBase(window, Point(130, 40), "player", accountDescription.accountID, accountDescription.displayName)
{
OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
labelName = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, accountDescription.displayName);
labelName = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, accountDescription.displayName, 120);
labelStatus = std::make_shared<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, accountDescription.status);
}
@ -238,8 +238,8 @@ GlobalLobbyRoomCard::GlobalLobbyRoomCard(GlobalLobbyWindow * window, const Globa
else
backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1);
labelName = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, roomDescription.hostAccountDisplayName);
labelDescription = std::make_shared<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, roomDescription.description);
labelName = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, roomDescription.hostAccountDisplayName, 180);
labelDescription = std::make_shared<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, roomDescription.description, 160);
labelRoomSize = std::make_shared<CLabel>(212, 10, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::YELLOW, roomSizeText.toString());
labelRoomStatus = std::make_shared<CLabel>(225, 30, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::YELLOW, roomStatusText.toString());
iconRoomSize = std::make_shared<CPicture>(ImagePath::builtin("lobby/iconPlayer"), Point(214, 5));

View File

@ -19,6 +19,7 @@
#include "../eventsSDL/InputHandler.h"
#include "../CGameInfo.h"
#include "../adventureMap/AdventureMapInterface.h"
#include "../render/Colors.h"
#include "../render/Graphics.h"
#include "../render/IFont.h"
@ -145,7 +146,13 @@ CGuiHandler::CGuiHandler()
{
}
CGuiHandler::~CGuiHandler() = default;
CGuiHandler::~CGuiHandler()
{
// enforce deletion order on shutdown
// all UI elements including adventure map must be destroyed before Gui Handler
// proper solution would be removal of adventureInt global
adventureInt.reset();
}
ShortcutHandler & CGuiHandler::shortcuts()
{

View File

@ -345,7 +345,8 @@ std::shared_ptr<CLabel> InterfaceObjectConfigurable::buildLabel(const JsonNode &
auto color = readColor(config["color"]);
auto text = readText(config["text"]);
auto position = readPosition(config["position"]);
return std::make_shared<CLabel>(position.x, position.y, font, alignment, color, text);
auto maxWidth = config["maxWidth"].Integer();
return std::make_shared<CLabel>(position.x, position.y, font, alignment, color, text, maxWidth);
}
std::shared_ptr<CMultiLineLabel> InterfaceObjectConfigurable::buildMultiLineLabel(const JsonNode & config) const

View File

@ -340,10 +340,10 @@ void OptionsTabBase::recreate(bool campaign)
//Simultaneous turns
if(auto turnSlider = widget<CSlider>("simturnsDurationMin"))
turnSlider->setValue(SEL->getStartInfo()->simturnsInfo.requiredTurns);
turnSlider->setValue(SEL->getStartInfo()->simturnsInfo.requiredTurns, false);
if(auto turnSlider = widget<CSlider>("simturnsDurationMax"))
turnSlider->setValue(SEL->getStartInfo()->simturnsInfo.optionalTurns);
turnSlider->setValue(SEL->getStartInfo()->simturnsInfo.optionalTurns, false);
if(auto w = widget<CLabel>("labelSimturnsDurationValueMin"))
w->setText(generateSimturnsDurationText(SEL->getStartInfo()->simturnsInfo.requiredTurns));
@ -388,7 +388,7 @@ void OptionsTabBase::recreate(bool campaign)
auto & tpreset = variables["timerPresets"].Vector()[idx];
if(tpreset.Vector().at(1).Integer() == turnTimerRemote.turnTimer / 1000)
{
turnSlider->scrollTo(idx);
turnSlider->scrollTo(idx, false);
if(auto w = widget<CLabel>("labelTurnDurationValue"))
w->setText(CGI->generaltexth->turnDurations[idx]);
}

View File

@ -293,7 +293,10 @@ std::string CComponent::getSubtitle() const
return CGI->artifacts()->getById(data.subType.as<ArtifactID>())->getNameTranslated();
case ComponentType::SPELL_SCROLL:
case ComponentType::SPELL:
return CGI->spells()->getById(data.subType.as<SpellID>())->getNameTranslated();
if (data.value < 0)
return "{#A9A9A9|" + CGI->spells()->getById(data.subType.as<SpellID>())->getNameTranslated() + "}";
else
return CGI->spells()->getById(data.subType.as<SpellID>())->getNameTranslated();
case ComponentType::NONE:
case ComponentType::MORALE:
case ComponentType::LUCK:

View File

@ -70,7 +70,7 @@ int CSlider::getValue() const
return value;
}
void CSlider::setValue(int to)
void CSlider::setValue(int to, bool callCallbacks)
{
scrollTo(value);
}
@ -113,7 +113,7 @@ void CSlider::updateSliderPos()
}
}
void CSlider::scrollTo(int to)
void CSlider::scrollTo(int to, bool callCallbacks)
{
vstd::amax(to, 0);
vstd::amin(to, positions);
@ -125,7 +125,8 @@ void CSlider::scrollTo(int to)
updateSliderPos();
moved(getValue());
if (callCallbacks)
moved(getValue());
}
void CSlider::clickPressed(const Point & cursorPosition)
@ -321,7 +322,7 @@ int SliderNonlinear::getValue() const
return scaledValues.at(CSlider::getValue());
}
void SliderNonlinear::setValue(int to)
void SliderNonlinear::setValue(int to, bool callCallbacks)
{
size_t nearest = 0;
@ -334,5 +335,5 @@ void SliderNonlinear::setValue(int to)
nearest = i;
}
scrollTo(nearest);
scrollTo(nearest, callCallbacks);
}

View File

@ -52,14 +52,14 @@ public:
void clearScrollBounds();
/// Value modifiers
void scrollTo(int value);
void scrollTo(int value, bool callCallbacks = true);
void scrollBy(int amount) override;
void scrollToMin();
void scrollToMax();
/// Amount modifier
void setAmount(int to);
virtual void setValue(int to);
virtual void setValue(int to, bool callCallbacks = true);
/// Accessors
int getAmount() const;
@ -95,7 +95,7 @@ class SliderNonlinear : public CSlider
using CSlider::setAmount; // make private
public:
void setValue(int to) override;
void setValue(int to, bool callCallbacks) override;
int getValue() const override;
SliderNonlinear(Point position, int length, const std::function<void(int)> & Moved, const std::vector<int> & values, int Value, Orientation orientation, EStyle style);

View File

@ -155,18 +155,21 @@
"types" : {
"boatNecropolis" : {
"index" : 0,
"compatibilityIdentifiers" : [ "evil" ],
"actualAnimation" : "AB01_.def",
"overlayAnimation" : "ABM01_.def",
"flagAnimations" : ["ABF01L", "ABF01G", "ABF01R", "ABF01D", "ABF01B", "ABF01P", "ABF01W", "ABF01K"]
},
"boatCastle" : {
"index" : 1,
"compatibilityIdentifiers" : [ "good" ],
"actualAnimation" : "AB02_.def",
"overlayAnimation" : "ABM02_.def",
"flagAnimations" : ["ABF02L", "ABF02G", "ABF02R", "ABF02D", "ABF02B", "ABF02P", "ABF02W", "ABF02K"]
},
"boatFortress" : {
"index" : 2,
"compatibilityIdentifiers" : [ "neutral" ],
"actualAnimation" : "AB03_.def",
"overlayAnimation" : "ABM03_.def",
"flagAnimations" : ["ABF03L", "ABF03G", "ABF03R", "ABF03D", "ABF03B", "ABF03P", "ABF03W", "ABF03K"]

View File

@ -43,7 +43,8 @@
{
"name" : "accountNameLabel",
"type": "labelTitleMain",
"position": {"x": 15, "y": 10}
"position": {"x": 15, "y": 10},
"maxWidth": 230
},
{

6
debian/changelog vendored
View File

@ -4,6 +4,12 @@ vcmi (1.6.0) jammy; urgency=medium
-- Ivan Savenko <saven.ivan@gmail.com> Fri, 30 Aug 2024 12:00:00 +0200
vcmi (1.5.6) jammy; urgency=medium
* New upstream release
-- Ivan Savenko <saven.ivan@gmail.com> Sun, 4 Aug 2024 12:00:00 +0200
vcmi (1.5.5) jammy; urgency=medium
* New upstream release

View File

@ -1,7 +1,7 @@
[![VCMI](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg?branch=develop&event=push)](https://github.com/vcmi/vcmi/actions/workflows/github.yml?query=branch%3Adevelop+event%3Apush)
[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.0)
[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.4/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.4)
[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.5/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.5)
[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.4/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.5)
[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.5/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.6)
[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases)
# VCMI Project

View File

@ -463,7 +463,9 @@ Configurable object has following structure:
`"text"`: [text](#text),
`"position"`: [position](#position)
`"position"`: [position](#position),
`"maxWidth"`: int` optional, trim longer text
#### [VCMI-1.4] Multi-line label

View File

@ -91,6 +91,7 @@
<launchable type="desktop-id">vcmilauncher.desktop</launchable>
<releases>
<release version="1.6.0" date="2024-08-30" type="development"/>
<release version="1.5.6" date="2024-08-04" type="stable"/>
<release version="1.5.5" date="2024-07-17" type="stable"/>
<release version="1.5.4" date="2024-07-12" type="stable"/>
<release version="1.5.3" date="2024-06-21" type="stable"/>

View File

@ -755,15 +755,15 @@ DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit *
{
RETURN_IF_NOT_BATTLE({});
auto reachability = battleGetDistances(attacker, attacker->getPosition());
int getMovementRange = attackerPosition.isValid() ? reachability[attackerPosition] : 0;
return battleEstimateDamage(attacker, defender, getMovementRange, retaliationDmg);
int movementRange = attackerPosition.isValid() ? reachability[attackerPosition] : 0;
return battleEstimateDamage(attacker, defender, movementRange, retaliationDmg);
}
DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int getMovementRange, DamageEstimation * retaliationDmg) const
DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int movementRange, DamageEstimation * retaliationDmg) const
{
RETURN_IF_NOT_BATTLE({});
const bool shooting = battleCanShoot(attacker, defender->getPosition());
const BattleAttackInfo bai(attacker, defender, getMovementRange, shooting);
const BattleAttackInfo bai(attacker, defender, movementRange, shooting);
return battleEstimateDamage(bai, retaliationDmg);
}

View File

@ -32,18 +32,14 @@ public:
int32_t getNum() const
{
int32_t result;
std::visit([&result] (const auto& v) { result = v.getNum(); }, value);
return result;
}
std::string toString() const
{
std::string result;
std::visit([&result] (const auto& v) { result = v.encode(v.getNum()); }, value);
return result;
}
@ -58,6 +54,13 @@ public:
return IdentifierType();
}
bool hasValue() const
{
bool result = false;
std::visit([&result] (const auto& v) { result = v.hasValue(); }, value);
return result;
}
template <typename Handler> void serialize(Handler &h)
{
h & value;

View File

@ -133,13 +133,15 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(const CampaignTravel & tr
if(!art)
return false;
bool takeable = travelOptions.artifactsKeptByHero.count(art->artType->getId());
ArtifactLocation al(hero.hero->id, artifactPosition);
if (takeable)
bool takeable = travelOptions.artifactsKeptByHero.count(art->artType->getId());
bool locked = hero.hero->getSlot(al.slot)->locked;
if (!locked && takeable)
hero.transferrableArtifacts.push_back(artifactPosition);
ArtifactLocation al(hero.hero->id, artifactPosition);
if(!takeable && !hero.hero->getSlot(al.slot)->locked) //don't try removing locked artifacts -> it crashes #1719
if (!locked && !takeable)
{
hero.hero->getArt(al.slot)->removeFrom(*hero.hero, al.slot);
return true;

View File

@ -348,15 +348,19 @@ void CGDwelling::newTurn(vstd::RNG & rand) const
std::vector<Component> CGDwelling::getPopupComponents(PlayerColor player) const
{
if (getOwner() != player)
return {};
bool visitedByOwner = getOwner() == player;
std::vector<Component> result;
if (ID == Obj::CREATURE_GENERATOR1 && !creatures.empty())
{
for (auto const & creature : creatures.front().second)
result.emplace_back(ComponentType::CREATURE, creature, creatures.front().first);
{
if (visitedByOwner)
result.emplace_back(ComponentType::CREATURE, creature, creatures.front().first);
else
result.emplace_back(ComponentType::CREATURE, creature);
}
}
if (ID == Obj::CREATURE_GENERATOR4)
@ -364,7 +368,12 @@ std::vector<Component> CGDwelling::getPopupComponents(PlayerColor player) const
for (auto const & creatureLevel : creatures)
{
if (!creatureLevel.second.empty())
result.emplace_back(ComponentType::CREATURE, creatureLevel.second.back(), creatureLevel.first);
{
if (visitedByOwner)
result.emplace_back(ComponentType::CREATURE, creatureLevel.second.back(), creatureLevel.first);
else
result.emplace_back(ComponentType::CREATURE, creatureLevel.second.back());
}
}
}
return result;
@ -426,7 +435,7 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const
if(count) //there are available creatures
{
if (VLC->settings()->getBoolean(EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED))
if (VLC->settings()->getBoolean(EGameSettings::DWELLINGS_MERGE_ON_RECRUIT))
{
SlotID testSlot = h->getSlotFor(crid);
if(!testSlot.validSlot()) //no available slot - try merging army of visiting hero

View File

@ -1704,6 +1704,16 @@ void CGHeroInstance::serializeJsonOptions(JsonSerializeFormat & handler)
setHeroTypeName(typeName);
}
if(!handler.saving)
{
if(!appearance)
{
// crossoverDeserialize
type = getHeroType().toHeroType();
appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, type->heroClass->getIndex())->getTemplates().front();
}
}
CArmedInstance::serializeJsonOptions(handler);
{
@ -1719,13 +1729,6 @@ void CGHeroInstance::serializeJsonOptions(JsonSerializeFormat & handler)
if(!handler.saving)
{
if(!appearance)
{
// crossoverDeserialize
type = getHeroType().toHeroType();
appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, type->heroClass->getIndex())->getTemplates().front();
}
patrol.patrolling = (rawPatrolRadius > NO_PATROLING);
patrol.initialPos = visitablePos();
patrol.patrolRadius = (rawPatrolRadius > NO_PATROLING) ? rawPatrolRadius : 0;

View File

@ -184,7 +184,26 @@ void CRewardableObject::heroLevelUpDone(const CGHeroInstance *hero) const
void CRewardableObject::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
{
if(answer == 0)
{
switch (configuration.visitMode)
{
case Rewardable::VISIT_UNLIMITED:
case Rewardable::VISIT_BONUS:
case Rewardable::VISIT_HERO:
case Rewardable::VISIT_LIMITER:
{
// workaround for object with refusable reward not getting marked as visited
// TODO: better solution that would also work for player-visitable objects
if (!wasScouted(hero->getOwner()))
{
ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, hero->id);
cb->sendAndApply(&cov);
}
}
}
return; // player refused
}
if(answer > 0 && answer-1 < configuration.info.size())
{

View File

@ -115,8 +115,10 @@ void Rewardable::Reward::loadComponents(std::vector<Component> & comps, const CG
comps.emplace_back(ComponentType::ARTIFACT, entry);
for(const auto & entry : spells)
if (!h || h->canLearnSpell(entry.toEntity(VLC), true))
comps.emplace_back(ComponentType::SPELL, entry);
{
bool learnable = !h || h->canLearnSpell(entry.toEntity(VLC), true);
comps.emplace_back(ComponentType::SPELL, entry, learnable ? 0 : -1);
}
for(const auto & entry : creatures)
comps.emplace_back(ComponentType::CREATURE, entry.type->getId(), entry.count);

View File

@ -694,6 +694,15 @@ void CGameHandler::onNewTurn()
}
}
for (auto & player : gs->players)
{
if (player.second.status != EPlayerStatus::INGAME)
continue;
if (player.second.heroes.empty() && player.second.towns.empty())
throw std::runtime_error("Invalid player in player state! Player " + std::to_string(player.first.getNum()) + ", map name: " + gs->map->name.toString() + ", map description: " + gs->map->description.toString());
}
if (newWeek && !firstTurn)
{
n.specialWeek = NewTurn::NORMAL;

View File

@ -496,7 +496,7 @@ bool BattleActionProcessor::doHealAction(const CBattleInfoCallback & battle, con
else
destStack = battle.battleGetUnitByPos(target.at(0).hexValue);
if(stack == nullptr || destStack == nullptr || !healerAbility || healerAbility->subtype == BonusSubtypeID())
if(stack == nullptr || destStack == nullptr || !healerAbility || !healerAbility->subtype.hasValue())
{
gameHandler->complain("There is either no healer, no destination, or healer cannot heal :P");
}
@ -973,7 +973,7 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const
}
std::shared_ptr<const Bonus> bonus = attacker->getFirstBonus(Selector::type()(BonusType::SPELL_LIKE_ATTACK));
if(bonus && ranged) //TODO: make it work in melee?
if(bonus && ranged && bonus->subtype.hasValue()) //TODO: make it work in melee?
{
//this is need for displaying hit animation
bat.flags |= BattleAttack::SPELL_LIKE;