1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-03-19 21:10:12 +02:00

Merge branch 'develop' into bank_support

This commit is contained in:
Laserlicht 2024-08-09 23:36:59 +02:00 committed by GitHub
commit b4c25a148a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
117 changed files with 5308 additions and 746 deletions

2
.gitmodules vendored
View File

@ -1,7 +1,7 @@
[submodule "test/googletest"]
path = test/googletest
url = https://github.com/google/googletest
branch = v1.13.x
branch = v1.15.x
[submodule "AI/FuzzyLite"]
path = AI/FuzzyLite
url = https://github.com/fuzzylite/fuzzylite.git

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

@ -102,6 +102,12 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
# define STRONG_INLINE inline
#endif
// Required for building boost::stacktrace on macOS.
// See https://github.com/boostorg/stacktrace/issues/88
#if defined(VCMI_APPLE)
#define _GNU_SOURCE
#endif
#define _USE_MATH_DEFINES
#include <algorithm>
@ -700,6 +706,33 @@ namespace vstd
return a + (b - a) * f;
}
/// Divides dividend by divisor and rounds result up
/// For use with integer-only arithmetic
template<typename Integer1, typename Integer2>
Integer1 divideAndCeil(const Integer1 & dividend, const Integer2 & divisor)
{
static_assert(std::is_integral_v<Integer1> && std::is_integral_v<Integer2>, "This function should only be used with integral types");
return (dividend + divisor - 1) / divisor;
}
/// Divides dividend by divisor and rounds result to nearest
/// For use with integer-only arithmetic
template<typename Integer1, typename Integer2>
Integer1 divideAndRound(const Integer1 & dividend, const Integer2 & divisor)
{
static_assert(std::is_integral_v<Integer1> && std::is_integral_v<Integer2>, "This function should only be used with integral types");
return (dividend + divisor / 2 - 1) / divisor;
}
/// Divides dividend by divisor and rounds result down
/// For use with integer-only arithmetic
template<typename Integer1, typename Integer2>
Integer1 divideAndFloor(const Integer1 & dividend, const Integer2 & divisor)
{
static_assert(std::is_integral_v<Integer1> && std::is_integral_v<Integer2>, "This function should only be used with integral types");
return dividend / divisor;
}
template<typename Floating>
bool isAlmostZero(const Floating & value)
{

View File

@ -72,6 +72,11 @@
"vcmi.lobby.noUnderground" : "sem subterrâneo",
"vcmi.lobby.sortDate" : "Classifica mapas por data de alteração",
"vcmi.lobby.backToLobby" : "Voltar para a sala de espera",
"vcmi.lobby.author" : "Autor",
"vcmi.lobby.handicap" : "Desvant.",
"vcmi.lobby.handicap.resource" : "Fornece aos jogadores recursos apropriados para começar, além dos recursos iniciais normais. Valores negativos são permitidos, mas são limitados a 0 no total (o jogador nunca começa com recursos negativos).",
"vcmi.lobby.handicap.income" : "Altera as várias rendas do jogador em porcentagem. Arredondado para cima.",
"vcmi.lobby.handicap.growth" : "Altera a taxa de produção das criaturas nas cidades possuídas pelo jogador. Arredondado para cima.",
"vcmi.lobby.login.title" : "Sala de Espera Online do VCMI",
"vcmi.lobby.login.username" : "Nome de usuário:",

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

@ -35,6 +35,7 @@
#include "../lib/TurnTimerInfo.h"
#include "../lib/VCMIDirs.h"
#include "../lib/campaign/CampaignState.h"
#include "../lib/gameState/HighScore.h"
#include "../lib/CPlayerState.h"
#include "../lib/mapping/CMapInfo.h"
#include "../lib/mapObjects/CGTownInstance.h"
@ -672,39 +673,9 @@ void CServerHandler::startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameSta
setState(EClientState::GAMEPLAY);
}
HighScoreParameter CServerHandler::prepareHighScores(PlayerColor player, bool victory)
{
const auto * gs = client->gameState();
const auto * playerState = gs->getPlayerState(player);
HighScoreParameter param;
param.difficulty = gs->getStartInfo()->difficulty;
param.day = gs->getDate();
param.townAmount = gs->howManyTowns(player);
param.usedCheat = gs->getPlayerState(player)->cheated;
param.hasGrail = false;
for(const CGHeroInstance * h : playerState->heroes)
if(h->hasArt(ArtifactID::GRAIL))
param.hasGrail = true;
for(const CGTownInstance * t : playerState->towns)
if(t->builtBuildings.count(BuildingID::GRAIL))
param.hasGrail = true;
param.allEnemiesDefeated = true;
for (PlayerColor otherPlayer(0); otherPlayer < PlayerColor::PLAYER_LIMIT; ++otherPlayer)
{
auto ps = gs->getPlayerState(otherPlayer, false);
if(ps && otherPlayer != player && !ps->checkVanquished())
param.allEnemiesDefeated = false;
}
param.scenarioName = gs->getMapHeader()->name.toString();
param.playerName = gs->getStartInfo()->playerInfos.find(player)->second.name;
return param;
}
void CServerHandler::showHighScoresAndEndGameplay(PlayerColor player, bool victory)
{
HighScoreParameter param = prepareHighScores(player, victory);
HighScoreParameter param = HighScore::prepareHighScores(client->gameState(), player, victory);
if(victory && client->gameState()->getStartInfo()->campState)
{

View File

@ -40,8 +40,6 @@ class GlobalLobbyClient;
class GameChatHandler;
class IServerRunner;
class HighScoreCalculation;
enum class ESelectionScreen : ui8;
enum class ELoadMode : ui8;
@ -128,8 +126,6 @@ class CServerHandler final : public IServerAPI, public LobbyInfo, public INetwor
bool isServerLocal() const;
HighScoreParameter prepareHighScores(PlayerColor player, bool victory);
public:
/// High-level connection overlay that is capable of (de)serializing network data
std::shared_ptr<CConnection> logicConnection;

View File

@ -33,6 +33,7 @@
#include "../render/Canvas.h"
#include "../render/IImage.h"
#include "../render/IRenderHandler.h"
#include "../render/IScreenHandler.h"
#include "../CMT.h"
#include "../PlayerLocalState.h"
#include "../CPlayerInterface.h"
@ -232,7 +233,7 @@ void AdventureMapInterface::handleMapScrollingUpdate(uint32_t timePassed)
bool cursorInScrollArea = scrollDelta != Point(0,0);
bool scrollingActive = cursorInScrollArea && shortcuts->optionMapScrollingActive() && !scrollingWasBlocked;
bool scrollingBlocked = GH.isKeyboardCtrlDown() || !settings["adventure"]["borderScroll"].Bool();
bool scrollingBlocked = GH.isKeyboardCtrlDown() || !settings["adventure"]["borderScroll"].Bool() || !GH.screenHandler().hasFocus();
if (!scrollingWasActive && scrollingBlocked)
{
@ -375,7 +376,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

@ -433,6 +433,50 @@ QuickSpellPanel::QuickSpellPanel(BattleInterface & owner)
create();
}
std::vector<std::tuple<SpellID, bool>> QuickSpellPanel::getSpells()
{
std::vector<SpellID> spellIds;
std::vector<bool> spellIdsFromSetting;
for(int i = 0; i < QUICKSPELL_SLOTS; i++)
{
std::string spellIdentifier = persistentStorage["quickSpell"][std::to_string(i)].String();
SpellID id;
try
{
id = SpellID::decode(spellIdentifier);
}
catch(const IdentifierResolutionException& e)
{
id = SpellID::NONE;
}
spellIds.push_back(id);
spellIdsFromSetting.push_back(id != SpellID::NONE);
}
// autofill empty slots with spells if possible
auto hero = owner.getBattle()->battleGetMyHero();
for(int i = 0; i < QUICKSPELL_SLOTS; i++)
{
if(spellIds[i] != SpellID::NONE)
continue;
for(const auto & availableSpellID : CGI->spellh->getDefaultAllowed())
{
const auto * availableSpell = availableSpellID.toSpell();
if(!availableSpell->isAdventure() && !availableSpell->isCreatureAbility() && hero->canCastThisSpell(availableSpell) && !vstd::contains(spellIds, availableSpell->getId()))
{
spellIds[i] = availableSpell->getId();
break;
}
}
}
std::vector<std::tuple<SpellID, bool>> ret;
for(int i = 0; i < QUICKSPELL_SLOTS; i++)
ret.push_back(std::make_tuple(spellIds[i], spellIdsFromSetting[i]));
return ret;
}
void QuickSpellPanel::create()
{
OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
@ -447,18 +491,11 @@ void QuickSpellPanel::create()
if(!hero)
return;
for(int i = 0; i < 12; i++) {
std::string spellIdentifier = persistentStorage["quickSpell"][std::to_string(i)].String();
auto spells = getSpells();
for(int i = 0; i < QUICKSPELL_SLOTS; i++) {
SpellID id;
try
{
id = SpellID::decode(spellIdentifier);
}
catch(const IdentifierResolutionException& e)
{
id = SpellID::NONE;
}
bool fromSettings;
std::tie(id, fromSettings) = spells[i];
auto button = std::make_shared<CButton>(Point(2, 7 + 50 * i), AnimationPath::builtin("spellint"), CButton::tooltip(), [this, id, hero](){
if(id.hasValue() && id.toSpell()->canBeCast(owner.getBattle().get(), spells::Mode::HERO, hero))
@ -466,16 +503,19 @@ void QuickSpellPanel::create()
owner.castThisSpell(id);
}
});
button->setOverlay(std::make_shared<CAnimImage>(AnimationPath::builtin("spellint"), !spellIdentifier.empty() ? id.num + 1 : 0));
button->setOverlay(std::make_shared<CAnimImage>(AnimationPath::builtin("spellint"), id != SpellID::NONE ? id.num + 1 : 0));
button->addPopupCallback([this, i, hero](){
GH.input().hapticFeedback();
GH.windows().createAndPushWindow<CSpellWindow>(hero, owner.curInt.get(), true, [this, i](SpellID spell){
Settings configID = persistentStorage.write["quickSpell"][std::to_string(i)];
configID->String() = spell.toSpell()->identifier;
configID->String() = spell == SpellID::NONE ? "" : spell.toSpell()->identifier;
create();
});
});
if(fromSettings)
buttonsIsAutoGenerated.push_back(std::make_shared<TransparentFilledRectangle>(Rect(45, 37 + 50 * i, 5, 5), Colors::ORANGE));
if(!id.hasValue() || !id.toSpell()->canBeCast(owner.getBattle().get(), spells::Mode::HERO, hero))
{
buttonsDisabled.push_back(std::make_shared<TransparentFilledRectangle>(Rect(2, 7 + 50 * i, 48, 36), ColorRGBA(0, 0, 0, 172)));

View File

@ -155,17 +155,22 @@ private:
std::shared_ptr<CFilledTexture> background;
std::shared_ptr<TransparentFilledRectangle> rect;
std::vector<std::shared_ptr<CButton>> buttons;
std::vector<std::shared_ptr<TransparentFilledRectangle>> buttonsIsAutoGenerated;
std::vector<std::shared_ptr<TransparentFilledRectangle>> buttonsDisabled;
std::vector<std::shared_ptr<CLabel>> labels;
BattleInterface & owner;
public:
int QUICKSPELL_SLOTS = 12;
bool isEnabled; // isActive() is not working on multiple conditions, because of this we need a seperate flag
QuickSpellPanel(BattleInterface & owner);
void create();
std::vector<std::tuple<SpellID, bool>> getSpells();
void show(Canvas & to) override;
void inputModeChanged(InputMode modi) override;
};

View File

@ -218,7 +218,9 @@ void BattleWindow::showStickyQuickSpellWindow()
Settings showStickyQuickSpellWindow = settings.write["battle"]["enableQuickSpellPanel"];
showStickyQuickSpellWindow->Bool() = true;
if(GH.screenDimensions().x >= 1050 && owner.getBattle()->battleGetMyHero()->hasSpellbook())
auto hero = owner.getBattle()->battleGetMyHero();
if(GH.screenDimensions().x >= 1050 && hero != nullptr && hero->hasSpellbook())
{
quickSpellWindow->enable();
quickSpellWindow->isEnabled = true;
@ -273,16 +275,13 @@ std::shared_ptr<BattleConsole> BattleWindow::buildBattleConsole(const JsonNode &
void BattleWindow::useSpellIfPossible(int slot)
{
std::string spellIdentifier = persistentStorage["quickSpell"][std::to_string(slot)].String();
SpellID id;
try
{
id = SpellID::decode(spellIdentifier);
}
catch(const IdentifierResolutionException& e)
{
bool fromSettings;
std::tie(id, fromSettings) = quickSpellWindow->getSpells()[slot];
if(id == SpellID::NONE)
return;
}
if(id.hasValue() && owner.getBattle()->battleGetMyHero() && id.toSpell()->canBeCast(owner.getBattle().get(), spells::Mode::HERO, owner.getBattle()->battleGetMyHero()))
{
owner.castThisSpell(id);

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

@ -258,6 +258,15 @@ void CIntObject::redraw()
}
}
void CIntObject::moveChildForeground(const CIntObject * childToMove)
{
for(auto child = children.begin(); child != children.end(); child++)
if(*child == childToMove && child != children.end())
{
std::rotate(child, child + 1, children.end());
}
}
bool CIntObject::receiveEvent(const Point & position, int eventType) const
{
return pos.isInside(position);

View File

@ -102,6 +102,8 @@ public:
void showAll(Canvas & to) override;
//request complete redraw of this object
void redraw() override;
// Move child object to foreground
void moveChildForeground(const CIntObject * childToMove);
/// returns true if this element is a popup window
/// called only for windows

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

@ -34,74 +34,6 @@
#include "../../lib/constants/EntityIdentifiers.h"
#include "../../lib/gameState/HighScore.h"
auto HighScoreCalculation::calculate()
{
struct Result
{
int basic = 0;
int total = 0;
int sumDays = 0;
bool cheater = false;
};
Result firstResult;
Result summary;
const std::array<double, 5> difficultyMultipliers{0.8, 1.0, 1.3, 1.6, 2.0};
for(auto & param : parameters)
{
double tmp = 200 - (param.day + 10) / (param.townAmount + 5) + (param.allEnemiesDefeated ? 25 : 0) + (param.hasGrail ? 25 : 0);
firstResult = Result{static_cast<int>(tmp), static_cast<int>(tmp * difficultyMultipliers.at(param.difficulty)), param.day, param.usedCheat};
summary.basic += firstResult.basic * 5.0 / parameters.size();
summary.total += firstResult.total * 5.0 / parameters.size();
summary.sumDays += firstResult.sumDays;
summary.cheater |= firstResult.cheater;
}
if(parameters.size() == 1)
return firstResult;
return summary;
}
struct HighScoreCreature
{
CreatureID creature;
int min;
int max;
};
static std::vector<HighScoreCreature> getHighscoreCreaturesList()
{
JsonNode configCreatures(JsonPath::builtin("CONFIG/highscoreCreatures.json"));
std::vector<HighScoreCreature> ret;
for(auto & json : configCreatures["creatures"].Vector())
{
HighScoreCreature entry;
entry.creature = CreatureID::decode(json["creature"].String());
entry.max = json["max"].isNull() ? std::numeric_limits<int>::max() : json["max"].Integer();
entry.min = json["min"].isNull() ? std::numeric_limits<int>::min() : json["min"].Integer();
ret.push_back(entry);
}
return ret;
}
CreatureID HighScoreCalculation::getCreatureForPoints(int points, bool campaign)
{
static const std::vector<HighScoreCreature> creatures = getHighscoreCreaturesList();
int divide = campaign ? 5 : 1;
for(auto & creature : creatures)
if(points / divide <= creature.max && points / divide >= creature.min)
return creature.creature;
throw std::runtime_error("Unable to find creature for score " + std::to_string(points));
}
CHighScoreScreen::CHighScoreScreen(HighScorePage highscorepage, int highlighted)
: CWindowObject(BORDERED), highscorepage(highscorepage), highlighted(highlighted)
{

View File

@ -21,16 +21,6 @@ class CFilledTexture;
class TransparentFilledRectangle;
class HighScoreCalculation
{
public:
std::vector<HighScoreParameter> parameters;
bool isCampaign = false;
auto calculate();
static CreatureID getCreatureForPoints(int points, bool campaign);
};
class CHighScoreScreen : public CWindowObject
{
public:

View File

@ -90,6 +90,7 @@ CArtPlace::CArtPlace(Point position, const CArtifactInstance * art)
image = std::make_shared<CAnimImage>(AnimationPath::builtin("artifact"), imageIndex);
image->disable();
moveSelectionForeground();
}
const CArtifactInstance * CArtPlace::getArt() const

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

@ -714,3 +714,8 @@ void SelectableSlot::setSelectionWidth(int width)
selection = std::make_shared<TransparentFilledRectangle>( selection->pos - pos.topLeft(), Colors::TRANSPARENCY, Colors::YELLOW, width);
selectSlot(selected);
}
void SelectableSlot::moveSelectionForeground()
{
moveChildForeground(selection.get());
}

View File

@ -261,4 +261,5 @@ public:
void selectSlot(bool on);
bool isSelected() const;
void setSelectionWidth(int width);
void moveSelectionForeground();
};

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

@ -29,6 +29,7 @@
#include "../widgets/CComponent.h"
#include "../widgets/CTextInput.h"
#include "../widgets/TextControls.h"
#include "../widgets/Buttons.h"
#include "../adventureMap/AdventureMapInterface.h"
#include "../render/IRenderHandler.h"
#include "../render/IImage.h"
@ -130,9 +131,9 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m
pos = background->center(Point(pos.w/2 + pos.x, pos.h/2 + pos.y));
Rect r(90, isBigSpellbook ? 480 : 420, isBigSpellbook ? 160 : 110, 16);
if(settings["general"]["enableUiEnhancements"].Bool())
{
Rect r(90, isBigSpellbook ? 480 : 420, isBigSpellbook ? 160 : 110, 16);
const ColorRGBA rectangleColor = ColorRGBA(0, 0, 0, 75);
const ColorRGBA borderColor = ColorRGBA(128, 100, 75);
const ColorRGBA grayedColor = ColorRGBA(158, 130, 105);
@ -143,6 +144,13 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m
searchBox->setCallback(std::bind(&CSpellWindow::searchInput, this));
}
if(onSpellSelect)
{
Point boxPos = r.bottomLeft() + Point(-2, 5);
showAllSpells = std::make_shared<CToggleButton>(boxPos, AnimationPath::builtin("sysopchk.def"), CButton::tooltip(CGI->generaltexth->translate("core.help.458.hover"), CGI->generaltexth->translate("core.help.458.hover")), [this](bool state){ searchInput(); });
showAllSpellsDescription = std::make_shared<CLabel>(boxPos.x + 40, boxPos.y + 12, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, CGI->generaltexth->translate("core.help.458.hover"));
}
processSpells();
//numbers of spell pages computed
@ -288,7 +296,7 @@ void CSpellWindow::processSpells()
if(onSpellSelect)
{
if(spell->isCombat() == openOnBattleSpells && !spell->isSpecial() && !spell->isCreatureAbility() && searchTextFound)
if(spell->isCombat() == openOnBattleSpells && !spell->isSpecial() && !spell->isCreatureAbility() && searchTextFound && (showAllSpells->isSelected() || myHero->canCastThisSpell(spell.get())))
mySpells.push_back(spell.get());
continue;
}
@ -359,6 +367,9 @@ void CSpellWindow::fexitb()
(myInt->battleInt ? myInt->localState->spellbookSettings.spellbookLastTabBattle : myInt->localState->spellbookSettings.spellbookLastTabAdvmap) = selectedTab;
(myInt->battleInt ? myInt->localState->spellbookSettings.spellbookLastPageBattle : myInt->localState->spellbookSettings.spellbookLastPageAdvmap) = currentPage;
if(onSpellSelect)
onSpellSelect(SpellID::NONE);
close();
}
@ -605,7 +616,7 @@ void CSpellWindow::SpellArea::clickPressed(const Point & cursorPosition)
if(owner->onSpellSelect)
{
owner->onSpellSelect(mySpell->id);
owner->fexitb();
owner->close();
return;
}

View File

@ -27,6 +27,7 @@ class CPlayerInterface;
class CSpellWindow;
class CTextInput;
class TransparentFilledRectangle;
class CToggleButton;
/// The spell window
class CSpellWindow : public CWindowObject
@ -82,6 +83,9 @@ class CSpellWindow : public CWindowObject
std::shared_ptr<TransparentFilledRectangle> searchBoxRectangle;
std::shared_ptr<CLabel> searchBoxDescription;
std::shared_ptr<CToggleButton> showAllSpells;
std::shared_ptr<CLabel> showAllSpellsDescription;
bool isBigSpellbook;
int spellsPerPage;
int offL;

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

@ -99,7 +99,7 @@
"type":
{
"type" : "string",
"enum" : ["wide", "fictive", "repulsive"]
"enum" : ["wide", "fictive", "repulsive", "forcePortal"]
}
}
},

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.6/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

@ -38,7 +38,7 @@
{ "a" : "zoneA", "b" : "zoneB", "guard" : 5000, "road" : "false" },
{ "a" : "zoneA", "b" : "zoneC", "guard" : 5000, "road" : "random" },
{ "a" : "zoneB", "b" : "zoneC", "type" : "wide" }
//"type" can be "guarded" (default), "wide", "fictive" or "repulsive"
//"type" can be "guarded" (default), "wide", "fictive", "repulsive" or "forcePortal"
//"wide" connections have no border, or guard. "fictive" and "repulsive" connections are virtual -
//they do not create actual path, but only attract or repulse zones, respectively
]

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

@ -13,6 +13,8 @@
#include "CThreadHelper.h"
#include <boost/stacktrace.hpp>
VCMI_LIB_NAMESPACE_BEGIN
std::mutex CConsoleHandler::smx;
@ -142,6 +144,30 @@ static void createMemoryDump(MINIDUMP_EXCEPTION_INFORMATION * meinfo)
MessageBoxA(0, "VCMI has crashed. We are sorry. File with information about encountered problem has been created.", "VCMI Crashhandler", MB_OK | MB_ICONERROR);
}
LONG WINAPI onUnhandledException(EXCEPTION_POINTERS* exception)
{
logGlobal->error("Disaster happened.");
PEXCEPTION_RECORD einfo = exception->ExceptionRecord;
logGlobal->error("Reason: 0x%x - %s at %04x:%x", einfo->ExceptionCode, exceptionName(einfo->ExceptionCode), exception->ContextRecord->SegCs, (void*)einfo->ExceptionAddress);
if (einfo->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)
{
logGlobal->error("Attempt to %s 0x%8x", (einfo->ExceptionInformation[0] == 1 ? "write to" : "read from"), (void*)einfo->ExceptionInformation[1]);
}
const DWORD threadId = ::GetCurrentThreadId();
logGlobal->error("Thread ID: %d", threadId);
//exception info to be placed in the dump
MINIDUMP_EXCEPTION_INFORMATION meinfo = {threadId, exception, TRUE};
createMemoryDump(&meinfo);
return EXCEPTION_EXECUTE_HANDLER;
}
#endif
[[noreturn]] static void onTerminate()
{
logGlobal->error("Disaster happened.");
@ -166,37 +192,20 @@ static void createMemoryDump(MINIDUMP_EXCEPTION_INFORMATION * meinfo)
logGlobal->error("Reason: unknown exception!");
}
logGlobal->error("Call stack information:");
std::stringstream stream;
stream << boost::stacktrace::stacktrace();
logGlobal->error("%s", stream.str());
#ifdef VCMI_WINDOWS
const DWORD threadId = ::GetCurrentThreadId();
logGlobal->error("Thread ID: %d", threadId);
createMemoryDump(nullptr);
#endif
std::abort();
}
LONG WINAPI onUnhandledException(EXCEPTION_POINTERS* exception)
{
logGlobal->error("Disaster happened.");
PEXCEPTION_RECORD einfo = exception->ExceptionRecord;
logGlobal->error("Reason: 0x%x - %s at %04x:%x", einfo->ExceptionCode, exceptionName(einfo->ExceptionCode), exception->ContextRecord->SegCs, (void*)einfo->ExceptionAddress);
if (einfo->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)
{
logGlobal->error("Attempt to %s 0x%8x", (einfo->ExceptionInformation[0] == 1 ? "write to" : "read from"), (void*)einfo->ExceptionInformation[1]);
}
const DWORD threadId = ::GetCurrentThreadId();
logGlobal->error("Thread ID: %d", threadId);
//exception info to be placed in the dump
MINIDUMP_EXCEPTION_INFORMATION meinfo = {threadId, exception, TRUE};
createMemoryDump(&meinfo);
return EXCEPTION_EXECUTE_HANDLER;
}
#endif
void CConsoleHandler::setColor(EConsoleTextColor::EConsoleTextColor color)
{
TColor colorCode;
@ -289,11 +298,14 @@ CConsoleHandler::CConsoleHandler():
defErrColor = csbi.wAttributes;
#ifndef _DEBUG
SetUnhandledExceptionFilter(onUnhandledException);
std::set_terminate(onTerminate);
#endif
#else
defColor = "\x1b[0m";
#endif
#ifndef _DEBUG
std::set_terminate(onTerminate);
#endif
}
CConsoleHandler::~CConsoleHandler()
{

View File

@ -99,9 +99,11 @@ set(lib_MAIN_SRCS
gameState/CGameState.cpp
gameState/CGameStateCampaign.cpp
gameState/HighScore.cpp
gameState/InfoAboutArmy.cpp
gameState/RumorState.cpp
gameState/TavernHeroesPool.cpp
gameState/GameStatistics.cpp
mapObjectConstructors/AObjectTypeHandler.cpp
mapObjectConstructors/CBankInstanceConstructor.cpp
@ -468,6 +470,7 @@ set(lib_MAIN_HEADERS
gameState/RumorState.h
gameState/SThievesGuildInfo.h
gameState/TavernHeroesPool.h
gameState/GameStatistics.h
gameState/TavernSlot.h
gameState/QuestInfo.h

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

@ -145,7 +145,8 @@ int DamageCalculator::getActorAttackIgnored() const
if(multAttackReductionPercent > 0)
{
int reduction = (getActorAttackBase() * multAttackReductionPercent + 49) / 100; //using ints so 1.5 for 5 attack is rounded down as in HotA / h3assist etc. (keep in mind h3assist 1.2 shows wrong value for 15 attack points and unupg. nix)
//using ints so 1.5 for 5 attack is rounded down as in HotA / h3assist etc. (keep in mind h3assist 1.2 shows wrong value for 15 attack points and unupg. nix)
int reduction = vstd::divideAndRound( getActorAttackBase() * multAttackReductionPercent, 100);
return -std::min(reduction, getActorAttackBase());
}
return 0;

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

@ -875,7 +875,7 @@ void CGameState::initTowns()
}
//init spells
vti->spells.resize(GameConstants::SPELL_LEVELS);
vti->possibleSpells -= SpellID::PRESET;
for(ui32 z=0; z<vti->obligatorySpells.size();z++)
{
const auto * s = vti->obligatorySpells[z].toSpell();
@ -1538,137 +1538,6 @@ bool CGameState::checkForStandardLoss(const PlayerColor & player) const
return pState.checkVanquished();
}
struct statsHLP
{
using TStat = std::pair<PlayerColor, si64>;
//converts [<player's color, value>] to vec[place] -> platers
static std::vector< std::vector< PlayerColor > > getRank( std::vector<TStat> stats )
{
std::sort(stats.begin(), stats.end(), statsHLP());
//put first element
std::vector< std::vector<PlayerColor> > ret;
std::vector<PlayerColor> tmp;
tmp.push_back( stats[0].first );
ret.push_back( tmp );
//the rest of elements
for(int g=1; g<stats.size(); ++g)
{
if(stats[g].second == stats[g-1].second)
{
(ret.end()-1)->push_back( stats[g].first );
}
else
{
//create next occupied rank
std::vector<PlayerColor> tmp;
tmp.push_back(stats[g].first);
ret.push_back(tmp);
}
}
return ret;
}
bool operator()(const TStat & a, const TStat & b) const
{
return a.second > b.second;
}
static const CGHeroInstance * findBestHero(CGameState * gs, const PlayerColor & color)
{
std::vector<ConstTransitivePtr<CGHeroInstance> > &h = gs->players[color].heroes;
if(h.empty())
return nullptr;
//best hero will be that with highest exp
int best = 0;
for(int b=1; b<h.size(); ++b)
{
if(h[b]->exp > h[best]->exp)
{
best = b;
}
}
return h[best];
}
//calculates total number of artifacts that belong to given player
static int getNumberOfArts(const PlayerState * ps)
{
int ret = 0;
for(auto h : ps->heroes)
{
ret += (int)h->artifactsInBackpack.size() + (int)h->artifactsWorn.size();
}
return ret;
}
// get total strength of player army
static si64 getArmyStrength(const PlayerState * ps)
{
si64 str = 0;
for(auto h : ps->heroes)
{
if(!h->inTownGarrison) //original h3 behavior
str += h->getArmyStrength();
}
return str;
}
// get total gold income
static int getIncome(const PlayerState * ps, int percentIncome)
{
int totalIncome = 0;
const CGObjectInstance * heroOrTown = nullptr;
//Heroes can produce gold as well - skill, specialty or arts
for(const auto & h : ps->heroes)
{
totalIncome += h->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, BonusSubtypeID(GameResID(GameResID::GOLD)))) * percentIncome / 100;
if(!heroOrTown)
heroOrTown = h;
}
//Add town income of all towns
for(const auto & t : ps->towns)
{
totalIncome += t->dailyIncome()[EGameResID::GOLD];
if(!heroOrTown)
heroOrTown = t;
}
/// FIXME: Dirty dirty hack
/// Stats helper need some access to gamestate.
std::vector<const CGObjectInstance *> ownedObjects;
for(const CGObjectInstance * obj : heroOrTown->cb->gameState()->map->objects)
{
if(obj && obj->tempOwner == ps->color)
ownedObjects.push_back(obj);
}
/// This is code from CPlayerSpecificInfoCallback::getMyObjects
/// I'm really need to find out about callback interface design...
for(const auto * object : ownedObjects)
{
//Mines
if ( object->ID == Obj::MINE )
{
const auto * mine = dynamic_cast<const CGMine *>(object);
assert(mine);
if (mine->producedResource == EGameResID::GOLD)
totalIncome += mine->getProducedQuantity();
}
}
return totalIncome;
}
};
void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level)
{
auto playerInactive = [&](const PlayerColor & color)
@ -1688,7 +1557,7 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level)
stat.second = VAL_GETTER; \
stats.push_back(stat); \
} \
tgi.FIELD = statsHLP::getRank(stats); \
tgi.FIELD = Statistic::getRank(stats); \
}
for(auto & elem : players)
@ -1710,7 +1579,7 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level)
{
if(playerInactive(player.second.color))
continue;
const CGHeroInstance * best = statsHLP::findBestHero(this, player.second.color);
const CGHeroInstance * best = Statistic::findBestHero(this, player.second.color);
InfoAboutHero iah;
iah.initFromHero(best, (level >= 2) ? InfoAboutHero::EInfoLevel::DETAILED : InfoAboutHero::EInfoLevel::BASIC);
iah.army.clear();
@ -1731,27 +1600,19 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level)
}
if(level >= 3) //obelisks found
{
auto getObeliskVisited = [&](const TeamID & t)
{
if(map->obelisksVisited.count(t))
return map->obelisksVisited[t];
else
return ui8(0);
};
FILL_FIELD(obelisks, getObeliskVisited(gs->getPlayerTeam(g->second.color)->id))
FILL_FIELD(obelisks, Statistic::getObeliskVisited(gs, gs->getPlayerTeam(g->second.color)->id))
}
if(level >= 4) //artifacts
{
FILL_FIELD(artifacts, statsHLP::getNumberOfArts(&g->second))
FILL_FIELD(artifacts, Statistic::getNumberOfArts(&g->second))
}
if(level >= 4) //army strength
{
FILL_FIELD(army, statsHLP::getArmyStrength(&g->second))
FILL_FIELD(army, Statistic::getArmyStrength(&g->second))
}
if(level >= 5) //income
{
FILL_FIELD(income, statsHLP::getIncome(&g->second, scenarioOps->getIthPlayersSettings(g->second.color).handicap.percentIncome))
FILL_FIELD(income, Statistic::getIncome(gs, &g->second))
}
if(level >= 2) //best hero's stats
{

View File

@ -15,6 +15,7 @@
#include "../ConstTransitivePtr.h"
#include "RumorState.h"
#include "GameStatistics.h"
namespace boost
{
@ -90,6 +91,8 @@ public:
CBonusSystemNode globalEffects;
RumorState currentRumor;
StatisticDataSet statistic;
static boost::shared_mutex mutex;
void updateEntity(Metatype metatype, int32_t index, const JsonNode & data) override;
@ -167,6 +170,8 @@ public:
h & currentRumor;
h & campaign;
h & allocatedArtifacts;
if (h.version >= Handler::Version::STATISTICS)
h & statistic;
BONUS_TREE_DESERIALIZATION_FIX
}

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

@ -0,0 +1,370 @@
/*
* GameStatistics.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 "GameStatistics.h"
#include "../CPlayerState.h"
#include "../constants/StringConstants.h"
#include "CGameState.h"
#include "TerrainHandler.h"
#include "CHeroHandler.h"
#include "StartInfo.h"
#include "HighScore.h"
#include "../mapObjects/CGHeroInstance.h"
#include "../mapObjects/CGTownInstance.h"
#include "../mapObjects/CGObjectInstance.h"
#include "../mapObjects/MiscObjects.h"
#include "../mapping/CMap.h"
#include "../entities/building/CBuilding.h"
VCMI_LIB_NAMESPACE_BEGIN
void StatisticDataSet::add(StatisticDataSetEntry entry)
{
data.push_back(entry);
}
StatisticDataSetEntry StatisticDataSet::createEntry(const PlayerState * ps, const CGameState * gs)
{
StatisticDataSetEntry data;
HighScoreParameter param = HighScore::prepareHighScores(gs, ps->color, false);
HighScoreCalculation scenarioHighScores;
scenarioHighScores.parameters.push_back(param);
scenarioHighScores.isCampaign = false;
data.map = gs->map->name.toString();
data.timestamp = std::time(0);
data.day = gs->getDate(Date::DAY);
data.player = ps->color;
data.team = ps->team;
data.isHuman = ps->isHuman();
data.status = ps->status;
data.resources = ps->resources;
data.numberHeroes = ps->heroes.size();
data.numberTowns = gs->howManyTowns(ps->color);
data.numberArtifacts = Statistic::getNumberOfArts(ps);
data.numberDwellings = gs->getPlayerState(ps->color)->dwellings.size();
data.armyStrength = Statistic::getArmyStrength(ps, true);
data.totalExperience = Statistic::getTotalExperience(ps);
data.income = Statistic::getIncome(gs, ps);
data.mapExploredRatio = Statistic::getMapExploredRatio(gs, ps->color);
data.obeliskVisitedRatio = Statistic::getObeliskVisitedRatio(gs, ps->team);
data.townBuiltRatio = Statistic::getTownBuiltRatio(ps);
data.hasGrail = param.hasGrail;
data.numMines = Statistic::getNumMines(gs, ps);
data.score = scenarioHighScores.calculate().total;
data.maxHeroLevel = Statistic::findBestHero(gs, ps->color) ? Statistic::findBestHero(gs, ps->color)->level : 0;
data.numBattlesNeutral = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numBattlesNeutral : 0;
data.numBattlesPlayer = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numBattlesPlayer : 0;
data.numWinBattlesNeutral = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numWinBattlesNeutral : 0;
data.numWinBattlesPlayer = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numWinBattlesPlayer : 0;
data.numHeroSurrendered = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numHeroSurrendered : 0;
data.numHeroEscaped = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numHeroEscaped : 0;
data.spentResourcesForArmy = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).spentResourcesForArmy : TResources();
data.spentResourcesForBuildings = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).spentResourcesForBuildings : TResources();
data.tradeVolume = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).tradeVolume : TResources();
data.movementPointsUsed = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).movementPointsUsed : 0;
return data;
}
std::string StatisticDataSet::toCsv()
{
std::stringstream ss;
auto resources = std::vector<EGameResID>{EGameResID::GOLD, EGameResID::WOOD, EGameResID::MERCURY, EGameResID::ORE, EGameResID::SULFUR, EGameResID::CRYSTAL, EGameResID::GEMS};
ss << "Map" << ";";
ss << "Timestamp" << ";";
ss << "Day" << ";";
ss << "Player" << ";";
ss << "Team" << ";";
ss << "IsHuman" << ";";
ss << "Status" << ";";
ss << "NumberHeroes" << ";";
ss << "NumberTowns" << ";";
ss << "NumberArtifacts" << ";";
ss << "NumberDwellings" << ";";
ss << "ArmyStrength" << ";";
ss << "TotalExperience" << ";";
ss << "Income" << ";";
ss << "MapExploredRatio" << ";";
ss << "ObeliskVisitedRatio" << ";";
ss << "TownBuiltRatio" << ";";
ss << "HasGrail" << ";";
ss << "Score" << ";";
ss << "MaxHeroLevel" << ";";
ss << "NumBattlesNeutral" << ";";
ss << "NumBattlesPlayer" << ";";
ss << "NumWinBattlesNeutral" << ";";
ss << "NumWinBattlesPlayer" << ";";
ss << "NumHeroSurrendered" << ";";
ss << "NumHeroEscaped" << ";";
ss << "MovementPointsUsed";
for(auto & resource : resources)
ss << ";" << GameConstants::RESOURCE_NAMES[resource];
for(auto & resource : resources)
ss << ";" << GameConstants::RESOURCE_NAMES[resource] + "Mines";
for(auto & resource : resources)
ss << ";" << GameConstants::RESOURCE_NAMES[resource] + "SpentResourcesForArmy";
for(auto & resource : resources)
ss << ";" << GameConstants::RESOURCE_NAMES[resource] + "SpentResourcesForBuildings";
for(auto & resource : resources)
ss << ";" << GameConstants::RESOURCE_NAMES[resource] + "TradeVolume";
ss << "\r\n";
for(auto & entry : data)
{
ss << entry.map << ";";
ss << vstd::getFormattedDateTime(entry.timestamp, "%Y-%m-%dT%H:%M:%S") << ";";
ss << entry.day << ";";
ss << GameConstants::PLAYER_COLOR_NAMES[entry.player] << ";";
ss << entry.team.getNum() << ";";
ss << entry.isHuman << ";";
ss << (int)entry.status << ";";
ss << entry.numberHeroes << ";";
ss << entry.numberTowns << ";";
ss << entry.numberArtifacts << ";";
ss << entry.numberDwellings << ";";
ss << entry.armyStrength << ";";
ss << entry.totalExperience << ";";
ss << entry.income << ";";
ss << entry.mapExploredRatio << ";";
ss << entry.obeliskVisitedRatio << ";";
ss << entry.townBuiltRatio << ";";
ss << entry.hasGrail << ";";
ss << entry.score << ";";
ss << entry.maxHeroLevel << ";";
ss << entry.numBattlesNeutral << ";";
ss << entry.numBattlesPlayer << ";";
ss << entry.numWinBattlesNeutral << ";";
ss << entry.numWinBattlesPlayer << ";";
ss << entry.numHeroSurrendered << ";";
ss << entry.numHeroEscaped << ";";
ss << entry.movementPointsUsed;
for(auto & resource : resources)
ss << ";" << entry.resources[resource];
for(auto & resource : resources)
ss << ";" << entry.numMines[resource];
for(auto & resource : resources)
ss << ";" << entry.spentResourcesForArmy[resource];
for(auto & resource : resources)
ss << ";" << entry.spentResourcesForBuildings[resource];
for(auto & resource : resources)
ss << ";" << entry.tradeVolume[resource];
ss << "\r\n";
}
return ss.str();
}
std::vector<const CGMine *> Statistic::getMines(const CGameState * gs, const PlayerState * ps)
{
std::vector<const CGMine *> tmp;
/// FIXME: Dirty dirty hack
/// Stats helper need some access to gamestate.
std::vector<const CGObjectInstance *> ownedObjects;
for(const CGObjectInstance * obj : gs->map->objects)
{
if(obj && obj->tempOwner == ps->color)
ownedObjects.push_back(obj);
}
/// This is code from CPlayerSpecificInfoCallback::getMyObjects
/// I'm really need to find out about callback interface design...
for(const auto * object : ownedObjects)
{
//Mines
if ( object->ID == Obj::MINE )
{
const auto * mine = dynamic_cast<const CGMine *>(object);
assert(mine);
tmp.push_back(mine);
}
}
return tmp;
}
//calculates total number of artifacts that belong to given player
int Statistic::getNumberOfArts(const PlayerState * ps)
{
int ret = 0;
for(auto h : ps->heroes)
{
ret += (int)h->artifactsInBackpack.size() + (int)h->artifactsWorn.size();
}
return ret;
}
// get total strength of player army
si64 Statistic::getArmyStrength(const PlayerState * ps, bool withTownGarrison)
{
si64 str = 0;
for(auto h : ps->heroes)
{
if(!h->inTownGarrison || withTownGarrison) //original h3 behavior
str += h->getArmyStrength();
}
return str;
}
// get total experience of all heroes
si64 Statistic::getTotalExperience(const PlayerState * ps)
{
si64 tmp = 0;
for(auto h : ps->heroes)
tmp += h->exp;
return tmp;
}
// get total gold income
int Statistic::getIncome(const CGameState * gs, const PlayerState * ps)
{
int percentIncome = gs->getStartInfo()->getIthPlayersSettings(ps->color).handicap.percentIncome;
int totalIncome = 0;
//Heroes can produce gold as well - skill, specialty or arts
for(const auto & h : ps->heroes)
totalIncome += h->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, BonusSubtypeID(GameResID(GameResID::GOLD)))) * percentIncome / 100;
//Add town income of all towns
for(const auto & t : ps->towns)
totalIncome += t->dailyIncome()[EGameResID::GOLD];
for(const CGMine * mine : getMines(gs, ps))
if(mine->producedResource == EGameResID::GOLD)
totalIncome += mine->getProducedQuantity();
return totalIncome;
}
float Statistic::getMapExploredRatio(const CGameState * gs, PlayerColor player)
{
float visible = 0.0;
float numTiles = 0.0;
for(int layer = 0; layer < (gs->map->twoLevel ? 2 : 1); layer++)
for(int y = 0; y < gs->map->height; ++y)
for(int x = 0; x < gs->map->width; ++x)
{
TerrainTile tile = gs->map->getTile(int3(x, y, layer));
if(tile.blocked && (!tile.visitable))
continue;
if(gs->isVisible(int3(x, y, layer), player))
visible++;
numTiles++;
}
return visible / numTiles;
}
const CGHeroInstance * Statistic::findBestHero(const CGameState * gs, const PlayerColor & color)
{
auto &h = gs->players.at(color).heroes;
if(h.empty())
return nullptr;
//best hero will be that with highest exp
int best = 0;
for(int b=1; b<h.size(); ++b)
{
if(h[b]->exp > h[best]->exp)
{
best = b;
}
}
return h[best];
}
std::vector<std::vector<PlayerColor>> Statistic::getRank(std::vector<std::pair<PlayerColor, si64>> stats)
{
std::sort(stats.begin(), stats.end(), [](const std::pair<PlayerColor, si64> & a, const std::pair<PlayerColor, si64> & b) { return a.second > b.second; });
//put first element
std::vector< std::vector<PlayerColor> > ret;
std::vector<PlayerColor> tmp;
tmp.push_back( stats[0].first );
ret.push_back( tmp );
//the rest of elements
for(int g=1; g<stats.size(); ++g)
{
if(stats[g].second == stats[g-1].second)
{
(ret.end()-1)->push_back( stats[g].first );
}
else
{
//create next occupied rank
std::vector<PlayerColor> tmp;
tmp.push_back(stats[g].first);
ret.push_back(tmp);
}
}
return ret;
}
int Statistic::getObeliskVisited(const CGameState * gs, const TeamID & t)
{
if(gs->map->obelisksVisited.count(t))
return gs->map->obelisksVisited.at(t);
else
return 0;
}
float Statistic::getObeliskVisitedRatio(const CGameState * gs, const TeamID & t)
{
if(!gs->map->obeliskCount)
return 0;
return (float)getObeliskVisited(gs, t) / (float)gs->map->obeliskCount;
}
std::map<EGameResID, int> Statistic::getNumMines(const CGameState * gs, const PlayerState * ps)
{
std::map<EGameResID, int> tmp;
for(auto & res : EGameResID::ALL_RESOURCES())
tmp[res] = 0;
for(const CGMine * mine : getMines(gs, ps))
tmp[mine->producedResource]++;
return tmp;
}
float Statistic::getTownBuiltRatio(const PlayerState * ps)
{
float built = 0.0;
float total = 0.0;
for(const auto & t : ps->towns)
{
built += t->builtBuildings.size();
for(const auto & b : t->town->buildings)
if(!t->forbiddenBuildings.count(b.first))
total += 1;
}
if(total < 1)
return 0;
return built / total;
}
VCMI_LIB_NAMESPACE_END

View File

@ -0,0 +1,156 @@
/*
* GameSTatistics.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 "../GameConstants.h"
#include "../ResourceSet.h"
VCMI_LIB_NAMESPACE_BEGIN
struct PlayerState;
class CGameState;
class CGHeroInstance;
class CGMine;
struct DLL_LINKAGE StatisticDataSetEntry
{
std::string map;
time_t timestamp;
int day;
PlayerColor player;
TeamID team;
bool isHuman;
EPlayerStatus status;
TResources resources;
int numberHeroes;
int numberTowns;
int numberArtifacts;
int numberDwellings;
si64 armyStrength;
si64 totalExperience;
int income;
float mapExploredRatio;
float obeliskVisitedRatio;
float townBuiltRatio;
bool hasGrail;
std::map<EGameResID, int> numMines;
int score;
int maxHeroLevel;
int numBattlesNeutral;
int numBattlesPlayer;
int numWinBattlesNeutral;
int numWinBattlesPlayer;
int numHeroSurrendered;
int numHeroEscaped;
TResources spentResourcesForArmy;
TResources spentResourcesForBuildings;
TResources tradeVolume;
si64 movementPointsUsed;
template <typename Handler> void serialize(Handler &h)
{
h & map;
h & timestamp;
h & day;
h & player;
h & team;
h & isHuman;
h & status;
h & resources;
h & numberHeroes;
h & numberTowns;
h & numberArtifacts;
h & numberDwellings;
h & armyStrength;
h & totalExperience;
h & income;
h & mapExploredRatio;
h & obeliskVisitedRatio;
h & townBuiltRatio;
h & hasGrail;
h & numMines;
h & score;
h & maxHeroLevel;
h & numBattlesNeutral;
h & numBattlesPlayer;
h & numWinBattlesNeutral;
h & numWinBattlesPlayer;
h & numHeroSurrendered;
h & numHeroEscaped;
h & spentResourcesForArmy;
h & spentResourcesForBuildings;
h & tradeVolume;
h & movementPointsUsed;
}
};
class DLL_LINKAGE StatisticDataSet
{
std::vector<StatisticDataSetEntry> data;
public:
void add(StatisticDataSetEntry entry);
static StatisticDataSetEntry createEntry(const PlayerState * ps, const CGameState * gs);
std::string toCsv();
struct PlayerAccumulatedValueStorage // holds some actual values needed for stats
{
int numBattlesNeutral;
int numBattlesPlayer;
int numWinBattlesNeutral;
int numWinBattlesPlayer;
int numHeroSurrendered;
int numHeroEscaped;
TResources spentResourcesForArmy;
TResources spentResourcesForBuildings;
TResources tradeVolume;
si64 movementPointsUsed;
template <typename Handler> void serialize(Handler &h)
{
h & numBattlesNeutral;
h & numBattlesPlayer;
h & numWinBattlesNeutral;
h & numWinBattlesPlayer;
h & numHeroSurrendered;
h & numHeroEscaped;
h & spentResourcesForArmy;
h & spentResourcesForBuildings;
h & tradeVolume;
h & movementPointsUsed;
}
};
std::map<PlayerColor, PlayerAccumulatedValueStorage> accumulatedValues;
template <typename Handler> void serialize(Handler &h)
{
h & data;
h & accumulatedValues;
}
};
class DLL_LINKAGE Statistic
{
static std::vector<const CGMine *> getMines(const CGameState * gs, const PlayerState * ps);
public:
static int getNumberOfArts(const PlayerState * ps);
static si64 getArmyStrength(const PlayerState * ps, bool withTownGarrison = false);
static si64 getTotalExperience(const PlayerState * ps);
static int getIncome(const CGameState * gs, const PlayerState * ps);
static float getMapExploredRatio(const CGameState * gs, PlayerColor player);
static const CGHeroInstance * findBestHero(const CGameState * gs, const PlayerColor & color);
static std::vector<std::vector<PlayerColor>> getRank(std::vector<std::pair<PlayerColor, si64>> stats);
static int getObeliskVisited(const CGameState * gs, const TeamID & t);
static float getObeliskVisitedRatio(const CGameState * gs, const TeamID & t);
static std::map<EGameResID, int> getNumMines(const CGameState * gs, const PlayerState * ps);
static float getTownBuiltRatio(const PlayerState * ps);
};
VCMI_LIB_NAMESPACE_END

111
lib/gameState/HighScore.cpp Normal file
View File

@ -0,0 +1,111 @@
/*
* HighScore.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 "HighScore.h"
#include "../CPlayerState.h"
#include "../constants/StringConstants.h"
#include "CGameState.h"
#include "StartInfo.h"
#include "../mapping/CMapHeader.h"
#include "../mapObjects/CGHeroInstance.h"
#include "../mapObjects/CGTownInstance.h"
VCMI_LIB_NAMESPACE_BEGIN
HighScoreParameter HighScore::prepareHighScores(const CGameState * gs, PlayerColor player, bool victory)
{
const auto * playerState = gs->getPlayerState(player);
HighScoreParameter param;
param.difficulty = gs->getStartInfo()->difficulty;
param.day = gs->getDate();
param.townAmount = gs->howManyTowns(player);
param.usedCheat = gs->getPlayerState(player)->cheated;
param.hasGrail = false;
for(const CGHeroInstance * h : playerState->heroes)
if(h->hasArt(ArtifactID::GRAIL))
param.hasGrail = true;
for(const CGTownInstance * t : playerState->towns)
if(t->builtBuildings.count(BuildingID::GRAIL))
param.hasGrail = true;
param.allEnemiesDefeated = true;
for (PlayerColor otherPlayer(0); otherPlayer < PlayerColor::PLAYER_LIMIT; ++otherPlayer)
{
auto ps = gs->getPlayerState(otherPlayer, false);
if(ps && otherPlayer != player && !ps->checkVanquished())
param.allEnemiesDefeated = false;
}
param.scenarioName = gs->getMapHeader()->name.toString();
param.playerName = gs->getStartInfo()->playerInfos.find(player)->second.name;
return param;
}
HighScoreCalculation::Result HighScoreCalculation::calculate()
{
Result firstResult;
Result summary;
const std::array<double, 5> difficultyMultipliers{0.8, 1.0, 1.3, 1.6, 2.0};
for(auto & param : parameters)
{
double tmp = 200 - (param.day + 10) / (param.townAmount + 5) + (param.allEnemiesDefeated ? 25 : 0) + (param.hasGrail ? 25 : 0);
firstResult = Result{static_cast<int>(tmp), static_cast<int>(tmp * difficultyMultipliers.at(param.difficulty)), param.day, param.usedCheat};
summary.basic += firstResult.basic * 5.0 / parameters.size();
summary.total += firstResult.total * 5.0 / parameters.size();
summary.sumDays += firstResult.sumDays;
summary.cheater |= firstResult.cheater;
}
if(parameters.size() == 1)
return firstResult;
return summary;
}
struct HighScoreCreature
{
CreatureID creature;
int min;
int max;
};
static std::vector<HighScoreCreature> getHighscoreCreaturesList()
{
JsonNode configCreatures(JsonPath::builtin("CONFIG/highscoreCreatures.json"));
std::vector<HighScoreCreature> ret;
for(auto & json : configCreatures["creatures"].Vector())
{
HighScoreCreature entry;
entry.creature = CreatureID::decode(json["creature"].String());
entry.max = json["max"].isNull() ? std::numeric_limits<int>::max() : json["max"].Integer();
entry.min = json["min"].isNull() ? std::numeric_limits<int>::min() : json["min"].Integer();
ret.push_back(entry);
}
return ret;
}
CreatureID HighScoreCalculation::getCreatureForPoints(int points, bool campaign)
{
static const std::vector<HighScoreCreature> creatures = getHighscoreCreaturesList();
int divide = campaign ? 5 : 1;
for(auto & creature : creatures)
if(points / divide <= creature.max && points / divide >= creature.min)
return creature.creature;
throw std::runtime_error("Unable to find creature for score " + std::to_string(points));
}
VCMI_LIB_NAMESPACE_END

View File

@ -9,8 +9,12 @@
*/
#pragma once
#include "../GameConstants.h"
VCMI_LIB_NAMESPACE_BEGIN
class CGameState;
class DLL_LINKAGE HighScoreParameter
{
public:
@ -37,5 +41,28 @@ public:
h & playerName;
}
};
class DLL_LINKAGE HighScore
{
public:
static HighScoreParameter prepareHighScores(const CGameState * gs, PlayerColor player, bool victory);
};
class DLL_LINKAGE HighScoreCalculation
{
public:
struct Result
{
int basic = 0;
int total = 0;
int sumDays = 0;
bool cheater = false;
};
std::vector<HighScoreParameter> parameters;
bool isCampaign = false;
Result calculate();
static CreatureID getCreatureForPoints(int points, bool campaign);
};
VCMI_LIB_NAMESPACE_END

