1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-03-05 15:05:40 +02:00

Merge remote-tracking branch 'upstream/develop' into develop

This commit is contained in:
Xilmi 2024-08-05 21:53:59 +02:00
commit f4c0bee003
91 changed files with 933 additions and 207 deletions

View File

@ -146,7 +146,13 @@ jobs:
HEROES_3_DATA_PASSWORD: ${{ secrets.HEROES_3_DATA_PASSWORD }}
if: ${{ env.HEROES_3_DATA_PASSWORD != '' && matrix.test == 1 }}
run: |
wget --progress=dot:giga https://github.com/vcmi-mods/vcmi-test-data/releases/download/v1.0/h3_assets.zip
if [[ ${{github.repository_owner}} == vcmi ]]
then
data_url="https://github.com/vcmi-mods/vcmi-test-data/releases/download/v1.0/h3_assets.zip"
else
data_url="https://github.com/${{github.repository_owner}}/vcmi-test-data/releases/download/v1.0/h3_assets.zip"
fi
wget --progress=dot:giga "$data_url" -O h3_assets.zip
7za x h3_assets.zip -p$HEROES_3_DATA_PASSWORD
mkdir -p ~/.local/share/vcmi/
mv h3_assets/* ~/.local/share/vcmi/

View File

@ -316,7 +316,7 @@ void BuildAnalyzer::updateDailyIncome()
if(mine)
{
dailyIncome[mine->producedResource.getNum()] += mine->producedQuantity;
dailyIncome[mine->producedResource.getNum()] += mine->getProducedQuantity();
}
}

View File

@ -1,11 +1,11 @@
# 1.5.5 -> 1.6.0
# 1.5.5 -> 1.6.0 (in development)
# General
### General
* Saved game size reduced by approximately 3 times, especially for large maps or games with a large number of mods.
* Added option to start vcmi server on randomly selected TCP port
* Fixed potential desynchronization between server and clients on randomization of map objects if client and server run on different operating systems
# Interface
### Interface
* Implemented spell quick selection panel in combat
* The number of units resurrected using the Life Drain ability is now written to the combat log.
* Fixed playback of audio stream with different formats from video files in some Heroes 3 versions
@ -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

@ -73,7 +73,11 @@
"vcmi.lobby.sortDate" : "Sorts maps by change date",
"vcmi.lobby.backToLobby" : "Return to lobby",
"vcmi.lobby.author" : "Author",
"vcmi.lobby.handicap" : "Handicap",
"vcmi.lobby.handicap.resource" : "Gives players appropriate resources to start with in addition to the normal starting resources. Negative values are allowed, but are limited to 0 in total (the player never starts with negative resources).",
"vcmi.lobby.handicap.income" : "Changes the player's various incomes by the percentage. Is rounded up.",
"vcmi.lobby.handicap.growth" : "Changes the growth rate of creatures in the towns owned by the player. Is rounded up.",
"vcmi.lobby.login.title" : "VCMI Online Lobby",
"vcmi.lobby.login.username" : "Username:",
"vcmi.lobby.login.connecting" : "Connecting...",

View File

@ -73,6 +73,10 @@
"vcmi.lobby.sortDate" : "Ordnet Karten nach Änderungsdatum",
"vcmi.lobby.backToLobby" : "Zur Lobby zurückkehren",
"vcmi.lobby.author" : "Author",
"vcmi.lobby.handicap" : "Handicap",
"vcmi.lobby.handicap.resource" : "Gibt den Spielern entsprechende Ressourcen zum Start zusätzlich zu den normalen Startressourcen. Negative Werte sind erlaubt, werden aber insgesamt auf 0 begrenzt (der Spieler beginnt nie mit negativen Ressourcen).",
"vcmi.lobby.handicap.income" : "Verändert die verschiedenen Einkommen des Spielers um den Prozentsatz. Wird aufgerundet.",
"vcmi.lobby.handicap.growth" : "Verändert die Wachstumsrate der Kreaturen in den Städten, die der Spieler besitzt. Wird aufgerundet.",
"vcmi.lobby.login.title" : "VCMI Online Lobby",
"vcmi.lobby.login.username" : "Benutzername:",

View File

@ -63,7 +63,7 @@
"vcmi.mainMenu.serverClosing" : "Fechando...",
"vcmi.mainMenu.hostTCP" : "Hospedar jogo TCP/IP",
"vcmi.mainMenu.joinTCP" : "Entrar em jogo TCP/IP",
"vcmi.lobby.filepath" : "Caminho do arquivo",
"vcmi.lobby.creationDate" : "Data de criação",
"vcmi.lobby.scenarioName" : "Nome do cenário",
@ -237,6 +237,8 @@
"vcmi.battleOptions.skipBattleIntroMusic.help": "{Pula a Música de Introdução}\n\nPermite ações durante a música de introdução que toca no início de cada batalha.",
"vcmi.battleOptions.endWithAutocombat.hover": "Terminar a batalha",
"vcmi.battleOptions.endWithAutocombat.help": "{Termina a batalha}\n\nO Combate Automático reproduz a batalha até o final instantâneo.",
"vcmi.battleOptions.showQuickSpell.hover": "Mostrar Painel de Feitiço Rápido",
"vcmi.battleOptions.showQuickSpell.help": "{Mostrar Painel de Feitiço Rápido}\n\nMostra um painel para seleção rápida de feitiços",
"vcmi.adventureMap.revisitObject.hover" : "Revisitar Objeto",
"vcmi.adventureMap.revisitObject.help" : "{Revisitar Objeto}\n\nSe um herói estiver atualmente em um Objeto do Mapa, ele pode revisitar o local.",

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

@ -161,11 +161,6 @@ CServerHandler::CServerHandler()
registerTypesLobbyPacks(*applier);
}
void CServerHandler::setHighScoreCalc(const std::shared_ptr<HighScoreCalculation> &newHighScoreCalc)
{
campaignScoreCalculator = newHighScoreCalc;
}
void CServerHandler::threadRunNetwork()
{
logGlobal->info("Starting network thread");
@ -497,6 +492,14 @@ void CServerHandler::setPlayerName(PlayerColor color, const std::string & name)
sendLobbyPack(lspn);
}
void CServerHandler::setPlayerHandicap(PlayerColor color, Handicap handicap) const
{
LobbySetPlayerHandicap lsph;
lsph.color = color;
lsph.handicap = handicap;
sendLobbyPack(lsph);
}
void CServerHandler::setPlayerOption(ui8 what, int32_t value, PlayerColor player) const
{
LobbyChangePlayerOption lcpo;
@ -655,7 +658,7 @@ void CServerHandler::startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameSta
break;
case EStartMode::CAMPAIGN:
if(si->campState->conqueredScenarios().empty())
campaignScoreCalculator.reset();
si->campState->highscoreParameters.clear();
client->newGame(gameState);
break;
case EStartMode::LOAD_GAME:
@ -686,12 +689,12 @@ HighScoreParameter CServerHandler::prepareHighScores(PlayerColor player, bool vi
for(const CGTownInstance * t : playerState->towns)
if(t->builtBuildings.count(BuildingID::GRAIL))
param.hasGrail = true;
param.allDefeated = 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.allDefeated = false;
param.allEnemiesDefeated = false;
}
param.scenarioName = gs->getMapHeader()->name.toString();
param.playerName = gs->getStartInfo()->playerInfos.find(player)->second.name;
@ -756,19 +759,16 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared
if (!cs)
ourCampaign = si->campState;
if(campaignScoreCalculator == nullptr)
{
campaignScoreCalculator = std::make_shared<HighScoreCalculation>();
campaignScoreCalculator->isCampaign = true;
campaignScoreCalculator->parameters.clear();
}
param.campaignName = cs->getNameTranslated();
campaignScoreCalculator->parameters.push_back(param);
cs->highscoreParameters.push_back(param);
auto campaignScoreCalculator = std::make_shared<HighScoreCalculation>();
campaignScoreCalculator->isCampaign = true;
campaignScoreCalculator->parameters = cs->highscoreParameters;
endGameplay();
auto & epilogue = ourCampaign->scenario(*ourCampaign->lastScenario()).epilog;
auto finisher = [this, ourCampaign]()
auto finisher = [ourCampaign, campaignScoreCalculator]()
{
if(ourCampaign->campaignSet != "" && ourCampaign->isCampaignFinished())
{

View File

@ -28,6 +28,8 @@ struct CPack;
struct CPackForLobby;
struct CPackForClient;
class HighScoreParameter;
template<typename T> class CApplier;
VCMI_LIB_NAMESPACE_END
@ -39,7 +41,6 @@ class GameChatHandler;
class IServerRunner;
class HighScoreCalculation;
class HighScoreParameter;
enum class ESelectionScreen : ui8;
enum class ELoadMode : ui8;
@ -81,6 +82,7 @@ public:
virtual void setMapInfo(std::shared_ptr<CMapInfo> to, std::shared_ptr<CMapGenOptions> mapGenOpts = {}) const = 0;
virtual void setPlayer(PlayerColor color) const = 0;
virtual void setPlayerName(PlayerColor color, const std::string & name) const = 0;
virtual void setPlayerHandicap(PlayerColor color, Handicap handicap) const = 0;
virtual void setPlayerOption(ui8 what, int32_t value, PlayerColor player) const = 0;
virtual void setDifficulty(int to) const = 0;
virtual void setTurnTimerInfo(const TurnTimerInfo &) const = 0;
@ -105,7 +107,6 @@ class CServerHandler final : public IServerAPI, public LobbyInfo, public INetwor
std::unique_ptr<IServerRunner> serverRunner;
std::shared_ptr<CMapInfo> mapToStart;
std::vector<std::string> localPlayerNames;
std::shared_ptr<HighScoreCalculation> campaignScoreCalculator;
boost::thread threadNetwork;
@ -191,6 +192,7 @@ public:
void setMapInfo(std::shared_ptr<CMapInfo> to, std::shared_ptr<CMapGenOptions> mapGenOpts = {}) const override;
void setPlayer(PlayerColor color) const override;
void setPlayerName(PlayerColor color, const std::string & name) const override;
void setPlayerHandicap(PlayerColor color, Handicap handicap) const override;
void setPlayerOption(ui8 what, int32_t value, PlayerColor player) const override;
void setDifficulty(int to) const override;
void setTurnTimerInfo(const TurnTimerInfo &) const override;
@ -219,7 +221,6 @@ public:
void visitForLobby(CPackForLobby & lobbyPack);
void visitForClient(CPackForClient & clientPack);
void setHighScoreCalc(const std::shared_ptr<HighScoreCalculation> &newHighScoreCalc);
};
extern CServerHandler * CSH;

View File

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

View File

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

View File

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

View File

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

View File

@ -218,7 +218,7 @@ void BattleWindow::showStickyQuickSpellWindow()
Settings showStickyQuickSpellWindow = settings.write["battle"]["enableQuickSpellPanel"];
showStickyQuickSpellWindow->Bool() = true;
if(GH.screenDimensions().x >= 1050)
if(GH.screenDimensions().x >= 1050 && owner.getBattle()->battleGetMyHero()->hasSpellbook())
{
quickSpellWindow->enable();
quickSpellWindow->isEnabled = true;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -91,6 +91,7 @@ enum class EShortcut
LOBBY_FLIP_COIN,
LOBBY_RANDOM_TOWN,
LOBBY_RANDOM_TOWN_VS,
LOBBY_HANDICAP,
MAPS_SIZE_S,
MAPS_SIZE_M,

View File

@ -288,6 +288,7 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
{"lobbyFlipCoin", EShortcut::LOBBY_FLIP_COIN },
{"lobbyRandomTown", EShortcut::LOBBY_RANDOM_TOWN },
{"lobbyRandomTownVs", EShortcut::LOBBY_RANDOM_TOWN_VS },
{"lobbyHandicap", EShortcut::LOBBY_HANDICAP },
{"mapsSizeS", EShortcut::MAPS_SIZE_S },
{"mapsSizeM", EShortcut::MAPS_SIZE_M },
{"mapsSizeL", EShortcut::MAPS_SIZE_L },

View File

@ -437,6 +437,13 @@ PvPBox::PvPBox(const Rect & rect)
CSH->sendLobbyPack(lpa);
}, EShortcut::LOBBY_RANDOM_TOWN_VS);
buttonRandomTownVs->setTextOverlay(CGI->generaltexth->translate("vcmi.lobby.pvp.randomTownVs.hover"), EFonts::FONT_SMALL, Colors::WHITE);
buttonHandicap = std::make_shared<CButton>(Point(190, 81), AnimationPath::builtin("GSPBUT2.DEF"), CButton::tooltip("", CGI->generaltexth->translate("vcmi.lobby.handicap")), [](){
if(!CSH->isHost())
return;
GH.windows().createAndPushWindow<OptionsTab::HandicapWindow>();
}, EShortcut::LOBBY_HANDICAP);
buttonHandicap->setTextOverlay(CGI->generaltexth->translate("vcmi.lobby.handicap"), EFonts::FONT_SMALL, Colors::WHITE);
}
TownSelector::TownSelector(const Point & loc)

View File

@ -153,6 +153,7 @@ class PvPBox : public CIntObject
std::shared_ptr<CButton> buttonFlipCoin;
std::shared_ptr<CButton> buttonRandomTown;
std::shared_ptr<CButton> buttonRandomTownVs;
std::shared_ptr<CButton> buttonHandicap;
public:
PvPBox(const Rect & rect);
};

View File

@ -29,6 +29,7 @@
#include "../widgets/ObjectLists.h"
#include "../widgets/Slider.h"
#include "../widgets/TextControls.h"
#include "../widgets/GraphicalPrimitiveCanvas.h"
#include "../windows/GUIClasses.h"
#include "../windows/InfoWindows.h"
#include "../windows/CHeroOverview.h"
@ -793,6 +794,119 @@ void OptionsTab::SelectionWindow::showPopupWindow(const Point & cursorPosition)
setElement(elem, false);
}
OptionsTab::HandicapWindow::HandicapWindow()
: CWindowObject(BORDERED)
{
OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
addUsedEvents(LCLICK);
pos = Rect(0, 0, 660, 100 + SEL->getStartInfo()->playerInfos.size() * 30);
backgroundTexture = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), pos);
backgroundTexture->setPlayerColor(PlayerColor(1));
labels.push_back(std::make_shared<CLabel>(pos.w / 2 + 8, 15, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.handicap")));
enum Columns : int32_t
{
INCOME = 1000,
GROWTH = 2000,
};
auto columns = std::vector<EGameResID>{EGameResID::GOLD, EGameResID::WOOD, EGameResID::MERCURY, EGameResID::ORE, EGameResID::SULFUR, EGameResID::CRYSTAL, EGameResID::GEMS, Columns::INCOME, Columns::GROWTH};
int i = 0;
for(auto & pInfo : SEL->getStartInfo()->playerInfos)
{
PlayerColor player = pInfo.first;
anim.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("ITGFLAGS"), player.getNum(), 0, 7, 57 + i * 30));
for(int j = 0; j < columns.size(); j++)
{
bool isIncome = int(columns[j]) == Columns::INCOME;
bool isGrowth = int(columns[j]) == Columns::GROWTH;
EGameResID resource = columns[j];
const PlayerSettings &ps = SEL->getStartInfo()->getIthPlayersSettings(player);
int xPos = 30 + j * 70;
xPos += j > 0 ? 10 : 0; // Gold field is larger
if(i == 0)
{
if(isIncome)
labels.push_back(std::make_shared<CLabel>(xPos, 35, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("core.jktext.32")));
else if(isGrowth)
labels.push_back(std::make_shared<CLabel>(xPos, 35, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.194")));
else
anim.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("SMALRES"), GameResID(resource), 0, 15 + xPos + (j == 0 ? 10 : 0), 35));
}
auto area = Rect(xPos, 60 + i * 30, j == 0 ? 60 : 50, 16);
textinputbackgrounds.push_back(std::make_shared<TransparentFilledRectangle>(area.resize(3), ColorRGBA(0,0,0,128), ColorRGBA(64,64,64,64)));
textinputs[player][resource] = std::make_shared<CTextInput>(area, FONT_SMALL, ETextAlignment::CENTERLEFT, true);
textinputs[player][resource]->setText(std::to_string(isIncome ? ps.handicap.percentIncome : (isGrowth ? ps.handicap.percentGrowth : ps.handicap.startBonus[resource])));
textinputs[player][resource]->setCallback([this, player, resource, isIncome, isGrowth](const std::string & s){
// text input processing: add/remove sign when pressing "-"; remove non digits; cut length; fill empty field with 0
std::string tmp = s;
bool negative = std::count_if( s.begin(), s.end(), []( char c ){ return c == '-'; }) == 1 && !isIncome && !isGrowth;
tmp.erase(std::remove_if(tmp.begin(), tmp.end(), [](char c) { return !isdigit(c); }), tmp.end());
int maxLength = isIncome || isGrowth ? 3 : (resource == EGameResID::GOLD ? 6 : 5);
tmp = tmp.substr(0, maxLength);
textinputs[player][resource]->setText(tmp.length() == 0 ? "0" : (negative ? "-" : "") + std::to_string(stoi(tmp)));
});
textinputs[player][resource]->setPopupCallback([isIncome, isGrowth](){
// Help for the textinputs
if(isIncome)
CRClickPopup::createAndPush(CGI->generaltexth->translate("vcmi.lobby.handicap.income"));
else if(isGrowth)
CRClickPopup::createAndPush(CGI->generaltexth->translate("vcmi.lobby.handicap.growth"));
else
CRClickPopup::createAndPush(CGI->generaltexth->translate("vcmi.lobby.handicap.resource"));
});
if(isIncome || isGrowth)
labels.push_back(std::make_shared<CLabel>(area.topRight().x, area.center().y, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::WHITE, "%"));
}
i++;
}
buttons.push_back(std::make_shared<CButton>(Point(pos.w / 2 - 32, 60 + SEL->getStartInfo()->playerInfos.size() * 30), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){
for (const auto& player : textinputs)
{
TResources resources = TResources();
int income = 100;
int growth = 100;
for (const auto& resource : player.second)
{
bool isIncome = int(resource.first) == Columns::INCOME;
bool isGrowth = int(resource.first) == Columns::GROWTH;
if(isIncome)
income = std::stoi(resource.second->getText());
else if(isGrowth)
growth = std::stoi(resource.second->getText());
else
resources[resource.first] = std::stoi(resource.second->getText());
}
CSH->setPlayerHandicap(player.first, Handicap{resources, income, growth});
}
close();
}, EShortcut::GLOBAL_RETURN));
updateShadow();
center();
}
bool OptionsTab::HandicapWindow::receiveEvent(const Point & position, int eventType) const
{
return true; // capture click also outside of window
}
void OptionsTab::HandicapWindow::clickReleased(const Point & cursorPosition)
{
if(!pos.isInside(cursorPosition)) // make it possible to close window by touching/clicking outside of window
close();
}
OptionsTab::SelectedBox::SelectedBox(Point position, PlayerSettings & playerSettings, SelType type)
: Scrollable(LCLICK | SHOW_POPUP, position, Orientation::HORIZONTAL)
, CPlayerSettingsHelper(playerSettings, type)
@ -923,6 +1037,47 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con
}
labelWhoCanPlay = std::make_shared<CMultiLineLabel>(Rect(6, 23, 45, (int)graphics->fonts[EFonts::FONT_TINY]->getLineHeight()*2), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->arraytxt[206 + whoCanPlay]);
auto hasHandicap = [this](){ return s->handicap.startBonus.empty() && s->handicap.percentIncome == 100 && s->handicap.percentGrowth == 100; };
std::string labelHandicapText = hasHandicap() ? CGI->generaltexth->arraytxt[210] : MetaString::createFromTextID("vcmi.lobby.handicap").toString();
labelHandicap = std::make_shared<CMultiLineLabel>(Rect(57, 24, 47, (int)graphics->fonts[EFonts::FONT_TINY]->getLineHeight()*2), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, labelHandicapText);
handicap = std::make_shared<LRClickableArea>(Rect(56, 24, 49, (int)graphics->fonts[EFonts::FONT_TINY]->getLineHeight()*2), [](){
if(!CSH->isHost())
return;
GH.windows().createAndPushWindow<HandicapWindow>();
}, [this, hasHandicap](){
if(hasHandicap())
CRClickPopup::createAndPush(MetaString::createFromTextID("core.help.124.help").toString());
else
{
auto str = MetaString::createFromTextID("vcmi.lobby.handicap");
str.appendRawString(":\n");
for(auto & res : EGameResID::ALL_RESOURCES())
if(s->handicap.startBonus[res] != 0)
{
str.appendRawString("\n");
str.appendName(res);
str.appendRawString(": ");
str.appendRawString(std::to_string(s->handicap.startBonus[res]));
}
if(s->handicap.percentIncome != 100)
{
str.appendRawString("\n");
str.appendTextID("core.jktext.32");
str.appendRawString(": ");
str.appendRawString(std::to_string(s->handicap.percentIncome) + "%");
}
if(s->handicap.percentGrowth != 100)
{
str.appendRawString("\n");
str.appendTextID("core.genrltxt.194");
str.appendRawString(": ");
str.appendRawString(std::to_string(s->handicap.percentGrowth) + "%");
}
CRClickPopup::createAndPush(str.toString());
}
});
if(SEL->screenType == ESelectionScreen::newGame)
{
buttonTownLeft = std::make_shared<CButton>(Point(107, 5), AnimationPath::builtin("ADOPLFA.DEF"), CGI->generaltexth->zelp[132], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::TOWN, -1, s->color));

View File

@ -27,8 +27,10 @@ class CComponentBox;
class CTextBox;
class CButton;
class CSlider;
class LRClickableArea;
class FilledTexturePlayerColored;
class TransparentFilledRectangle;
/// The options tab which is shown at the map selection phase.
class OptionsTab : public OptionsTabBase
@ -51,6 +53,22 @@ public:
BONUS
};
class HandicapWindow : public CWindowObject
{
std::shared_ptr<FilledTexturePlayerColored> backgroundTexture;
std::vector<std::shared_ptr<CLabel>> labels;
std::vector<std::shared_ptr<CAnimImage>> anim;
std::vector<std::shared_ptr<TransparentFilledRectangle>> textinputbackgrounds;
std::map<PlayerColor, std::map<EGameResID, std::shared_ptr<CTextInput>>> textinputs;
std::vector<std::shared_ptr<CButton>> buttons;
bool receiveEvent(const Point & position, int eventType) const override;
void clickReleased(const Point & cursorPosition) override;
public:
HandicapWindow();
};
private:
struct CPlayerSettingsHelper
@ -192,6 +210,8 @@ private:
std::shared_ptr<SelectedBox> town;
std::shared_ptr<SelectedBox> hero;
std::shared_ptr<SelectedBox> bonus;
std::shared_ptr<LRClickableArea> handicap;
std::shared_ptr<CMultiLineLabel> labelHandicap;
enum {HUMAN_OR_CPU, HUMAN, CPU} whoCanPlay;
PlayerOptionsEntry(const PlayerSettings & S, const OptionsTab & parentTab);

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

@ -32,6 +32,7 @@
#include "../../lib/CConfigHandler.h"
#include "../../lib/CCreatureHandler.h"
#include "../../lib/constants/EntityIdentifiers.h"
#include "../../lib/gameState/HighScore.h"
auto HighScoreCalculation::calculate()
{
@ -48,7 +49,7 @@ auto HighScoreCalculation::calculate()
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.allDefeated ? 25 : 0) + (param.hasGrail ? 25 : 0);
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();

View File

@ -9,6 +9,7 @@
*/
#pragma once
#include "../windows/CWindowObject.h"
#include "../../lib/gameState/HighScore.h"
class CButton;
class CLabel;
@ -20,24 +21,10 @@ class CFilledTexture;
class TransparentFilledRectangle;
class HighScoreParameter
{
public:
int difficulty;
int day;
int townAmount;
bool usedCheat;
bool hasGrail;
bool allDefeated;
std::string campaignName;
std::string scenarioName;
std::string playerName;
};
class HighScoreCalculation
{
public:
std::vector<HighScoreParameter> parameters = std::vector<HighScoreParameter>();
std::vector<HighScoreParameter> parameters;
bool isCampaign = false;
auto calculate();

View File

@ -16,6 +16,7 @@ class Point;
class CGObjectInstance;
class ObjectInstanceID;
struct TerrainTile;
class ColorRGBA;
struct CGPath;
VCMI_LIB_NAMESPACE_END
@ -67,6 +68,12 @@ public:
/// returns index of image for overlay on specific tile, or numeric_limits::max if none
virtual size_t overlayImageIndex(const int3 & coordinates) const = 0;
/// returns text that should be used as overlay for current tile
virtual std::string overlayText(const int3 & coordinates) const = 0;
/// returns text that should be used as overlay for current tile
virtual ColorRGBA overlayTextColor(const int3 & coordinates) const = 0;
/// returns animation frame for terrain
virtual size_t terrainImageIndex(size_t groupSize) const = 0;
@ -80,7 +87,10 @@ public:
virtual bool showBorder() const = 0;
/// if true, world view overlay will be shown
virtual bool showOverlay() const = 0;
virtual bool showImageOverlay() const = 0;
// if true, new text overlay will be shown
virtual bool showTextOverlay() const = 0;
/// if true, map grid should be visible on map
virtual bool showGrid() const = 0;

View File

@ -113,7 +113,7 @@ const CGPath * MapRendererBaseContext::currentPath() const
size_t MapRendererBaseContext::objectGroupIndex(ObjectInstanceID objectID) const
{
static const std::vector<size_t> idleGroups = {0, 13, 0, 1, 2, 3, 4, 15, 14};
static const std::array<size_t, 9> idleGroups = {0, 13, 0, 1, 2, 3, 4, 15, 14};
return idleGroups[getObjectRotation(objectID)];
}
@ -156,6 +156,16 @@ size_t MapRendererBaseContext::overlayImageIndex(const int3 & coordinates) const
return std::numeric_limits<size_t>::max();
}
std::string MapRendererBaseContext::overlayText(const int3 & coordinates) const
{
return {};
}
ColorRGBA MapRendererBaseContext::overlayTextColor(const int3 & coordinates) const
{
return {};
}
double MapRendererBaseContext::viewTransitionProgress() const
{
return 0;
@ -181,7 +191,12 @@ bool MapRendererBaseContext::showBorder() const
return false;
}
bool MapRendererBaseContext::showOverlay() const
bool MapRendererBaseContext::showImageOverlay() const
{
return false;
}
bool MapRendererBaseContext::showTextOverlay() const
{
return false;
}
@ -253,6 +268,59 @@ size_t MapRendererAdventureContext::terrainImageIndex(size_t groupSize) const
return frameIndex;
}
std::string MapRendererAdventureContext::overlayText(const int3 & coordinates) const
{
if(!isVisible(coordinates))
return {};
const auto & tile = getMapTile(coordinates);
if (!tile.visitable)
return {};
return tile.visitableObjects.back()->getObjectName();
}
ColorRGBA MapRendererAdventureContext::overlayTextColor(const int3 & coordinates) const
{
if(!isVisible(coordinates))
return {};
const auto & tile = getMapTile(coordinates);
if (!tile.visitable)
return {};
auto * object = tile.visitableObjects.back();
if (object->getOwner() == LOCPLINT->playerID)
return { 0, 192, 0};
if (LOCPLINT->cb->getPlayerRelations(object->getOwner(), LOCPLINT->playerID) == PlayerRelations::ALLIES)
return { 0, 128, 255};
if (object->getOwner().isValidPlayer())
return { 255, 0, 0};
if (object->ID == MapObjectID::MONSTER)
return { 255, 0, 0};
auto hero = LOCPLINT->localState->getCurrentHero();
if (hero)
{
if (object->wasVisited(hero))
return { 160, 160, 160 };
}
else
{
if (object->wasVisited(LOCPLINT->playerID))
return { 160, 160, 160 };
}
return { 255, 192, 0 };
}
bool MapRendererAdventureContext::showBorder() const
{
return true;
@ -273,6 +341,11 @@ bool MapRendererAdventureContext::showBlocked() const
return settingShowBlocked;
}
bool MapRendererAdventureContext::showTextOverlay() const
{
return settingTextOverlay;
}
bool MapRendererAdventureContext::showSpellRange(const int3 & position) const
{
if (!settingSpellRange)
@ -330,7 +403,7 @@ size_t MapRendererAdventureMovingContext::objectGroupIndex(ObjectInstanceID obje
{
if(target == objectID)
{
static const std::vector<size_t> moveGroups = {0, 10, 5, 6, 7, 8, 9, 12, 11};
static const std::array<size_t, 9> moveGroups = {0, 10, 5, 6, 7, 8, 9, 12, 11};
return moveGroups[getObjectRotation(objectID)];
}
return MapRendererAdventureContext::objectGroupIndex(objectID);
@ -411,7 +484,7 @@ MapRendererWorldViewContext::MapRendererWorldViewContext(const MapRendererContex
{
}
bool MapRendererWorldViewContext::showOverlay() const
bool MapRendererWorldViewContext::showImageOverlay() const
{
return true;
}

View File

@ -48,13 +48,16 @@ public:
size_t objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const override;
size_t terrainImageIndex(size_t groupSize) const override;
size_t overlayImageIndex(const int3 & coordinates) const override;
std::string overlayText(const int3 & coordinates) const override;
ColorRGBA overlayTextColor(const int3 & coordinates) const override;
double viewTransitionProgress() const override;
bool filterGrayscale() const override;
bool showRoads() const override;
bool showRivers() const override;
bool showBorder() const override;
bool showOverlay() const override;
bool showImageOverlay() const override;
bool showTextOverlay() const override;
bool showGrid() const override;
bool showVisitable() const override;
bool showBlocked() const override;
@ -69,6 +72,7 @@ public:
bool settingShowVisitable = false;
bool settingShowBlocked = false;
bool settingSpellRange= false;
bool settingTextOverlay = false;
bool settingsAdventureObjectAnimation = true;
bool settingsAdventureTerrainAnimation = true;
@ -77,11 +81,14 @@ public:
const CGPath * currentPath() const override;
size_t objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const override;
size_t terrainImageIndex(size_t groupSize) const override;
std::string overlayText(const int3 & coordinates) const override;
ColorRGBA overlayTextColor(const int3 & coordinates) const override;
bool showBorder() const override;
bool showGrid() const override;
bool showVisitable() const override;
bool showBlocked() const override;
bool showTextOverlay() const override;
bool showSpellRange(const int3 & position) const override;
};
@ -133,7 +140,7 @@ public:
explicit MapRendererWorldViewContext(const MapRendererContextState & viewState);
size_t overlayImageIndex(const int3 & coordinates) const override;
bool showOverlay() const override;
bool showImageOverlay() const override;
};
class MapRendererSpellViewContext : public MapRendererWorldViewContext

View File

@ -18,9 +18,12 @@
#include "../render/CAnimation.h"
#include "../render/Canvas.h"
#include "../render/IImage.h"
#include "../render/IFont.h"
#include "../render/IRenderHandler.h"
#include "../render/Graphics.h"
#include "../gui/CGuiHandler.h"
#include "../widgets/TextControls.h"
#include "../../lib/mapObjects/CObjectHandler.h"
#include "../../lib/int3.h"
@ -30,6 +33,7 @@ MapViewCache::~MapViewCache() = default;
MapViewCache::MapViewCache(const std::shared_ptr<MapViewModel> & model)
: model(model)
, cachedLevel(0)
, overlayWasVisible(false)
, mapRenderer(new MapRenderer())
, iconsStorage(GH.renderHandler().loadAnimation(AnimationPath::builtin("VwSymbol"), EImageBlitMode::COLORKEY))
, intermediate(new Canvas(Point(32, 32)))
@ -137,7 +141,9 @@ void MapViewCache::update(const std::shared_ptr<IMapRendererContext> & context)
void MapViewCache::render(const std::shared_ptr<IMapRendererContext> & context, Canvas & target, bool fullRedraw)
{
bool mapMoved = (cachedPosition != model->getMapViewCenter());
bool lazyUpdate = !mapMoved && !fullRedraw && vstd::isAlmostZero(context->viewTransitionProgress());
bool overlayVisible = context->showImageOverlay() || context->showTextOverlay();
bool overlayVisibilityChanged = overlayVisible != overlayWasVisible;
bool lazyUpdate = !overlayVisibilityChanged && !mapMoved && !fullRedraw && vstd::isAlmostZero(context->viewTransitionProgress());
Rect dimensions = model->getTilesTotalRect();
@ -161,18 +167,18 @@ void MapViewCache::render(const std::shared_ptr<IMapRendererContext> & context,
}
}
if(context->showOverlay())
if(context->showImageOverlay())
{
for(int y = dimensions.top(); y < dimensions.bottom(); ++y)
{
for(int x = dimensions.left(); x < dimensions.right(); ++x)
{
int3 tile(x, y, model->getLevel());
Rect targetRect = model->getTargetTileArea(tile);
auto overlay = getOverlayImageForTile(context, tile);
if(overlay)
{
Rect targetRect = model->getTargetTileArea(tile);
Point position = targetRect.center() - overlay->dimensions() / 2;
target.draw(overlay, position);
}
@ -180,10 +186,42 @@ void MapViewCache::render(const std::shared_ptr<IMapRendererContext> & context,
}
}
if(context->showTextOverlay())
{
for(int y = dimensions.top(); y < dimensions.bottom(); ++y)
{
for(int x = dimensions.left(); x < dimensions.right(); ++x)
{
int3 tile(x, y, model->getLevel());
auto overlay = context->overlayText(tile);
if(!overlay.empty())
{
Rect targetRect = model->getTargetTileArea(tile);
Point position = targetRect.center();
if (x % 2 == 0)
position.y += targetRect.h / 4;
else
position.y -= targetRect.h / 4;
const auto font = graphics->fonts[EFonts::FONT_TINY];
Point dimensions(font->getStringWidth(overlay), font->getLineHeight());
Rect textRect = Rect(position - dimensions / 2, dimensions).resize(2);
target.drawColor(textRect, context->overlayTextColor(tile));
target.drawBorder(textRect, Colors::BRIGHT_YELLOW);
target.drawText(position, EFonts::FONT_TINY, Colors::BLACK, ETextAlignment::CENTER, overlay);
}
}
}
}
if(!vstd::isAlmostZero(context->viewTransitionProgress()))
target.drawTransparent(*terrainTransition, Point(0, 0), 1.0 - context->viewTransitionProgress());
cachedPosition = model->getMapViewCenter();
overlayWasVisible = overlayVisible;
}
void MapViewCache::createTransitionSnapshot(const std::shared_ptr<IMapRendererContext> & context)

View File

@ -44,6 +44,7 @@ class MapViewCache
Point cachedSize;
Point cachedPosition;
int cachedLevel;
bool overlayWasVisible;
std::shared_ptr<MapViewModel> model;

View File

@ -224,6 +224,7 @@ void MapViewController::updateState()
adventureContext->settingShowVisitable = settings["session"]["showVisitable"].Bool();
adventureContext->settingShowBlocked = settings["session"]["showBlocked"].Bool();
adventureContext->settingSpellRange = settings["session"]["showSpellRange"].Bool();
adventureContext->settingTextOverlay = GH.isKeyboardAltDown();
}
}

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

@ -30,7 +30,7 @@ CTextInput::CTextInput(const Rect & Pos)
pos.h = Pos.h;
pos.w = Pos.w;
addUsedEvents(LCLICK | KEYBOARD | TEXTINPUT);
addUsedEvents(LCLICK | SHOW_POPUP | KEYBOARD | TEXTINPUT);
}
void CTextInput::createLabel(bool giveFocusToInput)
@ -106,6 +106,11 @@ void CTextInput::setCallback(const TextEditedCallback & cb)
onTextEdited = cb;
}
void CTextInput::setPopupCallback(const std::function<void()> & cb)
{
callbackPopup = cb;
}
void CTextInput::setFilterFilename()
{
assert(!onTextFiltering);
@ -122,6 +127,12 @@ std::string CTextInput::getVisibleText() const
return hasFocus() ? currentText + composedText + "_" : currentText;
}
void CTextInput::showPopupWindow(const Point & cursorPosition)
{
if(callbackPopup)
callbackPopup();
}
void CTextInput::clickPressed(const Point & cursorPosition)
{
// attempt to give focus unconditionally, even if we already have it

View File

@ -14,6 +14,7 @@
#include "../render/EFont.h"
#include "../../lib/filesystem/ResourcePath.h"
#include "../../lib/FunctionList.h"
class CLabel;
class IImage;
@ -58,6 +59,7 @@ class CTextInput final : public CFocusable
TextEditedCallback onTextEdited;
TextFilterCallback onTextFiltering;
CFunctionList<void()> callbackPopup;
//Filter that will block all characters not allowed in filenames
static void filenameFilter(std::string & text, const std::string & oldText);
@ -74,6 +76,7 @@ class CTextInput final : public CFocusable
void textEdited(const std::string & enteredText) final;
void onFocusGot() final;
void onFocusLost() final;
void showPopupWindow(const Point & cursorPosition) final;
CTextInput(const Rect & Pos);
public:
@ -89,6 +92,9 @@ public:
/// Set callback that will be called whenever player enters new text
void setCallback(const TextEditedCallback & cb);
/// Set callback when player want to open popup
void setPopupCallback(const std::function<void()> & cb);
/// Enables filtering entered text that ensures that text is valid filename (existing or not)
void setFilterFilename();
/// Enable filtering entered text that ensures that text is valid number in provided range [min, max]

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

@ -37,6 +37,7 @@
#include "../../lib/CHeroHandler.h"
#include "../../lib/GameSettings.h"
#include "../../lib/CSkillHandler.h"
#include "../../lib/StartInfo.h"
#include "../../lib/mapObjects/CGHeroInstance.h"
#include "../../lib/mapObjects/CGTownInstance.h"
#include "../../lib/mapObjects/MiscObjects.h"
@ -586,15 +587,16 @@ void CKingdomInterface::generateMinesList(const std::vector<const CGObjectInstan
minesCount[mine->producedResource]++;
if (mine->producedResource == EGameResID::GOLD)
totalIncome += mine->producedQuantity;
totalIncome += mine->getProducedQuantity();
}
}
//Heroes can produce gold as well - skill, specialty or arts
std::vector<const CGHeroInstance*> heroes = LOCPLINT->cb->getHeroesInfo(true);
auto * playerSettings = LOCPLINT->cb->getPlayerSettings(LOCPLINT->playerID);
for(auto & hero : heroes)
{
totalIncome += hero->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, BonusSubtypeID(GameResID(EGameResID::GOLD))));
totalIncome += hero->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, BonusSubtypeID(GameResID(EGameResID::GOLD)))) * playerSettings->handicap.percentIncome / 100;
}
//Add town income of all towns
@ -605,8 +607,8 @@ void CKingdomInterface::generateMinesList(const std::vector<const CGObjectInstan
}
//if player has some modded boosts we want to show that as well
totalIncome += LOCPLINT->cb->getPlayerState(LOCPLINT->playerID)->valOfBonuses(BonusType::RESOURCES_CONSTANT_BOOST, BonusSubtypeID(GameResID(EGameResID::GOLD)));
totalIncome += LOCPLINT->cb->getPlayerState(LOCPLINT->playerID)->valOfBonuses(BonusType::RESOURCES_TOWN_MULTIPLYING_BOOST, BonusSubtypeID(GameResID(EGameResID::GOLD))) * towns.size();
totalIncome += LOCPLINT->cb->getPlayerState(LOCPLINT->playerID)->valOfBonuses(BonusType::RESOURCES_CONSTANT_BOOST, BonusSubtypeID(GameResID(EGameResID::GOLD))) * playerSettings->handicap.percentIncome / 100;
totalIncome += LOCPLINT->cb->getPlayerState(LOCPLINT->playerID)->valOfBonuses(BonusType::RESOURCES_TOWN_MULTIPLYING_BOOST, BonusSubtypeID(GameResID(EGameResID::GOLD))) * towns.size() * playerSettings->handicap.percentIncome / 100;
for(int i=0; i<7; i++)
{

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

@ -150,10 +150,11 @@
"lobbyRandomMap": "R",
"lobbyRandomTown": "T",
"lobbyRandomTownVs": "V",
"lobbyHandicap": "H",
"lobbyReplayVideo": "R",
"lobbySaveGame": [ "S", "Return", "Keypad Enter"],
"lobbySelectScenario": "S",
"lobbyToggleChat": "H",
"lobbyToggleChat": "C",
"lobbyTurnOptions": "T",
"mainMenuBack": [ "B", "Escape" ],
"mainMenuCampaign": "C",

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

@ -45,6 +45,27 @@ It can be found at https://aur.archlinux.org/packages/vcmi-git/
Information about building packages from the Arch User Repository (AUR) can be found at the Arch wiki.
### On NixOS or Nix
On NixOS or any system with nix available, [it is recommended](https://nixos.wiki/wiki/C) to use nix-shell. Create a shell.nix file with the following content:
```nix
with import <nixpkgs> {};
stdenv.mkDerivation {
name = "build";
nativeBuildInputs = [ cmake ];
buildInputs = [
cmake clang clang-tools llvm ccache ninja
boost zlib minizip xz
SDL2 SDL2_ttf SDL2_net SDL2_image SDL2_sound SDL2_mixer SDL2_gfx
ffmpeg tbb vulkan-headers libxkbcommon
qt6.full luajit
];
}
```
And put it into build directory. Then run `nix-shell` before running any build commands.
## Getting the sources
We recommend the following directory structure:

View File

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

View File

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

View File

@ -463,6 +463,7 @@ set(lib_MAIN_HEADERS
gameState/CGameState.h
gameState/CGameStateCampaign.h
gameState/EVictoryLossCheckResult.h
gameState/HighScore.h
gameState/InfoAboutArmy.h
gameState/RumorState.h
gameState/SThievesGuildInfo.h

View File

@ -25,7 +25,7 @@
VCMI_LIB_NAMESPACE_BEGIN
PlayerSettings::PlayerSettings()
: bonus(PlayerStartingBonus::RANDOM), color(0), handicap(NO_HANDICAP), compOnly(false)
: bonus(PlayerStartingBonus::RANDOM), color(0), compOnly(false)
{
}

View File

@ -16,6 +16,7 @@
#include "ExtraOptionsInfo.h"
#include "campaign/CampaignConstants.h"
#include "serializer/Serializeable.h"
#include "ResourceSet.h"
VCMI_LIB_NAMESPACE_BEGIN
@ -65,6 +66,20 @@ enum class PlayerStartingBonus : int8_t
RESOURCE = 2
};
struct DLL_LINKAGE Handicap {
TResources startBonus = TResources();
int percentIncome = 100;
int percentGrowth = 100;
template <typename Handler>
void serialize(Handler &h)
{
h & startBonus;
h & percentIncome;
h & percentGrowth;
}
};
/// Struct which describes the name, the color, the starting bonus of a player
struct DLL_LINKAGE PlayerSettings
{
@ -77,8 +92,8 @@ struct DLL_LINKAGE PlayerSettings
std::string heroNameTextId;
PlayerColor color; //from 0 -
enum EHandicap {NO_HANDICAP, MILD, SEVERE};
EHandicap handicap;//0-no, 1-mild, 2-severe
Handicap handicap;
std::string name;
std::set<ui8> connectedPlayerIDs; //Empty - AI, or connectrd player ids
@ -92,7 +107,14 @@ struct DLL_LINKAGE PlayerSettings
h & heroNameTextId;
h & bonus;
h & color;
h & handicap;
if (h.version >= Handler::Version::PLAYER_HANDICAP)
h & handicap;
else
{
enum EHandicap {NO_HANDICAP, MILD, SEVERE};
EHandicap handicapLegacy = NO_HANDICAP;
h & handicapLegacy;
}
h & name;
h & connectedPlayerIDs;
h & compOnly;

View File

@ -50,6 +50,12 @@ static bool sameSideOfWall(BattleHex pos1, BattleHex pos2)
return stackLeft == destLeft;
}
static bool isInsideWalls(BattleHex pos)
{
const int wallInStackLine = lineToWallHex(pos.getY());
return wallInStackLine < pos;
}
// parts of wall
static const std::pair<int, EWallPart> wallParts[] =
{
@ -128,6 +134,8 @@ ESpellCastProblem CBattleInfoCallback::battleCanCastSpell(const spells::Caster *
return ESpellCastProblem::NO_HERO_TO_CAST_SPELL;
if(hero->hasBonusOfType(BonusType::BLOCK_ALL_MAGIC))
return ESpellCastProblem::MAGIC_IS_BLOCKED;
if(!hero->hasSpellbook())
return ESpellCastProblem::NO_SPELLBOOK;
}
break;
default:
@ -158,6 +166,11 @@ std::pair< std::vector<BattleHex>, int > CBattleInfoCallback::getPath(BattleHex
return std::make_pair(path, reachability.distances[dest]);
}
bool CBattleInfoCallback::battleIsInsideWalls(BattleHex from) const
{
return isInsideWalls(from);
}
bool CBattleInfoCallback::battleHasPenaltyOnLine(BattleHex from, BattleHex dest, bool checkWall, bool checkMoat) const
{
auto isTileBlocked = [&](BattleHex tile)
@ -742,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

@ -104,6 +104,7 @@ public:
DamageEstimation battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPosition, DamageEstimation * retaliationDmg = nullptr) const;
DamageEstimation battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int getMovementRange, DamageEstimation * retaliationDmg = nullptr) const;
bool battleIsInsideWalls(BattleHex from) const;
bool battleHasPenaltyOnLine(BattleHex from, BattleHex dest, bool checkWall, bool checkMoat) const;
bool battleHasDistancePenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const;
bool battleHasWallPenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const;

View File

@ -53,7 +53,7 @@ public:
JsonNode toJsonNode() const;
};
#define BONUS_TREE_DESERIALIZATION_FIX if(!h.saving && h.smartPointerSerialization) deserializationFix();
#define BONUS_TREE_DESERIALIZATION_FIX if(!h.saving && h.loadingGamestate) deserializationFix();
/// Struct for handling bonuses of several types. Can be transferred to any hero
struct DLL_LINKAGE Bonus : public std::enable_shared_from_this<Bonus>, public Serializeable

View File

@ -15,6 +15,7 @@
#include "../texts/TextLocalizationContainer.h"
#include "CampaignConstants.h"
#include "CampaignScenarioPrologEpilog.h"
#include "../gameState/HighScore.h"
VCMI_LIB_NAMESPACE_BEGIN
@ -318,6 +319,8 @@ public:
std::string campaignSet;
std::vector<HighScoreParameter> highscoreParameters;
template <typename Handler> void serialize(Handler &h)
{
h & static_cast<Campaign&>(*this);
@ -330,6 +333,8 @@ public:
h & campaignSet;
if (h.version >= Handler::Version::CAMPAIGN_MAP_TRANSLATIONS)
h & mapTranslations;
if (h.version >= Handler::Version::HIGHSCORE_PARAMETERS)
h & highscoreParameters;
}
};

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