View File

@ -393,7 +393,7 @@ void CBank::battleFinished(const CGHeroInstance *hero, const BattleResult &resul
}
}
void CBank::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
void CBank::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
{
if (answer)
{

View File

@ -40,7 +40,7 @@ public:
bool isCoastVisitable() const override;
void onHeroVisit(const CGHeroInstance * h) const override;
void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
std::vector<Component> getPopupComponents(PlayerColor player) const override;

View File

@ -523,7 +523,7 @@ void CGCreature::battleFinished(const CGHeroInstance *hero, const BattleResult &
}
}
void CGCreature::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
void CGCreature::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
{
auto action = takenAction(hero);
if(!refusedJoining && action >= JOIN_FOR_FREE) //higher means price

View File

@ -48,7 +48,7 @@ public:
void pickRandomObject(vstd::RNG & rand) override;
void newTurn(vstd::RNG & rand) const override;
void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
CreatureID getCreature() const;
//stack formation depends on position,

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
@ -507,7 +516,7 @@ void CGDwelling::battleFinished(const CGHeroInstance *hero, const BattleResult &
}
}
void CGDwelling::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
void CGDwelling::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
{
auto relations = cb->getPlayerRelations(getOwner(), hero->getOwner());
if(stacksCount() > 0 && relations == PlayerRelations::ENEMIES) //guards present

View File

@ -54,7 +54,7 @@ private:
void newTurn(vstd::RNG & rand) const override;
void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override;
void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
std::vector<Component> getPopupComponents(PlayerColor player) const override;
void updateGuards() const;

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

@ -186,7 +186,7 @@ void CGPandoraBox::battleFinished(const CGHeroInstance *hero, const BattleResult
}
}
void CGPandoraBox::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
void CGPandoraBox::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
{
if(answer)
{

View File

@ -26,7 +26,7 @@ public:
void initObj(vstd::RNG & rand) override;
void onHeroVisit(const CGHeroInstance * h) const override;
void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
template <typename Handler> void serialize(Handler &h)
{

View File

@ -385,7 +385,7 @@ void CTownRewardableBuilding::heroLevelUpDone(const CGHeroInstance *hero) const
grantRewardAfterLevelup(cb, configuration.info.at(selectedReward), town, hero);
}
void CTownRewardableBuilding::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
void CTownRewardableBuilding::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
{
if(answer == 0)
return; // player refused

View File

@ -133,7 +133,7 @@ public:
void initObj(vstd::RNG & rand) override;
/// applies player selection of reward
void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
CTownRewardableBuilding(const BuildingID & index, BuildingSubID::EBuildingSubID subId, CGTownInstance * town, vstd::RNG & rand);
CTownRewardableBuilding(IGameCallback *cb);

View File

@ -227,7 +227,7 @@ TResources CGTownInstance::dailyIncome() const
auto playerSettings = cb->gameState()->scenarioOps->getIthPlayersSettings(getOwner());
for(TResources::nziterator it(ret); it.valid(); it++)
// always round up income - we don't want to always produce zero if handicap in use
ret[it->resType] = (ret[it->resType] * playerSettings.handicap.percentIncome + 99) / 100;
ret[it->resType] = vstd::divideAndCeil(ret[it->resType] * playerSettings.handicap.percentIncome, 100);
return ret;
}
@ -281,7 +281,7 @@ void CGTownInstance::setOwner(const PlayerColor & player) const
cb->setOwner(this, player);
}
void CGTownInstance::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
void CGTownInstance::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
{
for (auto building : bonusingBuildings)
building->blockingDialogAnswered(hero, answer);
@ -1229,6 +1229,12 @@ void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler)
handler.serializeIdArray( "possibleSpells", possibleSpells);
handler.serializeIdArray( "obligatorySpells", obligatorySpells);
}
{
auto eventsHandler = handler.enterArray("events");
eventsHandler.syncSize(events, JsonNode::JsonType::DATA_VECTOR);
eventsHandler.serializeStruct(events);
}
}
FactionID CGTownInstance::getFaction() const
@ -1279,7 +1285,7 @@ int GrowthInfo::totalGrowth() const
ret += entry.count;
// always round up income - we don't want buildings to always produce zero if handicap in use
return (ret * handicapPercentage + 99) / 100;
return vstd::divideAndCeil(ret * handicapPercentage, 100);
}
void CGTownInstance::fillUpgradeInfo(UpgradeInfo & info, const CStackInstance &stack) const