@ -387,10 +387,14 @@ void CGameState::initDifficulty()
const JsonNode & difficultyAI(config["ai"][GameConstants::DIFFICULTY_NAMES[scenarioOps->difficulty]]);
const JsonNode & difficultyHuman(config["human"][GameConstants::DIFFICULTY_NAMES[scenarioOps->difficulty]]);
auto setDifficulty = [](PlayerState & state, const JsonNode & json)
auto setDifficulty = [this](PlayerState & state, const JsonNode & json)
{
//set starting resources
state.resources = TResources(json["resources"]);
//handicap
const PlayerSettings &ps = scenarioOps->getIthPlayersSettings(state.color);
state.resources += ps.handicap.startBonus;
//set global bonuses
for(auto & jsonBonus : json["globalBonuses"].Vector())
@ -1614,7 +1618,7 @@ struct statsHLP
}
// get total gold income
static int getIncome(const PlayerState * ps)
static int getIncome(const PlayerState * ps, int percentIncome)
{
int totalIncome = 0;
const CGObjectInstance * heroOrTown = nullptr;
@ -1622,7 +1626,7 @@ struct statsHLP
//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))));
totalIncome += h->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, BonusSubtypeID(GameResID(GameResID::GOLD)))) * percentIncome / 100;
if(!heroOrTown)
heroOrTown = h;
@ -1657,7 +1661,7 @@ struct statsHLP
assert(mine);
if (mine->producedResource == EGameResID::GOLD)
totalIncome += mine->producedQuantity;
totalIncome += mine->getProducedQuantity();
}
}
@ -1747,7 +1751,7 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level)
}
if(level >= 5) //income
{
FILL_FIELD(income, statsHLP::getIncome(&g->second))
FILL_FIELD(income, statsHLP::getIncome(&g->second, scenarioOps->getIthPlayersSettings(g->second.color).handicap.percentIncome))
}
if(level >= 2) //best hero's stats
{

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;

41
lib/gameState/HighScore.h Normal file
View File

@ -0,0 +1,41 @@
/*
* HighScore.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
VCMI_LIB_NAMESPACE_BEGIN
class DLL_LINKAGE HighScoreParameter
{
public:
int difficulty;
int day;
int townAmount;
bool usedCheat;
bool hasGrail;
bool allEnemiesDefeated;
std::string campaignName;
std::string scenarioName;
std::string playerName;
template <typename Handler> void serialize(Handler &h)
{
h & difficulty;
h & day;
h & townAmount;
h & usedCheat;
h & hasGrail;
h & allEnemiesDefeated;
h & campaignName;
h & scenarioName;
h & playerName;
}
};
VCMI_LIB_NAMESPACE_END

View File

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

View File

@ -1717,6 +1717,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);
{
@ -1732,13 +1742,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

@ -137,6 +137,14 @@ GrowthInfo CGTownInstance::getGrowthInfo(int level) const
const int base = creature->getGrowth();
int castleBonus = 0;
if(tempOwner.isValidPlayer())
{
auto * playerSettings = cb->getPlayerSettings(tempOwner);
ret.handicapPercentage = playerSettings->handicap.percentGrowth;
}
else
ret.handicapPercentage = 100;
ret.entries.emplace_back(VLC->generaltexth->allTexts[590], base); // \n\nBasic growth %d"
if (hasBuilt(BuildingID::CASTLE))
@ -215,6 +223,11 @@ TResources CGTownInstance::dailyIncome() const
ret += p.second->produce;
}
}
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;
return ret;
}
@ -1257,7 +1270,8 @@ int GrowthInfo::totalGrowth() const
for(const Entry &entry : entries)
ret += entry.count;
return ret;
// always round up income - we don't want buildings to always produce zero if handicap in use
return (ret * handicapPercentage + 99) / 100;
}
void CGTownInstance::fillUpgradeInfo(UpgradeInfo & info, const CStackInstance &stack) const

View File

@ -41,6 +41,7 @@ struct DLL_LINKAGE GrowthInfo
std::vector<Entry> entries;
int totalGrowth() const;
int handicapPercentage;
};
class DLL_LINKAGE CGTownInstance : public CGDwelling, public IShipyard, public IMarket, public INativeTerrainProvider, public ICreatureUpgrader

View File

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

View File

@ -23,6 +23,7 @@
#include "../gameState/CGameState.h"
#include "../mapping/CMap.h"
#include "../CPlayerState.h"
#include "../StartInfo.h"
#include "../serializer/JsonSerializeFormat.h"
#include "../mapObjectConstructors/AObjectTypeHandler.h"
#include "../mapObjectConstructors/CObjectClassesHandler.h"
@ -103,7 +104,7 @@ void CGMine::newTurn(vstd::RNG & rand) const
if (tempOwner == PlayerColor::NEUTRAL)
return;
cb->giveResource(tempOwner, producedResource, producedQuantity);
cb->giveResource(tempOwner, producedResource, getProducedQuantity());
}
void CGMine::initObj(vstd::RNG & rand)
@ -177,7 +178,7 @@ void CGMine::flagMine(const PlayerColor & player) const
iw.type = EInfoWindowMode::AUTO;
iw.text.appendTextID(TextIdentifier("core.mineevnt", producedResource.getNum()).get()); //not use subID, abandoned mines uses default mine texts
iw.player = player;
iw.components.emplace_back(ComponentType::RESOURCE_PER_DAY, producedResource, producedQuantity);
iw.components.emplace_back(ComponentType::RESOURCE_PER_DAY, producedResource, getProducedQuantity());
cb->showInfoDialog(&iw);
}
@ -195,6 +196,13 @@ ui32 CGMine::defaultResProduction() const
}
}
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;
}
void CGMine::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const
{
if(result.winner == 0) //attacker won

View File

@ -181,6 +181,7 @@ public:
h & abandonedMineResources;
}
ui32 defaultResProduction() const;
ui32 getProducedQuantity() const;
protected:
void serializeJsonOptions(JsonSerializeFormat & handler) override;

View File

@ -168,6 +168,7 @@ public:
virtual void visitLobbyChangePlayerOption(LobbyChangePlayerOption & pack) {}
virtual void visitLobbySetPlayer(LobbySetPlayer & pack) {}
virtual void visitLobbySetPlayerName(LobbySetPlayerName & pack) {}
virtual void visitLobbySetPlayerHandicap(LobbySetPlayerHandicap & pack) {}
virtual void visitLobbySetSimturns(LobbySetSimturns & pack) {}
virtual void visitLobbySetTurnTime(LobbySetTurnTime & pack) {}
virtual void visitLobbySetExtraOptions(LobbySetExtraOptions & pack) {}

View File

@ -790,6 +790,11 @@ void LobbySetPlayerName::visitTyped(ICPackVisitor & visitor)
visitor.visitLobbySetPlayerName(*this);
}
void LobbySetPlayerHandicap::visitTyped(ICPackVisitor & visitor)
{
visitor.visitLobbySetPlayerHandicap(*this);
}
void LobbySetSimturns::visitTyped(ICPackVisitor & visitor)
{
visitor.visitLobbySetSimturns(*this);

View File

@ -155,9 +155,13 @@ struct DLL_LINKAGE LobbyStartGame : public CLobbyPackToPropagate
template <typename Handler> void serialize(Handler &h)
{
if (!h.saving)
h.loadingGamestate = true;
h & clientId;
h & initializedStartInfo;
h & initializedGameState;
if (!h.saving)
h.loadingGamestate = false;
}
};
@ -281,6 +285,20 @@ struct DLL_LINKAGE LobbySetPlayerName : public CLobbyPackToServer
}
};
struct DLL_LINKAGE LobbySetPlayerHandicap : public CLobbyPackToServer
{
PlayerColor color = PlayerColor::CANNOT_DETERMINE;
Handicap handicap = Handicap();
void visitTyped(ICPackVisitor & visitor) override;
template <typename Handler> void serialize(Handler &h)
{
h & color;
h & handicap;
}
};
struct DLL_LINKAGE LobbySetSimturns : public CLobbyPackToServer
{
SimturnsInfo simturnsInfo;

View File

@ -55,6 +55,7 @@ void registerTypesLobbyPacks(Serializer &s)
s.template registerType<CLobbyPackToServer, LobbySetCampaignBonus>();
s.template registerType<CLobbyPackToServer, LobbySetPlayer>();
s.template registerType<CLobbyPackToServer, LobbySetPlayerName>();
s.template registerType<CLobbyPackToServer, LobbySetPlayerHandicap>();
s.template registerType<CLobbyPackToServer, LobbySetTurnTime>();
s.template registerType<CLobbyPackToServer, LobbySetSimturns>();
s.template registerType<CLobbyPackToServer, LobbySetDifficulty>();

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

@ -15,9 +15,7 @@ VCMI_LIB_NAMESPACE_BEGIN
BinaryDeserializer::BinaryDeserializer(IBinaryReader * r): CLoaderBase(r)
{
saving = false;
version = Version::NONE;
smartPointerSerialization = true;
reverseEndianness = false;
registerTypes(*this);

View File

@ -165,8 +165,9 @@ public:
std::map<uint32_t, Serializeable*> loadedPointers;
std::map<const Serializeable*, std::shared_ptr<Serializeable>> loadedSharedPointers;
IGameCallback * cb = nullptr;
bool smartPointerSerialization;
bool saving;
static constexpr bool trackSerializedPointers = true;
static constexpr bool saving = false;
bool loadingGamestate = false;
bool hasFeature(Version what) const
{
@ -342,7 +343,7 @@ public:
}
uint32_t pid = 0xffffffff; //pointer id (or maybe rather pointee id)
if(smartPointerSerialization)
if(trackSerializedPointers)
{
load( pid ); //get the id
auto i = loadedPointers.find(pid); //lookup
@ -383,7 +384,7 @@ public:
template <typename T>
void ptrAllocated(T *ptr, uint32_t pid)
{
if(smartPointerSerialization && pid != 0xffffffff)
if(trackSerializedPointers && pid != 0xffffffff)
loadedPointers[pid] = const_cast<Serializeable*>(dynamic_cast<const Serializeable*>(ptr)); //add loaded pointer to our lookup map; cast is to avoid errors with const T* pt
}

View File

@ -15,8 +15,6 @@ VCMI_LIB_NAMESPACE_BEGIN
BinarySerializer::BinarySerializer(IBinaryWriter * w): CSaverBase(w)
{
saving=true;
smartPointerSerialization = true;
registerTypes(*this);
}

View File

@ -118,8 +118,9 @@ public:
std::map<const Serializeable*, uint32_t> savedPointers;
Version version = Version::CURRENT;
bool smartPointerSerialization;
bool saving;
static constexpr bool trackSerializedPointers = true;
static constexpr bool saving = true;
bool loadingGamestate = false;
bool hasFeature(Version what) const
{
@ -257,7 +258,7 @@ public:
return;
}
if(smartPointerSerialization)
if(trackSerializedPointers)
{
// We might have an object that has multiple inheritance and store it via the non-first base pointer.
// Therefore, all pointers need to be normalized to the actual object address.

View File

@ -29,6 +29,7 @@ int CLoadFile::read(std::byte * data, unsigned size)
void CLoadFile::openNextFile(const boost::filesystem::path & fname, ESerializationVersion minimalVersion)
{
serializer.loadingGamestate = true;
assert(!serializer.reverseEndianness);
assert(minimalVersion <= ESerializationVersion::CURRENT);

View File

@ -151,18 +151,6 @@ void CConnection::enterGameplayConnectionMode(CGameState * gs)
enableSmartVectorMemberSerializatoin(gs);
}
void CConnection::disableSmartPointerSerialization()
{
deserializer->smartPointerSerialization = false;
serializer->smartPointerSerialization = false;
}
void CConnection::enableSmartPointerSerialization()
{
deserializer->smartPointerSerialization = true;
serializer->smartPointerSerialization = true;
}
void CConnection::disableSmartVectorMemberSerialization()
{
packReader->smartVectorMembersSerialization = false;

View File

@ -38,8 +38,6 @@ class DLL_LINKAGE CConnection : boost::noncopyable
void disableStackSendingByID();
void enableStackSendingByID();
void disableSmartPointerSerialization();
void enableSmartPointerSerialization();
void disableSmartVectorMemberSerialization();
void enableSmartVectorMemberSerializatoin(CGameState * gs);

View File

@ -57,6 +57,8 @@ enum class ESerializationVersion : int32_t
SIMPLE_TEXT_CONTAINER_SERIALIZATION, // 847 - text container is serialized using common routine instead of custom approach
MAP_FORMAT_ADDITIONAL_INFOS, // 848 - serialize new infos in map format
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
CURRENT = REMOVE_LIB_RNG
CURRENT = PLAYER_HANDICAP
};

View File

@ -367,6 +367,11 @@ void MetaString::appendName(const CreatureID & id, TQuantity count)
appendNamePlural(id);
}
void MetaString::appendName(const GameResID& id)
{
appendTextID(TextIdentifier("core.restypes", id.getNum()).get());
}
void MetaString::appendNameSingular(const CreatureID & id)
{
appendTextID(id.toEntity(VLC)->getNameSingularTextID());

View File

@ -80,6 +80,7 @@ public:
void appendName(const SpellID& id);
void appendName(const PlayerColor& id);
void appendName(const CreatureID & id, TQuantity count);
void appendName(const GameResID& id);
void appendNameSingular(const CreatureID & id);
void appendNamePlural(const CreatureID & id);
void appendEOL();

View File

@ -694,6 +694,15 @@ void CGameHandler::onNewTurn()
}
}
for (auto & player : gs->players)
{
if (player.second.status != EPlayerStatus::INGAME)
continue;
if (player.second.heroes.empty() && player.second.towns.empty())
throw std::runtime_error("Invalid player in player state! Player " + std::to_string(player.first.getNum()) + ", map name: " + gs->map->name.toString() + ", map description: " + gs->map->description.toString());
}
if (newWeek && !firstTurn)
{
n.specialWeek = NewTurn::NORMAL;
@ -760,6 +769,8 @@ void CGameHandler::onNewTurn()
continue;
assert(elem.first.isValidPlayer());//illegal player number!
auto playerSettings = gameState()->scenarioOps->getIthPlayersSettings(elem.first);
std::pair<PlayerColor, si32> playerGold(elem.first, elem.second.resources[EGameResID::GOLD]);
hadGold.insert(playerGold);
@ -773,8 +784,8 @@ void CGameHandler::onNewTurn()
{
for (GameResID k = GameResID::WOOD; k < GameResID::COUNT; k++)
{
n.res[elem.first][k] += elem.second.valOfBonuses(BonusType::RESOURCES_CONSTANT_BOOST, BonusSubtypeID(k));
n.res[elem.first][k] += elem.second.valOfBonuses(BonusType::RESOURCES_TOWN_MULTIPLYING_BOOST, BonusSubtypeID(k)) * elem.second.towns.size();
n.res[elem.first][k] += elem.second.valOfBonuses(BonusType::RESOURCES_CONSTANT_BOOST, BonusSubtypeID(k)) * playerSettings.handicap.percentIncome / 100;
n.res[elem.first][k] += elem.second.valOfBonuses(BonusType::RESOURCES_TOWN_MULTIPLYING_BOOST, BonusSubtypeID(k)) * elem.second.towns.size() * playerSettings.handicap.percentIncome / 100;
}
if(newWeek) //weekly crystal generation if 1 or more crystal dragons in any hero army or town garrison
@ -806,7 +817,7 @@ void CGameHandler::onNewTurn()
}
}
if(hasCrystalGenCreature)
n.res[elem.first][EGameResID::CRYSTAL] += 3;
n.res[elem.first][EGameResID::CRYSTAL] += 3 * playerSettings.handicap.percentIncome / 100;
}
}
@ -828,7 +839,7 @@ void CGameHandler::onNewTurn()
{
for (GameResID k = GameResID::WOOD; k < GameResID::COUNT; k++)
{
n.res[elem.first][k] += h->valOfBonuses(BonusType::GENERATE_RESOURCE, BonusSubtypeID(k));
n.res[elem.first][k] += h->valOfBonuses(BonusType::GENERATE_RESOURCE, BonusSubtypeID(k)) * playerSettings.handicap.percentIncome / 100;
}
}
}

View File

@ -624,8 +624,6 @@ void CVCMIServer::updateStartInfoOnMapChange(std::shared_ptr<CMapInfo> mapInfo,
pset.heroNameTextId = pinfo.mainCustomHeroNameTextId;
pset.heroPortrait = pinfo.mainCustomHeroPortrait;
}
pset.handicap = PlayerSettings::NO_HANDICAP;
}
if(mi->isRandomMap && mapGenOpts)
@ -765,6 +763,60 @@ void CVCMIServer::setPlayerName(PlayerColor color, std::string name)
setPlayerConnectedId(player, nameID);
}
void CVCMIServer::setPlayerHandicap(PlayerColor color, Handicap handicap)
{
if(color == PlayerColor::CANNOT_DETERMINE)
return;
si->playerInfos[color].handicap = handicap;
int humanPlayer = 0;
for (const auto & pi : si->playerInfos)
if(pi.second.isControlledByHuman())
humanPlayer++;
if(humanPlayer < 2) // Singleplayer
return;
MetaString str;
str.appendTextID("vcmi.lobby.handicap");
str.appendRawString(" ");
str.appendName(color);
str.appendRawString(":");
if(handicap.startBonus.empty() && handicap.percentIncome == 100 && handicap.percentGrowth == 100)
{
str.appendRawString(" ");
str.appendTextID("core.genrltxt.523");
announceTxt(str);
return;
}
for(auto & res : EGameResID::ALL_RESOURCES())
if(handicap.startBonus[res] != 0)
{
str.appendRawString(" ");
str.appendName(res);
str.appendRawString(":");
str.appendRawString(std::to_string(handicap.startBonus[res]));
}
if(handicap.percentIncome != 100)
{
str.appendRawString(" ");
str.appendTextID("core.jktext.32");
str.appendRawString(":");
str.appendRawString(std::to_string(handicap.percentIncome) + "%");
}
if(handicap.percentGrowth != 100)
{
str.appendRawString(" ");
str.appendTextID("core.genrltxt.194");
str.appendRawString(":");
str.appendRawString(std::to_string(handicap.percentGrowth) + "%");
}
announceTxt(str);
}
void CVCMIServer::optionNextCastle(PlayerColor player, int dir)
{
PlayerSettings & s = si->playerInfos[player];
@ -1011,6 +1063,39 @@ void CVCMIServer::multiplayerWelcomeMessage()
gh->playerMessages->broadcastSystemMessage("Use '!help' to list available commands");
for (const auto & pi : si->playerInfos)
if(!pi.second.handicap.startBonus.empty() || pi.second.handicap.percentIncome != 100 || pi.second.handicap.percentGrowth != 100)
{
MetaString str;
str.appendTextID("vcmi.lobby.handicap");
str.appendRawString(" ");
str.appendName(pi.first);
str.appendRawString(":");
for(auto & res : EGameResID::ALL_RESOURCES())
if(pi.second.handicap.startBonus[res] != 0)
{
str.appendRawString(" ");
str.appendName(res);
str.appendRawString(":");
str.appendRawString(std::to_string(pi.second.handicap.startBonus[res]));
}
if(pi.second.handicap.percentIncome != 100)
{
str.appendRawString(" ");
str.appendTextID("core.jktext.32");
str.appendRawString(":");
str.appendRawString(std::to_string(pi.second.handicap.percentIncome) + "%");
}
if(pi.second.handicap.percentGrowth != 100)
{
str.appendRawString(" ");
str.appendTextID("core.genrltxt.194");
str.appendRawString(":");
str.appendRawString(std::to_string(pi.second.handicap.percentGrowth) + "%");
}
gh->playerMessages->broadcastSystemMessage(str);
}
std::vector<std::string> optionIds;
if(si->extraOptionsInfo.cheatsAllowed)
optionIds.emplace_back("vcmi.optionsTab.cheatAllowed.hover");

View File

@ -117,6 +117,7 @@ public:
// Work with LobbyInfo
void setPlayer(PlayerColor clickedColor);
void setPlayerName(PlayerColor player, std::string name);
void setPlayerHandicap(PlayerColor player, Handicap handicap);
void optionNextHero(PlayerColor player, int dir); //dir == -1 or +1
void optionSetHero(PlayerColor player, HeroTypeID id);
HeroTypeID nextAllowedHero(PlayerColor player, HeroTypeID id, int direction);

View File

@ -89,6 +89,7 @@ public:
void visitLobbyChangePlayerOption(LobbyChangePlayerOption & pack) override;
void visitLobbySetPlayer(LobbySetPlayer & pack) override;
void visitLobbySetPlayerName(LobbySetPlayerName & pack) override;
void visitLobbySetPlayerHandicap(LobbySetPlayerHandicap & pack) override;
void visitLobbySetTurnTime(LobbySetTurnTime & pack) override;
void visitLobbySetExtraOptions(LobbySetExtraOptions & pack) override;
void visitLobbySetSimturns(LobbySetSimturns & pack) override;

View File

@ -346,6 +346,12 @@ void ApplyOnServerNetPackVisitor::visitLobbySetPlayerName(LobbySetPlayerName & p
result = true;
}
void ApplyOnServerNetPackVisitor::visitLobbySetPlayerHandicap(LobbySetPlayerHandicap & pack)
{
srv.setPlayerHandicap(pack.color, pack.handicap);
result = true;
}
void ApplyOnServerNetPackVisitor::visitLobbySetSimturns(LobbySetSimturns & pack)
{
srv.si->simturnsInfo = pack.simturnsInfo;

View File

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

View File

@ -389,20 +389,47 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CBattleInfoCallback & bat
attack.side = next->unitSide();
attack.stackNumber = next->unitId();
//TODO: select target by priority
// TODO: unify logic with AI?
// Find best target using logic similar to H3 AI
const auto & isBetterTarget = [&battle](const battle::Unit * candidate, const battle::Unit * current)
{
bool candidateInsideWalls = battle.battleIsInsideWalls(candidate->getPosition());
bool currentInsideWalls = battle.battleIsInsideWalls(current->getPosition());
if (candidateInsideWalls != currentInsideWalls)
return candidateInsideWalls > currentInsideWalls;
// also check for war machines - shooters are more dangerous than war machines, ballista or catapult
bool candidateCanShoot = candidate->canShoot() && candidate->unitType()->warMachine == ArtifactID::NONE;
bool currentCanShoot = current->canShoot() && current->unitType()->warMachine == ArtifactID::NONE;
if (candidateCanShoot != currentCanShoot)
return candidateCanShoot > currentCanShoot;
int64_t candidateTargetValue = static_cast<int64_t>(candidate->unitType()->getAIValue() * candidate->getCount());
int64_t currentTargetValue = static_cast<int64_t>(current->unitType()->getAIValue() * current->getCount());
return candidateTargetValue > currentTargetValue;
};
const battle::Unit * target = nullptr;
for(auto & elem : battle.battleGetAllStacks(true))
{
if(elem->unitType()->getId() != CreatureID::CATAPULT
&& elem->unitOwner() != next->unitOwner()
&& elem->isValidTarget()
&& battle.battleCanShoot(next, elem->getPosition()))
{
target = elem;
break;
}
if (elem->unitOwner() == next->unitOwner())
continue;
if (!elem->isValidTarget())
continue;
if (!battle.battleCanShoot(next, elem->getPosition()))
continue;
if (target && !isBetterTarget(elem, target))
continue;
target = elem;
}
if(target == nullptr)

View File

@ -175,8 +175,6 @@ public:
pset.heroNameTextId = pinfo.mainCustomHeroNameTextId;
pset.heroPortrait = HeroTypeID(pinfo.mainCustomHeroPortrait);
}
pset.handicap = PlayerSettings::NO_HANDICAP;
}