View File

@ -65,7 +65,7 @@ public:
std::vector<CGTownBuilding*> bonusingBuildings;
std::vector<SpellID> possibleSpells, obligatorySpells;
std::vector<std::vector<SpellID> > spells; //spells[level] -> vector of spells, first will be available in guild
std::list<CCastleEvent> events;
std::vector<CCastleEvent> events;
std::pair<si32, si32> bonusValue;//var to store town bonuses (rampart = resources from mystic pond, factory = save debts);
//////////////////////////////////////////////////////////////////////////
@ -223,7 +223,7 @@ public:
protected:
void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override;
void serializeJsonOptions(JsonSerializeFormat & handler) override;
void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
private:
FactionID randomizeFaction(vstd::RNG & rand);

View File

@ -660,7 +660,7 @@ const CGCreature * CGSeerHut::getCreatureToKill(bool allowNull) const
return dynamic_cast<const CGCreature *>(o);
}
void CGSeerHut::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
void CGSeerHut::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
{
CRewardableObject::blockingDialogAnswered(hero, answer);
if(answer)
@ -865,7 +865,7 @@ void CGBorderGuard::onHeroVisit(const CGHeroInstance * h) const
}
}
void CGBorderGuard::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
void CGBorderGuard::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
{
if (answer)
cb->removeObject(this, hero->getOwner());

View File

@ -150,7 +150,7 @@ public:
std::vector<Component> getPopupComponents(const CGHeroInstance * hero) const override;
void newTurn(vstd::RNG & rand) const override;
void onHeroVisit(const CGHeroInstance * h) const override;
void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
void getVisitText (MetaString &text, std::vector<Component> &components, bool FirstVisit, const CGHeroInstance * h = nullptr) const override;
virtual void init(vstd::RNG & rand);
@ -229,7 +229,7 @@ public:
void initObj(vstd::RNG & rand) override;
void onHeroVisit(const CGHeroInstance * h) const override;
void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
void getVisitText (MetaString &text, std::vector<Component> &components, bool FirstVisit, const CGHeroInstance * h = nullptr) const override;
void getRolloverText (MetaString &text, bool onHover) const;

View File

@ -181,10 +181,29 @@ void CRewardableObject::heroLevelUpDone(const CGHeroInstance *hero) const
grantRewardAfterLevelup(cb, configuration.info.at(selectedReward), this, hero);
}
void CRewardableObject::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
void CRewardableObject::blockingDialogAnswered(const CGHeroInstance *hero, int32_t 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

@ -63,7 +63,7 @@ public:
void heroLevelUpDone(const CGHeroInstance *hero) const override;
/// applies player selection of reward
void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
void initObj(vstd::RNG & rand) override;

View File

@ -68,7 +68,7 @@ void IObjectInterface::preInit()
void IObjectInterface::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const
{}
void IObjectInterface::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
void IObjectInterface::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
{}
void IObjectInterface::garrisonDialogClosed(const CGHeroInstance *hero) const

View File

@ -61,7 +61,7 @@ public:
//Called when queries created DURING HERO VISIT are resolved
//First parameter is always hero that visited object and triggered the query
virtual void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const;
virtual void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const;
virtual void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const;
virtual void garrisonDialogClosed(const CGHeroInstance *hero) const;
virtual void heroLevelUpDone(const CGHeroInstance *hero) const;

View File

@ -200,7 +200,7 @@ ui32 CGMine::getProducedQuantity() const
{
auto * playerSettings = cb->getPlayerSettings(getOwner());
// always round up income - we don't want mines to always produce zero if handicap in use
return (producedQuantity * playerSettings->handicap.percentIncome + 99) / 100;
return vstd::divideAndCeil(producedQuantity * playerSettings->handicap.percentIncome, 100);
}
void CGMine::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const
@ -215,7 +215,7 @@ void CGMine::battleFinished(const CGHeroInstance *hero, const BattleResult &resu
}
}
void CGMine::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
void CGMine::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
{
if(answer)
cb->startBattleI(hero, this);
@ -348,7 +348,7 @@ void CGResource::battleFinished(const CGHeroInstance *hero, const BattleResult &
collectRes(hero->getOwner());
}
void CGResource::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
void CGResource::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
{
if(answer)
cb->startBattleI(hero, this);
@ -915,7 +915,7 @@ void CGArtifact::battleFinished(const CGHeroInstance *hero, const BattleResult &
pick(hero);
}
void CGArtifact::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
void CGArtifact::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
{
if(answer)
cb->startBattleI(hero, this);

View File

@ -91,7 +91,7 @@ public:
void onHeroVisit(const CGHeroInstance * h) const override;
void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
std::string getObjectName() const override;
std::string getPopupText(PlayerColor player) const override;
@ -132,7 +132,7 @@ public:
void initObj(vstd::RNG & rand) override;
void pickRandomObject(vstd::RNG & rand) override;
void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
std::string getHoverText(PlayerColor player) const override;
void collectRes(const PlayerColor & player) const;
@ -163,7 +163,7 @@ private:
void onHeroVisit(const CGHeroInstance * h) const override;
void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
void flagMine(const PlayerColor & player) const;
void newTurn(vstd::RNG & rand) const override;

View File

@ -561,7 +561,7 @@ struct DLL_LINKAGE UpdateMapEvents : public CPackForClient
struct DLL_LINKAGE UpdateCastleEvents : public CPackForClient
{
ObjectInstanceID town;
std::list<CCastleEvent> events;
std::vector<CCastleEvent> events;
void applyGs(CGameState * gs) const;
void visitTyped(ICPackVisitor & visitor) override;

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

@ -463,7 +463,8 @@ void ZoneConnection::serializeJson(JsonSerializeFormat & handler)
"guarded",
"fictive",
"repulsive",
"wide"
"wide",
"forcePortal"
};
static const std::vector<std::string> roadOptions =

View File

@ -75,7 +75,8 @@ enum class EConnectionType
GUARDED = 0, //default
FICTIVE,
REPULSIVE,
WIDE
WIDE,
FORCE_PORTAL
};
enum class ERoadOption

View File

@ -80,12 +80,21 @@ void CZonePlacer::findPathsBetweenZones()
for (auto & connection : connectedZoneIds)
{
if (connection.getConnectionType() == rmg::EConnectionType::REPULSIVE)
switch (connection.getConnectionType())
{
//Do not consider virtual connections for graph distance
continue;
case rmg::EConnectionType::REPULSIVE:
case rmg::EConnectionType::FORCE_PORTAL:
continue;
}
auto neighbor = connection.getOtherZoneId(current);
if (current == neighbor)
{
//Do not consider self-connections
continue;
}
if (!visited[neighbor])
{
visited[neighbor] = true;
@ -552,8 +561,16 @@ void CZonePlacer::attractConnectedZones(TZoneMap & zones, TForceVector & forces,
for (const auto & connection : zone.second->getConnections())
{
if (connection.getConnectionType() == rmg::EConnectionType::REPULSIVE)
switch (connection.getConnectionType())
{
//Do not consider virtual connections for graph distance
case rmg::EConnectionType::REPULSIVE:
case rmg::EConnectionType::FORCE_PORTAL:
continue;
}
if (connection.getZoneA() == connection.getZoneB())
{
//Do not consider self-connections
continue;
}
@ -710,11 +727,19 @@ void CZonePlacer::moveOneZone(TZoneMap& zones, TForceVector& totalForces, TDista
std::set<TRmgTemplateZoneId> connectedZones;
for (const auto& connection : firstZone->getConnections())
{
//FIXME: Should we also exclude fictive connections?
if (connection.getConnectionType() != rmg::EConnectionType::REPULSIVE)
switch (connection.getConnectionType())
{
connectedZones.insert(connection.getOtherZoneId(firstZone->getId()));
//Do not consider virtual connections for graph distance
case rmg::EConnectionType::REPULSIVE:
case rmg::EConnectionType::FORCE_PORTAL:
continue;
}
if (connection.getZoneA() == connection.getZoneB())
{
//Do not consider self-connections
continue;
}
connectedZones.insert(connection.getOtherZoneId(firstZone->getId()));
}
auto level = firstZone->getCenter().z;

View File

@ -55,6 +55,17 @@ void ConnectionsPlacer::process()
{
for (auto& c : dConnections)
{
if (c.getZoneA() == c.getZoneB())
{
// Zone can always be connected to itself, but only by monolith pair
RecursiveLock lock(externalAccessMutex);
if (!vstd::contains(dCompleted, c))
{
placeMonolithConnection(c);
continue;
}
}
auto otherZone = map.getZones().at(c.getZoneB());
auto* cp = otherZone->getModificator<ConnectionsPlacer>();
@ -74,6 +85,11 @@ void ConnectionsPlacer::process()
}
};
diningPhilosophers([this](const rmg::ZoneConnection& c)
{
forcePortalConnection(c);
});
diningPhilosophers([this](const rmg::ZoneConnection& c)
{
selfSideDirectConnection(c);
@ -115,6 +131,15 @@ void ConnectionsPlacer::otherSideConnection(const rmg::ZoneConnection & connecti
dCompleted.push_back(connection);
}
void ConnectionsPlacer::forcePortalConnection(const rmg::ZoneConnection & connection)
{
// This should always succeed
if (connection.getConnectionType() == rmg::EConnectionType::FORCE_PORTAL)
{
placeMonolithConnection(connection);
}
}
void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & connection)
{
bool success = false;
@ -410,23 +435,30 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c
//4. place monoliths/portals
if(!success)
{
auto factory = VLC->objtypeh->getHandlerFor(Obj::MONOLITH_TWO_WAY, generator.getNextMonlithIndex());
auto * teleport1 = factory->create(map.mapInstance->cb, nullptr);
auto * teleport2 = factory->create(map.mapInstance->cb, nullptr);
RequiredObjectInfo obj1(teleport1, connection.getGuardStrength(), allowRoad);
RequiredObjectInfo obj2(teleport2, connection.getGuardStrength(), allowRoad);
zone.getModificator<ObjectManager>()->addRequiredObject(obj1);
otherZone->getModificator<ObjectManager>()->addRequiredObject(obj2);
assert(otherZone->getModificator<ConnectionsPlacer>());
otherZone->getModificator<ConnectionsPlacer>()->otherSideConnection(connection);
success = true;
placeMonolithConnection(connection);
}
}
void ConnectionsPlacer::placeMonolithConnection(const rmg::ZoneConnection & connection)
{
auto otherZoneId = (connection.getZoneA() == zone.getId() ? connection.getZoneB() : connection.getZoneA());
auto & otherZone = map.getZones().at(otherZoneId);
bool allowRoad = shouldGenerateRoad(connection);
auto factory = VLC->objtypeh->getHandlerFor(Obj::MONOLITH_TWO_WAY, generator.getNextMonlithIndex());
auto * teleport1 = factory->create(map.mapInstance->cb, nullptr);
auto * teleport2 = factory->create(map.mapInstance->cb, nullptr);
RequiredObjectInfo obj1(teleport1, connection.getGuardStrength(), allowRoad);
RequiredObjectInfo obj2(teleport2, connection.getGuardStrength(), allowRoad);
zone.getModificator<ObjectManager>()->addRequiredObject(obj1);
otherZone->getModificator<ObjectManager>()->addRequiredObject(obj2);
dCompleted.push_back(connection);
if(success)
dCompleted.push_back(connection);
assert(otherZone->getModificator<ConnectionsPlacer>());
otherZone->getModificator<ConnectionsPlacer>()->otherSideConnection(connection);
}
void ConnectionsPlacer::collectNeighbourZones()

View File

@ -23,7 +23,8 @@ public:
void init() override;
void addConnection(const rmg::ZoneConnection& connection);
void placeMonolithConnection(const rmg::ZoneConnection& connection);
void forcePortalConnection(const rmg::ZoneConnection & connection);
void selfSideDirectConnection(const rmg::ZoneConnection & connection);
void selfSideIndirectConnection(const rmg::ZoneConnection & connection);
void otherSideConnection(const rmg::ZoneConnection & connection);

View File

@ -59,6 +59,7 @@ enum class ESerializationVersion : int32_t
REMOVE_LIB_RNG, // 849 - removed random number generators from library classes
HIGHSCORE_PARAMETERS, // 850 - saves parameter for campaign
PLAYER_HANDICAP, // 851 - player handicap selection at game start
STATISTICS, // 852 - removed random number generators from library classes
CURRENT = PLAYER_HANDICAP
CURRENT = STATISTICS
};

View File

@ -29,6 +29,9 @@ set(editor_SRCS
validator.cpp
inspector/inspector.cpp
inspector/townbuildingswidget.cpp
inspector/towneventdialog.cpp
inspector/towneventswidget.cpp
inspector/townspellswidget.cpp
inspector/armywidget.cpp
inspector/messagewidget.cpp
inspector/rewardswidget.cpp
@ -70,6 +73,9 @@ set(editor_HEADERS
validator.h
inspector/inspector.h
inspector/townbuildingswidget.h
inspector/towneventdialog.h
inspector/towneventswidget.h
inspector/townspellswidget.h
inspector/armywidget.h
inspector/messagewidget.h
inspector/rewardswidget.h
@ -79,6 +85,7 @@ set(editor_HEADERS
inspector/PickObjectDelegate.h
inspector/portraitwidget.h
resourceExtractor/ResourceConverter.h
mapeditorroles.h
)
set(editor_FORMS
@ -98,6 +105,9 @@ set(editor_FORMS
playerparams.ui
validator.ui
inspector/townbuildingswidget.ui
inspector/towneventdialog.ui
inspector/towneventswidget.ui
inspector/townspellswidget.ui
inspector/armywidget.ui
inspector/messagewidget.ui
inspector/rewardswidget.ui

View File

@ -21,6 +21,8 @@
#include "../lib/constants/StringConstants.h"
#include "townbuildingswidget.h"
#include "towneventswidget.h"
#include "townspellswidget.h"
#include "armywidget.h"
#include "messagewidget.h"
#include "rewardswidget.h"
@ -342,6 +344,8 @@ void Inspector::updateProperties(CGTownInstance * o)
auto * delegate = new TownBuildingsDelegate(*o);
addProperty("Buildings", PropertyEditorPlaceholder(), delegate, false);
addProperty("Spells", PropertyEditorPlaceholder(), new TownSpellsDelegate(*o), false);
addProperty("Events", PropertyEditorPlaceholder(), new TownEventsDelegate(*o, controller), false);
}
void Inspector::updateProperties(CGArtifact * o)

View File

@ -10,6 +10,7 @@
#include "StdInc.h"
#include "townbuildingswidget.h"
#include "ui_townbuildingswidget.h"
#include "mapeditorroles.h"
#include "../lib/entities/building/CBuilding.h"
#include "../lib/entities/faction/CTownHandler.h"
#include "../lib/texts/CGeneralTextHandler.h"
@ -68,6 +69,56 @@ std::string defaultBuildingIdConversion(BuildingID bId)
}
}
QStandardItem * getBuildingParentFromTreeModel(const CBuilding * building, const QStandardItemModel & model)
{
QStandardItem * parent = nullptr;
std::vector<QModelIndex> stack(1);
do
{
auto pindex = stack.back();
stack.pop_back();
auto rowCount = model.rowCount(pindex);
for (int i = 0; i < rowCount; ++i)
{
QModelIndex index = model.index(i, 0, pindex);
if (building->upgrade.getNum() == model.itemFromIndex(index)->data(MapEditorRoles::BuildingIDRole).toInt())
{
parent = model.itemFromIndex(index);
break;
}
if (model.hasChildren(index))
stack.push_back(index);
}
} while(!parent && !stack.empty());
return parent;
}
QVariantList getBuildingVariantsFromModel(const QStandardItemModel & model, int modelColumn, Qt::CheckState checkState)
{
QVariantList result;
std::vector<QModelIndex> stack(1);
do
{
auto pindex = stack.back();
stack.pop_back();
auto rowCount = model.rowCount(pindex);
for (int i = 0; i < rowCount; ++i)
{
QModelIndex index = model.index(i, modelColumn, pindex);
auto * item = model.itemFromIndex(index);
if(item && item->checkState() == checkState)
result.push_back(item->data(MapEditorRoles::BuildingIDRole));
index = model.index(i, 0, pindex);
if (model.hasChildren(index))
stack.push_back(index);
}
} while(!stack.empty());
return result;
}
TownBuildingsWidget::TownBuildingsWidget(CGTownInstance & t, QWidget *parent) :
town(t),
QDialog(parent),
@ -76,8 +127,8 @@ TownBuildingsWidget::TownBuildingsWidget(CGTownInstance & t, QWidget *parent) :
ui->setupUi(this);
ui->treeView->setModel(&model);
//ui->treeView->setColumnCount(3);
model.setHorizontalHeaderLabels(QStringList() << QStringLiteral("Type") << QStringLiteral("Enabled") << QStringLiteral("Built"));
model.setHorizontalHeaderLabels(QStringList() << tr("Type") << tr("Enabled") << tr("Built"));
connect(&model, &QStandardItemModel::itemChanged, this, &TownBuildingsWidget::onItemChanged);
//setAttribute(Qt::WA_DeleteOnClose);
}
@ -96,7 +147,7 @@ QStandardItem * TownBuildingsWidget::addBuilding(const CTown & ctown, int bId, s
return nullptr;
}
QString name = tr(building->getNameTranslated().c_str());
QString name = QString::fromStdString(building->getNameTranslated());
if(name.isEmpty())
name = QString::fromStdString(defaultBuildingIdConversion(buildingId));
@ -104,17 +155,17 @@ QStandardItem * TownBuildingsWidget::addBuilding(const CTown & ctown, int bId, s
QList<QStandardItem *> checks;
checks << new QStandardItem(name);
checks.back()->setData(bId, Qt::UserRole);
checks.back()->setData(bId, MapEditorRoles::BuildingIDRole);
checks << new QStandardItem;
checks.back()->setCheckable(true);
checks.back()->setCheckState(town.forbiddenBuildings.count(buildingId) ? Qt::Unchecked : Qt::Checked);
checks.back()->setData(bId, Qt::UserRole);
checks.back()->setData(bId, MapEditorRoles::BuildingIDRole);
checks << new QStandardItem;
checks.back()->setCheckable(true);
checks.back()->setCheckState(town.builtBuildings.count(buildingId) ? Qt::Checked : Qt::Unchecked);
checks.back()->setData(bId, Qt::UserRole);
checks.back()->setData(bId, MapEditorRoles::BuildingIDRole);
if(building->getBase() == buildingId)
{
@ -122,25 +173,7 @@ QStandardItem * TownBuildingsWidget::addBuilding(const CTown & ctown, int bId, s
}
else
{
QStandardItem * parent = nullptr;
std::vector<QModelIndex> stack;
stack.push_back(QModelIndex());
while(!parent && !stack.empty())
{
auto pindex = stack.back();
stack.pop_back();
for(int i = 0; i < model.rowCount(pindex); ++i)
{
QModelIndex index = model.index(i, 0, pindex);
if(building->upgrade.getNum() == model.itemFromIndex(index)->data(Qt::UserRole).toInt())
{
parent = model.itemFromIndex(index);
break;
}
if(model.hasChildren(index))
stack.push_back(index);
}
}
QStandardItem * parent = getBuildingParentFromTreeModel(building, model);
if(!parent)
parent = addBuilding(ctown, building->upgrade.getNum(), remaining);
@ -172,36 +205,23 @@ void TownBuildingsWidget::addBuildings(const CTown & ctown)
std::set<BuildingID> TownBuildingsWidget::getBuildingsFromModel(int modelColumn, Qt::CheckState checkState)
{
auto buildingVariants = getBuildingVariantsFromModel(model, modelColumn, checkState);
std::set<BuildingID> result;
std::vector<QModelIndex> stack;
stack.push_back(QModelIndex());
while(!stack.empty())
for (const auto & buildingId : buildingVariants)
{
auto pindex = stack.back();
stack.pop_back();
for(int i = 0; i < model.rowCount(pindex); ++i)
{
QModelIndex index = model.index(i, modelColumn, pindex);
if(auto * item = model.itemFromIndex(index))
if(item->checkState() == checkState)
result.emplace(item->data(Qt::UserRole).toInt());
index = model.index(i, 0, pindex); //children are linked to first column of the model
if(model.hasChildren(index))
stack.push_back(index);
}
result.insert(buildingId.toInt());
}
return result;
}
std::set<BuildingID> TownBuildingsWidget::getForbiddenBuildings()
{
return getBuildingsFromModel(1, Qt::Unchecked);
return getBuildingsFromModel(Column::ENABLED, Qt::Unchecked);
}
std::set<BuildingID> TownBuildingsWidget::getBuiltBuildings()
{
return getBuildingsFromModel(2, Qt::Checked);
return getBuildingsFromModel(Column::BUILT, Qt::Checked);
}
void TownBuildingsWidget::on_treeView_expanded(const QModelIndex &index)
@ -214,6 +234,87 @@ void TownBuildingsWidget::on_treeView_collapsed(const QModelIndex &index)
ui->treeView->resizeColumnToContents(0);
}
void TownBuildingsWidget::on_buildAll_clicked()
{
setAllRowsColumnCheckState(Column::BUILT, Qt::Checked);
}
void TownBuildingsWidget::on_demolishAll_clicked()
{
setAllRowsColumnCheckState(Column::BUILT, Qt::Unchecked);
}
void TownBuildingsWidget::on_enableAll_clicked()
{
setAllRowsColumnCheckState(Column::ENABLED, Qt::Checked);
}
void TownBuildingsWidget::on_disableAll_clicked()
{
setAllRowsColumnCheckState(Column::ENABLED, Qt::Unchecked);
}
void TownBuildingsWidget::setRowColumnCheckState(const QStandardItem * item, Column column, Qt::CheckState checkState) {
auto sibling = item->model()->sibling(item->row(), column, item->index());
model.itemFromIndex(sibling)->setCheckState(checkState);
}
void TownBuildingsWidget::setAllRowsColumnCheckState(Column column, Qt::CheckState checkState)
{
std::vector<QModelIndex> stack(1);
do
{
auto parentIndex = stack.back();
stack.pop_back();
auto rowCount = model.rowCount(parentIndex);
for (int i = 0; i < rowCount; ++i)
{
QModelIndex index = model.index(i, column, parentIndex);
if (auto* item = model.itemFromIndex(index))
item->setCheckState(checkState);
index = model.index(i, 0, parentIndex);
if (model.hasChildren(index))
stack.push_back(index);
}
} while(!stack.empty());
}
void TownBuildingsWidget::onItemChanged(const QStandardItem * item) {
disconnect(&model, &QStandardItemModel::itemChanged, this, &TownBuildingsWidget::onItemChanged);
auto rowFirstColumnIndex = item->model()->sibling(item->row(), Column::TYPE, item->index());
QStandardItem * nextRow = model.itemFromIndex(rowFirstColumnIndex);
if (item->checkState() == Qt::Checked) {
while (nextRow) {
setRowColumnCheckState(nextRow, Column(item->column()), Qt::Checked);
if (item->column() == Column::BUILT) {
setRowColumnCheckState(nextRow, Column::ENABLED, Qt::Checked);
}
nextRow = nextRow->parent();
}
}
else if (item->checkState() == Qt::Unchecked) {
std::vector<QStandardItem*> stack;
stack.push_back(nextRow);
do
{
nextRow = stack.back();
stack.pop_back();
setRowColumnCheckState(nextRow, Column(item->column()), Qt::Unchecked);
if (item->column() == Column::ENABLED) {
setRowColumnCheckState(nextRow, Column::BUILT, Qt::Unchecked);
}
if (nextRow->hasChildren()) {
for (int i = 0; i < nextRow->rowCount(); ++i) {
stack.push_back(nextRow->child(i, Column::TYPE));
}
}
} while(!stack.empty());
}
connect(&model, &QStandardItemModel::itemChanged, this, &TownBuildingsWidget::onItemChanged);
}
TownBuildingsDelegate::TownBuildingsDelegate(CGTownInstance & t): town(t), QStyledItemDelegate()
{

View File

@ -19,6 +19,10 @@ class TownBuildingsWidget;
std::string defaultBuildingIdConversion(BuildingID bId);
QStandardItem * getBuildingParentFromTreeModel(const CBuilding * building, const QStandardItemModel & model);
QVariantList getBuildingVariantsFromModel(const QStandardItemModel & model, int modelColumn, Qt::CheckState checkState);
class TownBuildingsWidget : public QDialog
{
Q_OBJECT
@ -26,9 +30,13 @@ class TownBuildingsWidget : public QDialog
QStandardItem * addBuilding(const CTown & ctown, int bId, std::set<si32> & remaining);
public:
enum Column
{
TYPE, ENABLED, BUILT
};
explicit TownBuildingsWidget(CGTownInstance &, QWidget *parent = nullptr);
~TownBuildingsWidget();
void addBuildings(const CTown & ctown);
std::set<BuildingID> getForbiddenBuildings();
std::set<BuildingID> getBuiltBuildings();
@ -38,9 +46,21 @@ private slots:
void on_treeView_collapsed(const QModelIndex &index);
void on_buildAll_clicked();
void on_demolishAll_clicked();
void on_enableAll_clicked();
void on_disableAll_clicked();
void onItemChanged(const QStandardItem * item);
private:
std::set<BuildingID> getBuildingsFromModel(int modelColumn, Qt::CheckState checkState);
void setRowColumnCheckState(const QStandardItem * item, Column column, Qt::CheckState checkState);
void setAllRowsColumnCheckState(Column column, Qt::CheckState checkState);
Ui::TownBuildingsWidget *ui;
CGTownInstance & town;
mutable QStandardItemModel model;

View File

@ -9,7 +9,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>480</width>
<width>580</width>
<height>280</height>
</rect>
</property>
@ -21,7 +21,7 @@
</property>
<property name="minimumSize">
<size>
<width>480</width>
<width>580</width>
<height>280</height>
</size>
</property>
@ -45,6 +45,38 @@
</attribute>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="buildAll">
<property name="text">
<string>Build all</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="demolishAll">
<property name="text">
<string>Demolish all</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="enableAll">
<property name="text">
<string>Enable all</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="disableAll">
<property name="text">
<string>Disable all</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>

View File

@ -0,0 +1,289 @@
/*
* towneventdialog.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 "townbuildingswidget.h"
#include "towneventdialog.h"
#include "ui_towneventdialog.h"
#include "mapeditorroles.h"
#include "../../lib/entities/building/CBuilding.h"
#include "../../lib/entities/faction/CTownHandler.h"
#include "../../lib/constants/NumericConstants.h"
#include "../../lib/constants/StringConstants.h"
static const int FIRST_DAY_FOR_EVENT = 1;
static const int LAST_DAY_FOR_EVENT = 999;
static const int MAXIMUM_EVENT_REPEAT_AFTER = 999;
static const int MAXIMUM_GOLD_CHANGE = 999999;
static const int MAXIMUM_RESOURCE_CHANGE = 999;
static const int GOLD_STEP = 100;
static const int RESOURCE_STEP = 1;
static const int MAXIMUM_CREATURES_CHANGE = 999999;
TownEventDialog::TownEventDialog(CGTownInstance & t, QListWidgetItem * item, QWidget * parent) :
QDialog(parent),
ui(new Ui::TownEventDialog),
town(t),
townEventListItem(item)
{
ui->setupUi(this);
ui->buildingsTree->setModel(&buildingsModel);
params = townEventListItem->data(MapEditorRoles::TownEventRole).toMap();
ui->eventFirstOccurrence->setMinimum(FIRST_DAY_FOR_EVENT);
ui->eventFirstOccurrence->setMaximum(LAST_DAY_FOR_EVENT);
ui->eventRepeatAfter->setMaximum(MAXIMUM_EVENT_REPEAT_AFTER);
ui->eventNameText->setText(params.value("name").toString());
ui->eventMessageText->setPlainText(params.value("message").toString());
ui->eventAffectsCpu->setChecked(params.value("computerAffected").toBool());
ui->eventAffectsHuman->setChecked(params.value("humanAffected").toBool());
ui->eventFirstOccurrence->setValue(params.value("firstOccurrence").toInt()+1);
ui->eventRepeatAfter->setValue(params.value("nextOccurrence").toInt());
initPlayers();
initResources();
initBuildings();
initCreatures();
}
TownEventDialog::~TownEventDialog()
{
delete ui;
}
void TownEventDialog::initPlayers()
{
for (int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i)
{
bool isAffected = (1 << i) & params.value("players").toInt();
auto * item = new QListWidgetItem(QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[i]));
item->setData(MapEditorRoles::PlayerIDRole, QVariant::fromValue(i));
item->setCheckState(isAffected ? Qt::Checked : Qt::Unchecked);
ui->playersAffected->addItem(item);
}
}
void TownEventDialog::initResources()
{
ui->resourcesTable->setRowCount(GameConstants::RESOURCE_QUANTITY);
auto resourcesMap = params.value("resources").toMap();
for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i)
{
auto name = QString::fromStdString(GameConstants::RESOURCE_NAMES[i]);
auto * item = new QTableWidgetItem();
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
item->setText(name);
ui->resourcesTable->setItem(i, 0, item);
int val = resourcesMap.value(name).toInt();
auto * edit = new QSpinBox(ui->resourcesTable);
edit->setMaximum(i == GameResID::GOLD ? MAXIMUM_GOLD_CHANGE : MAXIMUM_RESOURCE_CHANGE);
edit->setMinimum(i == GameResID::GOLD ? -MAXIMUM_GOLD_CHANGE : -MAXIMUM_RESOURCE_CHANGE);
edit->setSingleStep(i == GameResID::GOLD ? GOLD_STEP : RESOURCE_STEP);
edit->setValue(val);
ui->resourcesTable->setCellWidget(i, 1, edit);
}
}
void TownEventDialog::initBuildings()
{
auto * ctown = town.town;
if (!ctown)
ctown = VLC->townh->randomTown;
if (!ctown)
throw std::runtime_error("No Town defined for type selected");
auto allBuildings = ctown->getAllBuildings();
while (!allBuildings.empty())
{
addBuilding(*ctown, *allBuildings.begin(), allBuildings);
}
ui->buildingsTree->resizeColumnToContents(0);
connect(&buildingsModel, &QStandardItemModel::itemChanged, this, &TownEventDialog::onItemChanged);
}
QStandardItem * TownEventDialog::addBuilding(const CTown& ctown, BuildingID buildingId, std::set<si32>& remaining)
{
auto bId = buildingId.num;
const CBuilding * building = ctown.buildings.at(buildingId);
QString name = QString::fromStdString(building->getNameTranslated());
if (name.isEmpty())
name = QString::fromStdString(defaultBuildingIdConversion(buildingId));
QList<QStandardItem *> checks;
checks << new QStandardItem(name);
checks.back()->setData(bId, MapEditorRoles::BuildingIDRole);
checks << new QStandardItem;
checks.back()->setCheckable(true);
checks.back()->setCheckState(params["buildings"].toList().contains(bId) ? Qt::Checked : Qt::Unchecked);
checks.back()->setData(bId, MapEditorRoles::BuildingIDRole);
if (building->getBase() == buildingId)
{
buildingsModel.appendRow(checks);
}
else
{
QStandardItem * parent = getBuildingParentFromTreeModel(building, buildingsModel);
if (!parent)
parent = addBuilding(ctown, building->upgrade.getNum(), remaining);
parent->appendRow(checks);
}
remaining.erase(bId);
return checks.front();
}
void TownEventDialog::initCreatures()
{
auto creatures = params.value("creatures").toList();
auto * ctown = town.town;
for (int i = 0; i < GameConstants::CREATURES_PER_TOWN; ++i)
{
QString creatureNames;
if (!ctown)
{
creatureNames.append(tr("Creature level %1 / Creature level %1 Upgrade").arg(i + 1));
}
else
{
auto creaturesOnLevel = ctown->creatures.at(i);
for (auto& creature : creaturesOnLevel)
{
auto cre = VLC->creatures()->getById(creature);
auto creatureName = QString::fromStdString(cre->getNameSingularTranslated());
creatureNames.append(creatureNames.isEmpty() ? creatureName : " / " + creatureName);
}
}
auto * item = new QTableWidgetItem();
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
item->setText(creatureNames);
ui->creaturesTable->setItem(i, 0, item);
auto creatureNumber = creatures.size() > i ? creatures.at(i).toInt() : 0;
auto * edit = new QSpinBox(ui->creaturesTable);
edit->setValue(creatureNumber);
edit->setMaximum(MAXIMUM_CREATURES_CHANGE);
ui->creaturesTable->setCellWidget(i, 1, edit);
}
ui->creaturesTable->resizeColumnToContents(0);
}
void TownEventDialog::on_TownEventDialog_finished(int result)
{
QVariantMap descriptor;
descriptor["name"] = ui->eventNameText->text();
descriptor["message"] = ui->eventMessageText->toPlainText();
descriptor["humanAffected"] = QVariant::fromValue(ui->eventAffectsHuman->isChecked());
descriptor["computerAffected"] = QVariant::fromValue(ui->eventAffectsCpu->isChecked());
descriptor["firstOccurrence"] = QVariant::fromValue(ui->eventFirstOccurrence->value()-1);
descriptor["nextOccurrence"] = QVariant::fromValue(ui->eventRepeatAfter->value());
descriptor["players"] = playersToVariant();
descriptor["resources"] = resourcesToVariant();
descriptor["buildings"] = buildingsToVariant();
descriptor["creatures"] = creaturesToVariant();
townEventListItem->setData(MapEditorRoles::TownEventRole, descriptor);
auto itemText = tr("Day %1 - %2").arg(ui->eventFirstOccurrence->value(), 3).arg(ui->eventNameText->text());
townEventListItem->setText(itemText);
}
QVariant TownEventDialog::playersToVariant()
{
int players = 0;
for (int i = 0; i < ui->playersAffected->count(); ++i)
{
auto * item = ui->playersAffected->item(i);
if (item->checkState() == Qt::Checked)
players |= 1 << i;
}
return QVariant::fromValue(players);
}
QVariantMap TownEventDialog::resourcesToVariant()
{
auto res = params.value("resources").toMap();
for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i)
{
auto * itemType = ui->resourcesTable->item(i, 0);
auto * itemQty = static_cast<QSpinBox *> (ui->resourcesTable->cellWidget(i, 1));
res[itemType->text()] = QVariant::fromValue(itemQty->value());
}
return res;
}
QVariantList TownEventDialog::buildingsToVariant()
{
return getBuildingVariantsFromModel(buildingsModel, 1, Qt::Checked);
}
QVariantList TownEventDialog::creaturesToVariant()
{
QVariantList creaturesList;
for (int i = 0; i < GameConstants::CREATURES_PER_TOWN; ++i)
{
auto * item = static_cast<QSpinBox *>(ui->creaturesTable->cellWidget(i, 1));
creaturesList.push_back(item->value());
}
return creaturesList;
}
void TownEventDialog::on_okButton_clicked()
{
close();
}
void TownEventDialog::setRowColumnCheckState(const QStandardItem * item, int column, Qt::CheckState checkState) {
auto sibling = item->model()->sibling(item->row(), column, item->index());
buildingsModel.itemFromIndex(sibling)->setCheckState(checkState);
}
void TownEventDialog::onItemChanged(const QStandardItem * item)
{
disconnect(&buildingsModel, &QStandardItemModel::itemChanged, this, &TownEventDialog::onItemChanged);
auto rowFirstColumnIndex = item->model()->sibling(item->row(), 0, item->index());
QStandardItem * nextRow = buildingsModel.itemFromIndex(rowFirstColumnIndex);
if (item->checkState() == Qt::Checked) {
while (nextRow) {
setRowColumnCheckState(nextRow,item->column(), Qt::Checked);
nextRow = nextRow->parent();
}
}
else if (item->checkState() == Qt::Unchecked) {
std::vector<QStandardItem *> stack;
stack.push_back(nextRow);
do
{
nextRow = stack.back();
stack.pop_back();
setRowColumnCheckState(nextRow, item->column(), Qt::Unchecked);
if (nextRow->hasChildren()) {
for (int i = 0; i < nextRow->rowCount(); ++i) {
stack.push_back(nextRow->child(i, 0));
}
}
} while(!stack.empty());
}
connect(&buildingsModel, &QStandardItemModel::itemChanged, this, &TownEventDialog::onItemChanged);
}

View File

@ -0,0 +1,53 @@
/*
* towneventdialog.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 "../StdInc.h"
#include <QDialog>
#include "../lib/mapObjects/CGTownInstance.h"
namespace Ui {
class TownEventDialog;
}
class TownEventDialog : public QDialog
{
Q_OBJECT
public:
explicit TownEventDialog(CGTownInstance & town, QListWidgetItem * item, QWidget * parent);
~TownEventDialog();
private slots:
void onItemChanged(const QStandardItem * item);
void on_TownEventDialog_finished(int result);
void on_okButton_clicked();
void setRowColumnCheckState(const QStandardItem * item, int column, Qt::CheckState checkState);
private:
void initPlayers();
void initResources();
void initBuildings();
void initCreatures();
QVariant playersToVariant();
QVariantMap resourcesToVariant();
QVariantList buildingsToVariant();
QVariantList creaturesToVariant();
QStandardItem * addBuilding(const CTown & ctown, BuildingID bId, std::set<si32> & remaining);
Ui::TownEventDialog * ui;
CGTownInstance & town;
QListWidgetItem * townEventListItem;
QMap<QString, QVariant> params;
QStandardItemModel buildingsModel;
};

View File

@ -0,0 +1,266 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TownEventDialog</class>
<widget class="QDialog" name="TownEventDialog">
<property name="windowModality">
<enum>Qt::ApplicationModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>693</width>
<height>525</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Town event</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_16">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="generalTab">
<attribute name="title">
<string>General</string>
</attribute>
<widget class="QWidget" name="verticalLayoutWidget">
<property name="geometry">
<rect>
<x>9</x>
<y>9</y>
<width>511</width>
<height>351</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0">
<item>
<widget class="QLineEdit" name="eventNameText">
<property name="placeholderText">
<string>Event name</string>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="eventMessageText">
<property name="placeholderText">
<string>Type event message text</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="horizontalLayoutWidget">
<property name="geometry">
<rect>
<x>10</x>
<y>370</y>
<width>511</width>
<height>61</height>
</rect>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="eventFirstOccurrenceText">
<property name="text">
<string>Day of first occurrence</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="eventFirstOccurrence"/>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="eventRepeatAfterText">
<property name="text">
<string>Repeat after (0 = no repeat)</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="eventRepeatAfter"/>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="verticalLayoutWidget_4">
<property name="geometry">
<rect>
<x>529</x>
<y>9</y>
<width>141</width>
<height>421</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QLabel" name="playersAffectedText">
<property name="text">
<string>Affected players</string>
</property>
</widget>
</item>
<item>
<widget class="QListWidget" name="playersAffected">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="eventAffectsHuman">
<property name="text">
<string>affects human</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QCheckBox" name="eventAffectsCpu">
<property name="text">
<string>affects AI</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<widget class="QWidget" name="resourcesTab">
<attribute name="title">
<string>Resources</string>
</attribute>
<widget class="QTableWidget" name="resourcesTable">
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<width>661</width>
<height>421</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="columnCount">
<number>2</number>
</property>
<attribute name="horizontalHeaderVisible">
<bool>false</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<column/>
<column/>
</widget>
</widget>
<widget class="QWidget" name="buildingsTab">
<attribute name="title">
<string>Buildings</string>
</attribute>
<widget class="QTreeView" name="buildingsTree">
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<width>661</width>
<height>421</height>
</rect>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
</widget>
</widget>
<widget class="QWidget" name="creaturesTab">
<attribute name="title">
<string>Creatures</string>
</attribute>
<widget class="QTableWidget" name="creaturesTable">
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<width>661</width>
<height>421</height>
</rect>
</property>
<property name="rowCount">
<number>7</number>
</property>
<property name="columnCount">
<number>2</number>
</property>
<attribute name="horizontalHeaderVisible">
<bool>false</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<row/>
<row/>
<row/>
<row/>
<row/>
<row/>
<row/>
<column/>
<column/>
</widget>
</widget>
</widget>
</item>
<item>
<widget class="QPushButton" name="okButton">
<property name="text">
<string>OK</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,177 @@
/*
* towneventswidget.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 "towneventswidget.h"
#include "ui_towneventswidget.h"
#include "towneventdialog.h"
#include "mapeditorroles.h"
#include "mapsettings/eventsettings.h"
#include "../../lib/constants/NumericConstants.h"
#include "../../lib/constants/StringConstants.h"
TownEventsWidget::TownEventsWidget(CGTownInstance & town, QWidget * parent) :
QDialog(parent),
ui(new Ui::TownEventsWidget),
town(town)
{
ui->setupUi(this);
}
TownEventsWidget::~TownEventsWidget()
{
delete ui;
}
QVariant toVariant(const std::set<BuildingID> & buildings)
{
QVariantList result;
for (auto b : buildings)
result.push_back(QVariant::fromValue(b.num));
return result;
}
QVariant toVariant(const std::vector<si32> & creatures)
{
QVariantList result;
for (auto c : creatures)
result.push_back(QVariant::fromValue(c));
return result;
}
std::set<BuildingID> buildingsFromVariant(const QVariant& v)
{
std::set<BuildingID> result;
for (const auto & r : v.toList()) {
result.insert(BuildingID(r.toInt()));
}
return result;
}
std::vector<si32> creaturesFromVariant(const QVariant& v)
{
std::vector<si32> result;
for (const auto & r : v.toList()) {
result.push_back(r.toInt());
}
return result;
}
QVariant toVariant(const CCastleEvent& event)
{
QVariantMap result;
result["name"] = QString::fromStdString(event.name);
result["message"] = QString::fromStdString(event.message.toString());
result["players"] = QVariant::fromValue(event.players);
result["humanAffected"] = QVariant::fromValue(event.humanAffected);
result["computerAffected"] = QVariant::fromValue(event.computerAffected);
result["firstOccurrence"] = QVariant::fromValue(event.firstOccurrence);
result["nextOccurrence"] = QVariant::fromValue(event.nextOccurrence);
result["resources"] = toVariant(event.resources);
result["buildings"] = toVariant(event.buildings);
result["creatures"] = toVariant(event.creatures);
return QVariant(result);
}
CCastleEvent eventFromVariant(CMapHeader& map, const CGTownInstance& town, const QVariant& variant)
{
CCastleEvent result;
auto v = variant.toMap();
result.name = v.value("name").toString().toStdString();
result.message.appendTextID(mapRegisterLocalizedString("map", map, TextIdentifier("town", town.instanceName, "event", result.name, "message"), v.value("message").toString().toStdString()));
result.players = v.value("players").toInt();
result.humanAffected = v.value("humanAffected").toInt();
result.computerAffected = v.value("computerAffected").toInt();
result.firstOccurrence = v.value("firstOccurrence").toInt();
result.nextOccurrence = v.value("nextOccurrence").toInt();
result.resources = resourcesFromVariant(v.value("resources"));
result.buildings = buildingsFromVariant(v.value("buildings"));
result.creatures = creaturesFromVariant(v.value("creatures"));
return result;
}
void TownEventsWidget::obtainData()
{
for (const auto & event : town.events)
{
auto eventName = QString::fromStdString(event.name);
auto itemText = tr("Day %1 - %2").arg(event.firstOccurrence+1, 3).arg(eventName);
auto * item = new QListWidgetItem(itemText);
item->setData(MapEditorRoles::TownEventRole, toVariant(event));
ui->eventsList->addItem(item);
}
}
void TownEventsWidget::commitChanges(MapController& controller)
{
town.events.clear();
for (int i = 0; i < ui->eventsList->count(); ++i)
{
const auto * item = ui->eventsList->item(i);
town.events.push_back(eventFromVariant(*controller.map(), town, item->data(MapEditorRoles::TownEventRole)));
}
}
void TownEventsWidget::on_timedEventAdd_clicked()
{
CCastleEvent event;
event.name = tr("New event").toStdString();
auto* item = new QListWidgetItem(QString::fromStdString(event.name));
item->setData(MapEditorRoles::TownEventRole, toVariant(event));
ui->eventsList->addItem(item);
on_eventsList_itemActivated(item);
}
void TownEventsWidget::on_timedEventRemove_clicked()
{
delete ui->eventsList->takeItem(ui->eventsList->currentRow());
}
void TownEventsWidget::on_eventsList_itemActivated(QListWidgetItem* item)
{
TownEventDialog dlg{ town, item, parentWidget() };
dlg.exec();
}
TownEventsDelegate::TownEventsDelegate(CGTownInstance & town, MapController & c) : QStyledItemDelegate(), town(town), controller(c)
{
}
QWidget* TownEventsDelegate::createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const
{
return new TownEventsWidget(town, parent);
}
void TownEventsDelegate::setEditorData(QWidget * editor, const QModelIndex & index) const
{
if (auto * ed = qobject_cast<TownEventsWidget *>(editor))
{
ed->obtainData();
}
else
{
QStyledItemDelegate::setEditorData(editor, index);
}
}
void TownEventsDelegate::setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const
{
if (auto * ed = qobject_cast<TownEventsWidget *>(editor))
{
ed->commitChanges(controller);
}
else
{
QStyledItemDelegate::setModelData(editor, model, index);
}
}

View File

@ -0,0 +1,58 @@
/*
* towneventswidget.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 "../StdInc.h"
#include <QDialog>
#include "../lib/mapping/CMapDefines.h"
#include "../lib/mapObjects/CGTownInstance.h"
#include "../mapcontroller.h"
namespace Ui {
class TownEventsWidget;
}
class TownEventsWidget : public QDialog
{
Q_OBJECT
public:
explicit TownEventsWidget(CGTownInstance &, QWidget * parent = nullptr);
~TownEventsWidget();
void obtainData();
void commitChanges(MapController & controller);
private slots:
void on_timedEventAdd_clicked();
void on_timedEventRemove_clicked();
void on_eventsList_itemActivated(QListWidgetItem * item);
private:
Ui::TownEventsWidget * ui;
CGTownInstance & town;
};
class TownEventsDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
using QStyledItemDelegate::QStyledItemDelegate;
TownEventsDelegate(CGTownInstance &, MapController &);
QWidget* createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const override;
void setEditorData(QWidget * editor, const QModelIndex & index) const override;
void setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const override;
private:
CGTownInstance & town;
MapController & controller;
};

View File

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TownEventsWidget</class>
<widget class="QDialog" name="TownEventsWidget">
<property name="windowModality">
<enum>Qt::ApplicationModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>691</width>
<height>462</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>400</width>
<height>400</height>
</size>
</property>
<property name="windowTitle">
<string>Town events</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QLabel" name="timedEventText">
<property name="text">
<string>Timed events</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="timedEventAdd">
<property name="minimumSize">
<size>
<width>90</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Add</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="timedEventRemove">
<property name="minimumSize">
<size>
<width>90</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Remove</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QListWidget" name="eventsList">
<property name="sortingEnabled">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,166 @@
/*
* townspellswidget.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 "townspellswidget.h"
#include "ui_townspellswidget.h"
#include "inspector.h"
#include "mapeditorroles.h"
#include "../../lib/constants/StringConstants.h"
#include "../../lib/spells/CSpellHandler.h"
TownSpellsWidget::TownSpellsWidget(CGTownInstance & town, QWidget * parent) :
QDialog(parent),
ui(new Ui::TownSpellsWidget),
town(town)
{
ui->setupUi(this);
possibleSpellLists = { ui->possibleSpellList1, ui->possibleSpellList2, ui->possibleSpellList3, ui->possibleSpellList4, ui->possibleSpellList5 };
requiredSpellLists = { ui->requiredSpellList1, ui->requiredSpellList2, ui->requiredSpellList3, ui->requiredSpellList4, ui->requiredSpellList5 };
std::array<BuildingID, 5> mageGuilds = {BuildingID::MAGES_GUILD_1, BuildingID::MAGES_GUILD_2, BuildingID::MAGES_GUILD_3, BuildingID::MAGES_GUILD_4, BuildingID::MAGES_GUILD_5};
for (int i = 0; i < mageGuilds.size(); i++)
{
ui->tabWidget->setTabEnabled(i, vstd::contains(town.getTown()->buildings, mageGuilds[i]));
}
}
TownSpellsWidget::~TownSpellsWidget()
{
delete ui;
}
void TownSpellsWidget::obtainData()
{
initSpellLists();
if (vstd::contains(town.possibleSpells, SpellID::PRESET)) {
ui->customizeSpells->setChecked(true);
}
else
{
ui->customizeSpells->setChecked(false);
ui->tabWidget->setEnabled(false);
}
}
void TownSpellsWidget::resetSpells()
{
town.possibleSpells.clear();
town.obligatorySpells.clear();
for (auto spellID : VLC->spellh->getDefaultAllowed())
town.possibleSpells.push_back(spellID);
}
void TownSpellsWidget::initSpellLists()
{
auto spells = VLC->spellh->getDefaultAllowed();
for (int i = 0; i < GameConstants::SPELL_LEVELS; i++)
{
std::vector<SpellID> spellsByLevel;
auto getSpellsByLevel = [i](auto spellID) {
return spellID.toEntity(VLC)->getLevel() == i + 1;
};
vstd::copy_if(spells, std::back_inserter(spellsByLevel), getSpellsByLevel);
possibleSpellLists[i]->clear();
requiredSpellLists[i]->clear();
for (auto spellID : spellsByLevel)
{
auto spell = spellID.toEntity(VLC);
auto * possibleItem = new QListWidgetItem(QString::fromStdString(spell->getNameTranslated()));
possibleItem->setData(MapEditorRoles::SpellIDRole, QVariant::fromValue(spell->getIndex()));
possibleItem->setFlags(possibleItem->flags() | Qt::ItemIsUserCheckable);
possibleItem->setCheckState(vstd::contains(town.possibleSpells, spell->getId()) ? Qt::Checked : Qt::Unchecked);
possibleSpellLists[i]->addItem(possibleItem);
auto * requiredItem = new QListWidgetItem(QString::fromStdString(spell->getNameTranslated()));
requiredItem->setData(MapEditorRoles::SpellIDRole, QVariant::fromValue(spell->getIndex()));
requiredItem->setFlags(requiredItem->flags() | Qt::ItemIsUserCheckable);
requiredItem->setCheckState(vstd::contains(town.obligatorySpells, spell->getId()) ? Qt::Checked : Qt::Unchecked);
requiredSpellLists[i]->addItem(requiredItem);
}
}
}
void TownSpellsWidget::commitChanges()
{
if (!ui->tabWidget->isEnabled())
{
resetSpells();
return;
}
auto updateTownSpellList = [](auto uiSpellLists, auto & townSpellList) {
for (const QListWidget * spellList : uiSpellLists)
{
for (int i = 0; i < spellList->count(); ++i)
{
const auto * item = spellList->item(i);
if (item->checkState() == Qt::Checked)
{
townSpellList.push_back(item->data(MapEditorRoles::SpellIDRole).toInt());
}
}
}
};
town.possibleSpells.clear();
town.obligatorySpells.clear();
town.possibleSpells.push_back(SpellID::PRESET);
updateTownSpellList(possibleSpellLists, town.possibleSpells);
updateTownSpellList(requiredSpellLists, town.obligatorySpells);
}
void TownSpellsWidget::on_customizeSpells_toggled(bool checked)
{
if (checked)
{
town.possibleSpells.push_back(SpellID::PRESET);
}
else
{
resetSpells();
}
ui->tabWidget->setEnabled(checked);
initSpellLists();
}
TownSpellsDelegate::TownSpellsDelegate(CGTownInstance & town) : QStyledItemDelegate(), town(town)
{
}
QWidget * TownSpellsDelegate::createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const
{
return new TownSpellsWidget(town, parent);
}
void TownSpellsDelegate::setEditorData(QWidget * editor, const QModelIndex & index) const
{
if (auto * ed = qobject_cast<TownSpellsWidget *>(editor))
{
ed->obtainData();
}
else
{
QStyledItemDelegate::setEditorData(editor, index);
}
}
void TownSpellsDelegate::setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const
{
if (auto * ed = qobject_cast<TownSpellsWidget *>(editor))
{
ed->commitChanges();
}
else
{
QStyledItemDelegate::setModelData(editor, model, index);
}
}

View File

@ -0,0 +1,60 @@
/*
* townspellswidget.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 <QDialog>
#include "../../lib/mapObjects/CGTownInstance.h"
namespace Ui {
class TownSpellsWidget;
}
class TownSpellsWidget : public QDialog
{
Q_OBJECT
public:
explicit TownSpellsWidget(CGTownInstance &, QWidget * parent = nullptr);
~TownSpellsWidget();
void obtainData();
void commitChanges();
private slots:
void on_customizeSpells_toggled(bool checked);
private:
Ui::TownSpellsWidget * ui;
CGTownInstance & town;
std::array<QListWidget *, 5> possibleSpellLists;
std::array<QListWidget *, 5> requiredSpellLists;
void resetSpells();
void initSpellLists();
};
class TownSpellsDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
using QStyledItemDelegate::QStyledItemDelegate;
TownSpellsDelegate(CGTownInstance&);
QWidget * createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex& index) const override;
void setEditorData(QWidget * editor, const QModelIndex & index) const override;
void setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const override;
private:
CGTownInstance& town;
};

View File

@ -0,0 +1,304 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TownSpellsWidget</class>
<widget class="QDialog" name="TownSpellsWidget">
<property name="windowModality">
<enum>Qt::NonModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<height>480</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>600</width>
<height>480</height>
</size>
</property>
<property name="windowTitle">
<string>Spells</string>
</property>
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>10</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<item>
<widget class="QCheckBox" name="customizeSpells">
<property name="text">
<string>Customize spells</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<property name="documentMode">
<bool>true</bool>
</property>
<widget class="QWidget" name="level1">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<attribute name="title">
<string>Level 1</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_level1">
<property name="leftMargin">
<number>12</number>
</property>
<property name="rightMargin">
<number>12</number>
</property>
<property name="bottomMargin">
<number>12</number>
</property>
<item>
<layout class="QGridLayout" name="gridLayout1">
<item row="0" column="0">
<widget class="QLabel" name="possibleSpellsText1">
<property name="text">
<string>Spell that may appear in mage guild</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="requiredSpellsText1">
<property name="text">
<string>Spell that must appear in mage guild</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QListWidget" name="possibleSpellList1"/>
</item>
<item row="1" column="1">
<widget class="QListWidget" name="requiredSpellList1"/>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="level2">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<attribute name="title">
<string>Level 2</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_level2">
<property name="leftMargin">
<number>12</number>
</property>
<property name="rightMargin">
<number>12</number>
</property>
<property name="bottomMargin">
<number>12</number>
</property>
<item>
<layout class="QGridLayout" name="gridLayout2">
<item row="0" column="0">
<widget class="QLabel" name="possibleSpellsText2">
<property name="text">
<string>Spell that may appear in mage guild</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="requiredSpellsText2">
<property name="text">
<string>Spell that must appear in mage guild</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QListWidget" name="possibleSpellList2"/>
</item>
<item row="1" column="1">
<widget class="QListWidget" name="requiredSpellList2"/>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="level3">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<attribute name="title">
<string>Level 3</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_level3">
<property name="leftMargin">
<number>12</number>
</property>
<property name="rightMargin">
<number>12</number>
</property>
<property name="bottomMargin">
<number>12</number>
</property>
<item>
<layout class="QGridLayout" name="gridLayout3">
<item row="0" column="0">
<widget class="QLabel" name="possibleSpellsText3">
<property name="text">
<string>Spell that may appear in mage guild</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="requiredSpellsText3">
<property name="text">
<string>Spell that must appear in mage guild</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QListWidget" name="possibleSpellList3"/>
</item>
<item row="1" column="1">
<widget class="QListWidget" name="requiredSpellList3"/>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="level4">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<attribute name="title">
<string>Level 4</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_level4">
<property name="leftMargin">
<number>12</number>
</property>
<property name="rightMargin">
<number>12</number>
</property>
<property name="bottomMargin">
<number>12</number>
</property>
<item>
<layout class="QGridLayout" name="gridLayout4">
<item row="0" column="0">
<widget class="QLabel" name="possibleSpellsText4">
<property name="text">
<string>Spell that may appear in mage guild</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="requiredSpellsText4">
<property name="text">
<string>Spell that must appear in mage guild</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QListWidget" name="possibleSpellList4"/>
</item>
<item row="1" column="1">
<widget class="QListWidget" name="requiredSpellList4"/>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="level5">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<attribute name="title">
<string>Level 5</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_level5">
<property name="leftMargin">
<number>12</number>
</property>
<property name="rightMargin">
<number>12</number>
</property>
<property name="bottomMargin">
<number>12</number>
</property>
<item>
<layout class="QGridLayout" name="gridLayout5">
<item row="0" column="0">
<widget class="QLabel" name="possibleSpellsText5">
<property name="text">
<string>Spell that may appear in mage guild</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="requiredSpellsText5">
<property name="text">
<string>Spell that must appear in mage guild</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QListWidget" name="possibleSpellList5"/>
</item>
<item row="1" column="1">
<widget class="QListWidget" name="requiredSpellList5"/>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,20 @@
/*
* mapeditorroles.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 "StdInc.h"
enum MapEditorRoles
{
TownEventRole = Qt::UserRole + 1,
PlayerIDRole,
BuildingIDRole,
SpellIDRole
};

View File

@ -15,6 +15,9 @@ namespace Ui {
class EventSettings;
}
QVariant toVariant(const TResources & resources);
TResources resourcesFromVariant(const QVariant & v);
class EventSettings : public AbstractSettings
{
Q_OBJECT

View File

@ -715,7 +715,7 @@
<context>
<name>MapView</name>
<message>
<location filename="../mapview.cpp" line="625"/>
<location filename="../mapview.cpp" line="626"/>
<source>Can&apos;t place object</source>
<translation></translation>
</message>
@ -889,38 +889,38 @@
<translation></translation>
</message>
<message>
<location filename="../inspector/inspector.cpp" line="36"/>
<location filename="../inspector/inspector.cpp" line="38"/>
<source>Compliant</source>
<translation></translation>
</message>
<message>
<location filename="../inspector/inspector.cpp" line="37"/>
<location filename="../inspector/inspector.cpp" line="39"/>
<source>Friendly</source>
<translation></translation>
</message>
<message>
<location filename="../inspector/inspector.cpp" line="38"/>
<location filename="../inspector/inspector.cpp" line="40"/>
<source>Aggressive</source>
<translation></translation>
</message>
<message>
<location filename="../inspector/inspector.cpp" line="39"/>
<location filename="../inspector/inspector.cpp" line="41"/>
<source>Hostile</source>
<translation></translation>
</message>
<message>
<location filename="../inspector/inspector.cpp" line="40"/>
<location filename="../inspector/inspector.cpp" line="42"/>
<source>Savage</source>
<translation></translation>
</message>
<message>
<location filename="../inspector/inspector.cpp" line="843"/>
<location filename="../inspector/inspector.cpp" line="932"/>
<location filename="../inspector/inspector.cpp" line="847"/>
<location filename="../inspector/inspector.cpp" line="936"/>
<source>neutral</source>
<translation></translation>
</message>
<message>
<location filename="../inspector/inspector.cpp" line="841"/>
<location filename="../inspector/inspector.cpp" line="845"/>
<source>UNFLAGGABLE</source>
<translation></translation>
</message>
@ -1435,6 +1435,208 @@
<source>Buildings</source>
<translation></translation>
</message>
<message>
<location filename="../inspector/townbuildingswidget.ui" line="53"/>
<source>Build all</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/townbuildingswidget.ui" line="60"/>
<source>Demolish all</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/townbuildingswidget.ui" line="67"/>
<source>Enable all</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/townbuildingswidget.ui" line="74"/>
<source>Disable all</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/townbuildingswidget.cpp" line="77"/>
<source>Type</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/townbuildingswidget.cpp" line="77"/>
<source>Enabled</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/townbuildingswidget.cpp" line="77"/>
<source>Built</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TownEventDialog</name>
<message>
<location filename="../inspector/towneventdialog.ui" line="23"/>
<source>Town event</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/towneventdialog.ui" line="42"/>
<source>General</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/towneventdialog.ui" line="57"/>
<source>Event name</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/towneventdialog.ui" line="64"/>
<source>Type event message text</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/towneventdialog.ui" line="85"/>
<source>Day of first occurrence</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/towneventdialog.ui" line="99"/>
<source>Repeat after (0 = no repeat)</source>
<translation type="unfinished"> (0 = )</translation>
</message>
<message>
<location filename="../inspector/towneventdialog.ui" line="123"/>
<source>Affected players</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/towneventdialog.ui" line="146"/>
<source>affects human</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/towneventdialog.ui" line="155"/>
<source>affects AI</source>
<translation type="unfinished">AI玩家生效</translation>
</message>
<message>
<location filename="../inspector/towneventdialog.ui" line="166"/>
<source>Resources</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/towneventdialog.ui" line="198"/>
<source>Buildings</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/towneventdialog.ui" line="216"/>
<source>Creatures</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/towneventdialog.ui" line="255"/>
<source>OK</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/towneventdialog.cpp" line="177"/>
<source>Creature level %1 / Creature level %1 Upgrade</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/towneventdialog.cpp" line="219"/>
<source>Day %1 - %2</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TownEventsWidget</name>
<message>
<location filename="../inspector/towneventswidget.ui" line="29"/>
<source>Town events</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/towneventswidget.ui" line="37"/>
<source>Timed events</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/towneventswidget.ui" line="63"/>
<source>Add</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/towneventswidget.ui" line="76"/>
<source>Remove</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/towneventswidget.cpp" line="105"/>
<source>Day %1 - %2</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/towneventswidget.cpp" line="126"/>
<source>New event</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TownSpellsWidget</name>
<message>
<location filename="../inspector/townspellswidget.ui" line="29"/>
<source>Spells</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/townspellswidget.ui" line="47"/>
<source>Customize spells</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/townspellswidget.ui" line="76"/>
<source>Level 1</source>
<translation type="unfinished">1</translation>
</message>
<message>
<location filename="../inspector/townspellswidget.ui" line="93"/>
<location filename="../inspector/townspellswidget.ui" line="139"/>
<location filename="../inspector/townspellswidget.ui" line="185"/>
<location filename="../inspector/townspellswidget.ui" line="231"/>
<location filename="../inspector/townspellswidget.ui" line="277"/>
<source>Spell that may appear in mage guild</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/townspellswidget.ui" line="100"/>
<location filename="../inspector/townspellswidget.ui" line="146"/>
<location filename="../inspector/townspellswidget.ui" line="192"/>
<location filename="../inspector/townspellswidget.ui" line="238"/>
<location filename="../inspector/townspellswidget.ui" line="284"/>
<source>Spell that must appear in mage guild</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../inspector/townspellswidget.ui" line="122"/>
<source>Level 2</source>
<translation type="unfinished">2</translation>
</message>
<message>
<location filename="../inspector/townspellswidget.ui" line="168"/>
<source>Level 3</source>
<translation type="unfinished">3</translation>
</message>
<message>
<location filename="../inspector/townspellswidget.ui" line="214"/>
<source>Level 4</source>
<translation type="unfinished">4</translation>
</message>
<message>
<location filename="../inspector/townspellswidget.ui" line="260"/>
<source>Level 5</source>
<translation type="unfinished">5</translation>
</message>
</context>
<context>
<name>Translations</name>
@ -1698,18 +1900,6 @@
<source>Width</source>
<translation></translation>
</message>
<message>
<source>S (36x36)</source>
<translation type="vanished">(36x36)</translation>
</message>
<message>
<source>M (72x72)</source>
<translation type="vanished">(72x72)</translation>
</message>
<message>
<source>L (108x108)</source>
<translation type="vanished">(108x108)</translation>
</message>
<message>
<location filename="../windownewmap.ui" line="179"/>
<source>XL (144x144)</source>

Some files were not shown because too many files have changed in this diff Show More