diff --git a/AI/BattleAI/BattleAI.h b/AI/BattleAI/BattleAI.h index 56745778a..679d54f0a 100644 --- a/AI/BattleAI/BattleAI.h +++ b/AI/BattleAI/BattleAI.h @@ -80,7 +80,7 @@ public: //void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero //void actionStarted(const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero //void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack - //void battleStacksAttacked(const std::vector & bsa) override; //called when stack receives damage (after battleAttack()) + //void battleStacksAttacked(const std::vector & bsa, bool ranged) override; //called when stack receives damage (after battleAttack()) //void battleEnd(const BattleResult *br) override; //void battleResultsApplied() override; //called when all effects of last battle are applied //void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied; diff --git a/AI/Nullkiller/Engine/FuzzyEngines.cpp b/AI/Nullkiller/Engine/FuzzyEngines.cpp index 04f1fe758..ad63c0196 100644 --- a/AI/Nullkiller/Engine/FuzzyEngines.cpp +++ b/AI/Nullkiller/Engine/FuzzyEngines.cpp @@ -31,7 +31,7 @@ engineBase::engineBase() void engineBase::configure() { engine.configure("Minimum", "Maximum", "Minimum", "AlgebraicSum", "Centroid", "Proportional"); - logAi->info(engine.toString()); + logAi->trace(engine.toString()); } void engineBase::addRule(const std::string & txt) diff --git a/AI/StupidAI/StupidAI.cpp b/AI/StupidAI/StupidAI.cpp index 8658e7955..4baaa0742 100644 --- a/AI/StupidAI/StupidAI.cpp +++ b/AI/StupidAI/StupidAI.cpp @@ -177,7 +177,7 @@ void CStupidAI::battleAttack(const BattleAttack *ba) print("battleAttack called"); } -void CStupidAI::battleStacksAttacked(const std::vector & bsa) +void CStupidAI::battleStacksAttacked(const std::vector & bsa, bool ranged) { print("battleStacksAttacked called"); } @@ -202,7 +202,7 @@ void CStupidAI::battleNewRound(int round) print("battleNewRound called"); } -void CStupidAI::battleStackMoved(const CStack * stack, std::vector dest, int distance) +void CStupidAI::battleStackMoved(const CStack * stack, std::vector dest, int distance, bool teleport) { print("battleStackMoved called"); } diff --git a/AI/StupidAI/StupidAI.h b/AI/StupidAI/StupidAI.h index 7c0378b6c..12481759a 100644 --- a/AI/StupidAI/StupidAI.h +++ b/AI/StupidAI/StupidAI.h @@ -31,12 +31,12 @@ public: BattleAction activeStack(const CStack * stack) override; //called when it's turn of that stack void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack - void battleStacksAttacked(const std::vector & bsa) override; //called when stack receives damage (after battleAttack()) + void battleStacksAttacked(const std::vector & bsa, bool ranged) override; //called when stack receives damage (after battleAttack()) void battleEnd(const BattleResult *br) override; //void battleResultsApplied() override; //called when all effects of last battle are applied void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied; void battleNewRound(int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn - void battleStackMoved(const CStack * stack, std::vector dest, int distance) override; + void battleStackMoved(const CStack * stack, std::vector dest, int distance, bool teleport) override; void battleSpellCast(const BattleSpellCast *sc) override; void battleStacksEffectsSet(const SetStackEffect & sse) override;//called when a specific effect is set to stacks //void battleTriggerEffect(const BattleTriggerEffect & bte) override; diff --git a/AI/VCAI/FuzzyEngines.cpp b/AI/VCAI/FuzzyEngines.cpp index bb640ed80..97449c466 100644 --- a/AI/VCAI/FuzzyEngines.cpp +++ b/AI/VCAI/FuzzyEngines.cpp @@ -30,7 +30,7 @@ engineBase::engineBase() void engineBase::configure() { engine.configure("Minimum", "Maximum", "Minimum", "AlgebraicSum", "Centroid", "Proportional"); - logAi->info(engine.toString()); + logAi->trace(engine.toString()); } void engineBase::addRule(const std::string & txt) diff --git a/CI/linux/before_install.sh b/CI/linux/before_install.sh index ce49cc637..8b0c75d59 100644 --- a/CI/linux/before_install.sh +++ b/CI/linux/before_install.sh @@ -8,4 +8,4 @@ sudo apt-get install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf sudo apt-get install qtbase5-dev sudo apt-get install ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev # Optional dependencies -sudo apt-get install libminizip-dev libfuzzylite-dev +sudo apt-get install libminizip-dev libfuzzylite-dev qttools5-dev diff --git a/CI/mxe/before_install.sh b/CI/mxe/before_install.sh index 102df868d..0a4855f2d 100644 --- a/CI/mxe/before_install.sh +++ b/CI/mxe/before_install.sh @@ -1,5 +1,14 @@ #!/bin/sh +# steps to upgrade MXE dependencies: +# 1) Use Debian/Ubuntu system or install one (virtual machines will work too) +# 2) update following script to include any new dependencies +# You can also run it to upgrade existing ones, but don't expect much +# MXE repository only provides ancient versions for the sake of "stability" +# https://github.com/vcmi/vcmi-deps-mxe/blob/master/mirror-mxe.sh +# 3) make release in vcmi-deps-mxe repository using resulting tar archive +# 4) update paths to tar archive in this script + # Install nsis for installer creation sudo add-apt-repository 'deb http://security.ubuntu.com/ubuntu bionic-security main' sudo apt-get install -qq nsis ninja-build libssl1.0.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index d5d6ab5d8..5f084b770 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,7 +48,9 @@ option(ENABLE_ERM "Enable compilation of ERM scripting module" OFF) option(ENABLE_LUA "Enable compilation of LUA scripting module" OFF) option(ENABLE_LAUNCHER "Enable compilation of launcher" ON) option(ENABLE_EDITOR "Enable compilation of map editor" ON) +option(ENABLE_TRANSLATIONS "Enable generation of translations for launcher and editor" ON) option(ENABLE_NULLKILLER_AI "Enable compilation of Nullkiller AI library" ON) + if(APPLE_IOS) set(BUNDLE_IDENTIFIER_PREFIX "" CACHE STRING "Bundle identifier prefix") set(APP_DISPLAY_NAME "VCMI" CACHE STRING "App name on the home screen") @@ -127,14 +129,22 @@ else() endif(ENABLE_GITVERSION) # Precompiled header configuration -if(ENABLE_PCH AND NOT ${CMAKE_VERSION} VERSION_LESS "3.16.0") +if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 6.0 ) + set(ENABLE_PCH OFF) # broken +endif() + +if( ${CMAKE_VERSION} VERSION_LESS "3.16.0") + set(ENABLE_PCH OFF) #not supported +endif() + +if(ENABLE_PCH) macro(enable_pch name) target_precompile_headers(${name} PRIVATE $<$:>) endmacro(enable_pch) -else(ENABLE_PCH AND NOT ${CMAKE_VERSION} VERSION_LESS "3.16.0") +else() macro(enable_pch ignore) endmacro(enable_pch) -endif(ENABLE_PCH AND NOT ${CMAKE_VERSION} VERSION_LESS "3.16.0") +endif() ############################################ # Documentation section # @@ -349,6 +359,15 @@ if(ENABLE_LAUNCHER OR ENABLE_EDITOR) # Widgets finds its own dependencies (QtGui and QtCore). find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets Network) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets Network) + + find_package(QT NAMES Qt6 Qt5 COMPONENTS LinguistTools) + find_package(Qt${QT_VERSION_MAJOR} COMPONENTS LinguistTools) + if(NOT Qt${QT_VERSION_MAJOR}LinguistTools_DIR) + set(ENABLE_TRANSLATIONS OFF) + endif() + if(ENABLE_TRANSLATIONS) + add_definitions(-DENABLE_QT_TRANSLATIONS) + endif() endif() if(ENABLE_NULLKILLER_AI) diff --git a/Global.h b/Global.h index d425a6023..39ac92848 100644 --- a/Global.h +++ b/Global.h @@ -718,6 +718,13 @@ namespace vstd return v; } + //c++20 feature + template + Arithmetic lerp(const Arithmetic & a, const Arithmetic & b, const Floating & f) + { + return a + (b - a) * f; + } + using boost::math::round; } using vstd::operator-=; diff --git a/client/CBitmapHandler.cpp b/client/CBitmapHandler.cpp index ad0c73792..a8f716e94 100644 --- a/client/CBitmapHandler.cpp +++ b/client/CBitmapHandler.cpp @@ -20,7 +20,7 @@ namespace BitmapHandler { SDL_Surface * loadH3PCX(ui8 * data, size_t size); - SDL_Surface * loadBitmapFromDir(std::string path, std::string fname, bool setKey=true); + SDL_Surface * loadBitmapFromDir(std::string path, std::string fname); } bool isPCX(const ui8 *header)//check whether file can be PCX according to header @@ -102,7 +102,7 @@ SDL_Surface * BitmapHandler::loadH3PCX(ui8 * pcx, size_t size) return ret; } -SDL_Surface * BitmapHandler::loadBitmapFromDir(std::string path, std::string fname, bool setKey) +SDL_Surface * BitmapHandler::loadBitmapFromDir(std::string path, std::string fname) { if(!fname.size()) { @@ -121,14 +121,7 @@ SDL_Surface * BitmapHandler::loadBitmapFromDir(std::string path, std::string fna if (isPCX(readFile.first.get())) {//H3-style PCX ret = loadH3PCX(readFile.first.get(), readFile.second); - if (ret) - { - if(ret->format->BytesPerPixel == 1 && setKey) - { - CSDL_Ext::setColorKey(ret,ret->format->palette->colors[0]); - } - } - else + if (!ret) { logGlobal->error("Failed to open %s as H3 PCX!", fname); return nullptr; @@ -144,7 +137,8 @@ SDL_Surface * BitmapHandler::loadBitmapFromDir(std::string path, std::string fna { if (ret->format->palette) { - //set correct value for alpha\unused channel + // set correct value for alpha\unused channel + // NOTE: might be unnecessary with SDL2 for (int i=0; i < ret->format->palette->ncolors; i++) ret->format->palette->colors[i].a = SDL_ALPHA_OPAQUE; } @@ -196,12 +190,12 @@ SDL_Surface * BitmapHandler::loadBitmapFromDir(std::string path, std::string fna return ret; } -SDL_Surface * BitmapHandler::loadBitmap(std::string fname, bool setKey) +SDL_Surface * BitmapHandler::loadBitmap(std::string fname) { SDL_Surface * bitmap = nullptr; - if (!(bitmap = loadBitmapFromDir("DATA/", fname, setKey)) && - !(bitmap = loadBitmapFromDir("SPRITES/", fname, setKey))) + if (!(bitmap = loadBitmapFromDir("DATA/", fname)) && + !(bitmap = loadBitmapFromDir("SPRITES/", fname))) { logGlobal->error("Error: Failed to find file %s", fname); } diff --git a/client/CBitmapHandler.h b/client/CBitmapHandler.h index 539dc1212..b36156211 100644 --- a/client/CBitmapHandler.h +++ b/client/CBitmapHandler.h @@ -14,5 +14,5 @@ struct SDL_Surface; namespace BitmapHandler { //Load file from /DATA or /SPRITES - SDL_Surface * loadBitmap(std::string fname, bool setKey=true); + SDL_Surface * loadBitmap(std::string fname); } diff --git a/client/CMT.cpp b/client/CMT.cpp index 8cf16809e..56d851271 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -460,10 +460,9 @@ int main(int argc, char * argv[]) if(!settings["session"]["headless"].Bool()) { pomtime.getDiff(); - CCS->curh = new CCursorHandler(); - graphics = new Graphics(); // should be before curh->init() + graphics = new Graphics(); // should be before curh - CCS->curh->initCursor(); + CCS->curh = new CCursorHandler(); logGlobal->info("Screen handler: %d ms", pomtime.getDiff()); pomtime.getDiff(); @@ -1040,80 +1039,89 @@ static void mainLoop() } } +static void quitApplication() +{ + if(!settings["session"]["headless"].Bool()) + { + if(CSH->client) + CSH->endGameplay(); + } + + GH.listInt.clear(); + GH.objsToBlit.clear(); + + CMM.reset(); + + if(!settings["session"]["headless"].Bool()) + { + // cleanup, mostly to remove false leaks from analyzer + if(CCS) + { + CCS->musich->release(); + CCS->soundh->release(); + + vstd::clear_pointer(CCS); + } + CMessage::dispose(); + + vstd::clear_pointer(graphics); + } + + vstd::clear_pointer(VLC); + + vstd::clear_pointer(console);// should be removed after everything else since used by logging + + boost::this_thread::sleep(boost::posix_time::milliseconds(750));//??? + if(!settings["session"]["headless"].Bool()) + { + if(settings["general"]["notifications"].Bool()) + { + NotificationHandler::destroy(); + } + + cleanupRenderer(); + + if(nullptr != mainRenderer) + { + SDL_DestroyRenderer(mainRenderer); + mainRenderer = nullptr; + } + + if(nullptr != mainWindow) + { + SDL_DestroyWindow(mainWindow); + mainWindow = nullptr; + } + + SDL_Quit(); + } + + if(logConfig != nullptr) + { + logConfig->deconfigure(); + delete logConfig; + logConfig = nullptr; + } + + std::cout << "Ending...\n"; + exit(0); +} + void handleQuit(bool ask) { - auto quitApplication = []() - { - if(!settings["session"]["headless"].Bool()) - { - if(CSH->client) - CSH->endGameplay(); - } - - GH.listInt.clear(); - GH.objsToBlit.clear(); - - CMM.reset(); - - if(!settings["session"]["headless"].Bool()) - { - // cleanup, mostly to remove false leaks from analyzer - if(CCS) - { - CCS->musich->release(); - CCS->soundh->release(); - - vstd::clear_pointer(CCS); - } - CMessage::dispose(); - - vstd::clear_pointer(graphics); - } - - vstd::clear_pointer(VLC); - - vstd::clear_pointer(console);// should be removed after everything else since used by logging - - boost::this_thread::sleep(boost::posix_time::milliseconds(750));//??? - if(!settings["session"]["headless"].Bool()) - { - if(settings["general"]["notifications"].Bool()) - { - NotificationHandler::destroy(); - } - - cleanupRenderer(); - - if(nullptr != mainRenderer) - { - SDL_DestroyRenderer(mainRenderer); - mainRenderer = nullptr; - } - - if(nullptr != mainWindow) - { - SDL_DestroyWindow(mainWindow); - mainWindow = nullptr; - } - - SDL_Quit(); - } - - if(logConfig != nullptr) - { - logConfig->deconfigure(); - delete logConfig; - logConfig = nullptr; - } - - std::cout << "Ending...\n"; - exit(0); - }; if(CSH->client && LOCPLINT && ask) { - CCS->curh->changeGraphic(ECursor::ADVENTURE, 0); - LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[69], quitApplication, nullptr); + CCS->curh->set(Cursor::Map::POINTER); + LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[69], [](){ + // Workaround for assertion failure on exit: + // handleQuit() is alway called during SDL event processing + // during which, eventsM is kept locked + // this leads to assertion failure if boost::mutex is in locked state + eventsM.unlock(); + + quitApplication(); + }, nullptr); } else { diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 81dafb61d..328987208 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -4,7 +4,6 @@ set(client_SRCS battle/BattleActionsController.cpp battle/BattleAnimationClasses.cpp - battle/BattleControlPanel.cpp battle/BattleEffectsController.cpp battle/BattleFieldController.cpp battle/BattleInterfaceClasses.cpp @@ -14,6 +13,7 @@ set(client_SRCS battle/BattleRenderer.cpp battle/BattleSiegeController.cpp battle/BattleStacksController.cpp + battle/BattleWindow.cpp battle/CreatureAnimation.cpp gui/CAnimation.cpp @@ -21,6 +21,7 @@ set(client_SRCS gui/CCursorHandler.cpp gui/CGuiHandler.cpp gui/CIntObject.cpp + gui/ColorFilter.cpp gui/Fonts.cpp gui/Geometries.cpp gui/SDL_Extensions.cpp @@ -89,7 +90,6 @@ set(client_HEADERS battle/BattleActionsController.h battle/BattleAnimationClasses.h - battle/BattleControlPanel.h battle/BattleEffectsController.h battle/BattleFieldController.h battle/BattleInterfaceClasses.h @@ -99,12 +99,15 @@ set(client_HEADERS battle/BattleRenderer.h battle/BattleSiegeController.h battle/BattleStacksController.h + battle/BattleWindow.h battle/CreatureAnimation.h + battle/BattleConstants.h gui/CAnimation.h gui/Canvas.h gui/CCursorHandler.h gui/CGuiHandler.h + gui/ColorFilter.h gui/CIntObject.h gui/Fonts.h gui/Geometries.h diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 22b475da9..20c0be95f 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -16,7 +16,7 @@ #include "battle/BattleEffectsController.h" #include "battle/BattleFieldController.h" #include "battle/BattleInterfaceClasses.h" -#include "battle/BattleControlPanel.h" +#include "battle/BattleWindow.h" #include "../CCallback.h" #include "windows/CCastleInterface.h" #include "gui/CCursorHandler.h" @@ -93,7 +93,7 @@ boost::recursive_mutex * CPlayerInterface::pim = new boost::recursive_mutex; CPlayerInterface * LOCPLINT; -BattleInterface * CPlayerInterface::battleInt; +std::shared_ptr CPlayerInterface::battleInt; enum EMoveState {STOP_MOVE, WAITING_MOVE, CONTINUE_MOVE, DURING_MOVE}; CondSh stillMoveHero(STOP_MOVE); //used during hero movement @@ -142,7 +142,9 @@ CPlayerInterface::CPlayerInterface(PlayerColor Player) CPlayerInterface::~CPlayerInterface() { - if(CCS->soundh) CCS->soundh->ambientStopAllChannels(); + if(CCS && CCS->soundh) + CCS->soundh->ambientStopAllChannels(); + logGlobal->trace("\tHuman player interface for player %s being destructed", playerID.getStr()); delete showingDialog; delete cingconsole; @@ -707,7 +709,7 @@ void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet BATTLE_EVENT_POSSIBLE_RETURN; } -void CPlayerInterface::battleUnitsChanged(const std::vector & units, const std::vector & customEffects) +void CPlayerInterface::battleUnitsChanged(const std::vector & units) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; @@ -747,8 +749,6 @@ void CPlayerInterface::battleUnitsChanged(const std::vector & units break; } } - - battleInt->effectsController->displayCustomEffects(customEffects); } void CPlayerInterface::battleObstaclesChanged(const std::vector & obstacles) @@ -838,9 +838,9 @@ BattleAction CPlayerInterface::activeStack(const CStack * stack) //called when i autofightingAI.reset(); } - BattleInterface *b = battleInt; + assert(battleInt); - if(!b) + if(!battleInt) { return BattleAction::makeDefend(stack); // probably battle is finished already } @@ -853,7 +853,7 @@ BattleAction CPlayerInterface::activeStack(const CStack * stack) //called when i { boost::unique_lock un(*pim); - b->stackActivated(stack); + battleInt->stackActivated(stack); //Regeneration & mana drain go there } //wait till BattleInterface sets its command @@ -915,12 +915,12 @@ void CPlayerInterface::battleLogMessage(const std::vector & lines) battleInt->displayBattleLog(lines); } -void CPlayerInterface::battleStackMoved(const CStack * stack, std::vector dest, int distance) +void CPlayerInterface::battleStackMoved(const CStack * stack, std::vector dest, int distance, bool teleport) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; - battleInt->stackMoved(stack, dest, distance); + battleInt->stackMoved(stack, dest, distance, teleport); } void CPlayerInterface::battleSpellCast( const BattleSpellCast *sc ) { @@ -944,7 +944,7 @@ void CPlayerInterface::battleTriggerEffect (const BattleTriggerEffect & bte) RETURN_IF_QUICK_COMBAT; battleInt->effectsController->battleTriggerEffect(bte); } -void CPlayerInterface::battleStacksAttacked(const std::vector & bsa) +void CPlayerInterface::battleStacksAttacked(const std::vector & bsa, bool ranged) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; @@ -954,24 +954,25 @@ void CPlayerInterface::battleStacksAttacked(const std::vectorbattleGetStackByID(elem.stackAttacked, false); const CStack * attacker = cb->battleGetStackByID(elem.attackerID, false); - if(elem.isEffect()) - { - if(defender && !elem.isSecondary()) - battleInt->effectsController->displayEffect(EBattleEffect::EBattleEffect(elem.effect), defender->getPosition()); - } - if(elem.isSpell()) - { - if(defender) - battleInt->displaySpellEffect(elem.spellID, defender->getPosition()); - } - //FIXME: why action is deleted during enchanter cast? - bool remoteAttack = false; - if(LOCPLINT->curAction) - remoteAttack |= LOCPLINT->curAction->actionType != EActionType::WALK_AND_ATTACK; + assert(defender); - StackAttackedInfo to_put = {defender, elem.damageAmount, elem.killedAmount, attacker, remoteAttack, elem.killed(), elem.willRebirth(), elem.cloneKilled()}; - arg.push_back(to_put); + StackAttackedInfo info; + info.defender = defender; + info.attacker = attacker; + info.damageDealt = elem.damageAmount; + info.amountKilled = elem.killedAmount; + info.spellEffect = SpellID::NONE; + info.indirectAttack = ranged; + info.killed = elem.killed(); + info.rebirth = elem.willRebirth(); + info.cloneKilled = elem.cloneKilled(); + info.fireShield = elem.fireShield(); + + if (elem.isSpell()) + info.spellEffect = elem.spellID; + + arg.push_back(info); } battleInt->stacksAreAttacked(arg); } @@ -982,94 +983,36 @@ void CPlayerInterface::battleAttack(const BattleAttack * ba) assert(curAction); - const CStack * attacker = cb->battleGetStackByID(ba->stackAttacking); + StackAttackInfo info; + info.attacker = cb->battleGetStackByID(ba->stackAttacking); + info.defender = nullptr; + info.indirectAttack = ba->shot(); + info.lucky = ba->lucky(); + info.unlucky = ba->unlucky(); + info.deathBlow = ba->deathBlow(); + info.lifeDrain = ba->lifeDrain(); + info.tile = ba->tile; + info.spellEffect = SpellID::NONE; - if(!attacker) - { - logGlobal->error("Attacking stack not found"); - return; - } + if (ba->spellLike()) + info.spellEffect = ba->spellID; - if(ba->lucky()) //lucky hit + for(auto & elem : ba->bsa) { - battleInt->controlPanel->console->addText(attacker->formatGeneralMessage(-45)); - battleInt->effectsController->displayEffect(EBattleEffect::GOOD_LUCK, soundBase::GOODLUCK, attacker->getPosition()); - } - if(ba->unlucky()) //unlucky hit - { - battleInt->controlPanel->console->addText(attacker->formatGeneralMessage(-44)); - battleInt->effectsController->displayEffect(EBattleEffect::BAD_LUCK, soundBase::BADLUCK, attacker->getPosition()); - } - if(ba->deathBlow()) - { - battleInt->controlPanel->console->addText(attacker->formatGeneralMessage(365)); - for(auto & elem : ba->bsa) + if(!elem.isSecondary()) { - const CStack * attacked = cb->battleGetStackByID(elem.stackAttacked); - battleInt->effectsController->displayEffect(EBattleEffect::DEATH_BLOW, attacked->getPosition()); + assert(info.defender == nullptr); + info.defender = cb->battleGetStackByID(elem.stackAttacked); } - CCS->soundh->playSound(soundBase::deathBlow); - } - - battleInt->effectsController->displayCustomEffects(ba->customEffects); - - battleInt->waitForAnims(); - - auto actionTarget = curAction->getTarget(cb.get()); - - if(actionTarget.empty() || (actionTarget.size() < 2 && !ba->shot())) - { - logNetwork->error("Invalid current action: no destination."); - return; - } - - if(ba->shot()) - { - for(auto & elem : ba->bsa) + else { - if(!elem.isSecondary()) //display projectile only for primary target - { - const CStack * attacked = cb->battleGetStackByID(elem.stackAttacked); - battleInt->stackAttacking(attacker, attacked->getPosition(), attacked, true); - } + info.secondaryDefender.push_back(cb->battleGetStackByID(elem.stackAttacked)); } } - else - { - auto attackTarget = actionTarget.at(1).hexValue; + assert(info.defender != nullptr); + assert(info.attacker != nullptr); - //TODO: use information from BattleAttack but not curAction - - int shift = 0; - if(ba->counter() && BattleHex::mutualPosition(attackTarget, attacker->getPosition()) < 0) - { - int distp = BattleHex::getDistance(attackTarget + 1, attacker->getPosition()); - int distm = BattleHex::getDistance(attackTarget - 1, attacker->getPosition()); - - if(distp < distm) - shift = 1; - else - shift = -1; - } - - if(!ba->bsa.empty()) - { - const CStack * attacked = cb->battleGetStackByID(ba->bsa.begin()->stackAttacked); - battleInt->stackAttacking(attacker, ba->counter() ? BattleHex(attackTarget + shift) : attackTarget, attacked, false); - } - } - - //battleInt->waitForAnims(); //FIXME: freeze - - if(ba->spellLike()) - { - //TODO: use information from BattleAttack but not curAction - - auto destination = actionTarget.at(0).hexValue; - //display hit animation - SpellID spellID = ba->spellID; - battleInt->displaySpellHit(spellID, destination); - } + battleInt->stackAttacking(info); } void CPlayerInterface::battleGateStateChanged(const EGateState state) @@ -1603,7 +1546,7 @@ void CPlayerInterface::playerBlocked(int reason, bool start) GH.curInt = this; adventureInt->selection = nullptr; adventureInt->setPlayer(playerID); - std::string msg = CGI->generaltexth->localizedTexts["adventureMap"]["playerAttacked"].String(); + std::string msg = CGI->generaltexth->translate("vcmi.adventureMap.playerAttacked"); boost::replace_first(msg, "%s", cb->getStartInfo()->playerInfos.find(playerID)->second.name); std::vector> cmp; cmp.push_back(std::make_shared(CComponent::flag, playerID.getNum(), 0)); diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index e6e2a2117..bc72c4fa5 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -84,7 +84,7 @@ public: static const int SAVES_COUNT = 5; CCastleInterface * castleInt; //nullptr if castle window isn't opened - static BattleInterface * battleInt; //nullptr if no battle + static std::shared_ptr battleInt; //nullptr if no battle CInGameConsole * cingconsole; std::shared_ptr cb; //to communicate with engine @@ -193,14 +193,14 @@ public: void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied; used for HP regen handling void battleNewRound(int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn void battleLogMessage(const std::vector & lines) override; - void battleStackMoved(const CStack * stack, std::vector dest, int distance) override; + void battleStackMoved(const CStack * stack, std::vector dest, int distance, bool teleport) override; void battleSpellCast(const BattleSpellCast *sc) override; void battleStacksEffectsSet(const SetStackEffect & sse) override; //called when a specific effect is set to stacks void battleTriggerEffect(const BattleTriggerEffect & bte) override; //various one-shot effect - void battleStacksAttacked(const std::vector & bsa) override; + void battleStacksAttacked(const std::vector & bsa, bool ranged) override; void battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) override; //called by engine just before battle starts; side=0 - left, side=1 - right void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override; //called by engine when battle starts; side=0 - left, side=1 - right - void battleUnitsChanged(const std::vector & units, const std::vector & customEffects) override; + void battleUnitsChanged(const std::vector & units) override; void battleObstaclesChanged(const std::vector & obstacles) override; void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack void battleGateStateChanged(const EGateState state) override; diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 4da7b2a32..bf20cee36 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -176,7 +176,7 @@ void CServerHandler::startLocalServerAndConnect() th->update(); - auto errorMsg = CGI->generaltexth->localizedTexts["server"]["errors"]["existingProcess"].String(); + auto errorMsg = CGI->generaltexth->translate("vcmi.server.errors.existingProcess"); try { CConnection testConnection(localhostAddress, getDefaultPort(), NAME, uuid); @@ -718,7 +718,7 @@ void CServerHandler::restoreLastSession() saveSession->Bool() = false; }; - CInfoWindow::showYesNoDialog(VLC->generaltexth->localizedTexts["server"]["confirmReconnect"].String(), {}, loadSession, cleanUpSession); + CInfoWindow::showYesNoDialog(VLC->generaltexth->translate("vcmi.server.confirmReconnect"), {}, loadSession, cleanUpSession); } void CServerHandler::debugStartTest(std::string filename, bool save) diff --git a/client/Client.cpp b/client/Client.cpp index 891d876d5..1a6b9c4b2 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -380,20 +380,12 @@ void CClient::endGame() { boost::unique_lock un(*CPlayerInterface::pim); logNetwork->info("Ending current game!"); - if(GH.topInt()) - { - GH.topInt()->deactivate(); - } - GH.listInt.clear(); - GH.objsToBlit.clear(); - GH.statusbar = nullptr; - logNetwork->info("Removed GUI."); + removeGUI(); vstd::clear_pointer(const_cast(CGI)->mh); vstd::clear_pointer(gs); logNetwork->info("Deleted mapHandler and gameState."); - LOCPLINT = nullptr; } playerint.clear(); @@ -594,11 +586,10 @@ void CClient::battleStarted(const BattleInfo * info) if(!settings["session"]["headless"].Bool()) { - Rect battleIntRect((screen->w - 800)/2, (screen->h - 600)/2, 800, 600); if(!!att || !!def) { boost::unique_lock un(*CPlayerInterface::pim); - GH.pushIntT(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, battleIntRect, att, def); + CPlayerInterface::battleInt = std::make_shared(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def); } else if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) { @@ -606,7 +597,7 @@ void CClient::battleStarted(const BattleInfo * info) auto spectratorInt = std::dynamic_pointer_cast(playerint[PlayerColor::SPECTATOR]); spectratorInt->cb->setBattle(info); boost::unique_lock un(*CPlayerInterface::pim); - GH.pushIntT(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, battleIntRect, att, def, spectratorInt); + CPlayerInterface::battleInt = std::make_shared(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def, spectratorInt); } } diff --git a/client/CreatureCostBox.cpp b/client/CreatureCostBox.cpp index 07a72a2cf..6a4d56286 100644 --- a/client/CreatureCostBox.cpp +++ b/client/CreatureCostBox.cpp @@ -18,7 +18,7 @@ CreatureCostBox::CreatureCostBox(Rect position, std::string titleText) OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); type |= REDRAW_PARENT; - pos = position + pos; + pos = position + pos.topLeft(); title = std::make_shared(pos.w/2, 10, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, titleText); } diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index ee6f3eb41..30a09d750 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -741,7 +741,7 @@ void BattleResult::applyFirstCl(CClient *cl) void BattleStackMoved::applyFirstCl(CClient *cl) { const CStack * movedStack = GS(cl)->curB->battleGetStackByID(stack); - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStackMoved, movedStack, tilesToMove, distance); + callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStackMoved, movedStack, tilesToMove, distance, teleporting); } void BattleAttack::applyFirstCl(CClient *cl) @@ -751,7 +751,7 @@ void BattleAttack::applyFirstCl(CClient *cl) void BattleAttack::applyCl(CClient *cl) { - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStacksAttacked, bsa); + callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStacksAttacked, bsa, shot()); } void StartAction::applyFirstCl(CClient *cl) @@ -773,7 +773,7 @@ void SetStackEffect::applyCl(CClient *cl) void StacksInjured::applyCl(CClient *cl) { - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStacksAttacked, stacks); + callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStacksAttacked, stacks, false); } void BattleResultsApplied::applyCl(CClient *cl) @@ -785,7 +785,7 @@ void BattleResultsApplied::applyCl(CClient *cl) void BattleUnitsChanged::applyCl(CClient * cl) { - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleUnitsChanged, changedStacks, customEffects); + callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleUnitsChanged, changedStacks); } void BattleObstaclesChanged::applyCl(CClient *cl) diff --git a/client/battle/BattleActionsController.cpp b/client/battle/BattleActionsController.cpp index 68a4a0aef..e0dc61229 100644 --- a/client/battle/BattleActionsController.cpp +++ b/client/battle/BattleActionsController.cpp @@ -10,7 +10,7 @@ #include "StdInc.h" #include "BattleActionsController.h" -#include "BattleControlPanel.h" +#include "BattleWindow.h" #include "BattleStacksController.h" #include "BattleInterface.h" #include "BattleFieldController.h" @@ -60,7 +60,7 @@ void BattleActionsController::endCastingSpell() currentSpell = nullptr; spellDestSelectMode = false; - CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER); + CCS->curh->set(Cursor::Combat::POINTER); if(owner.stacksController->getActiveStack()) { @@ -122,7 +122,7 @@ void BattleActionsController::enterCreatureCastingMode() owner.giveCommand(EActionType::MONSTER_SPELL, BattleHex::INVALID, owner.stacksController->activeStackSpellToCast()); owner.stacksController->setSelectedStack(nullptr); - CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER); + CCS->curh->set(Cursor::Combat::POINTER); } } else @@ -171,8 +171,6 @@ void BattleActionsController::reorderPossibleActionsPriority(const CStack * stac break; case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: return 2; break; - case PossiblePlayerBattleAction::RISE_DEMONS: - return 3; break; case PossiblePlayerBattleAction::SHOOT: return 4; break; case PossiblePlayerBattleAction::ATTACK_AND_RETURN: @@ -233,7 +231,7 @@ void BattleActionsController::castThisSpell(SpellID spellID) void BattleActionsController::handleHex(BattleHex myNumber, int eventType) { - if (!owner.myTurn || !owner.battleActionsStarted) //we are not permit to do anything + if (!owner.myTurn) //we are not permit to do anything return; // This function handles mouse move over hexes and l-clicking on them. @@ -245,8 +243,8 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType) std::string newConsoleMsg; //used when hovering -> tooltip message and cursor to be set bool setCursor = true; //if we want to suppress setting cursor - ECursor::ECursorTypes cursorType = ECursor::COMBAT; - int cursorFrame = ECursor::COMBAT_POINTER; //TODO: is this line used? + bool spellcastingCursor = false; + auto cursorFrame = Cursor::Combat::POINTER; //used when l-clicking -> action to be called upon the click std::function realizeAction; @@ -263,9 +261,6 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType) if (shere) ourStack = shere->owner == owner.curInt->playerID; - //stack may have changed, update selection border - owner.stacksController->setHoveredStack(shere); - localActions.clear(); illegalActions.clear(); @@ -301,7 +296,6 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType) { if (owner.fieldController->isTileAttackable(myNumber)) // move isTileAttackable to be part of battleCanAttack? { - owner.fieldController->setBattleCursor(myNumber); // temporary - needed for following function :( BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(myNumber); if (attackFromHex >= 0) //we can be in this line when unreachable creature is L - clicked (as of revision 1308) @@ -378,19 +372,6 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType) if (shere && ourStack && shere->canBeHealed()) legalAction = true; break; - case PossiblePlayerBattleAction::RISE_DEMONS: - if (shere && ourStack && !shere->alive()) - { - if (!(shere->hasBonusOfType(Bonus::UNDEAD) - || shere->hasBonusOfType(Bonus::NON_LIVING) - || shere->hasBonusOfType(Bonus::GARGOYLE) - || shere->summoned - || shere->isClone() - || shere->hasBonusOfType(Bonus::SIEGE_WEAPON) - )) - legalAction = true; - } - break; } if (legalAction) localActions.push_back (action); @@ -434,12 +415,12 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType) case PossiblePlayerBattleAction::MOVE_STACK: if (owner.stacksController->getActiveStack()->hasBonusOfType(Bonus::FLYING)) { - cursorFrame = ECursor::COMBAT_FLY; + cursorFrame = Cursor::Combat::FLY; newConsoleMsg = (boost::format(CGI->generaltexth->allTexts[295]) % owner.stacksController->getActiveStack()->getName()).str(); //Fly %s here } else { - cursorFrame = ECursor::COMBAT_MOVE; + cursorFrame = Cursor::Combat::MOVE; newConsoleMsg = (boost::format(CGI->generaltexth->allTexts[294]) % owner.stacksController->getActiveStack()->getName()).str(); //Move %s here } @@ -464,7 +445,7 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType) case PossiblePlayerBattleAction::WALK_AND_ATTACK: case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return { - owner.fieldController->setBattleCursor(myNumber); //handle direction of cursor and attackable tile + owner.fieldController->setBattleCursor(myNumber); //handle direction of cursor setCursor = false; //don't overwrite settings from the call above //TODO: what does it mean? bool returnAfterAttack = currentAction == PossiblePlayerBattleAction::ATTACK_AND_RETURN; @@ -487,9 +468,9 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType) case PossiblePlayerBattleAction::SHOOT: { if (owner.curInt->cb->battleHasShootingPenalty(owner.stacksController->getActiveStack(), myNumber)) - cursorFrame = ECursor::COMBAT_SHOOT_PENALTY; + cursorFrame = Cursor::Combat::SHOOT_PENALTY; else - cursorFrame = ECursor::COMBAT_SHOOT; + cursorFrame = Cursor::Combat::SHOOT; realizeAction = [=](){owner.giveCommand(EActionType::SHOOT, myNumber);}; TDmgRange damage = owner.curInt->cb->battleEstimateDamage(owner.stacksController->getActiveStack(), shere); @@ -524,7 +505,7 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType) break; case PossiblePlayerBattleAction::TELEPORT: newConsoleMsg = CGI->generaltexth->allTexts[25]; //Teleport Here - cursorFrame = ECursor::COMBAT_TELEPORT; + cursorFrame = Cursor::Combat::TELEPORT; isCastingPossible = true; break; case PossiblePlayerBattleAction::OBSTACLE: @@ -534,7 +515,7 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType) break; case PossiblePlayerBattleAction::SACRIFICE: newConsoleMsg = (boost::format(CGI->generaltexth->allTexts[549]) % shere->getName()).str(); //sacrifice the %s - cursorFrame = ECursor::COMBAT_SACRIFICE; + cursorFrame = Cursor::Combat::SACRIFICE; isCastingPossible = true; break; case PossiblePlayerBattleAction::FREE_LOCATION: @@ -542,24 +523,17 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType) isCastingPossible = true; break; case PossiblePlayerBattleAction::HEAL: - cursorFrame = ECursor::COMBAT_HEAL; + cursorFrame = Cursor::Combat::HEAL; newConsoleMsg = (boost::format(CGI->generaltexth->allTexts[419]) % shere->getName()).str(); //Apply first aid to the %s realizeAction = [=](){ owner.giveCommand(EActionType::STACK_HEAL, myNumber); }; //command healing break; - case PossiblePlayerBattleAction::RISE_DEMONS: - cursorType = ECursor::SPELLBOOK; - realizeAction = [=]() - { - owner.giveCommand(EActionType::DAEMON_SUMMONING, myNumber); - }; - break; case PossiblePlayerBattleAction::CATAPULT: - cursorFrame = ECursor::COMBAT_SHOOT_CATAPULT; + cursorFrame = Cursor::Combat::SHOOT_CATAPULT; realizeAction = [=](){ owner.giveCommand(EActionType::CATAPULT, myNumber); }; break; case PossiblePlayerBattleAction::CREATURE_INFO: { - cursorFrame = ECursor::COMBAT_QUERY; + cursorFrame = Cursor::Combat::QUERY; newConsoleMsg = (boost::format(CGI->generaltexth->allTexts[297]) % shere->getName()).str(); realizeAction = [=](){ GH.pushIntT(shere, false); }; break; @@ -572,25 +546,25 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType) { case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: - cursorFrame = ECursor::COMBAT_BLOCKED; + cursorFrame = Cursor::Combat::BLOCKED; newConsoleMsg = CGI->generaltexth->allTexts[23]; break; case PossiblePlayerBattleAction::TELEPORT: - cursorFrame = ECursor::COMBAT_BLOCKED; + cursorFrame = Cursor::Combat::BLOCKED; newConsoleMsg = CGI->generaltexth->allTexts[24]; //Invalid Teleport Destination break; case PossiblePlayerBattleAction::SACRIFICE: newConsoleMsg = CGI->generaltexth->allTexts[543]; //choose army to sacrifice break; case PossiblePlayerBattleAction::FREE_LOCATION: - cursorFrame = ECursor::COMBAT_BLOCKED; + cursorFrame = Cursor::Combat::BLOCKED; newConsoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[181]) % currentSpell->name); //No room to place %s here break; default: if (myNumber == -1) - CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER); //set neutral cursor over menu etc. + CCS->curh->set(Cursor::Combat::POINTER); else - cursorFrame = ECursor::COMBAT_BLOCKED; + cursorFrame = Cursor::Combat::BLOCKED; break; } } @@ -603,8 +577,7 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType) case PossiblePlayerBattleAction::SACRIFICE: break; default: - cursorType = ECursor::SPELLBOOK; - cursorFrame = 0; + spellcastingCursor = true; if (newConsoleMsg.empty() && currentSpell) newConsoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[26]) % currentSpell->name); //Cast %s break; @@ -665,12 +638,17 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType) if (eventType == CIntObject::MOVE) { if (setCursor) - CCS->curh->changeGraphic(cursorType, cursorFrame); + { + if (spellcastingCursor) + CCS->curh->set(Cursor::Spellcast::SPELL); + else + CCS->curh->set(cursorFrame); + } if (!currentConsoleMsg.empty()) - owner.controlPanel->console->clearIfMatching(currentConsoleMsg); + GH.statusbar->clearIfMatching(currentConsoleMsg); if (!newConsoleMsg.empty()) - owner.controlPanel->console->write(newConsoleMsg); + GH.statusbar->write(newConsoleMsg); currentConsoleMsg = newConsoleMsg; } @@ -683,8 +661,8 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType) } realizeAction(); if (!secondaryTarget) //do not replace teleport or sacrifice cursor - CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER); - owner.controlPanel->console->clear(); + CCS->curh->set(Cursor::Combat::POINTER); + GH.statusbar->clear(); } } } @@ -781,7 +759,7 @@ void BattleActionsController::activateStack() break; } } - owner.controlPanel->setAlternativeActions(actionsToSelect); + owner.windowObject->setAlternativeActions(actionsToSelect); } } diff --git a/client/battle/BattleAnimationClasses.cpp b/client/battle/BattleAnimationClasses.cpp index 3c41ffdca..d96f48414 100644 --- a/client/battle/BattleAnimationClasses.cpp +++ b/client/battle/BattleAnimationClasses.cpp @@ -10,10 +10,8 @@ #include "StdInc.h" #include "BattleAnimationClasses.h" -#include - -#include "BattleInterfaceClasses.h" #include "BattleInterface.h" +#include "BattleInterfaceClasses.h" #include "BattleProjectileController.h" #include "BattleSiegeController.h" #include "BattleFieldController.h" @@ -24,17 +22,13 @@ #include "../CGameInfo.h" #include "../CMusicHandler.h" #include "../CPlayerInterface.h" -#include "../Graphics.h" -#include "../gui/CAnimation.h" #include "../gui/CCursorHandler.h" #include "../gui/CGuiHandler.h" #include "../../CCallback.h" #include "../../lib/CStack.h" -#include "../../lib/CTownHandler.h" -#include "../../lib/mapObjects/CGTownInstance.h" -CBattleAnimation::CBattleAnimation(BattleInterface & owner) +BattleAnimation::BattleAnimation(BattleInterface & owner) : owner(owner), ID(owner.stacksController->animIDhelper++), initialized(false) @@ -42,7 +36,7 @@ CBattleAnimation::CBattleAnimation(BattleInterface & owner) logAnim->trace("Animation #%d created", ID); } -bool CBattleAnimation::tryInitialize() +bool BattleAnimation::tryInitialize() { assert(!initialized); @@ -54,12 +48,12 @@ bool CBattleAnimation::tryInitialize() return false; } -bool CBattleAnimation::isInitialized() +bool BattleAnimation::isInitialized() { return initialized; } -CBattleAnimation::~CBattleAnimation() +BattleAnimation::~BattleAnimation() { logAnim->trace("Animation #%d ended, type is %s", ID, typeid(this).name()); for(auto & elem : pendingAnimations()) @@ -70,106 +64,99 @@ CBattleAnimation::~CBattleAnimation() logAnim->trace("Animation #%d deleted", ID); } -std::vector & CBattleAnimation::pendingAnimations() +std::vector & BattleAnimation::pendingAnimations() { return owner.stacksController->currentAnimations; } -std::shared_ptr CBattleAnimation::stackAnimation(const CStack * stack) const +std::shared_ptr BattleAnimation::stackAnimation(const CStack * stack) const { return owner.stacksController->stackAnimation[stack->ID]; } -bool CBattleAnimation::stackFacingRight(const CStack * stack) +bool BattleAnimation::stackFacingRight(const CStack * stack) { return owner.stacksController->stackFacingRight[stack->ID]; } -void CBattleAnimation::setStackFacingRight(const CStack * stack, bool facingRight) +void BattleAnimation::setStackFacingRight(const CStack * stack, bool facingRight) { owner.stacksController->stackFacingRight[stack->ID] = facingRight; } -bool CBattleAnimation::checkInitialConditions() -{ - int lowestMoveID = ID; - auto * thAnim = dynamic_cast(this); - auto * thSen = dynamic_cast(this); - - for(auto & elem : pendingAnimations()) - { - auto * sen = dynamic_cast(elem); - - // all effect animations can play concurrently with each other - if(sen && thSen && sen != thSen) - continue; - - auto * revAnim = dynamic_cast(elem); - - // if there is high-priority reverse animation affecting our stack then this animation will wait - if(revAnim && thAnim && revAnim && revAnim->stack->ID == thAnim->stack->ID && revAnim->priority) - return false; - - if(elem) - vstd::amin(lowestMoveID, elem->ID); - } - return ID == lowestMoveID; -} - -CBattleStackAnimation::CBattleStackAnimation(BattleInterface & owner, const CStack * stack) - : CBattleAnimation(owner), +BattleStackAnimation::BattleStackAnimation(BattleInterface & owner, const CStack * stack) + : BattleAnimation(owner), myAnim(stackAnimation(stack)), stack(stack) { assert(myAnim); } -void CBattleStackAnimation::shiftColor(const ColorShifter * shifter) +StackActionAnimation::StackActionAnimation(BattleInterface & owner, const CStack * stack) + : BattleStackAnimation(owner, stack) + , nextGroup(ECreatureAnimType::HOLDING) + , currGroup(ECreatureAnimType::HOLDING) { - assert(myAnim); - myAnim->shiftColor(shifter); } -void CAttackAnimation::nextFrame() +ECreatureAnimType StackActionAnimation::getGroup() const { - if(myAnim->getType() != group) + return currGroup; +} + +void StackActionAnimation::setNextGroup( ECreatureAnimType group ) +{ + nextGroup = group; +} + +void StackActionAnimation::setGroup( ECreatureAnimType group ) +{ + currGroup = group; +} + +void StackActionAnimation::setSound( std::string sound ) +{ + this->sound = sound; +} + +bool StackActionAnimation::init() +{ + if (!sound.empty()) + CCS->soundh->playSound(sound); + + if (myAnim->framesInGroup(currGroup) > 0) { - myAnim->setType(group); + myAnim->playOnce(currGroup); myAnim->onAnimationReset += [&](){ delete this; }; } + else + delete this; - if(!soundPlayed) - { - if(shooting) - CCS->soundh->playSound(battle_sound(getCreature(), shoot)); - else - CCS->soundh->playSound(battle_sound(getCreature(), attack)); - soundPlayed = true; - } + return true; } -CAttackAnimation::~CAttackAnimation() +StackActionAnimation::~StackActionAnimation() { - myAnim->setType(CCreatureAnim::HOLDING); + if (stack->isFrozen()) + myAnim->setType(ECreatureAnimType::HOLDING); + else + myAnim->setType(nextGroup); + } -bool CAttackAnimation::checkInitialConditions() +ECreatureAnimType AttackAnimation::findValidGroup( const std::vector candidates ) const { - for(auto & elem : pendingAnimations()) + for ( auto group : candidates) { - CBattleStackAnimation * stAnim = dynamic_cast(elem); - CReverseAnimation * revAnim = dynamic_cast(stAnim); - - if(revAnim && attackedStack) // enemy must be fully reversed - { - if (revAnim->stack->ID == attackedStack->ID) - return false; - } + if(myAnim->framesInGroup(group) > 0) + return group; } - return CBattleAnimation::checkInitialConditions(); + + assert(0); + return ECreatureAnimType::HOLDING; } -const CCreature * CAttackAnimation::getCreature() const +const CCreature * AttackAnimation::getCreature() const { if (attackingStack->getCreature()->idNumber == CreatureID::ARROW_TOWERS) return owner.siegeController->getTurretCreature(); @@ -177,222 +164,119 @@ const CCreature * CAttackAnimation::getCreature() const return attackingStack->getCreature(); } -CAttackAnimation::CAttackAnimation(BattleInterface & owner, const CStack *attacker, BattleHex _dest, const CStack *defender) - : CBattleStackAnimation(owner, attacker), - shooting(false), - group(CCreatureAnim::SHOOT_FRONT), - soundPlayed(false), + +AttackAnimation::AttackAnimation(BattleInterface & owner, const CStack *attacker, BattleHex _dest, const CStack *defender) + : StackActionAnimation(owner, attacker), dest(_dest), - attackedStack(defender), + defendingStack(defender), attackingStack(attacker) { assert(attackingStack && "attackingStack is nullptr in CBattleAttack::CBattleAttack !\n"); attackingStackPosBeforeReturn = attackingStack->getPosition(); } -CDefenceAnimation::CDefenceAnimation(StackAttackedInfo _attackedInfo, BattleInterface & owner) - : CBattleStackAnimation(owner, _attackedInfo.defender), - attacker(_attackedInfo.attacker), - rangedAttack(_attackedInfo.indirectAttack), - killed(_attackedInfo.killed), - timeToWait(0) +HittedAnimation::HittedAnimation(BattleInterface & owner, const CStack * stack) + : StackActionAnimation(owner, stack) { - logAnim->debug("Created defence anim for %s", _attackedInfo.defender->getName()); + setGroup(ECreatureAnimType::HITTED); + setSound(battle_sound(stack->getCreature(), wince)); + logAnim->debug("Created HittedAnimation for %s", stack->getName()); } -bool CDefenceAnimation::init() +DefenceAnimation::DefenceAnimation(BattleInterface & owner, const CStack * stack) + : StackActionAnimation(owner, stack) { - ui32 lowestMoveID = ID; - for(auto & elem : pendingAnimations()) - { + setGroup(ECreatureAnimType::DEFENCE); + setSound(battle_sound(stack->getCreature(), defend)); + logAnim->debug("Created DefenceAnimation for %s", stack->getName()); +} - auto * defAnim = dynamic_cast(elem); - if(defAnim && defAnim->stack->ID != stack->ID) - continue; +DeathAnimation::DeathAnimation(BattleInterface & owner, const CStack * stack, bool ranged): + StackActionAnimation(owner, stack) +{ + setSound(battle_sound(stack->getCreature(), killed)); - auto * attAnim = dynamic_cast(elem); - if(attAnim && attAnim->stack->ID != stack->ID) - continue; - - auto * sen = dynamic_cast(elem); - if (sen && attacker == nullptr) - return false; - - if (sen) - continue; - - CReverseAnimation * animAsRev = dynamic_cast(elem); - - if(animAsRev) - return false; - - if(elem) - vstd::amin(lowestMoveID, elem->ID); - } - - if(ID > lowestMoveID) - return false; - - - //reverse unit if necessary - if(attacker && owner.getCurrentPlayerInterface()->cb->isToReverse(stack->getPosition(), attacker->getPosition(), stackFacingRight(stack), attacker->doubleWide(), stackFacingRight(attacker))) - { - owner.stacksController->addNewAnim(new CReverseAnimation(owner, stack, stack->getPosition(), true)); - return false; - } - //unit reversed - - if(rangedAttack && attacker != nullptr && owner.projectilesController->hasActiveProjectile(attacker)) //delay hit animation - { - return false; - } - - // synchronize animation with attacker, unless defending or attacked by shooter: - // wait for 1/2 of attack animation - if (!rangedAttack && getMyAnimType() != CCreatureAnim::DEFENCE) - { - float frameLength = AnimationControls::getCreatureAnimationSpeed( - stack->getCreature(), stackAnimation(stack).get(), getMyAnimType()); - - timeToWait = myAnim->framesInGroup(getMyAnimType()) * frameLength / 2; - - myAnim->setType(CCreatureAnim::HOLDING); - } + if(ranged && myAnim->framesInGroup(ECreatureAnimType::DEATH_RANGED) > 0) + setGroup(ECreatureAnimType::DEATH_RANGED); else - { - timeToWait = 0; - startAnimation(); - } + setGroup(ECreatureAnimType::DEATH); - return true; //initialized successfuly -} - -std::string CDefenceAnimation::getMySound() -{ - if(killed) - return battle_sound(stack->getCreature(), killed); - else if(stack->defendingAnim) - return battle_sound(stack->getCreature(), defend); + if(ranged && myAnim->framesInGroup(ECreatureAnimType::DEAD_RANGED) > 0) + setNextGroup(ECreatureAnimType::DEAD_RANGED); else - return battle_sound(stack->getCreature(), wince); + setNextGroup(ECreatureAnimType::DEAD); + + logAnim->debug("Created DeathAnimation for %s", stack->getName()); } -CCreatureAnim::EAnimType CDefenceAnimation::getMyAnimType() -{ - if(killed) - { - if(rangedAttack && myAnim->framesInGroup(CCreatureAnim::DEATH_RANGED) > 0) - return CCreatureAnim::DEATH_RANGED; - else - return CCreatureAnim::DEATH; - } - - if(stack->defendingAnim) - return CCreatureAnim::DEFENCE; - else - return CCreatureAnim::HITTED; -} - -void CDefenceAnimation::startAnimation() -{ - CCS->soundh->playSound(getMySound()); - myAnim->setType(getMyAnimType()); - myAnim->onAnimationReset += [&](){ delete this; }; -} - -void CDefenceAnimation::nextFrame() -{ - if (timeToWait > 0) - { - timeToWait -= float(GH.mainFPSmng->getElapsedMilliseconds()) / 1000; - if (timeToWait <= 0) - startAnimation(); - } - - CBattleAnimation::nextFrame(); -} - -CDefenceAnimation::~CDefenceAnimation() -{ - if(killed) - { - if(rangedAttack && myAnim->framesInGroup(CCreatureAnim::DEAD_RANGED) > 0) - myAnim->setType(CCreatureAnim::DEAD_RANGED); - else - myAnim->setType(CCreatureAnim::DEAD); - } - else - { - myAnim->setType(CCreatureAnim::HOLDING); - } -} - -CDummyAnimation::CDummyAnimation(BattleInterface & owner, int howManyFrames) - : CBattleAnimation(owner), +DummyAnimation::DummyAnimation(BattleInterface & owner, int howManyFrames) + : BattleAnimation(owner), counter(0), howMany(howManyFrames) { logAnim->debug("Created dummy animation for %d frames", howManyFrames); } -bool CDummyAnimation::init() +bool DummyAnimation::init() { return true; } -void CDummyAnimation::nextFrame() +void DummyAnimation::nextFrame() { counter++; if(counter > howMany) delete this; } -bool CMeleeAttackAnimation::init() +ECreatureAnimType MeleeAttackAnimation::getUpwardsGroup(bool multiAttack) const { - if(!CAttackAnimation::checkInitialConditions()) - return false; + if (!multiAttack) + return ECreatureAnimType::ATTACK_UP; - if(!attackingStack || myAnim->isDeadOrDying()) + return findValidGroup({ + ECreatureAnimType::GROUP_ATTACK_UP, + ECreatureAnimType::SPECIAL_UP, + ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3 + ECreatureAnimType::ATTACK_UP + }); +} + +ECreatureAnimType MeleeAttackAnimation::getForwardGroup(bool multiAttack) const +{ + if (!multiAttack) + return ECreatureAnimType::ATTACK_FRONT; + + return findValidGroup({ + ECreatureAnimType::GROUP_ATTACK_FRONT, + ECreatureAnimType::SPECIAL_FRONT, + ECreatureAnimType::ATTACK_FRONT + }); +} + +ECreatureAnimType MeleeAttackAnimation::getDownwardsGroup(bool multiAttack) const +{ + if (!multiAttack) + return ECreatureAnimType::ATTACK_DOWN; + + return findValidGroup({ + ECreatureAnimType::GROUP_ATTACK_DOWN, + ECreatureAnimType::SPECIAL_DOWN, + ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3 + ECreatureAnimType::ATTACK_DOWN + }); +} + +ECreatureAnimType MeleeAttackAnimation::selectGroup(bool multiAttack) +{ + const ECreatureAnimType mutPosToGroup[] = { - delete this; - return false; - } - - bool toReverse = owner.getCurrentPlayerInterface()->cb->isToReverse(attackingStackPosBeforeReturn, attackedStack->getPosition(), stackFacingRight(stack), attackedStack->doubleWide(), stackFacingRight(attackedStack)); - - if(toReverse) - { - owner.stacksController->addNewAnim(new CReverseAnimation(owner, stack, attackingStackPosBeforeReturn, true)); - return false; - } - - // opponent must face attacker ( = different directions) before he can be attacked - if(attackingStack && attackedStack && - stackFacingRight(attackingStack) == stackFacingRight(attackedStack)) - return false; - - //reversed - - shooting = false; - - static const CCreatureAnim::EAnimType mutPosToGroup[] = - { - CCreatureAnim::ATTACK_UP, - CCreatureAnim::ATTACK_UP, - CCreatureAnim::ATTACK_FRONT, - CCreatureAnim::ATTACK_DOWN, - CCreatureAnim::ATTACK_DOWN, - CCreatureAnim::ATTACK_FRONT - }; - - static const CCreatureAnim::EAnimType mutPosToGroup2H[] = - { - CCreatureAnim::VCMI_2HEX_UP, - CCreatureAnim::VCMI_2HEX_UP, - CCreatureAnim::VCMI_2HEX_FRONT, - CCreatureAnim::VCMI_2HEX_DOWN, - CCreatureAnim::VCMI_2HEX_DOWN, - CCreatureAnim::VCMI_2HEX_FRONT + getUpwardsGroup (multiAttack), + getUpwardsGroup (multiAttack), + getForwardGroup (multiAttack), + getDownwardsGroup(multiAttack), + getDownwardsGroup(multiAttack), + getForwardGroup (multiAttack) }; int revShiftattacker = (attackingStack->side == BattleSide::ATTACKER ? -1 : 1); @@ -400,95 +284,76 @@ bool CMeleeAttackAnimation::init() int mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn, dest); if(mutPos == -1 && attackingStack->doubleWide()) { - mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn + revShiftattacker, attackedStack->getPosition()); + mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn + revShiftattacker, defendingStack->getPosition()); } - if (mutPos == -1 && attackedStack->doubleWide()) + if (mutPos == -1 && defendingStack->doubleWide()) { - mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn, attackedStack->occupiedHex()); + mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn, defendingStack->occupiedHex()); } - if (mutPos == -1 && attackedStack->doubleWide() && attackingStack->doubleWide()) + if (mutPos == -1 && defendingStack->doubleWide() && attackingStack->doubleWide()) { - mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn + revShiftattacker, attackedStack->occupiedHex()); + mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn + revShiftattacker, defendingStack->occupiedHex()); } + assert(mutPos >= 0 && mutPos <=5); - switch(mutPos) //attack direction - { - case 0: - case 1: - case 2: - case 3: - case 4: - case 5: - group = mutPosToGroup[mutPos]; - if(attackingStack->hasBonusOfType(Bonus::TWO_HEX_ATTACK_BREATH)) - { - CCreatureAnim::EAnimType group2H = mutPosToGroup2H[mutPos]; - if(myAnim->framesInGroup(group2H)>0) - group = group2H; - } - - break; - default: - logGlobal->error("Critical Error! Wrong dest in stackAttacking! dest: %d; attacking stack pos: %d; mutual pos: %d", dest.hex, attackingStackPosBeforeReturn, mutPos); - group = CCreatureAnim::ATTACK_FRONT; - break; - } - - return true; + return mutPosToGroup[mutPos]; } -CMeleeAttackAnimation::CMeleeAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked) - : CAttackAnimation(owner, attacker, _dest, _attacked) +void MeleeAttackAnimation::nextFrame() { - logAnim->debug("Created melee attack anim for %s", attacker->getName()); -} + size_t currentFrame = stackAnimation(attackingStack)->getCurrentFrame(); + size_t totalFrames = stackAnimation(attackingStack)->framesInGroup(getGroup()); -CStackMoveAnimation::CStackMoveAnimation(BattleInterface & owner, const CStack * _stack, BattleHex _currentHex): - CBattleStackAnimation(owner, _stack), - currentHex(_currentHex) -{ - -} - -bool CMovementAnimation::init() -{ - if( !CBattleAnimation::checkInitialConditions() ) - return false; - - if(!stack || myAnim->isDeadOrDying()) + if ( currentFrame * 2 >= totalFrames ) { + if(owner.getAnimationCondition(EAnimationEvents::HIT) == false) + owner.setAnimationCondition(EAnimationEvents::HIT, true); + } + AttackAnimation::nextFrame(); +} + +MeleeAttackAnimation::MeleeAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, bool multiAttack) + : AttackAnimation(owner, attacker, _dest, _attacked) +{ + logAnim->debug("Created MeleeAttackAnimation for %s", attacker->getName()); + setSound(battle_sound(getCreature(), attack)); + setGroup(selectGroup(multiAttack)); +} + +StackMoveAnimation::StackMoveAnimation(BattleInterface & owner, const CStack * _stack, BattleHex prevHex, BattleHex nextHex): + BattleStackAnimation(owner, _stack), + prevHex(prevHex), + nextHex(nextHex) +{ +} + +bool MovementAnimation::init() +{ + assert(stack); + assert(!myAnim->isDeadOrDying()); + assert(stackAnimation(stack)->framesInGroup(ECreatureAnimType::MOVING) > 0); + + if(stackAnimation(stack)->framesInGroup(ECreatureAnimType::MOVING) == 0) + { + //no movement, end immediately delete this; return false; } - if(stackAnimation(stack)->framesInGroup(CCreatureAnim::MOVING) == 0 || - stack->hasBonus(Selector::typeSubtype(Bonus::FLYING, 1))) - { - //no movement or teleport, end immediately - delete this; - return false; - } + logAnim->debug("CMovementAnimation::init: stack %s moves %d -> %d", stack->getName(), prevHex, nextHex); //reverse unit if necessary - if(owner.stacksController->shouldRotate(stack, oldPos, currentHex)) + if(owner.stacksController->shouldRotate(stack, prevHex, nextHex)) { - // it seems that H3 does NOT plays full rotation animation here in most situations + // it seems that H3 does NOT plays full rotation animation during movement // Logical since it takes quite a lot of time - if (curentMoveIndex == 0) // full rotation only for moving towards first tile. - { - owner.stacksController->addNewAnim(new CReverseAnimation(owner, stack, oldPos, true)); - return false; - } - else - { - rotateStack(oldPos); - } + rotateStack(prevHex); } - if(myAnim->getType() != CCreatureAnim::MOVING) + if(myAnim->getType() != ECreatureAnimType::MOVING) { - myAnim->setType(CCreatureAnim::MOVING); + myAnim->setType(ECreatureAnimType::MOVING); } if (owner.moveSoundHander == -1) @@ -496,14 +361,14 @@ bool CMovementAnimation::init() owner.moveSoundHander = CCS->soundh->playSound(battle_sound(stack->getCreature(), move), -1); } - Point begPosition = owner.stacksController->getStackPositionAtHex(oldPos, stack); - Point endPosition = owner.stacksController->getStackPositionAtHex(currentHex, stack); + Point begPosition = owner.stacksController->getStackPositionAtHex(prevHex, stack); + Point endPosition = owner.stacksController->getStackPositionAtHex(nextHex, stack); timeToMove = AnimationControls::getMovementDuration(stack->getCreature()); begX = begPosition.x; begY = begPosition.y; - progress = 0; + //progress = 0; distanceX = endPosition.x - begPosition.x; distanceY = endPosition.y - begPosition.y; @@ -517,7 +382,7 @@ bool CMovementAnimation::init() return true; } -void CMovementAnimation::nextFrame() +void MovementAnimation::nextFrame() { progress += float(GH.mainFPSmng->getElapsedMilliseconds()) / 1000 * timeToMove; @@ -525,21 +390,22 @@ void CMovementAnimation::nextFrame() myAnim->pos.x = static_cast(begX + distanceX * progress ); myAnim->pos.y = static_cast(begY + distanceY * progress ); - CBattleAnimation::nextFrame(); + BattleAnimation::nextFrame(); if(progress >= 1.0) { + progress -= 1.0; // Sets the position of the creature animation sprites - Point coords = owner.stacksController->getStackPositionAtHex(currentHex, stack); - myAnim->pos = coords; + Point coords = owner.stacksController->getStackPositionAtHex(nextHex, stack); + myAnim->pos.moveTo(coords); // true if creature haven't reached the final destination hex if ((curentMoveIndex + 1) < destTiles.size()) { // update the next hex field which has to be reached by the stack curentMoveIndex++; - oldPos = currentHex; - currentHex = destTiles[curentMoveIndex]; + prevHex = nextHex; + nextHex = destTiles[curentMoveIndex]; // request re-initialization initialized = false; @@ -549,13 +415,10 @@ void CMovementAnimation::nextFrame() } } -CMovementAnimation::~CMovementAnimation() +MovementAnimation::~MovementAnimation() { assert(stack); - myAnim->pos = owner.stacksController->getStackPositionAtHex(currentHex, stack); - owner.stacksController->addNewAnim(new CMovementEndAnimation(owner, stack, currentHex)); - if(owner.moveSoundHander != -1) { CCS->soundh->stopSound(owner.moveSoundHander); @@ -563,65 +426,28 @@ CMovementAnimation::~CMovementAnimation() } } -CMovementAnimation::CMovementAnimation(BattleInterface & owner, const CStack *_stack, std::vector _destTiles, int _distance) - : CStackMoveAnimation(owner, _stack, _destTiles.front()), +MovementAnimation::MovementAnimation(BattleInterface & owner, const CStack *stack, std::vector _destTiles, int _distance) + : StackMoveAnimation(owner, stack, stack->getPosition(), _destTiles.front()), destTiles(_destTiles), curentMoveIndex(0), - oldPos(stack->getPosition()), begX(0), begY(0), distanceX(0), distanceY(0), timeToMove(0.0), progress(0.0) { - logAnim->debug("Created movement anim for %s", stack->getName()); + logAnim->debug("Created MovementAnimation for %s", stack->getName()); } -CMovementEndAnimation::CMovementEndAnimation(BattleInterface & owner, const CStack * _stack, BattleHex destTile) -: CStackMoveAnimation(owner, _stack, destTile) +MovementEndAnimation::MovementEndAnimation(BattleInterface & owner, const CStack * _stack, BattleHex destTile) +: StackMoveAnimation(owner, _stack, destTile, destTile) { - logAnim->debug("Created movement end anim for %s", stack->getName()); + logAnim->debug("Created MovementEndAnimation for %s", stack->getName()); } -bool CMovementEndAnimation::init() +bool MovementEndAnimation::init() { - //if( !isEarliest(true) ) - // return false; - - if(!stack || myAnim->framesInGroup(CCreatureAnim::MOVE_END) == 0 || - myAnim->isDeadOrDying()) - { - delete this; - return false; - } - - CCS->soundh->playSound(battle_sound(stack->getCreature(), endMoving)); - - myAnim->setType(CCreatureAnim::MOVE_END); - - myAnim->onAnimationReset += [&](){ delete this; }; - - return true; -} - -CMovementEndAnimation::~CMovementEndAnimation() -{ - if(myAnim->getType() != CCreatureAnim::DEAD) - myAnim->setType(CCreatureAnim::HOLDING); //resetting to default - - CCS->curh->show(); -} - -CMovementStartAnimation::CMovementStartAnimation(BattleInterface & owner, const CStack * _stack) - : CStackMoveAnimation(owner, _stack, _stack->getPosition()) -{ - logAnim->debug("Created movement start anim for %s", stack->getName()); -} - -bool CMovementStartAnimation::init() -{ - if( !CBattleAnimation::checkInitialConditions() ) - return false; - + assert(stack); + assert(!myAnim->isDeadOrDying()); if(!stack || myAnim->isDeadOrDying()) { @@ -629,35 +455,85 @@ bool CMovementStartAnimation::init() return false; } - CCS->soundh->playSound(battle_sound(stack->getCreature(), startMoving)); - myAnim->setType(CCreatureAnim::MOVE_START); + logAnim->debug("CMovementEndAnimation::init: stack %s", stack->getName()); + myAnim->pos.moveTo(owner.stacksController->getStackPositionAtHex(nextHex, stack)); + + CCS->soundh->playSound(battle_sound(stack->getCreature(), endMoving)); + + if(!myAnim->framesInGroup(ECreatureAnimType::MOVE_END)) + { + delete this; + return false; + } + + + myAnim->setType(ECreatureAnimType::MOVE_END); myAnim->onAnimationReset += [&](){ delete this; }; return true; } -CReverseAnimation::CReverseAnimation(BattleInterface & owner, const CStack * stack, BattleHex dest, bool _priority) - : CStackMoveAnimation(owner, stack, dest), - priority(_priority) +MovementEndAnimation::~MovementEndAnimation() { - logAnim->debug("Created reverse anim for %s", stack->getName()); + if(myAnim->getType() != ECreatureAnimType::DEAD) + myAnim->setType(ECreatureAnimType::HOLDING); //resetting to default + + CCS->curh->show(); } -bool CReverseAnimation::init() +MovementStartAnimation::MovementStartAnimation(BattleInterface & owner, const CStack * _stack) + : StackMoveAnimation(owner, _stack, _stack->getPosition(), _stack->getPosition()) { + logAnim->debug("Created MovementStartAnimation for %s", stack->getName()); +} + +bool MovementStartAnimation::init() +{ + assert(stack); + assert(!myAnim->isDeadOrDying()); + + if(!stack || myAnim->isDeadOrDying()) + { + delete this; + return false; + } + + logAnim->debug("CMovementStartAnimation::init: stack %s", stack->getName()); + CCS->soundh->playSound(battle_sound(stack->getCreature(), startMoving)); + + if(!myAnim->framesInGroup(ECreatureAnimType::MOVE_START)) + { + delete this; + return false; + } + + myAnim->setType(ECreatureAnimType::MOVE_START); + myAnim->onAnimationReset += [&](){ delete this; }; + return true; +} + +ReverseAnimation::ReverseAnimation(BattleInterface & owner, const CStack * stack, BattleHex dest) + : StackMoveAnimation(owner, stack, dest, dest) +{ + logAnim->debug("Created ReverseAnimation for %s", stack->getName()); +} + +bool ReverseAnimation::init() +{ + assert(myAnim); + assert(!myAnim->isDeadOrDying()); + if(myAnim == nullptr || myAnim->isDeadOrDying()) { delete this; return false; //there is no such creature } - if(!priority && !CBattleAnimation::checkInitialConditions()) - return false; - - if(myAnim->framesInGroup(CCreatureAnim::TURN_L)) + logAnim->debug("CReverseAnimation::init: stack %s", stack->getName()); + if(myAnim->framesInGroup(ECreatureAnimType::TURN_L)) { - myAnim->setType(CCreatureAnim::TURN_L); - myAnim->onAnimationReset += std::bind(&CReverseAnimation::setupSecondPart, this); + myAnim->playOnce(ECreatureAnimType::TURN_L); + myAnim->onAnimationReset += std::bind(&ReverseAnimation::setupSecondPart, this); } else { @@ -666,79 +542,116 @@ bool CReverseAnimation::init() return true; } -CReverseAnimation::~CReverseAnimation() -{ - if( stack && stack->alive() )//don't do that if stack is dead - myAnim->setType(CCreatureAnim::HOLDING); -} - -void CBattleStackAnimation::rotateStack(BattleHex hex) +void BattleStackAnimation::rotateStack(BattleHex hex) { setStackFacingRight(stack, !stackFacingRight(stack)); - stackAnimation(stack)->pos = owner.stacksController->getStackPositionAtHex(hex, stack); + stackAnimation(stack)->pos.moveTo(owner.stacksController->getStackPositionAtHex(hex, stack)); } -void CReverseAnimation::setupSecondPart() +void ReverseAnimation::setupSecondPart() { + assert(stack); + if(!stack) { delete this; return; } - rotateStack(currentHex); + rotateStack(nextHex); - if(myAnim->framesInGroup(CCreatureAnim::TURN_R)) + if(myAnim->framesInGroup(ECreatureAnimType::TURN_R)) { - myAnim->setType(CCreatureAnim::TURN_R); + myAnim->playOnce(ECreatureAnimType::TURN_R); myAnim->onAnimationReset += [&](){ delete this; }; } else delete this; } -CRangedAttackAnimation::CRangedAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest_, const CStack * defender) - : CAttackAnimation(owner, attacker, dest_, defender), - projectileEmitted(false) +ResurrectionAnimation::ResurrectionAnimation(BattleInterface & owner, const CStack * _stack): + StackActionAnimation(owner, _stack) { - logAnim->info("Ranged attack animation created"); + setGroup(ECreatureAnimType::RESURRECTION); + logAnim->debug("Created ResurrectionAnimation for %s", stack->getName()); } -bool CRangedAttackAnimation::init() +bool ColorTransformAnimation::init() { - if( !CAttackAnimation::checkInitialConditions() ) - return false; - - assert(attackingStack); - assert(!myAnim->isDeadOrDying()); - - if(!attackingStack || myAnim->isDeadOrDying()) - { - //FIXME: how is this possible? - logAnim->warn("Shooting animation has not started yet but attacker is dead! Aborting..."); - delete this; - return false; - } - - //reverse unit if necessary - if (attackingStack && attackedStack && owner.getCurrentPlayerInterface()->cb->isToReverse(attackingStack->getPosition(), attackedStack->getPosition(), stackFacingRight(attackingStack), attackingStack->doubleWide(), stackFacingRight(attackedStack))) - { - owner.stacksController->addNewAnim(new CReverseAnimation(owner, attackingStack, attackingStack->getPosition(), true)); - return false; - } - - logAnim->info("Ranged attack animation initialized"); - setAnimationGroup(); - initializeProjectile(); - shooting = true; return true; } -void CRangedAttackAnimation::setAnimationGroup() +void ColorTransformAnimation::nextFrame() +{ + float elapsed = GH.mainFPSmng->getElapsedMilliseconds() / 1000.f; + float fullTime = AnimationControls::getFadeInDuration(); + float delta = elapsed / fullTime; + totalProgress += delta; + + size_t index = 0; + + while (index < timePoints.size() && timePoints[index] < totalProgress ) + ++index; + + if (index == timePoints.size()) + { + //end of animation. Apply ColorShifter using final values and die + const auto & shifter = steps[index - 1]; + owner.stacksController->setStackColorFilter(shifter, stack, spell, false); + delete this; + return; + } + + assert(index != 0); + + const auto & prevShifter = steps[index - 1]; + const auto & nextShifter = steps[index]; + + float prevPoint = timePoints[index-1]; + float nextPoint = timePoints[index]; + float localProgress = totalProgress - prevPoint; + float stepDuration = (nextPoint - prevPoint); + float factor = localProgress / stepDuration; + + auto shifter = ColorFilter::genInterpolated(prevShifter, nextShifter, factor); + + owner.stacksController->setStackColorFilter(shifter, stack, spell, true); +} + +ColorTransformAnimation::ColorTransformAnimation(BattleInterface & owner, const CStack * _stack, const std::string & colorFilterName, const CSpell * spell): + BattleStackAnimation(owner, _stack), + spell(spell), + totalProgress(0.f) +{ + auto effect = owner.effectsController->getMuxerEffect(colorFilterName); + steps = effect.filters; + timePoints = effect.timePoints; + + assert(!steps.empty() && steps.size() == timePoints.size()); + + logAnim->debug("Created ColorTransformAnimation for %s", stack->getName()); +} + +RangedAttackAnimation::RangedAttackAnimation(BattleInterface & owner_, const CStack * attacker, BattleHex dest_, const CStack * defender) + : AttackAnimation(owner_, attacker, dest_, defender), + projectileEmitted(false) +{ + setSound(battle_sound(getCreature(), shoot)); +} + +bool RangedAttackAnimation::init() +{ + setAnimationGroup(); + initializeProjectile(); + + return AttackAnimation::init(); +} + +void RangedAttackAnimation::setAnimationGroup() { Point shooterPos = stackAnimation(attackingStack)->pos.topLeft(); - Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, attackedStack); + Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, defendingStack); //maximal angle in radians between straight horizontal line and shooting line for which shot is considered to be straight (absoulte value) static const double straightAngle = 0.2; @@ -747,31 +660,31 @@ void CRangedAttackAnimation::setAnimationGroup() // Calculate projectile start position. Offsets are read out of the CRANIM.TXT. if (projectileAngle > straightAngle) - group = getUpwardsGroup(); + setGroup(getUpwardsGroup()); else if (projectileAngle < -straightAngle) - group = getDownwardsGroup(); + setGroup(getDownwardsGroup()); else - group = getForwardGroup(); + setGroup(getForwardGroup()); } -void CRangedAttackAnimation::initializeProjectile() +void RangedAttackAnimation::initializeProjectile() { const CCreature *shooterInfo = getCreature(); - Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, attackedStack) + Point(225, 225); + Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, defendingStack) + Point(225, 225); Point shotOrigin = stackAnimation(attackingStack)->pos.topLeft() + Point(222, 265); int multiplier = stackFacingRight(attackingStack) ? 1 : -1; - if (group == getUpwardsGroup()) + if (getGroup() == getUpwardsGroup()) { shotOrigin.x += ( -25 + shooterInfo->animation.upperRightMissleOffsetX ) * multiplier; shotOrigin.y += shooterInfo->animation.upperRightMissleOffsetY; } - else if (group == getDownwardsGroup()) + else if (getGroup() == getDownwardsGroup()) { shotOrigin.x += ( -25 + shooterInfo->animation.lowerRightMissleOffsetX ) * multiplier; shotOrigin.y += shooterInfo->animation.lowerRightMissleOffsetY; } - else if (group == getForwardGroup()) + else if (getGroup() == getForwardGroup()) { shotOrigin.x += ( -25 + shooterInfo->animation.rightMissleOffsetX ) * multiplier; shotOrigin.y += shooterInfo->animation.rightMissleOffsetY; @@ -784,59 +697,41 @@ void CRangedAttackAnimation::initializeProjectile() createProjectile(shotOrigin, shotTarget); } -void CRangedAttackAnimation::emitProjectile() +void RangedAttackAnimation::emitProjectile() { - logAnim->info("Ranged attack projectile emitted"); + logAnim->debug("Ranged attack projectile emitted"); owner.projectilesController->emitStackProjectile(attackingStack); projectileEmitted = true; } -void CRangedAttackAnimation::nextFrame() +void RangedAttackAnimation::nextFrame() { - for(auto & it : pendingAnimations()) - { - CMovementStartAnimation * anim = dynamic_cast(it); - CReverseAnimation * anim2 = dynamic_cast(it); - if( (anim && anim->stack->ID == stack->ID) || (anim2 && anim2->stack->ID == stack->ID && anim2->priority ) ) - { - assert(0); // FIXME: our stack started to move even though we are playing shooting animation? How? - return; - } - } - // animation should be paused if there is an active projectile if (projectileEmitted) { - if (owner.projectilesController->hasActiveProjectile(attackingStack)) - stackAnimation(attackingStack)->pause(); - else - stackAnimation(attackingStack)->play(); + if (!owner.projectilesController->hasActiveProjectile(attackingStack, false)) + { + if(owner.getAnimationCondition(EAnimationEvents::HIT) == false) + owner.setAnimationCondition(EAnimationEvents::HIT, true); + } } - CAttackAnimation::nextFrame(); + AttackAnimation::nextFrame(); if (!projectileEmitted) { - logAnim->info("Ranged attack executing, %d / %d / %d", - stackAnimation(attackingStack)->getCurrentFrame(), - getAttackClimaxFrame(), - stackAnimation(attackingStack)->framesInGroup(group)); - // emit projectile once animation playback reached "climax" frame if ( stackAnimation(attackingStack)->getCurrentFrame() >= getAttackClimaxFrame() ) { emitProjectile(); - stackAnimation(attackingStack)->pause(); return; } } } -CRangedAttackAnimation::~CRangedAttackAnimation() +RangedAttackAnimation::~RangedAttackAnimation() { - logAnim->info("Ranged attack animation is over"); - //FIXME: this assert triggers under some unclear, rare conditions. Possibly - if game window is inactive and/or in foreground/minimized? - assert(!owner.projectilesController->hasActiveProjectile(attackingStack)); + assert(!owner.projectilesController->hasActiveProjectile(attackingStack, false)); assert(projectileEmitted); // FIXME: is this possible? Animation is over but we're yet to fire projectile? @@ -847,49 +742,49 @@ CRangedAttackAnimation::~CRangedAttackAnimation() } } -CShootingAnimation::CShootingAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked) - : CRangedAttackAnimation(owner, attacker, _dest, _attacked) +ShootingAnimation::ShootingAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked) + : RangedAttackAnimation(owner, attacker, _dest, _attacked) { - logAnim->debug("Created shooting anim for %s", stack->getName()); + logAnim->debug("Created ShootingAnimation for %s", stack->getName()); } -void CShootingAnimation::createProjectile(const Point & from, const Point & dest) const +void ShootingAnimation::createProjectile(const Point & from, const Point & dest) const { owner.projectilesController->createProjectile(attackingStack, from, dest); } -uint32_t CShootingAnimation::getAttackClimaxFrame() const +uint32_t ShootingAnimation::getAttackClimaxFrame() const { const CCreature *shooterInfo = getCreature(); return shooterInfo->animation.attackClimaxFrame; } -CCreatureAnim::EAnimType CShootingAnimation::getUpwardsGroup() const +ECreatureAnimType ShootingAnimation::getUpwardsGroup() const { - return CCreatureAnim::SHOOT_UP; + return ECreatureAnimType::SHOOT_UP; } -CCreatureAnim::EAnimType CShootingAnimation::getForwardGroup() const +ECreatureAnimType ShootingAnimation::getForwardGroup() const { - return CCreatureAnim::SHOOT_FRONT; + return ECreatureAnimType::SHOOT_FRONT; } -CCreatureAnim::EAnimType CShootingAnimation::getDownwardsGroup() const +ECreatureAnimType ShootingAnimation::getDownwardsGroup() const { - return CCreatureAnim::SHOOT_DOWN; + return ECreatureAnimType::SHOOT_DOWN; } -CCatapultAnimation::CCatapultAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, int _catapultDmg) - : CShootingAnimation(owner, attacker, _dest, _attacked), +CatapultAnimation::CatapultAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, int _catapultDmg) + : ShootingAnimation(owner, attacker, _dest, _attacked), catapultDamage(_catapultDmg), explosionEmitted(false) { logAnim->debug("Created shooting anim for %s", stack->getName()); } -void CCatapultAnimation::nextFrame() +void CatapultAnimation::nextFrame() { - CShootingAnimation::nextFrame(); + ShootingAnimation::nextFrame(); if ( explosionEmitted) return; @@ -897,141 +792,129 @@ void CCatapultAnimation::nextFrame() if ( !projectileEmitted) return; - if (owner.projectilesController->hasActiveProjectile(attackingStack)) + if (owner.projectilesController->hasActiveProjectile(attackingStack, false)) return; explosionEmitted = true; - Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, attackedStack) + Point(225, 225) - Point(126, 105); + Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, defendingStack) + Point(225, 225) - Point(126, 105); - if(catapultDamage > 0) - owner.stacksController->addNewAnim( new CPointEffectAnimation(owner, soundBase::WALLHIT, "SGEXPL.DEF", shotTarget)); - else - owner.stacksController->addNewAnim( new CPointEffectAnimation(owner, soundBase::WALLMISS, "CSGRCK.DEF", shotTarget)); + std::string soundFilename = (catapultDamage > 0) ? "WALLHIT" : "WALLMISS"; + std::string effectFilename = (catapultDamage > 0) ? "SGEXPL" : "CSGRCK"; + + CCS->soundh->playSound( soundFilename ); + owner.stacksController->addNewAnim( new EffectAnimation(owner, effectFilename, shotTarget)); } -void CCatapultAnimation::createProjectile(const Point & from, const Point & dest) const +void CatapultAnimation::createProjectile(const Point & from, const Point & dest) const { owner.projectilesController->createCatapultProjectile(attackingStack, from, dest); } - -CCastAnimation::CCastAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest_, const CStack * defender, const CSpell * spell) - : CRangedAttackAnimation(owner, attacker, dest_, defender), +CastAnimation::CastAnimation(BattleInterface & owner_, const CStack * attacker, BattleHex dest, const CStack * defender, const CSpell * spell) + : RangedAttackAnimation(owner_, attacker, dest, defender), spell(spell) { - assert(dest.isValid());// FIXME: when? - - if(!dest_.isValid() && defender) - dest = defender->getPosition(); -} - -CCreatureAnim::EAnimType CCastAnimation::findValidGroup( const std::vector candidates ) const -{ - for ( auto group : candidates) + if(!dest.isValid()) { - if(myAnim->framesInGroup(group) > 0) - return group; + assert(spell->animationInfo.projectile.empty()); + + if (defender) + dest = defender->getPosition(); + else + dest = attacker->getPosition(); } - - assert(0); - return CCreatureAnim::HOLDING; } -CCreatureAnim::EAnimType CCastAnimation::getUpwardsGroup() const +ECreatureAnimType CastAnimation::getUpwardsGroup() const { return findValidGroup({ - CCreatureAnim::VCMI_CAST_UP, - CCreatureAnim::CAST_UP, - CCreatureAnim::SHOOT_UP, - CCreatureAnim::ATTACK_UP + ECreatureAnimType::CAST_UP, + ECreatureAnimType::SPECIAL_UP, + ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3 + ECreatureAnimType::SHOOT_UP, + ECreatureAnimType::ATTACK_UP }); } -CCreatureAnim::EAnimType CCastAnimation::getForwardGroup() const +ECreatureAnimType CastAnimation::getForwardGroup() const { return findValidGroup({ - CCreatureAnim::VCMI_CAST_FRONT, - CCreatureAnim::CAST_FRONT, - CCreatureAnim::SHOOT_FRONT, - CCreatureAnim::ATTACK_FRONT + ECreatureAnimType::CAST_FRONT, + ECreatureAnimType::SPECIAL_FRONT, + ECreatureAnimType::SHOOT_FRONT, + ECreatureAnimType::ATTACK_FRONT }); } -CCreatureAnim::EAnimType CCastAnimation::getDownwardsGroup() const +ECreatureAnimType CastAnimation::getDownwardsGroup() const { return findValidGroup({ - CCreatureAnim::VCMI_CAST_DOWN, - CCreatureAnim::CAST_DOWN, - CCreatureAnim::SHOOT_DOWN, - CCreatureAnim::ATTACK_DOWN + ECreatureAnimType::CAST_DOWN, + ECreatureAnimType::SPECIAL_DOWN, + ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3 + ECreatureAnimType::SHOOT_DOWN, + ECreatureAnimType::ATTACK_DOWN }); } -void CCastAnimation::createProjectile(const Point & from, const Point & dest) const +void CastAnimation::createProjectile(const Point & from, const Point & dest) const { if (!spell->animationInfo.projectile.empty()) owner.projectilesController->createSpellProjectile(attackingStack, from, dest, spell); } -uint32_t CCastAnimation::getAttackClimaxFrame() const +uint32_t CastAnimation::getAttackClimaxFrame() const { - //FIXME: allow defining this parameter in config file, separately from attackClimaxFrame of missile attacks - uint32_t maxFrames = stackAnimation(attackingStack)->framesInGroup(group); + //TODO: allow defining this parameter in config file, separately from attackClimaxFrame of missile attacks + uint32_t maxFrames = stackAnimation(attackingStack)->framesInGroup(getGroup()); - if (maxFrames > 2) - return maxFrames - 2; - return 0; + return maxFrames / 2; } -CPointEffectAnimation::CPointEffectAnimation(BattleInterface & owner, soundBase::soundID sound, std::string animationName, int effects): - CBattleAnimation(owner), +EffectAnimation::EffectAnimation(BattleInterface & owner, std::string animationName, int effects): + BattleAnimation(owner), animation(std::make_shared(animationName)), - sound(sound), effectFlags(effects), - soundPlayed(false), - soundFinished(false), effectFinished(false) { + logAnim->debug("CPointEffectAnimation::init: effect %s", animationName); } -CPointEffectAnimation::CPointEffectAnimation(BattleInterface & owner, soundBase::soundID sound, std::string animationName, std::vector hex, int effects): - CPointEffectAnimation(owner, sound, animationName, effects) +EffectAnimation::EffectAnimation(BattleInterface & owner, std::string animationName, std::vector hex, int effects): + EffectAnimation(owner, animationName, effects) { battlehexes = hex; } -CPointEffectAnimation::CPointEffectAnimation(BattleInterface & owner, soundBase::soundID sound, std::string animationName, BattleHex hex, int effects): - CPointEffectAnimation(owner, sound, animationName, effects) +EffectAnimation::EffectAnimation(BattleInterface & owner, std::string animationName, BattleHex hex, int effects): + EffectAnimation(owner, animationName, effects) { assert(hex.isValid()); battlehexes.push_back(hex); } -CPointEffectAnimation::CPointEffectAnimation(BattleInterface & owner, soundBase::soundID sound, std::string animationName, std::vector pos, int effects): - CPointEffectAnimation(owner, sound, animationName, effects) +EffectAnimation::EffectAnimation(BattleInterface & owner, std::string animationName, std::vector pos, int effects): + EffectAnimation(owner, animationName, effects) { positions = pos; } -CPointEffectAnimation::CPointEffectAnimation(BattleInterface & owner, soundBase::soundID sound, std::string animationName, Point pos, int effects): - CPointEffectAnimation(owner, sound, animationName, effects) +EffectAnimation::EffectAnimation(BattleInterface & owner, std::string animationName, Point pos, int effects): + EffectAnimation(owner, animationName, effects) { positions.push_back(pos); } -CPointEffectAnimation::CPointEffectAnimation(BattleInterface & owner, soundBase::soundID sound, std::string animationName, Point pos, BattleHex hex, int effects): - CPointEffectAnimation(owner, sound, animationName, effects) +EffectAnimation::EffectAnimation(BattleInterface & owner, std::string animationName, Point pos, BattleHex hex, int effects): + EffectAnimation(owner, animationName, effects) { assert(hex.isValid()); battlehexes.push_back(hex); positions.push_back(pos); } -bool CPointEffectAnimation::init() +bool EffectAnimation::init() { - if(!CBattleAnimation::checkInitialConditions()) - return false; - animation->preload(); auto first = animation->getImage(0, 0, true); @@ -1043,9 +926,9 @@ bool CPointEffectAnimation::init() if (screenFill()) { - for(int i=0; i * first->width() < owner.pos.w ; ++i) - for(int j=0; j * first->height() < owner.pos.h ; ++j) - positions.push_back(Point( owner.pos.x + i * first->width(), owner.pos.y + j * first->height())); + for(int i=0; i * first->width() < owner.fieldController->pos.w ; ++i) + for(int j=0; j * first->height() < owner.fieldController->pos.h ; ++j) + positions.push_back(Point( i * first->width(), j * first->height())); } BattleEffect be; @@ -1059,41 +942,40 @@ bool CPointEffectAnimation::init() bool hasPosition = i < positions.size(); if (hasTile && !forceOnTop()) - be.position = battlehexes[i]; + be.tile = battlehexes[i]; else - be.position = BattleHex::INVALID; + be.tile = BattleHex::INVALID; if (hasPosition) { - be.x = positions[i].x; - be.y = positions[i].y; + be.pos.x = positions[i].x; + be.pos.y = positions[i].y; } else { const CStack * destStack = owner.getCurrentPlayerInterface()->cb->battleGetStackByPos(battlehexes[i], false); - Rect tilePos = owner.fieldController->hexPositionAbsolute(battlehexes[i]); + Rect tilePos = owner.fieldController->hexPositionLocal(battlehexes[i]); - be.x = tilePos.x + tilePos.w/2 - first->width()/2; + be.pos.x = tilePos.x + tilePos.w/2 - first->width()/2; if(destStack && destStack->doubleWide()) // Correction for 2-hex creatures. - be.x += (destStack->side == BattleSide::ATTACKER ? -1 : 1)*tilePos.w/2; + be.pos.x += (destStack->side == BattleSide::ATTACKER ? -1 : 1)*tilePos.w/2; if (alignToBottom()) - be.y = tilePos.y + tilePos.h - first->height(); + be.pos.y = tilePos.y + tilePos.h - first->height(); else - be.y = tilePos.y - first->height()/2; + be.pos.y = tilePos.y - first->height()/2; } owner.effectsController->battleEffects.push_back(be); } return true; } -void CPointEffectAnimation::nextFrame() +void EffectAnimation::nextFrame() { - playSound(); playEffect(); - if (soundFinished && effectFinished) + if (effectFinished) { //remove visual effect itself only if sound has finished as well - necessary for obstacles like force field clearEffect(); @@ -1101,57 +983,27 @@ void CPointEffectAnimation::nextFrame() } } -bool CPointEffectAnimation::alignToBottom() const +bool EffectAnimation::alignToBottom() const { return effectFlags & ALIGN_TO_BOTTOM; } -bool CPointEffectAnimation::waitForSound() const -{ - return effectFlags & WAIT_FOR_SOUND; -} - -bool CPointEffectAnimation::forceOnTop() const +bool EffectAnimation::forceOnTop() const { return effectFlags & FORCE_ON_TOP; } -bool CPointEffectAnimation::screenFill() const +bool EffectAnimation::screenFill() const { return effectFlags & SCREEN_FILL; } -void CPointEffectAnimation::onEffectFinished() +void EffectAnimation::onEffectFinished() { effectFinished = true; } -void CPointEffectAnimation::onSoundFinished() -{ - soundFinished = true; -} - -void CPointEffectAnimation::playSound() -{ - if (soundPlayed) - return; - - soundPlayed = true; - if (sound == soundBase::invalid) - { - onSoundFinished(); - return; - } - - int channel = CCS->soundh->playSound(sound); - - if (!waitForSound() || channel == -1) - onSoundFinished(); - else - CCS->soundh->setCallback(channel, [&](){ onSoundFinished(); }); -} - -void CPointEffectAnimation::playEffect() +void EffectAnimation::playEffect() { if ( effectFinished ) return; @@ -1172,7 +1024,7 @@ void CPointEffectAnimation::playEffect() } } -void CPointEffectAnimation::clearEffect() +void EffectAnimation::clearEffect() { auto & effects = owner.effectsController->battleEffects; @@ -1181,42 +1033,89 @@ void CPointEffectAnimation::clearEffect() }); } -CPointEffectAnimation::~CPointEffectAnimation() +EffectAnimation::~EffectAnimation() { assert(effectFinished); - assert(soundFinished); } -CWaitingAnimation::CWaitingAnimation(BattleInterface & owner): - CBattleAnimation(owner) -{} - -void CWaitingAnimation::nextFrame() +HeroCastAnimation::HeroCastAnimation(BattleInterface & owner, std::shared_ptr hero, BattleHex dest, const CStack * defender, const CSpell * spell): + BattleAnimation(owner), + projectileEmitted(false), + hero(hero), + target(defender), + tile(dest), + spell(spell) { - // initialization conditions fulfilled, delay is over - delete this; } -CWaitingProjectileAnimation::CWaitingProjectileAnimation(BattleInterface & owner, const CStack * shooter): - CWaitingAnimation(owner), - shooter(shooter) -{} - -bool CWaitingProjectileAnimation::init() +bool HeroCastAnimation::init() { - for(auto & elem : pendingAnimations()) - { - auto * attackAnim = dynamic_cast(elem); + hero->setPhase(EHeroAnimType::CAST_SPELL); - if( attackAnim && shooter && attackAnim->stack->ID == shooter->ID && !attackAnim->isInitialized() ) - { - // there is ongoing ranged attack that involves our stack, but projectile was not created yet - return false; - } - } + hero->onPhaseFinished([&](){ + assert(owner.getAnimationCondition(EAnimationEvents::HIT) == true); + delete this; + }); - if(owner.projectilesController->hasActiveProjectile(shooter)) - return false; + initializeProjectile(); return true; } + +void HeroCastAnimation::initializeProjectile() +{ + // spell has no projectile to play, ignore this step + if (spell->animationInfo.projectile.empty()) + return; + + // targeted spells should have well, target + assert(tile.isValid()); + + Point srccoord = hero->pos.center() - hero->parent->pos.topLeft(); + Point destcoord = owner.stacksController->getStackPositionAtHex(tile, target); //position attacked by projectile + + destcoord += Point(222, 265); // FIXME: what are these constants? + owner.projectilesController->createSpellProjectile( nullptr, srccoord, destcoord, spell); +} + +void HeroCastAnimation::emitProjectile() +{ + if (projectileEmitted) + return; + + //spell has no projectile to play, skip this step and immediately play hit animations + if (spell->animationInfo.projectile.empty()) + emitAnimationEvent(); + else + owner.projectilesController->emitStackProjectile( nullptr ); + + projectileEmitted = true; +} + +void HeroCastAnimation::emitAnimationEvent() +{ + if(owner.getAnimationCondition(EAnimationEvents::HIT) == false) + owner.setAnimationCondition(EAnimationEvents::HIT, true); +} + +void HeroCastAnimation::nextFrame() +{ + float frame = hero->getFrame(); + + if (frame < 4.0f) // middle point of animation //TODO: un-hardcode + return; + + if (!projectileEmitted) + { + emitProjectile(); + hero->pause(); + return; + } + + if (!owner.projectilesController->hasActiveProjectile(nullptr, false)) + { + emitAnimationEvent(); + //TODO: check H3 - it is possible that hero animation should be paused until hit effect is over, not just projectile + hero->play(); + } +} diff --git a/client/battle/BattleAnimationClasses.h b/client/battle/BattleAnimationClasses.h index 1c266c8d8..a42503259 100644 --- a/client/battle/BattleAnimationClasses.h +++ b/client/battle/BattleAnimationClasses.h @@ -10,36 +10,39 @@ #pragma once #include "../../lib/battle/BattleHex.h" -#include "../../lib/CSoundBase.h" -#include "../widgets/Images.h" +#include "../gui/Geometries.h" +#include "BattleConstants.h" VCMI_LIB_NAMESPACE_BEGIN class CStack; +class CCreature; +class CSpell; VCMI_LIB_NAMESPACE_END +struct SDL_Color; +class ColorFilter; +class BattleHero; +class CAnimation; class BattleInterface; class CreatureAnimation; -class CBattleAnimation; -struct CatapultProjectileInfo; struct StackAttackedInfo; +struct Point; /// Base class of battle animations -class CBattleAnimation +class BattleAnimation { - protected: BattleInterface & owner; bool initialized; - std::vector & pendingAnimations(); + std::vector & pendingAnimations(); std::shared_ptr stackAnimation(const CStack * stack) const; bool stackFacingRight(const CStack * stack); void setStackFacingRight(const CStack * stack, bool facingRight); virtual bool init() = 0; //to be called - if returned false, call again until returns true - bool checkInitialConditions(); //determines if this animation is earliest of all public: ui32 ID; //unique identifier @@ -47,107 +50,101 @@ public: bool isInitialized(); bool tryInitialize(); virtual void nextFrame() {} //call every new frame - virtual ~CBattleAnimation(); + virtual ~BattleAnimation(); - CBattleAnimation(BattleInterface & owner); + BattleAnimation(BattleInterface & owner); }; /// Sub-class which is responsible for managing the battle stack animation. -class CBattleStackAnimation : public CBattleAnimation +class BattleStackAnimation : public BattleAnimation { public: - std::shared_ptr myAnim; //animation for our stack, managed by CBattleInterface + std::shared_ptr myAnim; //animation for our stack, managed by BattleInterface const CStack * stack; //id of stack whose animation it is - CBattleStackAnimation(BattleInterface & owner, const CStack * _stack); - - void shiftColor(const ColorShifter * shifter); + BattleStackAnimation(BattleInterface & owner, const CStack * _stack); void rotateStack(BattleHex hex); }; -/// This class is responsible for managing the battle attack animation -class CAttackAnimation : public CBattleStackAnimation +class StackActionAnimation : public BattleStackAnimation { - bool soundPlayed; - -protected: - BattleHex dest; //attacked hex - bool shooting; - CCreatureAnim::EAnimType group; //if shooting is true, print this animation group - const CStack *attackedStack; - const CStack *attackingStack; - int attackingStackPosBeforeReturn; //for stacks with return_after_strike feature - - const CCreature * getCreature() const; + ECreatureAnimType nextGroup; + ECreatureAnimType currGroup; + std::string sound; public: - void nextFrame() override; - bool checkInitialConditions(); + void setNextGroup( ECreatureAnimType group ); + void setGroup( ECreatureAnimType group ); + void setSound( std::string sound ); - CAttackAnimation(BattleInterface & owner, const CStack *attacker, BattleHex _dest, const CStack *defender); - ~CAttackAnimation(); + ECreatureAnimType getGroup() const; + + StackActionAnimation(BattleInterface & owner, const CStack * _stack); + ~StackActionAnimation(); + + bool init() override; }; /// Animation of a defending unit -class CDefenceAnimation : public CBattleStackAnimation +class DefenceAnimation : public StackActionAnimation { - CCreatureAnim::EAnimType getMyAnimType(); - std::string getMySound(); - - void startAnimation(); - - const CStack * attacker; //attacking stack - bool rangedAttack; //if true, stack has been attacked by shooting - bool killed; //if true, stack has been killed - - float timeToWait; // for how long this animation should be paused public: + DefenceAnimation(BattleInterface & owner, const CStack * stack); +}; + +/// Animation of a hit unit +class HittedAnimation : public StackActionAnimation +{ +public: + HittedAnimation(BattleInterface & owner, const CStack * stack); +}; + +/// Animation of a dying unit +class DeathAnimation : public StackActionAnimation +{ +public: + DeathAnimation(BattleInterface & owner, const CStack * stack, bool ranged); +}; + +/// Resurrects stack from dead state +class ResurrectionAnimation : public StackActionAnimation +{ +public: + ResurrectionAnimation(BattleInterface & owner, const CStack * _stack); +}; + +class ColorTransformAnimation : public BattleStackAnimation +{ + std::vector steps; + std::vector timePoints; + const CSpell * spell; + + float totalProgress; + bool init() override; void nextFrame() override; - CDefenceAnimation(StackAttackedInfo _attackedInfo, BattleInterface & owner); - ~CDefenceAnimation(); -}; - -class CDummyAnimation : public CBattleAnimation -{ -private: - int counter; - int howMany; public: - bool init() override; - void nextFrame() override; - - CDummyAnimation(BattleInterface & owner, int howManyFrames); -}; - -/// Hand-to-hand attack -class CMeleeAttackAnimation : public CAttackAnimation -{ -public: - bool init() override; - - CMeleeAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked); + ColorTransformAnimation(BattleInterface & owner, const CStack * _stack, const std::string & colorFilterName, const CSpell * spell); }; /// Base class for all animations that play during stack movement -class CStackMoveAnimation : public CBattleStackAnimation +class StackMoveAnimation : public BattleStackAnimation { public: - BattleHex currentHex; + BattleHex nextHex; + BattleHex prevHex; protected: - CStackMoveAnimation(BattleInterface & owner, const CStack * _stack, BattleHex _currentHex); + StackMoveAnimation(BattleInterface & owner, const CStack * _stack, BattleHex prevHex, BattleHex nextHex); }; /// Move animation of a creature -class CMovementAnimation : public CStackMoveAnimation +class MovementAnimation : public StackMoveAnimation { private: std::vector destTiles; //full path, includes already passed hexes ui32 curentMoveIndex; // index of nextHex in destTiles - BattleHex oldPos; //position of stack before move - double begX, begY; // starting position double distanceX, distanceY; // full movement distance, may be negative if creture moves topleft @@ -158,45 +155,73 @@ public: bool init() override; void nextFrame() override; - CMovementAnimation(BattleInterface & owner, const CStack *_stack, std::vector _destTiles, int _distance); - ~CMovementAnimation(); + MovementAnimation(BattleInterface & owner, const CStack *_stack, std::vector _destTiles, int _distance); + ~MovementAnimation(); }; /// Move end animation of a creature -class CMovementEndAnimation : public CStackMoveAnimation +class MovementEndAnimation : public StackMoveAnimation { public: bool init() override; - CMovementEndAnimation(BattleInterface & owner, const CStack * _stack, BattleHex destTile); - ~CMovementEndAnimation(); + MovementEndAnimation(BattleInterface & owner, const CStack * _stack, BattleHex destTile); + ~MovementEndAnimation(); }; /// Move start animation of a creature -class CMovementStartAnimation : public CStackMoveAnimation +class MovementStartAnimation : public StackMoveAnimation { public: bool init() override; - CMovementStartAnimation(BattleInterface & owner, const CStack * _stack); + MovementStartAnimation(BattleInterface & owner, const CStack * _stack); }; /// Class responsible for animation of stack chaning direction (left <-> right) -class CReverseAnimation : public CStackMoveAnimation +class ReverseAnimation : public StackMoveAnimation { + void setupSecondPart(); public: - bool priority; //true - high, false - low bool init() override; - void setupSecondPart(); - - CReverseAnimation(BattleInterface & owner, const CStack * stack, BattleHex dest, bool _priority); - ~CReverseAnimation(); + ReverseAnimation(BattleInterface & owner, const CStack * stack, BattleHex dest); }; -class CRangedAttackAnimation : public CAttackAnimation +/// This class is responsible for managing the battle attack animation +class AttackAnimation : public StackActionAnimation { +protected: + BattleHex dest; //attacked hex + const CStack *defendingStack; + const CStack *attackingStack; + int attackingStackPosBeforeReturn; //for stacks with return_after_strike feature + const CCreature * getCreature() const; + ECreatureAnimType findValidGroup( const std::vector candidates ) const; + +public: + AttackAnimation(BattleInterface & owner, const CStack *attacker, BattleHex _dest, const CStack *defender); +}; + +/// Hand-to-hand attack +class MeleeAttackAnimation : public AttackAnimation +{ + ECreatureAnimType getUpwardsGroup(bool multiAttack) const; + ECreatureAnimType getForwardGroup(bool multiAttack) const; + ECreatureAnimType getDownwardsGroup(bool multiAttack) const; + + ECreatureAnimType selectGroup(bool multiAttack); + +public: + MeleeAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, bool multiAttack); + + void nextFrame() override; +}; + + +class RangedAttackAnimation : public AttackAnimation +{ void setAnimationGroup(); void initializeProjectile(); void emitProjectile(); @@ -205,85 +230,81 @@ class CRangedAttackAnimation : public CAttackAnimation protected: bool projectileEmitted; - virtual CCreatureAnim::EAnimType getUpwardsGroup() const = 0; - virtual CCreatureAnim::EAnimType getForwardGroup() const = 0; - virtual CCreatureAnim::EAnimType getDownwardsGroup() const = 0; + virtual ECreatureAnimType getUpwardsGroup() const = 0; + virtual ECreatureAnimType getForwardGroup() const = 0; + virtual ECreatureAnimType getDownwardsGroup() const = 0; virtual void createProjectile(const Point & from, const Point & dest) const = 0; virtual uint32_t getAttackClimaxFrame() const = 0; public: - CRangedAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender); - ~CRangedAttackAnimation(); + RangedAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender); + ~RangedAttackAnimation(); bool init() override; void nextFrame() override; }; /// Shooting attack -class CShootingAnimation : public CRangedAttackAnimation +class ShootingAnimation : public RangedAttackAnimation { - CCreatureAnim::EAnimType getUpwardsGroup() const override; - CCreatureAnim::EAnimType getForwardGroup() const override; - CCreatureAnim::EAnimType getDownwardsGroup() const override; + ECreatureAnimType getUpwardsGroup() const override; + ECreatureAnimType getForwardGroup() const override; + ECreatureAnimType getDownwardsGroup() const override; void createProjectile(const Point & from, const Point & dest) const override; uint32_t getAttackClimaxFrame() const override; public: - CShootingAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender); + ShootingAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender); }; /// Catapult attack -class CCatapultAnimation : public CShootingAnimation +class CatapultAnimation : public ShootingAnimation { private: bool explosionEmitted; int catapultDamage; public: - CCatapultAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender, int _catapultDmg = 0); + CatapultAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender, int _catapultDmg = 0); void createProjectile(const Point & from, const Point & dest) const override; void nextFrame() override; }; -class CCastAnimation : public CRangedAttackAnimation +class CastAnimation : public RangedAttackAnimation { const CSpell * spell; - CCreatureAnim::EAnimType findValidGroup( const std::vector candidates ) const; - CCreatureAnim::EAnimType getUpwardsGroup() const override; - CCreatureAnim::EAnimType getForwardGroup() const override; - CCreatureAnim::EAnimType getDownwardsGroup() const override; + ECreatureAnimType getUpwardsGroup() const override; + ECreatureAnimType getForwardGroup() const override; + ECreatureAnimType getDownwardsGroup() const override; void createProjectile(const Point & from, const Point & dest) const override; uint32_t getAttackClimaxFrame() const override; public: - CCastAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest_, const CStack * defender, const CSpell * spell); + CastAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest_, const CStack * defender, const CSpell * spell); }; -struct CPointEffectParameters +class DummyAnimation : public BattleAnimation { - std::vector positions; - std::vector tiles; - std::string animation; +private: + int counter; + int howMany; +public: + bool init() override; + void nextFrame() override; - soundBase::soundID sound = soundBase::invalid; - BattleHex boundHex = BattleHex::INVALID; - bool aligntoBottom = false; - bool waitForSound = false; - bool screenFill = false; + DummyAnimation(BattleInterface & owner, int howManyFrames); }; /// Class that plays effect at one or more positions along with (single) sound effect -class CPointEffectAnimation : public CBattleAnimation +class EffectAnimation : public BattleAnimation { - soundBase::soundID sound; - bool soundPlayed; - bool soundFinished; + std::string soundName; bool effectFinished; int effectFlags; @@ -297,54 +318,50 @@ class CPointEffectAnimation : public CBattleAnimation bool screenFill() const; void onEffectFinished(); - void onSoundFinished(); void clearEffect(); - - void playSound(); void playEffect(); public: enum EEffectFlags { ALIGN_TO_BOTTOM = 1, - WAIT_FOR_SOUND = 2, - FORCE_ON_TOP = 4, - SCREEN_FILL = 8, + FORCE_ON_TOP = 2, + SCREEN_FILL = 4, }; /// Create animation with screen-wide effect - CPointEffectAnimation(BattleInterface & owner, soundBase::soundID sound, std::string animationName, int effects = 0); + EffectAnimation(BattleInterface & owner, std::string animationName, int effects = 0); /// Create animation positioned at point(s). Note that positions must be are absolute, including battleint position offset - CPointEffectAnimation(BattleInterface & owner, soundBase::soundID sound, std::string animationName, Point pos , int effects = 0); - CPointEffectAnimation(BattleInterface & owner, soundBase::soundID sound, std::string animationName, std::vector pos , int effects = 0); + EffectAnimation(BattleInterface & owner, std::string animationName, Point pos , int effects = 0); + EffectAnimation(BattleInterface & owner, std::string animationName, std::vector pos , int effects = 0); /// Create animation positioned at certain hex(es) - CPointEffectAnimation(BattleInterface & owner, soundBase::soundID sound, std::string animationName, BattleHex hex , int effects = 0); - CPointEffectAnimation(BattleInterface & owner, soundBase::soundID sound, std::string animationName, std::vector hex, int effects = 0); + EffectAnimation(BattleInterface & owner, std::string animationName, BattleHex hex , int effects = 0); + EffectAnimation(BattleInterface & owner, std::string animationName, std::vector hex, int effects = 0); - CPointEffectAnimation(BattleInterface & owner, soundBase::soundID sound, std::string animationName, Point pos, BattleHex hex, int effects = 0); - ~CPointEffectAnimation(); + EffectAnimation(BattleInterface & owner, std::string animationName, Point pos, BattleHex hex, int effects = 0); + ~EffectAnimation(); bool init() override; void nextFrame() override; }; -/// Base class (e.g. for use in dynamic_cast's) for "animations" that wait for certain event -class CWaitingAnimation : public CBattleAnimation +class HeroCastAnimation : public BattleAnimation { -protected: - CWaitingAnimation(BattleInterface & owner); + std::shared_ptr hero; + const CStack * target; + const CSpell * spell; + BattleHex tile; + bool projectileEmitted; + + void initializeProjectile(); + void emitProjectile(); + void emitAnimationEvent(); + public: + HeroCastAnimation(BattleInterface & owner, std::shared_ptr hero, BattleHex dest, const CStack * defender, const CSpell * spell); + void nextFrame() override; -}; - -/// Class that waits till projectile of certain shooter hits a target -class CWaitingProjectileAnimation : public CWaitingAnimation -{ - const CStack * shooter; -public: - CWaitingProjectileAnimation(BattleInterface & owner, const CStack * shooter); - bool init() override; }; diff --git a/client/battle/BattleConstants.h b/client/battle/BattleConstants.h new file mode 100644 index 000000000..0a6b0deba --- /dev/null +++ b/client/battle/BattleConstants.h @@ -0,0 +1,92 @@ +/* + * BattleConstants.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 + +enum class EBattleEffect +{ + // list of battle effects that have hardcoded triggers + MAGIC_MIRROR = 3, + FIRE_SHIELD = 11, + FEAR = 15, + GOOD_LUCK = 18, + GOOD_MORALE = 20, + BAD_MORALE = 30, + BAD_LUCK = 48, + RESURRECT = 50, + DRAIN_LIFE = 52, + POISON = 67, + DEATH_BLOW = 73, + REGENERATION = 74, + MANA_DRAIN = 77, + RESISTANCE = 78, + + INVALID = -1, +}; + +enum class EAnimationEvents { + OPENING = 0, // battle opening sound is playing + ACTION = 1, // there are any ongoing animations + MOVEMENT = 2, // stacks are moving or turning around + BEFORE_HIT = 3, // effects played before all attack/defence/hit animations + ATTACK = 4, // attack and defence animations are playing + HIT = 5, // hit & death animations are playing + AFTER_HIT = 6, // after all hit & death animations are over + COUNT +}; + +enum class EHeroAnimType +{ + HOLDING = 0, + IDLE = 1, // idling movement that happens from time to time + DEFEAT = 2, // played when army loses stack or on friendly fire + VICTORY = 3, // when enemy stack killed or huge damage is dealt + CAST_SPELL = 4 // spellcasting +}; + +enum class ECreatureAnimType +{ + INVALID = -1, + + MOVING = 0, + MOUSEON = 1, + HOLDING = 2, // base idling animation + HITTED = 3, // base animation for when stack is taking damage + DEFENCE = 4, // alternative animation for defending in melee if stack spent its action on defending + DEATH = 5, + DEATH_RANGED = 6, // Optional, alternative animation for when stack is killed by ranged attack + TURN_L = 7, + TURN_R = 8, + //TURN_L2 = 9, //unused - identical to TURN_L + //TURN_R2 = 10, //unused - identical to TURN_R + ATTACK_UP = 11, + ATTACK_FRONT = 12, + ATTACK_DOWN = 13, + SHOOT_UP = 14, // Shooters only + SHOOT_FRONT = 15, // Shooters only + SHOOT_DOWN = 16, // Shooters only + SPECIAL_UP = 17, // If empty, fallback to SPECIAL_FRONT + SPECIAL_FRONT = 18, // Used for any special moves - dragon breath, spellcasting, Pit Lord/Ogre Mage ability + SPECIAL_DOWN = 19, // If empty, fallback to SPECIAL_FRONT + MOVE_START = 20, // small animation to be played before MOVING + MOVE_END = 21, // small animation to be played after MOVING + + DEAD = 22, // new group, used to show dead stacks. If empty - last frame from "DEATH" will be copied here + DEAD_RANGED = 23, // new group, used to show dead stacks (if DEATH_RANGED was used). If empty - last frame from "DEATH_RANGED" will be copied here + RESURRECTION = 24, // new group, used for animating resurrection, if empty - reversed "DEATH" animation will be copiend here + FROZEN = 25, // new group, used when stack animation is paused (e.g. petrified). If empty - consist of first frame from HOLDING animation + + CAST_UP = 30, + CAST_FRONT = 31, + CAST_DOWN = 32, + + GROUP_ATTACK_UP = 40, + GROUP_ATTACK_FRONT = 41, + GROUP_ATTACK_DOWN = 42 +}; diff --git a/client/battle/BattleEffectsController.cpp b/client/battle/BattleEffectsController.cpp index 3493ddc14..fddb27be0 100644 --- a/client/battle/BattleEffectsController.cpp +++ b/client/battle/BattleEffectsController.cpp @@ -11,7 +11,7 @@ #include "BattleEffectsController.h" #include "BattleAnimationClasses.h" -#include "BattleControlPanel.h" +#include "BattleWindow.h" #include "BattleInterface.h" #include "BattleInterfaceClasses.h" #include "BattleFieldController.h" @@ -26,6 +26,7 @@ #include "../../CCallback.h" #include "../../lib/battle/BattleAction.h" +#include "../../lib/filesystem/ResourceID.h" #include "../../lib/NetPacks.h" #include "../../lib/CStack.h" #include "../../lib/IGameEventsReceiver.h" @@ -33,35 +34,30 @@ BattleEffectsController::BattleEffectsController(BattleInterface & owner): owner(owner) -{} - -void BattleEffectsController::displayEffect(EBattleEffect::EBattleEffect effect, const BattleHex & destTile) { - displayEffect(effect, soundBase::invalid, destTile); + loadColorMuxers(); } -void BattleEffectsController::displayEffect(EBattleEffect::EBattleEffect effect, uint32_t soundID, const BattleHex & destTile) +void BattleEffectsController::displayEffect(EBattleEffect effect, const BattleHex & destTile) { - std::string customAnim = graphics->battleACToDef[effect][0]; - - owner.stacksController->addNewAnim(new CPointEffectAnimation(owner, soundBase::soundID(soundID), customAnim, destTile)); + displayEffect(effect, "", destTile); } -void BattleEffectsController::displayCustomEffects(const std::vector & customEffects) +void BattleEffectsController::displayEffect(EBattleEffect effect, std::string soundFile, const BattleHex & destTile) { - for(const CustomEffectInfo & one : customEffects) - { - const CStack * s = owner.curInt->cb->battleGetStackByID(one.stack, false); + size_t effectID = static_cast(effect); - assert(s); - assert(one.effect != 0); + std::string customAnim = graphics->battleACToDef[effectID][0]; - displayEffect(EBattleEffect::EBattleEffect(one.effect), soundBase::soundID(one.sound), s->getPosition()); - } + CCS->soundh->playSound( soundFile ); + + owner.stacksController->addNewAnim(new EffectAnimation(owner, customAnim, destTile)); } void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bte) { + assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false); + const CStack * stack = owner.curInt->cb->battleGetStackByID(bte.stackID); if(!stack) { @@ -71,45 +67,46 @@ void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bt //don't show animation when no HP is regenerated switch(bte.effect) { - //TODO: move to bonus type handler case Bonus::HP_REGENERATION: - displayEffect(EBattleEffect::REGENERATION, soundBase::REGENER, stack->getPosition()); + displayEffect(EBattleEffect::REGENERATION, "REGENER", stack->getPosition()); break; case Bonus::MANA_DRAIN: - displayEffect(EBattleEffect::MANA_DRAIN, soundBase::MANADRAI, stack->getPosition()); + displayEffect(EBattleEffect::MANA_DRAIN, "MANADRAI", stack->getPosition()); break; case Bonus::POISON: - displayEffect(EBattleEffect::POISON, soundBase::POISON, stack->getPosition()); + displayEffect(EBattleEffect::POISON, "POISON", stack->getPosition()); break; case Bonus::FEAR: - displayEffect(EBattleEffect::FEAR, soundBase::FEAR, stack->getPosition()); + displayEffect(EBattleEffect::FEAR, "FEAR", stack->getPosition()); break; case Bonus::MORALE: { std::string hlp = CGI->generaltexth->allTexts[33]; boost::algorithm::replace_first(hlp,"%s",(stack->getName())); - displayEffect(EBattleEffect::GOOD_MORALE, soundBase::GOODMRLE, stack->getPosition()); - owner.controlPanel->console->addText(hlp); + displayEffect(EBattleEffect::GOOD_MORALE, "GOODMRLE", stack->getPosition()); + owner.appendBattleLog(hlp); break; } default: return; } - //waitForAnims(); //fixme: freezes game :? + owner.waitForAnimationCondition(EAnimationEvents::ACTION, false); } void BattleEffectsController::startAction(const BattleAction* action) { + assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false); + const CStack *stack = owner.curInt->cb->battleGetStackByID(action->stackNumber); switch(action->actionType) { case EActionType::WAIT: - owner.controlPanel->console->addText(stack->formatGeneralMessage(136)); + owner.appendBattleLog(stack->formatGeneralMessage(136)); break; case EActionType::BAD_MORALE: - owner.controlPanel->console->addText(stack->formatGeneralMessage(-34)); - displayEffect(EBattleEffect::BAD_MORALE, soundBase::BADMRLE, stack->getPosition()); + owner.appendBattleLog(stack->formatGeneralMessage(-34)); + displayEffect(EBattleEffect::BAD_MORALE, "BADMRLE", stack->getPosition()); break; } @@ -118,23 +115,54 @@ void BattleEffectsController::startAction(const BattleAction* action) switch(action->actionType) { case EActionType::STACK_HEAL: - displayEffect(EBattleEffect::REGENERATION, soundBase::REGENER, actionTarget.at(0).hexValue); + displayEffect(EBattleEffect::REGENERATION, "REGENER", actionTarget.at(0).hexValue); break; } + + owner.waitForAnimationCondition(EAnimationEvents::ACTION, false); } void BattleEffectsController::collectRenderableObjects(BattleRenderer & renderer) { for (auto & elem : battleEffects) { - renderer.insert( EBattleFieldLayer::EFFECTS, elem.position, [&elem](BattleRenderer::RendererRef canvas) + renderer.insert( EBattleFieldLayer::EFFECTS, elem.tile, [&elem](BattleRenderer::RendererRef canvas) { int currentFrame = static_cast(floor(elem.currentFrame)); currentFrame %= elem.animation->size(); auto img = elem.animation->getImage(currentFrame); - canvas.draw(img, Point(elem.x, elem.y)); + canvas.draw(img, elem.pos); }); } } + +void BattleEffectsController::loadColorMuxers() +{ + const JsonNode config(ResourceID("config/battleEffects.json")); + + for(auto & muxer : config["colorMuxers"].Struct()) + { + ColorMuxerEffect effect; + std::string identifier = muxer.first; + + for (const JsonNode & entry : muxer.second.Vector() ) + { + effect.timePoints.push_back(entry["time"].Float()); + effect.filters.push_back(ColorFilter::genFromJson(entry)); + } + colorMuxerEffects[identifier] = effect; + } +} + +const ColorMuxerEffect & BattleEffectsController::getMuxerEffect(const std::string & name) +{ + static const ColorMuxerEffect emptyEffect; + + if (colorMuxerEffects.count(name)) + return colorMuxerEffects[name]; + + logAnim->error("Failed to find color muxer effect named '%s'!", name); + return emptyEffect; +} diff --git a/client/battle/BattleEffectsController.h b/client/battle/BattleEffectsController.h index a943937f7..cb7a1bf30 100644 --- a/client/battle/BattleEffectsController.h +++ b/client/battle/BattleEffectsController.h @@ -10,50 +10,31 @@ #pragma once #include "../../lib/battle/BattleHex.h" +#include "../gui/Geometries.h" +#include "BattleConstants.h" VCMI_LIB_NAMESPACE_BEGIN class BattleAction; -struct CustomEffectInfo; struct BattleTriggerEffect; VCMI_LIB_NAMESPACE_END +struct ColorMuxerEffect; class CAnimation; class Canvas; class BattleInterface; class BattleRenderer; -class CPointEffectAnimation; - -namespace EBattleEffect -{ - enum EBattleEffect - { - // list of battle effects that have hardcoded triggers - FEAR = 15, - GOOD_LUCK = 18, - GOOD_MORALE = 20, - BAD_MORALE = 30, - BAD_LUCK = 48, - RESURRECT = 50, - DRAIN_LIFE = 52, // hardcoded constant in CGameHandler - POISON = 67, - DEATH_BLOW = 73, - REGENERATION = 74, - MANA_DRAIN = 77, - - INVALID = -1, - }; -} +class EffectAnimation; /// Struct for battle effect animation e.g. morale, prayer, armageddon, bless,... struct BattleEffect { - int x, y; //position on the screen + Point pos; //position on the screen float currentFrame; std::shared_ptr animation; int effectID; //uniqueID equal ot ID of appropriate CSpellEffectAnim - BattleHex position; //Indicates if effect which hex the effect is drawn on + BattleHex tile; //Indicates if effect which hex the effect is drawn on }; /// Controls rendering of effects in battle, e.g. from spells, abilities and various other actions like morale @@ -64,21 +45,23 @@ class BattleEffectsController /// list of current effects that are being displayed on screen (spells & creature abilities) std::vector battleEffects; + std::map colorMuxerEffects; + + void loadColorMuxers(); public: + const ColorMuxerEffect &getMuxerEffect(const std::string & name); + BattleEffectsController(BattleInterface & owner); void startAction(const BattleAction* action); - void displayCustomEffects(const std::vector & customEffects); - //displays custom effect on the battlefield - void displayEffect(EBattleEffect::EBattleEffect effect, const BattleHex & destTile); - void displayEffect(EBattleEffect::EBattleEffect effect, uint32_t soundID, const BattleHex & destTile); - //void displayEffects(EBattleEffect::EBattleEffect effect, uint32_t soundID, const std::vector & destTiles); + void displayEffect(EBattleEffect effect, const BattleHex & destTile); + void displayEffect(EBattleEffect effect, std::string soundFile, const BattleHex & destTile); void battleTriggerEffect(const BattleTriggerEffect & bte); void collectRenderableObjects(BattleRenderer & renderer); - friend class CPointEffectAnimation; + friend class EffectAnimation; }; diff --git a/client/battle/BattleFieldController.cpp b/client/battle/BattleFieldController.cpp index 7d8f926ad..3155dc30d 100644 --- a/client/battle/BattleFieldController.cpp +++ b/client/battle/BattleFieldController.cpp @@ -22,6 +22,7 @@ #include "../CGameInfo.h" #include "../CPlayerInterface.h" +#include "../widgets/AdventureMapClasses.h" #include "../gui/CAnimation.h" #include "../gui/Canvas.h" #include "../gui/CGuiHandler.h" @@ -34,12 +35,10 @@ #include "../../lib/spells/ISpellMechanics.h" BattleFieldController::BattleFieldController(BattleInterface & owner): - owner(owner), - attackingHex(BattleHex::INVALID) + owner(owner) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - pos.w = owner.pos.w; - pos.h = owner.pos.h; + strongInterest = true; //preparing cells and hexes cellBorder = IImage::createFromFile("CCELLGRD.BMP"); @@ -59,6 +58,8 @@ BattleFieldController::BattleFieldController(BattleInterface & owner): std::string backgroundName = owner.siegeController->getBattleBackgroundName(); background = IImage::createFromFile(backgroundName); } + pos.w = background->width(); + pos.h = background->height(); //preparing graphic with cell borders cellBorders = std::make_unique(Point(background->width(), background->height())); @@ -87,17 +88,42 @@ BattleFieldController::BattleFieldController(BattleInterface & owner): auto accessibility = owner.curInt->cb->getAccesibility(); for(int i = 0; i < accessibility.size(); i++) stackCountOutsideHexes[i] = (accessibility[i] == EAccessibility::ACCESSIBLE); + + addUsedEvents(MOVE); + LOCPLINT->cingconsole->pos = this->pos; } +void BattleFieldController::createHeroes() +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + // create heroes as part of our constructor for correct positioning inside battlefield + if(owner.attackingHeroInstance) + owner.attackingHero = std::make_shared(owner, owner.attackingHeroInstance, false); + + if(owner.defendingHeroInstance) + owner.defendingHero = std::make_shared(owner, owner.defendingHeroInstance, true); +} + +void BattleFieldController::mouseMoved(const SDL_MouseMotionEvent &event) +{ + BattleHex selectedHex = getHoveredHex(); + + owner.actionsController->handleHex(selectedHex, MOVE); +} + + void BattleFieldController::renderBattlefield(Canvas & canvas) { - showBackground(canvas); + Canvas clippedCanvas(canvas, pos); + + showBackground(clippedCanvas); BattleRenderer renderer(owner); - renderer.execute(canvas); + renderer.execute(clippedCanvas); - owner.projectilesController->showProjectiles(canvas); + owner.projectilesController->showProjectiles(clippedCanvas); } void BattleFieldController::showBackground(Canvas & canvas) @@ -113,19 +139,19 @@ void BattleFieldController::showBackground(Canvas & canvas) void BattleFieldController::showBackgroundImage(Canvas & canvas) { - canvas.draw(background, owner.pos.topLeft()); + canvas.draw(background, Point(0, 0)); - owner.obstacleController->showAbsoluteObstacles(canvas, pos.topLeft()); + owner.obstacleController->showAbsoluteObstacles(canvas); if ( owner.siegeController ) - owner.siegeController->showAbsoluteObstacles(canvas, pos.topLeft()); + owner.siegeController->showAbsoluteObstacles(canvas); if (settings["battle"]["cellBorders"].Bool()) - canvas.draw(*cellBorders, owner.pos.topLeft()); + canvas.draw(*cellBorders, Point(0, 0)); } void BattleFieldController::showBackgroundImageWithHexes(Canvas & canvas) { - canvas.draw(*backgroundWithHexes.get(), owner.pos.topLeft()); + canvas.draw(*backgroundWithHexes.get(), Point(0, 0)); } void BattleFieldController::redrawBackgroundWithHexes() @@ -142,9 +168,9 @@ void BattleFieldController::redrawBackgroundWithHexes() //prepare background graphic with hexes and shaded hexes backgroundWithHexes->draw(background, Point(0,0)); - owner.obstacleController->showAbsoluteObstacles(*backgroundWithHexes, Point(0,0)); + owner.obstacleController->showAbsoluteObstacles(*backgroundWithHexes); if ( owner.siegeController ) - owner.siegeController->showAbsoluteObstacles(*backgroundWithHexes, Point(0,0)); + owner.siegeController->showAbsoluteObstacles(*backgroundWithHexes); if (settings["battle"]["stackRange"].Bool()) { @@ -162,7 +188,7 @@ void BattleFieldController::redrawBackgroundWithHexes() void BattleFieldController::showHighlightedHex(Canvas & canvas, BattleHex hex, bool darkBorder) { - Point hexPos = hexPositionAbsolute(hex).topLeft(); + Point hexPos = hexPositionLocal(hex).topLeft(); canvas.draw(cellShade, hexPos); if(!darkBorder && settings["battle"]["cellBorders"].Bool()) @@ -233,19 +259,58 @@ std::set BattleFieldController::getHighlightedHexesSpellRange() result.insert(shadedHex); } } - else if(owner.active) //always highlight pointed hex - { - if(hoveredHex.getX() != 0 && hoveredHex.getX() != GameConstants::BFIELD_WIDTH - 1) - result.insert(hoveredHex); - } - return result; } +std::set BattleFieldController::getHighlightedHexesMovementTarget() +{ + const CStack * stack = owner.stacksController->getActiveStack(); + auto hoveredHex = getHoveredHex(); + + if (stack) + { + std::vector v = owner.curInt->cb->battleGetAvailableHexes(stack, false, nullptr); + + auto hoveredStack = owner.curInt->cb->battleGetStackByPos(hoveredHex, true); + if(owner.curInt->cb->battleCanAttack(stack, hoveredStack, hoveredHex)) + { + if (isTileAttackable(hoveredHex)) + { + BattleHex attackFromHex = fromWhichHexAttack(hoveredHex); + + if (stack->doubleWide()) + return {attackFromHex, stack->occupiedHex(attackFromHex)}; + else + return {attackFromHex}; + } + } + + if (vstd::contains(v,hoveredHex)) + { + if (stack->doubleWide()) + return {hoveredHex, stack->occupiedHex(hoveredHex)}; + else + return {hoveredHex}; + } + if (stack->doubleWide()) + { + for (auto const & hex : v) + { + if (stack->occupiedHex(hex) == hoveredHex) + return { hoveredHex, hex }; + } + } + } + return {}; +} + void BattleFieldController::showHighlightedHexes(Canvas & canvas) { std::set hoveredStack = getHighlightedHexesStackRange(); - std::set hoveredMouse = getHighlightedHexesSpellRange(); + std::set hoveredSpell = getHighlightedHexesSpellRange(); + std::set hoveredMove = getHighlightedHexesMovementTarget(); + + auto const & hoveredMouse = owner.actionsController->spellcastingModeActive() ? hoveredSpell : hoveredMove; for(int b=0; bcurh; + Point cursorPos = CCS->curh->position(); - const double subdividingAngle = 2.0*M_PI/6.0; // Divide a hex into six sectors. - const double hexMidX = hoveredHexPos.x + hoveredHexPos.w/2.0; - const double hexMidY = hoveredHexPos.y + hoveredHexPos.h/2.0; - const double cursorHexAngle = M_PI - atan2(hexMidY - cursor->ypos, cursor->xpos - hexMidX) + subdividingAngle/2; //TODO: refactor this nightmare - const double sector = fmod(cursorHexAngle/subdividingAngle, 6.0); - const int zigzagCorrection = !((myNumber/GameConstants::BFIELD_WIDTH)%2); // Off-by-one correction needed to deal with the odd battlefield rows. + std::vector sectorCursor = { + Cursor::Combat::HIT_SOUTHEAST, + Cursor::Combat::HIT_SOUTHWEST, + Cursor::Combat::HIT_WEST, + Cursor::Combat::HIT_NORTHWEST, + Cursor::Combat::HIT_NORTHEAST, + Cursor::Combat::HIT_EAST, + Cursor::Combat::HIT_SOUTH, + Cursor::Combat::HIT_NORTH, + }; - std::vector sectorCursor; // From left to bottom left. - sectorCursor.push_back(8); - sectorCursor.push_back(9); - sectorCursor.push_back(10); - sectorCursor.push_back(11); - sectorCursor.push_back(12); - sectorCursor.push_back(7); + auto direction = static_cast(selectAttackDirection(myNumber, cursorPos)); - const bool doubleWide = owner.stacksController->getActiveStack()->doubleWide(); - bool aboveAttackable = true, belowAttackable = true; - - // Exclude directions which cannot be attacked from. - // Check to the left. - if (myNumber%GameConstants::BFIELD_WIDTH <= 1 || !vstd::contains(occupyableHexes, myNumber - 1)) - { - sectorCursor[0] = -1; - } - // Check top left, top right as well as above for 2-hex creatures. - if (myNumber/GameConstants::BFIELD_WIDTH == 0) - { - sectorCursor[1] = -1; - sectorCursor[2] = -1; - aboveAttackable = false; - } - else - { - if (doubleWide) - { - bool attackRow[4] = {true, true, true, true}; - - if (myNumber%GameConstants::BFIELD_WIDTH <= 1 || !vstd::contains(occupyableHexes, myNumber - GameConstants::BFIELD_WIDTH - 2 + zigzagCorrection)) - attackRow[0] = false; - if (!vstd::contains(occupyableHexes, myNumber - GameConstants::BFIELD_WIDTH - 1 + zigzagCorrection)) - attackRow[1] = false; - if (!vstd::contains(occupyableHexes, myNumber - GameConstants::BFIELD_WIDTH + zigzagCorrection)) - attackRow[2] = false; - if (myNumber%GameConstants::BFIELD_WIDTH >= GameConstants::BFIELD_WIDTH - 2 || !vstd::contains(occupyableHexes, myNumber - GameConstants::BFIELD_WIDTH + 1 + zigzagCorrection)) - attackRow[3] = false; - - if (!(attackRow[0] && attackRow[1])) - sectorCursor[1] = -1; - if (!(attackRow[1] && attackRow[2])) - aboveAttackable = false; - if (!(attackRow[2] && attackRow[3])) - sectorCursor[2] = -1; - } - else - { - if (!vstd::contains(occupyableHexes, myNumber - GameConstants::BFIELD_WIDTH - 1 + zigzagCorrection)) - sectorCursor[1] = -1; - if (!vstd::contains(occupyableHexes, myNumber - GameConstants::BFIELD_WIDTH + zigzagCorrection)) - sectorCursor[2] = -1; - } - } - // Check to the right. - if (myNumber%GameConstants::BFIELD_WIDTH >= GameConstants::BFIELD_WIDTH - 2 || !vstd::contains(occupyableHexes, myNumber + 1)) - { - sectorCursor[3] = -1; - } - // Check bottom right, bottom left as well as below for 2-hex creatures. - if (myNumber/GameConstants::BFIELD_WIDTH == GameConstants::BFIELD_HEIGHT - 1) - { - sectorCursor[4] = -1; - sectorCursor[5] = -1; - belowAttackable = false; - } - else - { - if (doubleWide) - { - bool attackRow[4] = {true, true, true, true}; - - if (myNumber%GameConstants::BFIELD_WIDTH <= 1 || !vstd::contains(occupyableHexes, myNumber + GameConstants::BFIELD_WIDTH - 2 + zigzagCorrection)) - attackRow[0] = false; - if (!vstd::contains(occupyableHexes, myNumber + GameConstants::BFIELD_WIDTH - 1 + zigzagCorrection)) - attackRow[1] = false; - if (!vstd::contains(occupyableHexes, myNumber + GameConstants::BFIELD_WIDTH + zigzagCorrection)) - attackRow[2] = false; - if (myNumber%GameConstants::BFIELD_WIDTH >= GameConstants::BFIELD_WIDTH - 2 || !vstd::contains(occupyableHexes, myNumber + GameConstants::BFIELD_WIDTH + 1 + zigzagCorrection)) - attackRow[3] = false; - - if (!(attackRow[0] && attackRow[1])) - sectorCursor[5] = -1; - if (!(attackRow[1] && attackRow[2])) - belowAttackable = false; - if (!(attackRow[2] && attackRow[3])) - sectorCursor[4] = -1; - } - else - { - if (!vstd::contains(occupyableHexes, myNumber + GameConstants::BFIELD_WIDTH + zigzagCorrection)) - sectorCursor[4] = -1; - if (!vstd::contains(occupyableHexes, myNumber + GameConstants::BFIELD_WIDTH - 1 + zigzagCorrection)) - sectorCursor[5] = -1; - } - } - - // Determine index from sector. - int cursorIndex; - if (doubleWide) - { - sectorCursor.insert(sectorCursor.begin() + 5, belowAttackable ? 13 : -1); - sectorCursor.insert(sectorCursor.begin() + 2, aboveAttackable ? 14 : -1); - - if (sector < 1.5) - cursorIndex = static_cast(sector); - else if (sector >= 1.5 && sector < 2.5) - cursorIndex = 2; - else if (sector >= 2.5 && sector < 4.5) - cursorIndex = (int) sector + 1; - else if (sector >= 4.5 && sector < 5.5) - cursorIndex = 6; - else - cursorIndex = (int) sector + 2; - } - else - { - cursorIndex = static_cast(sector); - } - - // Generally should NEVER happen, but to avoid the possibility of having endless loop below... [#1016] - if (!vstd::contains_if (sectorCursor, [](int sc) { return sc != -1; })) - { - logGlobal->error("Error: for hex %d cannot find a hex to attack from!", myNumber); - attackingHex = -1; - return; - } - - // Find the closest direction attackable, starting with the right one. - // FIXME: Is this really how the original H3 client does it? - int i = 0; - while (sectorCursor[(cursorIndex + i)%sectorCursor.size()] == -1) //Why hast thou forsaken me? - i = i <= 0 ? 1 - i : -i; // 0, 1, -1, 2, -2, 3, -3 etc.. - int index = (cursorIndex + i)%sectorCursor.size(); //hopefully we get elements from sectorCursor - cursor->changeGraphic(ECursor::COMBAT, sectorCursor[index]); - switch (index) - { - case 0: - attackingHex = myNumber - 1; //left - break; - case 1: - attackingHex = myNumber - GameConstants::BFIELD_WIDTH - 1 + zigzagCorrection; //top left - break; - case 2: - attackingHex = myNumber - GameConstants::BFIELD_WIDTH + zigzagCorrection; //top right - break; - case 3: - attackingHex = myNumber + 1; //right - break; - case 4: - attackingHex = myNumber + GameConstants::BFIELD_WIDTH + zigzagCorrection; //bottom right - break; - case 5: - attackingHex = myNumber + GameConstants::BFIELD_WIDTH - 1 + zigzagCorrection; //bottom left - break; - } - BattleHex hex(attackingHex); - if (!hex.isValid()) - attackingHex = -1; + assert(direction != -1); + if (direction != -1) + CCS->curh->set(sectorCursor[direction]); } -BattleHex BattleFieldController::fromWhichHexAttack(BattleHex myNumber) +BattleHex::EDir BattleFieldController::selectAttackDirection(BattleHex myNumber, const Point & cursorPos) { - //TODO far too much repeating code - BattleHex destHex; - switch(CCS->curh->frame) + const bool doubleWide = owner.stacksController->getActiveStack()->doubleWide(); + auto neighbours = myNumber.allNeighbouringTiles(); + // 0 1 + // 5 x 2 + // 4 3 + + // if true - our current stack can move into this hex (and attack) + std::array attackAvailability; + + if (doubleWide) { - case 12: //from bottom right + // For double-hexes we need to ensure that both hexes needed for this direction are occupyable: + // | -0- | -1- | -2- | -3- | -4- | -5- | -6- | -7- + // | o o - | - o o | - - | - - | - - | - - | o o | - - + // | - x - | - x - | - x o o| - x - | - x - |o o x - | - x - | - x - + // | - - | - - | - - | - o o | o o - | - - | - - | o o + + for (size_t i : { 1, 2, 3}) + attackAvailability[i] = vstd::contains(occupyableHexes, neighbours[i]) && vstd::contains(occupyableHexes, neighbours[i].cloneInDirection(BattleHex::RIGHT, false)); + + for (size_t i : { 4, 5, 0}) + attackAvailability[i] = vstd::contains(occupyableHexes, neighbours[i]) && vstd::contains(occupyableHexes, neighbours[i].cloneInDirection(BattleHex::LEFT, false)); + + attackAvailability[6] = vstd::contains(occupyableHexes, neighbours[0]) && vstd::contains(occupyableHexes, neighbours[1]); + attackAvailability[7] = vstd::contains(occupyableHexes, neighbours[3]) && vstd::contains(occupyableHexes, neighbours[4]); + } + else + { + for (size_t i = 0; i < 6; ++i) + attackAvailability[i] = vstd::contains(occupyableHexes, neighbours[i]); + + attackAvailability[6] = false; + attackAvailability[7] = false; + } + + // Zero available tiles to attack from + if ( vstd::find(attackAvailability, true) == attackAvailability.end()) + { + logGlobal->error("Error: cannot find a hex to attack hex %d from!", myNumber); + return BattleHex::NONE; + } + + // For each valid direction, select position to test against + std::array testPoint; + + for (size_t i = 0; i < 6; ++i) + if (attackAvailability[i]) + testPoint[i] = hexPositionAbsolute(neighbours[i]).center(); + + // For bottom/top directions select central point, but move it a bit away from true center to reduce zones allocated to them + if (attackAvailability[6]) + testPoint[6] = (hexPositionAbsolute(neighbours[0]).center() + hexPositionAbsolute(neighbours[1]).center()) / 2 + Point(0, -5); + + if (attackAvailability[7]) + testPoint[7] = (hexPositionAbsolute(neighbours[3]).center() + hexPositionAbsolute(neighbours[4]).center()) / 2 + Point(0, 5); + + // Compute distance between tested position & cursor position and pick nearest + std::array distance2; + + for (size_t i = 0; i < 8; ++i) + if (attackAvailability[i]) + distance2[i] = (testPoint[i].y - cursorPos.y)*(testPoint[i].y - cursorPos.y) + (testPoint[i].x - cursorPos.x)*(testPoint[i].x - cursorPos.x); + + size_t nearest = -1; + for (size_t i = 0; i < 8; ++i) + if (attackAvailability[i] && (nearest == -1 || distance2[i] < distance2[nearest]) ) + nearest = i; + + assert(nearest != -1); + return BattleHex::EDir(nearest); +} + +BattleHex BattleFieldController::fromWhichHexAttack(BattleHex attackTarget) +{ + BattleHex::EDir direction = selectAttackDirection(attackTarget, CCS->curh->position()); + + const CStack * attacker = owner.stacksController->getActiveStack(); + + assert(direction != BattleHex::NONE); + assert(attacker); + + if (!attacker->doubleWide()) + { + assert(direction != BattleHex::BOTTOM); + assert(direction != BattleHex::TOP); + return attackTarget.cloneInDirection(direction); + } + else + { + // We need to find position of right hex of double-hex creature (or left for defending side) + // | TOP_LEFT |TOP_RIGHT | RIGHT |BOTTOM_RIGHT|BOTTOM_LEFT| LEFT | TOP |BOTTOM + // | o o - | - o o | - - | - - | - - | - - | o o | - - + // | - x - | - x - | - x o o| - x - | - x - |o o x - | - x - | - x - + // | - - | - - | - - | - o o | o o - | - - | - - | o o + + switch (direction) { - bool doubleWide = owner.stacksController->getActiveStack()->doubleWide(); - destHex = myNumber + ( (myNumber/GameConstants::BFIELD_WIDTH)%2 ? GameConstants::BFIELD_WIDTH : GameConstants::BFIELD_WIDTH+1 ) + - (owner.stacksController->getActiveStack()->side == BattleSide::ATTACKER && doubleWide ? 1 : 0); - if(vstd::contains(occupyableHexes, destHex)) - return destHex; - else if(owner.stacksController->getActiveStack()->side == BattleSide::ATTACKER) - { - if (vstd::contains(occupyableHexes, destHex+1)) - return destHex+1; - } - else //if we are defender - { - if(vstd::contains(occupyableHexes, destHex-1)) - return destHex-1; - } - break; - } - case 7: //from bottom left + case BattleHex::TOP_LEFT: + case BattleHex::LEFT: + case BattleHex::BOTTOM_LEFT: { - destHex = myNumber + ( (myNumber/GameConstants::BFIELD_WIDTH)%2 ? GameConstants::BFIELD_WIDTH-1 : GameConstants::BFIELD_WIDTH ); - if (vstd::contains(occupyableHexes, destHex)) - return destHex; - else if(owner.stacksController->getActiveStack()->side == BattleSide::ATTACKER) - { - if(vstd::contains(occupyableHexes, destHex+1)) - return destHex+1; - } - else //we are defender - { - if(vstd::contains(occupyableHexes, destHex-1)) - return destHex-1; - } - break; - } - case 8: //from left - { - if(owner.stacksController->getActiveStack()->doubleWide() && owner.stacksController->getActiveStack()->side == BattleSide::DEFENDER) - { - std::vector acc = owner.curInt->cb->battleGetAvailableHexes(owner.stacksController->getActiveStack()); - if (vstd::contains(acc, myNumber)) - return myNumber - 1; - else - return myNumber - 2; - } + if ( attacker->side == BattleSide::ATTACKER ) + return attackTarget.cloneInDirection(direction); else - { - return myNumber - 1; - } - break; + return attackTarget.cloneInDirection(direction).cloneInDirection(BattleHex::LEFT); } - case 9: //from top left + + case BattleHex::TOP_RIGHT: + case BattleHex::RIGHT: + case BattleHex::BOTTOM_RIGHT: { - destHex = myNumber - ((myNumber/GameConstants::BFIELD_WIDTH) % 2 ? GameConstants::BFIELD_WIDTH + 1 : GameConstants::BFIELD_WIDTH); - if(vstd::contains(occupyableHexes, destHex)) - return destHex; - else if(owner.stacksController->getActiveStack()->side == BattleSide::ATTACKER) - { - if(vstd::contains(occupyableHexes, destHex+1)) - return destHex+1; - } - else //if we are defender - { - if(vstd::contains(occupyableHexes, destHex-1)) - return destHex-1; - } - break; - } - case 10: //from top right - { - bool doubleWide = owner.stacksController->getActiveStack()->doubleWide(); - destHex = myNumber - ( (myNumber/GameConstants::BFIELD_WIDTH)%2 ? GameConstants::BFIELD_WIDTH : GameConstants::BFIELD_WIDTH-1 ) + - (owner.stacksController->getActiveStack()->side == BattleSide::ATTACKER && doubleWide ? 1 : 0); - if(vstd::contains(occupyableHexes, destHex)) - return destHex; - else if(owner.stacksController->getActiveStack()->side == BattleSide::ATTACKER) - { - if(vstd::contains(occupyableHexes, destHex+1)) - return destHex+1; - } - else //if we are defender - { - if(vstd::contains(occupyableHexes, destHex-1)) - return destHex-1; - } - break; - } - case 11: //from right - { - if(owner.stacksController->getActiveStack()->doubleWide() && owner.stacksController->getActiveStack()->side == BattleSide::ATTACKER) - { - std::vector acc = owner.curInt->cb->battleGetAvailableHexes(owner.stacksController->getActiveStack()); - if(vstd::contains(acc, myNumber)) - return myNumber + 1; - else - return myNumber + 2; - } + if ( attacker->side == BattleSide::ATTACKER ) + return attackTarget.cloneInDirection(direction).cloneInDirection(BattleHex::RIGHT); else - { - return myNumber + 1; - } - break; + return attackTarget.cloneInDirection(direction); } - case 13: //from bottom + + case BattleHex::TOP: { - destHex = myNumber + ( (myNumber/GameConstants::BFIELD_WIDTH)%2 ? GameConstants::BFIELD_WIDTH : GameConstants::BFIELD_WIDTH+1 ); - if(vstd::contains(occupyableHexes, destHex)) - return destHex; - else if(owner.stacksController->getActiveStack()->side == BattleSide::ATTACKER) - { - if(vstd::contains(occupyableHexes, destHex+1)) - return destHex+1; - } - else //if we are defender - { - if(vstd::contains(occupyableHexes, destHex-1)) - return destHex-1; - } - break; + if ( attacker->side == BattleSide::ATTACKER ) + return attackTarget.cloneInDirection(BattleHex::TOP_RIGHT); + else + return attackTarget.cloneInDirection(BattleHex::TOP_LEFT); } - case 14: //from top + + case BattleHex::BOTTOM: { - destHex = myNumber - ( (myNumber/GameConstants::BFIELD_WIDTH)%2 ? GameConstants::BFIELD_WIDTH : GameConstants::BFIELD_WIDTH-1 ); - if (vstd::contains(occupyableHexes, destHex)) - return destHex; - else if(owner.stacksController->getActiveStack()->side == BattleSide::ATTACKER) - { - if(vstd::contains(occupyableHexes, destHex+1)) - return destHex+1; - } - else //if we are defender - { - if(vstd::contains(occupyableHexes, destHex-1)) - return destHex-1; - } - break; + if ( attacker->side == BattleSide::ATTACKER ) + return attackTarget.cloneInDirection(BattleHex::BOTTOM_RIGHT); + else + return attackTarget.cloneInDirection(BattleHex::BOTTOM_LEFT); + } + default: + assert(0); + return attackTarget.cloneInDirection(BattleHex::LEFT); } } - return -1; } bool BattleFieldController::isTileAttackable(const BattleHex & number) const @@ -632,3 +539,18 @@ bool BattleFieldController::stackCountOutsideHex(const BattleHex & number) const { return stackCountOutsideHexes[number]; } + +void BattleFieldController::showAll(SDL_Surface * to) +{ + show(to); +} + +void BattleFieldController::show(SDL_Surface * to) +{ + owner.stacksController->update(); + owner.obstacleController->update(); + + Canvas canvas(to); + + renderBattlefield(canvas); +} diff --git a/client/battle/BattleFieldController.h b/client/battle/BattleFieldController.h index dce7cd1b2..c15fc60cb 100644 --- a/client/battle/BattleFieldController.h +++ b/client/battle/BattleFieldController.h @@ -22,6 +22,7 @@ struct Rect; struct Point; class ClickableHex; +class BattleHero; class Canvas; class IImage; class BattleInterface; @@ -56,15 +57,24 @@ class BattleFieldController : public CIntObject std::set getHighlightedHexesStackRange(); std::set getHighlightedHexesSpellRange(); + std::set getHighlightedHexesMovementTarget(); void showBackground(Canvas & canvas); void showBackgroundImage(Canvas & canvas); void showBackgroundImageWithHexes(Canvas & canvas); void showHighlightedHexes(Canvas & canvas); + BattleHex::EDir selectAttackDirection(BattleHex myNumber, const Point & point); + + void mouseMoved(const SDL_MouseMotionEvent &event) override; + void showAll(SDL_Surface * to) override; + void show(SDL_Surface * to) override; + public: BattleFieldController(BattleInterface & owner); + void createHeroes(); + void redrawBackgroundWithHexes(); void renderBattlefield(Canvas & canvas); diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index 7a6824cd5..8a1e17bb9 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -19,7 +19,7 @@ #include "BattleObstacleController.h" #include "BattleSiegeController.h" #include "BattleFieldController.h" -#include "BattleControlPanel.h" +#include "BattleWindow.h" #include "BattleStacksController.h" #include "BattleRenderer.h" @@ -42,18 +42,23 @@ #include "../../lib/NetPacks.h" #include "../../lib/UnlockGuard.h" -CondSh BattleInterface::animsAreDisplayed(false); CondSh BattleInterface::givenCommand(nullptr); BattleInterface::BattleInterface(const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, - const SDL_Rect & myRect, - std::shared_ptr att, std::shared_ptr defen, std::shared_ptr spectatorInt) - : attackingHeroInstance(hero1), defendingHeroInstance(hero2), animCount(0), - attackerInt(att), defenderInt(defen), curInt(att), - myTurn(false), moveStarted(false), moveSoundHander(-1), bresult(nullptr), battleActionsStarted(false) + std::shared_ptr att, + std::shared_ptr defen, + std::shared_ptr spectatorInt) + : attackingHeroInstance(hero1) + , defendingHeroInstance(hero2) + , attackerInt(att) + , defenderInt(defen) + , curInt(att) + , myTurn(false) + , moveSoundHander(-1) { - OBJ_CONSTRUCTION; + for ( auto & event : animationEvents) + event.setn(false); if(spectatorInt) { @@ -65,9 +70,6 @@ BattleInterface::BattleInterface(const CCreatureSet *army1, const CCreatureSet * curInt = defenderInt; } - animsAreDisplayed.setn(false); - pos = myRect; - strongInterest = true; givenCommand.setn(nullptr); //hot-seat -> check tactics for both players (defender may be local human) @@ -79,27 +81,6 @@ BattleInterface::BattleInterface(const CCreatureSet *army1, const CCreatureSet * //if we found interface of player with tactics, then enter tactics mode tacticsMode = static_cast(tacticianInterface); - //create stack queue - bool embedQueue; - std::string queueSize = settings["battle"]["queueSize"].String(); - - if(queueSize == "auto") - embedQueue = screen->h < 700; - else - embedQueue = screen->h < 700 || queueSize == "small"; - - queue = std::make_shared(embedQueue, *this); - if(!embedQueue) - { - if (settings["battle"]["showQueue"].Bool()) - pos.y += queue->pos.h / 2; //center whole window - - queue->moveTo(Point(pos.x, pos.y - queue->pos.h)); - } - - - CPlayerInterface::battleInt = this; - //initializing armies this->army1 = army1; this->army2 = army2; @@ -108,84 +89,43 @@ BattleInterface::BattleInterface(const CCreatureSet *army1, const CCreatureSet * if(town && town->hasFort()) siegeController.reset(new BattleSiegeController(*this, town)); - controlPanel = std::make_shared(*this, Point(0, 556)); + windowObject = std::make_shared(*this); projectilesController.reset(new BattleProjectileController(*this)); - fieldController.reset( new BattleFieldController(*this)); stacksController.reset( new BattleStacksController(*this)); actionsController.reset( new BattleActionsController(*this)); effectsController.reset(new BattleEffectsController(*this)); - - //loading hero animations - if(hero1) // attacking hero - { - std::string battleImage; - if(!hero1->type->battleImage.empty()) - { - battleImage = hero1->type->battleImage; - } - else - { - if(hero1->sex) - battleImage = hero1->type->heroClass->imageBattleFemale; - else - battleImage = hero1->type->heroClass->imageBattleMale; - } - - attackingHero = std::make_shared(battleImage, false, hero1->tempOwner, hero1->tempOwner == curInt->playerID ? hero1 : nullptr, *this); - - auto img = attackingHero->animation->getImage(0, 0, true); - if(img) - attackingHero->pos = genRect(img->height(), img->width(), pos.x - 43, pos.y - 19); - } - - - if(hero2) // defending hero - { - std::string battleImage; - - if(!hero2->type->battleImage.empty()) - { - battleImage = hero2->type->battleImage; - } - else - { - if(hero2->sex) - battleImage = hero2->type->heroClass->imageBattleFemale; - else - battleImage = hero2->type->heroClass->imageBattleMale; - } - - defendingHero = std::make_shared(battleImage, true, hero2->tempOwner, hero2->tempOwner == curInt->playerID ? hero2 : nullptr, *this); - - auto img = defendingHero->animation->getImage(0, 0, true); - if(img) - defendingHero->pos = genRect(img->height(), img->width(), pos.x + 693, pos.y - 19); - } - obstacleController.reset(new BattleObstacleController(*this)); - if(tacticsMode) - tacticNextStack(nullptr); - CCS->musich->stopMusic(); + setAnimationCondition(EAnimationEvents::OPENING, true); battleIntroSoundChannel = CCS->soundh->playSoundFromSet(CCS->soundh->battleIntroSounds); - auto onIntroPlayed = [&]() + auto onIntroPlayed = [this]() { if(LOCPLINT->battleInt) { - CCS->musich->playMusicFromSet("battle", true, true); - battleActionsStarted = true; - activateStack(); - controlPanel->blockUI(settings["session"]["spectate"].Bool() || stacksController->getActiveStack() == nullptr); - battleIntroSoundChannel = -1; + boost::unique_lock un(*CPlayerInterface::pim); + onIntroSoundPlayed(); } }; - CCS->soundh->setCallback(battleIntroSoundChannel, onIntroPlayed); + GH.pushInt(windowObject); + windowObject->blockUI(true); + windowObject->updateQueue(); - addUsedEvents(RCLICK | MOVE | KEYBOARD); - controlPanel->blockUI(true); - queue->update(); + if (battleIntroSoundChannel != -1) + CCS->soundh->setCallback(battleIntroSoundChannel, onIntroPlayed); + else + onIntroSoundPlayed(); +} + +void BattleInterface::onIntroSoundPlayed() +{ + setAnimationCondition(EAnimationEvents::OPENING, false); + CCS->musich->playMusicFromSet("battle", true, true); + if(tacticsMode) + tacticNextStack(nullptr); + activateStack(); + battleIntroSoundChannel = -1; } BattleInterface::~BattleInterface() @@ -193,18 +133,17 @@ BattleInterface::~BattleInterface() CPlayerInterface::battleInt = nullptr; givenCommand.cond.notify_all(); //that two lines should make any stacksController->getActiveStack() waiting thread to finish - if (active) //dirty fix for #485 - { - deactivate(); - } - if (adventureInt && adventureInt->selection) { //FIXME: this should be moved to adventureInt which should restore correct track based on selection/active player const auto & terrain = *(LOCPLINT->cb->getTile(adventureInt->selection->visitablePos())->terType); CCS->musich->playMusicFromSet("terrain", terrain.name, true, false); } - animsAreDisplayed.setn(false); + + // may happen if user decided to close game while in battle + if (getAnimationCondition(EAnimationEvents::ACTION) == true) + logGlobal->error("Shutting down BattleInterface during animation playback!"); + setAnimationCondition(EAnimationEvents::ACTION, false); } void BattleInterface::setPrintCellBorders(bool set) @@ -231,84 +170,6 @@ void BattleInterface::setPrintMouseShadow(bool set) shadow->Bool() = set; } -void BattleInterface::activate() -{ - controlPanel->activate(); - - if (curInt->isAutoFightOn) - return; - - CIntObject::activate(); - - if (attackingHero) - attackingHero->activate(); - if (defendingHero) - defendingHero->activate(); - - fieldController->activate(); - - if (settings["battle"]["showQueue"].Bool()) - queue->activate(); - - LOCPLINT->cingconsole->activate(); -} - -void BattleInterface::deactivate() -{ - controlPanel->deactivate(); - CIntObject::deactivate(); - - fieldController->deactivate(); - - if (attackingHero) - attackingHero->deactivate(); - if (defendingHero) - defendingHero->deactivate(); - if (settings["battle"]["showQueue"].Bool()) - queue->deactivate(); - - LOCPLINT->cingconsole->deactivate(); -} - -void BattleInterface::keyPressed(const SDL_KeyboardEvent & key) -{ - if(key.keysym.sym == SDLK_q && key.state == SDL_PRESSED) - { - if(settings["battle"]["showQueue"].Bool()) //hide queue - hideQueue(); - else - showQueue(); - - } - else if(key.keysym.sym == SDLK_f && key.state == SDL_PRESSED) - { - actionsController->enterCreatureCastingMode(); - } - else if(key.keysym.sym == SDLK_ESCAPE) - { - if(!battleActionsStarted) - CCS->soundh->stopSound(battleIntroSoundChannel); - else - actionsController->endCastingSpell(); - } -} -void BattleInterface::mouseMoved(const SDL_MouseMotionEvent &event) -{ - BattleHex selectedHex = fieldController->getHoveredHex(); - - actionsController->handleHex(selectedHex, MOVE); - - controlPanel->mouseMoved(event); -} - -void BattleInterface::clickRight(tribool down, bool previousState) -{ - if (!down) - { - actionsController->endCastingSpell(); - } -} - void BattleInterface::stackReset(const CStack * stack) { stacksController->stackReset(stack); @@ -316,24 +177,27 @@ void BattleInterface::stackReset(const CStack * stack) void BattleInterface::stackAdded(const CStack * stack) { - stacksController->stackAdded(stack); + stacksController->stackAdded(stack, false); } void BattleInterface::stackRemoved(uint32_t stackID) { stacksController->stackRemoved(stackID); fieldController->redrawBackgroundWithHexes(); - queue->update(); + windowObject->updateQueue(); } -void BattleInterface::stackActivated(const CStack *stack) //TODO: check it all before game state is changed due to abilities +void BattleInterface::stackActivated(const CStack *stack) { stacksController->stackActivated(stack); } -void BattleInterface::stackMoved(const CStack *stack, std::vector destHex, int distance) +void BattleInterface::stackMoved(const CStack *stack, std::vector destHex, int distance, bool teleport) { - stacksController->stackMoved(stack, destHex, distance); + if (teleport) + stacksController->stackTeleported(stack, destHex, distance); + else + stacksController->stackMoved(stack, destHex, distance); } void BattleInterface::stacksAreAttacked(std::vector attackedInfos) @@ -354,25 +218,25 @@ void BattleInterface::stacksAreAttacked(std::vector attackedI for(ui8 side = 0; side < 2; side++) { if(killedBySide.at(side) > killedBySide.at(1-side)) - setHeroAnimation(side, CCreatureAnim::HERO_DEFEAT); + setHeroAnimation(side, EHeroAnimType::DEFEAT); else if(killedBySide.at(side) < killedBySide.at(1-side)) - setHeroAnimation(side, CCreatureAnim::HERO_VICTORY); + setHeroAnimation(side, EHeroAnimType::VICTORY); } } -void BattleInterface::stackAttacking( const CStack *attacker, BattleHex dest, const CStack *attacked, bool shooting ) +void BattleInterface::stackAttacking( const StackAttackInfo & attackInfo ) { - stacksController->stackAttacking(attacker, dest, attacked, shooting); + stacksController->stackAttacking(attackInfo); } void BattleInterface::newRoundFirst( int round ) { - waitForAnims(); + waitForAnimationCondition(EAnimationEvents::OPENING, false); } void BattleInterface::newRound(int number) { - controlPanel->console->addText(CGI->generaltexth->allTexts[412]); + console->addText(CGI->generaltexth->allTexts[412]); } void BattleInterface::giveCommand(EActionType action, BattleHex tile, si32 additional) @@ -435,11 +299,6 @@ const CGHeroInstance * BattleInterface::getActiveHero() return defendingHeroInstance; } -void BattleInterface::hexLclicked(int whichOne) -{ - actionsController->handleHex(whichOne, LCLICK); -} - void BattleInterface::stackIsCatapulting(const CatapultAttack & ca) { if (siegeController) @@ -454,34 +313,31 @@ void BattleInterface::gateStateChanged(const EGateState state) void BattleInterface::battleFinished(const BattleResult& br) { - bresult = &br; - { - auto unlockPim = vstd::makeUnlockGuard(*CPlayerInterface::pim); - animsAreDisplayed.waitUntil(false); - } + assert(getAnimationCondition(EAnimationEvents::ACTION) == false); + waitForAnimationCondition(EAnimationEvents::ACTION, false); stacksController->setActiveStack(nullptr); - displayBattleFinished(); -} -void BattleInterface::displayBattleFinished() -{ - CCS->curh->changeGraphic(ECursor::ADVENTURE,0); + CCS->curh->set(Cursor::Map::POINTER); curInt->waitWhileDialog(); + if(settings["session"]["spectate"].Bool() && settings["session"]["spectate-skip-battle-result"].Bool()) { - close(); + windowObject->close(); return; } - GH.pushInt(std::make_shared(*bresult, *(this->curInt))); + GH.pushInt(std::make_shared(br, *(this->curInt))); curInt->waitWhileDialog(); // Avoid freeze when AI end turn after battle. Check bug #1897 CPlayerInterface::battleInt = nullptr; } void BattleInterface::spellCast(const BattleSpellCast * sc) { + windowObject->blockUI(true); + const SpellID spellID = sc->spellID; const CSpell * spell = spellID.toSpell(); + auto targetedTile = sc->tile; assert(spell); if(!spell) @@ -490,7 +346,15 @@ void BattleInterface::spellCast(const BattleSpellCast * sc) const std::string & castSoundPath = spell->getCastSound(); if (!castSoundPath.empty()) - CCS->soundh->playSound(castSoundPath); + { + auto group = spell->animationInfo.projectile.empty() ? + EAnimationEvents::HIT: + EAnimationEvents::BEFORE_HIT;//FIXME: recheck whether this should be on projectile spawning + + executeOnAnimationCondition(group, true, [=]() { + CCS->soundh->playSound(castSoundPath); + }); + } if ( sc->activeCast ) { @@ -498,56 +362,77 @@ void BattleInterface::spellCast(const BattleSpellCast * sc) if(casterStack != nullptr ) { - displaySpellCast(spellID, casterStack->getPosition()); - - stacksController->addNewAnim(new CCastAnimation(*this, casterStack, sc->tile, curInt->cb->battleGetStackByPos(sc->tile), spell)); + executeOnAnimationCondition(EAnimationEvents::BEFORE_HIT, true, [=]() + { + stacksController->addNewAnim(new CastAnimation(*this, casterStack, targetedTile, curInt->cb->battleGetStackByPos(targetedTile), spell)); + displaySpellCast(spell, casterStack->getPosition()); + }); } else - if (sc->tile.isValid() && !spell->animationInfo.projectile.empty()) { - // this is spell cast by hero with valid destination & valid projectile -> play animation + auto hero = sc->side ? defendingHero : attackingHero; + assert(hero); - const CStack * target = curInt->cb->battleGetStackByPos(sc->tile); - Point srccoord = (sc->side ? Point(770, 60) : Point(30, 60)) + pos; //hero position - Point destcoord = stacksController->getStackPositionAtHex(sc->tile, target); //position attacked by projectile - destcoord += Point(250, 240); // FIXME: what are these constants? - - projectilesController->createSpellProjectile( nullptr, srccoord, destcoord, spell); - projectilesController->emitStackProjectile( nullptr ); - - // wait fo projectile to end - stacksController->addNewAnim(new CWaitingProjectileAnimation(*this, nullptr)); + executeOnAnimationCondition(EAnimationEvents::BEFORE_HIT, true, [=]() + { + stacksController->addNewAnim(new HeroCastAnimation(*this, hero, targetedTile, curInt->cb->battleGetStackByPos(targetedTile), spell)); + }); } } - waitForAnims(); //wait for projectile animation - - displaySpellHit(spellID, sc->tile); + executeOnAnimationCondition(EAnimationEvents::HIT, true, [=](){ + displaySpellHit(spell, targetedTile); + }); //queuing affect animation for(auto & elem : sc->affectedCres) { auto stack = curInt->cb->battleGetStackByID(elem, false); + assert(stack); if(stack) - displaySpellEffect(spellID, stack->getPosition()); + { + executeOnAnimationCondition(EAnimationEvents::HIT, true, [=](){ + displaySpellEffect(spell, stack->getPosition()); + }); + } } - //queuing additional animation - for(auto & elem : sc->customEffects) + for(auto & elem : sc->reflectedCres) { - auto stack = curInt->cb->battleGetStackByID(elem.stack, false); - if(stack) - effectsController->displayEffect(EBattleEffect::EBattleEffect(elem.effect), stack->getPosition()); + auto stack = curInt->cb->battleGetStackByID(elem, false); + assert(stack); + executeOnAnimationCondition(EAnimationEvents::HIT, true, [=](){ + effectsController->displayEffect(EBattleEffect::MAGIC_MIRROR, stack->getPosition()); + }); + } + + if (!sc->resistedCres.empty()) + { + executeOnAnimationCondition(EAnimationEvents::HIT, true, [=](){ + CCS->soundh->playSound("MAGICRES"); + }); + } + + for(auto & elem : sc->resistedCres) + { + auto stack = curInt->cb->battleGetStackByID(elem, false); + assert(stack); + executeOnAnimationCondition(EAnimationEvents::HIT, true, [=](){ + effectsController->displayEffect(EBattleEffect::RESISTANCE, stack->getPosition()); + }); } - waitForAnims(); //mana absorption if (sc->manaGained > 0) { - Point leftHero = Point(15, 30) + pos; - Point rightHero = Point(755, 30) + pos; - stacksController->addNewAnim(new CPointEffectAnimation(*this, soundBase::invalid, sc->side ? "SP07_A.DEF" : "SP07_B.DEF", leftHero)); - stacksController->addNewAnim(new CPointEffectAnimation(*this, soundBase::invalid, sc->side ? "SP07_B.DEF" : "SP07_A.DEF", rightHero)); + Point leftHero = Point(15, 30); + Point rightHero = Point(755, 30); + bool side = sc->side; + + executeOnAnimationCondition(EAnimationEvents::AFTER_HIT, true, [=](){ + stacksController->addNewAnim(new EffectAnimation(*this, side ? "SP07_A.DEF" : "SP07_B.DEF", leftHero)); + stacksController->addNewAnim(new EffectAnimation(*this, side ? "SP07_B.DEF" : "SP07_A.DEF", rightHero)); + }); } } @@ -557,7 +442,7 @@ void BattleInterface::battleStacksEffectsSet(const SetStackEffect & sse) fieldController->redrawBackgroundWithHexes(); } -void BattleInterface::setHeroAnimation(ui8 side, int phase) +void BattleInterface::setHeroAnimation(ui8 side, EHeroAnimType phase) { if(side == BattleSide::ATTACKER) { @@ -577,60 +462,62 @@ void BattleInterface::displayBattleLog(const std::vector & battleLog { std::string formatted = line.toString(); boost::algorithm::trim(formatted); - if(!controlPanel->console->addText(formatted)) - logGlobal->warn("Too long battle log line"); + appendBattleLog(formatted); } } -void BattleInterface::displaySpellAnimationQueue(const CSpell::TAnimationQueue & q, BattleHex destinationTile, bool isHit) +void BattleInterface::displaySpellAnimationQueue(const CSpell * spell, const CSpell::TAnimationQueue & q, BattleHex destinationTile, bool isHit) { for(const CSpell::TAnimation & animation : q) { if(animation.pause > 0) - stacksController->addNewAnim(new CDummyAnimation(*this, animation.pause)); - else + stacksController->addNewAnim(new DummyAnimation(*this, animation.pause)); + + if (!animation.effectName.empty()) + { + const CStack * destStack = getCurrentPlayerInterface()->cb->battleGetStackByPos(destinationTile, false); + + if (destStack) + stacksController->addNewAnim(new ColorTransformAnimation(*this, destStack, animation.effectName, spell )); + } + + if(!animation.resourceName.empty()) { int flags = 0; if (isHit) - flags |= CPointEffectAnimation::FORCE_ON_TOP; + flags |= EffectAnimation::FORCE_ON_TOP; if (animation.verticalPosition == VerticalPosition::BOTTOM) - flags |= CPointEffectAnimation::ALIGN_TO_BOTTOM; + flags |= EffectAnimation::ALIGN_TO_BOTTOM; if (!destinationTile.isValid()) - flags |= CPointEffectAnimation::SCREEN_FILL; + flags |= EffectAnimation::SCREEN_FILL; if (!destinationTile.isValid()) - stacksController->addNewAnim(new CPointEffectAnimation(*this, soundBase::invalid, animation.resourceName, flags)); + stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, flags)); else - stacksController->addNewAnim(new CPointEffectAnimation(*this, soundBase::invalid, animation.resourceName, destinationTile, flags)); + stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, destinationTile, flags)); } } } -void BattleInterface::displaySpellCast(SpellID spellID, BattleHex destinationTile) +void BattleInterface::displaySpellCast(const CSpell * spell, BattleHex destinationTile) { - const CSpell * spell = spellID.toSpell(); - if(spell) - displaySpellAnimationQueue(spell->animationInfo.cast, destinationTile, false); + displaySpellAnimationQueue(spell, spell->animationInfo.cast, destinationTile, false); } -void BattleInterface::displaySpellEffect(SpellID spellID, BattleHex destinationTile) +void BattleInterface::displaySpellEffect(const CSpell * spell, BattleHex destinationTile) { - const CSpell *spell = spellID.toSpell(); - if(spell) - displaySpellAnimationQueue(spell->animationInfo.affect, destinationTile, false); + displaySpellAnimationQueue(spell, spell->animationInfo.affect, destinationTile, false); } -void BattleInterface::displaySpellHit(SpellID spellID, BattleHex destinationTile) +void BattleInterface::displaySpellHit(const CSpell * spell, BattleHex destinationTile) { - const CSpell * spell = spellID.toSpell(); - if(spell) - displaySpellAnimationQueue(spell->animationInfo.hit, destinationTile, true); + displaySpellAnimationQueue(spell, spell->animationInfo.hit, destinationTile, true); } void BattleInterface::setAnimSpeed(int set) @@ -663,9 +550,6 @@ void BattleInterface::trySetActivePlayer( PlayerColor player ) void BattleInterface::activateStack() { - if(!battleActionsStarted) - return; //"show" function should re-call this function - stacksController->activateStack(); const CStack * s = stacksController->getActiveStack(); @@ -673,7 +557,8 @@ void BattleInterface::activateStack() return; myTurn = true; - queue->update(); + windowObject->updateQueue(); + windowObject->blockUI(false); fieldController->redrawBackgroundWithHexes(); actionsController->activateStack(); GH.fakeMouseMove(); @@ -683,66 +568,28 @@ void BattleInterface::endAction(const BattleAction* action) { const CStack *stack = curInt->cb->battleGetStackByID(action->stackNumber); - if(action->actionType == EActionType::HERO_SPELL) - setHeroAnimation(action->side, CCreatureAnim::HERO_HOLDING); - stacksController->endAction(action); + windowObject->updateQueue(); - queue->update(); - - if (tacticsMode) //stack ended movement in tactics phase -> select the next one + //stack ended movement in tactics phase -> select the next one + if (tacticsMode) tacticNextStack(stack); - if(action->actionType == EActionType::HERO_SPELL) //we have activated next stack after sending request that has been just realized -> blockmap due to movement has changed + //we have activated next stack after sending request that has been just realized -> blockmap due to movement has changed + if(action->actionType == EActionType::HERO_SPELL) fieldController->redrawBackgroundWithHexes(); - -// if (stacksController->getActiveStack() && !animsAreDisplayed.get() && pendingAnims.empty() && !active) -// { -// logGlobal->warn("Something wrong... interface was deactivated but there is no animation. Reactivating..."); -// controlPanel->blockUI(false); -// } -// else -// { - // block UI if no active stack (e.g. enemy turn); - controlPanel->blockUI(stacksController->getActiveStack() == nullptr); -// } } -void BattleInterface::hideQueue() +void BattleInterface::appendBattleLog(const std::string & newEntry) { - Settings showQueue = settings.write["battle"]["showQueue"]; - showQueue->Bool() = false; - - queue->deactivate(); - - if (!queue->embedded) - { - moveBy(Point(0, -queue->pos.h / 2)); - GH.totalRedraw(); - } -} - -void BattleInterface::showQueue() -{ - Settings showQueue = settings.write["battle"]["showQueue"]; - showQueue->Bool() = true; - - queue->activate(); - - if (!queue->embedded) - { - moveBy(Point(0, +queue->pos.h / 2)); - GH.totalRedraw(); - } + console->addText(newEntry); } void BattleInterface::startAction(const BattleAction* action) { - controlPanel->blockUI(true); - if(action->actionType == EActionType::END_TACTIC_PHASE) { - controlPanel->tacticPhaseEnded(); + windowObject->tacticPhaseEnded(); return; } @@ -750,7 +597,7 @@ void BattleInterface::startAction(const BattleAction* action) if (stack) { - queue->update(); + windowObject->updateQueue(); } else { @@ -759,13 +606,8 @@ void BattleInterface::startAction(const BattleAction* action) stacksController->startAction(action); - redraw(); // redraw after deactivation, including proper handling of hovered hexes - if(action->actionType == EActionType::HERO_SPELL) //when hero casts spell - { - setHeroAnimation(action->side, CCreatureAnim::HERO_CAST_SPELL); return; - } if (!stack) { @@ -776,16 +618,9 @@ void BattleInterface::startAction(const BattleAction* action) effectsController->startAction(action); } -void BattleInterface::waitForAnims() -{ - auto unlockPim = vstd::makeUnlockGuard(*CPlayerInterface::pim); - animsAreDisplayed.waitWhileTrue(); -} - void BattleInterface::tacticPhaseEnd() { stacksController->setActiveStack(nullptr); - controlPanel->blockUI(true); tacticsMode = false; } @@ -800,7 +635,8 @@ void BattleInterface::tacticNextStack(const CStack * current) current = stacksController->getActiveStack(); //no switching stacks when the current one is moving - waitForAnims(); + assert(getAnimationCondition(EAnimationEvents::ACTION) == false); + waitForAnimationCondition(EAnimationEvents::ACTION, false); TStacks stacksOfMine = tacticianInterface->cb->battleGetStacks(CBattleCallback::ONLY_MINE); vstd::erase_if (stacksOfMine, &immobile); @@ -867,7 +703,6 @@ void BattleInterface::requestAutofightingAIToTakeAction() //TODO implement the possibility that the AI will be triggered for further actions //TODO any solution to merge tactics phase & normal phase in the way it is handled by the player and battle interface? stacksController->setActiveStack(nullptr); - controlPanel->blockUI(true); tacticsMode = false; } else @@ -885,81 +720,49 @@ void BattleInterface::requestAutofightingAIToTakeAction() aiThread.detach(); } -void BattleInterface::showAll(SDL_Surface *to) -{ - show(to); -} - -void BattleInterface::show(SDL_Surface *to) -{ - Canvas canvas(to); - assert(to); - - SDL_Rect buf; - SDL_GetClipRect(to, &buf); - SDL_SetClipRect(to, &pos); - - ++animCount; - - fieldController->renderBattlefield(canvas); - - if(battleActionsStarted) - stacksController->updateBattleAnimations(); - - SDL_SetClipRect(to, &buf); //restoring previous clip_rect - - showInterface(to); - - //activation of next stack, if any - //TODO: should be moved to the very start of this method? - //activateStack(); -} - -void BattleInterface::collectRenderableObjects(BattleRenderer & renderer) -{ - if (attackingHero) - { - renderer.insert(EBattleFieldLayer::HEROES, BattleHex(0),[this](BattleRenderer::RendererRef canvas) - { - attackingHero->render(canvas); - }); - } - if (defendingHero) - { - renderer.insert(EBattleFieldLayer::HEROES, BattleHex(GameConstants::BFIELD_WIDTH-1),[this](BattleRenderer::RendererRef canvas) - { - defendingHero->render(canvas); - }); - } -} - -void BattleInterface::showInterface(SDL_Surface * to) -{ - //showing in-game console - LOCPLINT->cingconsole->show(to); - controlPanel->showAll(to); - - Rect posWithQueue = Rect(pos.x, pos.y, 800, 600); - - if (settings["battle"]["showQueue"].Bool()) - { - if (!queue->embedded) - { - posWithQueue.y -= queue->pos.h; - posWithQueue.h += queue->pos.h; - } - - queue->showAll(to); - } - - //printing border around interface - if (screen->w != 800 || screen->h !=600) - { - CMessage::drawBorder(curInt->playerID,to,posWithQueue.w + 28, posWithQueue.h + 28, posWithQueue.x-14, posWithQueue.y-15); - } -} - void BattleInterface::castThisSpell(SpellID spellID) { actionsController->castThisSpell(spellID); } + +void BattleInterface::setAnimationCondition( EAnimationEvents event, bool state) +{ + logAnim->debug("setAnimationCondition: %d -> %s", static_cast(event), state ? "ON" : "OFF"); + + size_t index = static_cast(event); + animationEvents[index].setn(state); + + decltype(awaitingEvents) executingEvents; + + for (auto it = awaitingEvents.begin(); it != awaitingEvents.end();) + { + if (it->event == event && it->eventState == state) + { + executingEvents.push_back(*it); + it = awaitingEvents.erase(it); + } + else + ++it; + } + + for (auto const & event : executingEvents) + event.action(); +} + +bool BattleInterface::getAnimationCondition( EAnimationEvents event) +{ + size_t index = static_cast(event); + return animationEvents[index].get(); +} + +void BattleInterface::waitForAnimationCondition( EAnimationEvents event, bool state) +{ + auto unlockPim = vstd::makeUnlockGuard(*CPlayerInterface::pim); + size_t index = static_cast(event); + animationEvents[index].waitUntil(state); +} + +void BattleInterface::executeOnAnimationCondition( EAnimationEvents event, bool state, const AwaitingAnimationAction & action) +{ + awaitingEvents.push_back({action, event, state}); +} diff --git a/client/battle/BattleInterface.h b/client/battle/BattleInterface.h index 0ca1f6d71..394ed3b9c 100644 --- a/client/battle/BattleInterface.h +++ b/client/battle/BattleInterface.h @@ -9,8 +9,10 @@ */ #pragma once +#include "BattleConstants.h" #include "../gui/CIntObject.h" #include "../../lib/spells/CSpellHandler.h" //CSpell::TAnimation +#include "../../lib/CondSh.h" VCMI_LIB_NAMESPACE_BEGIN @@ -20,7 +22,6 @@ class CStack; struct BattleResult; struct BattleSpellCast; struct CObstacleInstance; -template struct CondSh; struct SetStackEffect; class BattleAction; class CGTownInstance; @@ -28,7 +29,6 @@ struct CatapultAttack; struct BattleTriggerEffect; struct BattleHex; struct InfoAboutHero; -struct CustomEffectInfo; VCMI_LIB_NAMESPACE_END @@ -48,46 +48,109 @@ class BattleSiegeController; class BattleObstacleController; class BattleFieldController; class BattleRenderer; -class BattleControlPanel; +class BattleWindow; class BattleStacksController; class BattleActionsController; class BattleEffectsController; +class BattleConsole; /// Small struct which contains information about the id of the attacked stack, the damage dealt,... struct StackAttackedInfo { - const CStack *defender; //attacked stack - int64_t dmg; //damage dealt - unsigned int amountKilled; //how many creatures in stack has been killed - const CStack *attacker; //attacking stack + const CStack *defender; + const CStack *attacker; + + int64_t damageDealt; + uint32_t amountKilled; + SpellID spellEffect; + bool indirectAttack; //if true, stack was attacked indirectly - spell or ranged attack bool killed; //if true, stack has been killed bool rebirth; //if true, play rebirth animation after all bool cloneKilled; + bool fireShield; }; -/// Big class which handles the overall battle interface actions and it is also responsible for -/// drawing everything correctly. -class BattleInterface : public WindowBase +struct StackAttackInfo { -private: - std::shared_ptr attackingHero; - std::shared_ptr defendingHero; - std::shared_ptr queue; - std::shared_ptr controlPanel; + const CStack *attacker; + const CStack *defender; + std::vector< const CStack *> secondaryDefender; - std::shared_ptr tacticianInterface; //used during tactics mode, points to the interface of player with higher tactics (can be either attacker or defender in hot-seat), valid onloy for human players - std::shared_ptr attackerInt, defenderInt; //because LOCPLINT is not enough in hotSeat - std::shared_ptr curInt; //current player interface + SpellID spellEffect; + BattleHex tile; - const CCreatureSet *army1, *army2; //copy of initial armies (for result window) - const CGHeroInstance *attackingHeroInstance, *defendingHeroInstance; + bool indirectAttack; + bool lucky; + bool unlucky; + bool deathBlow; + bool lifeDrain; +}; - ui8 animCount; +/// Main class for battles, responsible for relaying information from server to various battle entities +class BattleInterface +{ + using AwaitingAnimationAction = std::function; + + struct AwaitingAnimationEvents { + AwaitingAnimationAction action; + EAnimationEvents event; + bool eventState; + }; + + /// Conditional variables that are set depending on ongoing animations on the battlefield + std::array< CondSh, static_cast(EAnimationEvents::COUNT)> animationEvents; + + /// List of events that are waiting to be triggered + std::vector awaitingEvents; + + /// used during tactics mode, points to the interface of player with higher tactics (can be either attacker or defender in hot-seat), valid onloy for human players + std::shared_ptr tacticianInterface; + + /// attacker interface, not null if attacker is human in our vcmiclient + std::shared_ptr attackerInt; + + /// defender interface, not null if attacker is human in our vcmiclient + std::shared_ptr defenderInt; + + void onIntroSoundPlayed(); +public: + /// copy of initial armies (for result window) + const CCreatureSet *army1; + const CCreatureSet *army2; + + /// ID of channel on which battle opening sound is playing, or -1 if none + int battleIntroSoundChannel; + + std::shared_ptr windowObject; + std::shared_ptr console; + + /// currently active player interface + std::shared_ptr curInt; + + const CGHeroInstance *attackingHeroInstance; + const CGHeroInstance *defendingHeroInstance; bool tacticsMode; - bool battleActionsStarted; //used for delaying battle actions until intro sound stops - int battleIntroSoundChannel; //required as variable for disabling it via ESC key + + std::unique_ptr projectilesController; + std::unique_ptr siegeController; + std::unique_ptr obstacleController; + std::unique_ptr fieldController; + std::unique_ptr stacksController; + std::unique_ptr actionsController; + std::unique_ptr effectsController; + + std::shared_ptr attackingHero; + std::shared_ptr defendingHero; + + static CondSh givenCommand; //data != nullptr if we have i.e. moved current unit + + bool myTurn; //if true, interface is active (commands can be ordered) + int moveSoundHander; // sound handler used when moving a unit + + BattleInterface(const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, std::shared_ptr att, std::shared_ptr defen, std::shared_ptr spectatorInt = nullptr); + ~BattleInterface(); void trySetActivePlayer( PlayerColor player ); // if in hotseat, will activate interface of chosen player void activateStack(); //sets activeStack to stackToActivate etc. //FIXME: No, it's not clear at all @@ -100,27 +163,11 @@ private: void showInterface(SDL_Surface * to); - void setHeroAnimation(ui8 side, int phase); -public: - std::unique_ptr projectilesController; - std::unique_ptr siegeController; - std::unique_ptr obstacleController; - std::unique_ptr fieldController; - std::unique_ptr stacksController; - std::unique_ptr actionsController; - std::unique_ptr effectsController; + void setHeroAnimation(ui8 side, EHeroAnimType phase); - static CondSh animsAreDisplayed; //for waiting with the end of battle for end of anims - static CondSh givenCommand; //data != nullptr if we have i.e. moved current unit + void executeSpellCast(); //called when a hero casts a spell - bool myTurn; //if true, interface is active (commands can be ordered) - bool moveStarted; //if true, the creature that is already moving is going to make its first step - int moveSoundHander; // sound handler used when moving a unit - - const BattleResult *bresult; //result of a battle; if non-zero then display when all animations end - - BattleInterface(const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const SDL_Rect & myRect, std::shared_ptr att, std::shared_ptr defen, std::shared_ptr spectatorInt = nullptr); - virtual ~BattleInterface(); + void appendBattleLog(const std::string & newEntry); void setPrintCellBorders(bool set); //if true, cell borders will be printed void setPrintStackRange(bool set); //if true,range of active stack will be printed @@ -131,19 +178,18 @@ public: void tacticNextStack(const CStack *current); void tacticPhaseEnd(); - void waitForAnims(); - //napisz tu klase odpowiadajaca za wyswietlanie bitwy i obsluge uzytkownika, polecenia ma przekazywac callbackiem - void activate() override; - void deactivate() override; - void keyPressed(const SDL_KeyboardEvent & key) override; - void mouseMoved(const SDL_MouseMotionEvent &sEvent) override; - void clickRight(tribool down, bool previousState) override; + /// sets condition to targeted state and executes any awaiting actions + void setAnimationCondition( EAnimationEvents event, bool state); - void show(SDL_Surface *to) override; - void showAll(SDL_Surface *to) override; + /// returns current state of condition + bool getAnimationCondition( EAnimationEvents event); - void collectRenderableObjects(BattleRenderer & renderer); + /// locks execution until selected condition reached targeted state + void waitForAnimationCondition( EAnimationEvents event, bool state); + + /// adds action that will be executed one selected condition reached targeted state + void executeOnAnimationCondition( EAnimationEvents event, bool state, const AwaitingAnimationAction & action); //call-ins void startAction(const BattleAction* action); @@ -151,29 +197,25 @@ public: void stackAdded(const CStack * stack); //new stack appeared on battlefield void stackRemoved(uint32_t stackID); //stack disappeared from batlefiled void stackActivated(const CStack *stack); //active stack has been changed - void stackMoved(const CStack *stack, std::vector destHex, int distance); //stack with id number moved to destHex + void stackMoved(const CStack *stack, std::vector destHex, int distance, bool teleport); //stack with id number moved to destHex void stacksAreAttacked(std::vector attackedInfos); //called when a certain amount of stacks has been attacked - void stackAttacking(const CStack *attacker, BattleHex dest, const CStack *attacked, bool shooting); //called when stack with id ID is attacking something on hex dest + void stackAttacking(const StackAttackInfo & attackInfo); //called when stack with id ID is attacking something on hex dest void newRoundFirst( int round ); void newRound(int number); //caled when round is ended; number is the number of round - void hexLclicked(int whichOne); //hex only call-in void stackIsCatapulting(const CatapultAttack & ca); //called when a stack is attacking walls void battleFinished(const BattleResult& br); //called when battle is finished - battleresult window should be printed - void displayBattleFinished(); //displays battle result void spellCast(const BattleSpellCast *sc); //called when a hero casts a spell void battleStacksEffectsSet(const SetStackEffect & sse); //called when a specific effect is set to stacks void castThisSpell(SpellID spellID); //called when player has chosen a spell from spellbook void displayBattleLog(const std::vector & battleLog); - void displaySpellAnimationQueue(const CSpell::TAnimationQueue & q, BattleHex destinationTile, bool isHit); - void displaySpellCast(SpellID spellID, BattleHex destinationTile); //displays spell`s cast animation - void displaySpellEffect(SpellID spellID, BattleHex destinationTile); //displays spell`s affected animation - void displaySpellHit(SpellID spellID, BattleHex destinationTile); //displays spell`s affected animation + void displaySpellAnimationQueue(const CSpell * spell, const CSpell::TAnimationQueue & q, BattleHex destinationTile, bool isHit); + void displaySpellCast(const CSpell * spell, BattleHex destinationTile); //displays spell`s cast animation + void displaySpellEffect(const CSpell * spell, BattleHex destinationTile); //displays spell`s affected animation + void displaySpellHit(const CSpell * spell, BattleHex destinationTile); //displays spell`s affected animation void endAction(const BattleAction* action); - void hideQueue(); - void showQueue(); void obstaclePlaced(const std::vector> oi); @@ -181,29 +223,4 @@ public: const CGHeroInstance *currentHero() const; InfoAboutHero enemyHero() const; - - // TODO: cleanup this list - friend class CPlayerInterface; - friend class CInGameConsole; - friend class StackQueue; - friend class BattleResultWindow; - friend class BattleHero; - friend class CBattleStackAnimation; - friend class CReverseAnimation; - friend class CDefenceAnimation; - friend class CMovementAnimation; - friend class CMovementStartAnimation; - friend class CAttackAnimation; - friend class CMeleeAttackAnimation; - friend class CShootingAnimation; - friend class CCastAnimation; - friend class ClickableHex; - friend class BattleProjectileController; - friend class BattleSiegeController; - friend class BattleObstacleController; - friend class BattleFieldController; - friend class BattleControlPanel; - friend class BattleStacksController; - friend class BattleActionsController; - friend class BattleEffectsController; }; diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index c9ba889e8..14f67822a 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -12,10 +12,11 @@ #include "BattleInterface.h" #include "BattleActionsController.h" +#include "BattleRenderer.h" #include "BattleSiegeController.h" #include "BattleFieldController.h" #include "BattleStacksController.h" -#include "BattleControlPanel.h" +#include "BattleWindow.h" #include "../CGameInfo.h" #include "../CMessage.h" @@ -41,6 +42,7 @@ #include "../../lib/CGameState.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CTownHandler.h" +#include "../../lib/CHeroHandler.h" #include "../../lib/NetPacks.h" #include "../../lib/StartInfo.h" #include "../../lib/CondSh.h" @@ -48,70 +50,99 @@ void BattleConsole::showAll(SDL_Surface * to) { - Point consolePos(pos.x + 10, pos.y + 17); - Point textPos (pos.x + pos.w/2, pos.y + 17); + CIntObject::showAll(to); - if (!consoleText.empty()) + Point line1 (pos.x + pos.w/2, pos.y + 8); + Point line2 (pos.x + pos.w/2, pos.y + 24); + + auto visibleText = getVisibleText(); + + if(visibleText.size() > 0) + graphics->fonts[FONT_SMALL]->renderTextCenter(to, visibleText[0], Colors::WHITE, line1); + + if(visibleText.size() > 1) + graphics->fonts[FONT_SMALL]->renderTextCenter(to, visibleText[1], Colors::WHITE, line2); +} + +std::vector BattleConsole::getVisibleText() +{ + // high priority texts that hide battle log entries + for (auto const & text : {consoleText, hoverText} ) { - graphics->fonts[FONT_SMALL]->renderTextLinesLeft(to, CMessage::breakText(consoleText, pos.w, FONT_SMALL), Colors::WHITE, consolePos); + if (text.empty()) + continue; + + auto result = CMessage::breakText(text, pos.w, FONT_SMALL); + + if(result.size() > 2) + result.resize(2); + return result; } - else if(!hoverText.empty()) + + // log is small enough to fit entirely - display it as such + if (logEntries.size() < 3) + return logEntries; + + return { logEntries[scrollPosition - 1], logEntries[scrollPosition] }; +} + +std::vector BattleConsole::splitText(const std::string &text) +{ + std::vector lines; + std::vector output; + + boost::split(lines, text, boost::is_any_of("\n")); + + for (auto const & line : lines) { - graphics->fonts[FONT_SMALL]->renderTextLinesCenter(to, CMessage::breakText(hoverText, pos.w, FONT_SMALL), Colors::WHITE, textPos); - } - else if(logEntries.size()) - { - if(logEntries.size()==1) + if (graphics->fonts[FONT_SMALL]->getStringWidth(text) < pos.w) { - graphics->fonts[FONT_SMALL]->renderTextLinesCenter(to, CMessage::breakText(logEntries[0], pos.w, FONT_SMALL), Colors::WHITE, textPos); + output.push_back(line); } else { - graphics->fonts[FONT_SMALL]->renderTextLinesCenter(to, CMessage::breakText(logEntries[scrollPosition - 1], pos.w, FONT_SMALL), Colors::WHITE, textPos); - textPos.y += 16; - graphics->fonts[FONT_SMALL]->renderTextLinesCenter(to, CMessage::breakText(logEntries[scrollPosition], pos.w, FONT_SMALL), Colors::WHITE, textPos); + std::vector substrings = CMessage::breakText(line, pos.w, FONT_SMALL); + output.insert(output.end(), substrings.begin(), substrings.end()); } } + return output; } bool BattleConsole::addText(const std::string & text) { logGlobal->trace("CBattleConsole message: %s", text); - if(text.size()>70) - return false; //text too long! - int firstInToken = 0; - for(size_t i = 0; i < text.size(); ++i) //tokenize - { - if(text[i] == 10) - { - logEntries.push_back( text.substr(firstInToken, i-firstInToken) ); - firstInToken = (int)i+1; - } - } - logEntries.push_back( text.substr(firstInToken, text.size()) ); + auto newLines = splitText(text); + + logEntries.insert(logEntries.end(), newLines.begin(), newLines.end()); scrollPosition = (int)logEntries.size()-1; + redraw(); return true; } void BattleConsole::scrollUp(ui32 by) { if(scrollPosition > static_cast(by)) scrollPosition -= by; + redraw(); } void BattleConsole::scrollDown(ui32 by) { if(scrollPosition + by < logEntries.size()) scrollPosition += by; + redraw(); } -BattleConsole::BattleConsole(const Rect & position) +BattleConsole::BattleConsole(std::shared_ptr backgroundSource, const Point & objectPos, const Point & imagePos, const Point &size) : scrollPosition(-1) , enteringText(false) { - pos += position.topLeft(); - pos.w = position.w; - pos.h = position.h; + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + pos += objectPos; + pos.w = size.x; + pos.h = size.y; + + background = std::make_shared(backgroundSource->getSurface(), Rect(imagePos, size), 0, 0 ); } void BattleConsole::deactivate() @@ -137,17 +168,20 @@ void BattleConsole::setEnteringMode(bool on) CSDL_Ext::stopTextInput(); } enteringText = on; + redraw(); } void BattleConsole::setEnteredText(const std::string & text) { assert(enteringText == true); consoleText = text; + redraw(); } void BattleConsole::write(const std::string & Text) { hoverText = Text; + redraw(); } void BattleConsole::clearIfMatching(const std::string & Text) @@ -161,75 +195,110 @@ void BattleConsole::clear() write({}); } +const CGHeroInstance * BattleHero::instance() +{ + return hero; +} + void BattleHero::render(Canvas & canvas) { - auto flagFrame = flagAnimation->getImage(flagAnim, 0, true); + size_t groupIndex = static_cast(phase); - if(!flagFrame) - return; + auto flagFrame = flagAnimation->getImage(flagCurrentFrame, 0, true); + auto heroFrame = animation->getImage(currentFrame, groupIndex, true); - //animation of flag - Point flagPosition = pos.topLeft(); + Point heroPosition = pos.center() - parent->pos.topLeft() - heroFrame->dimensions() / 2; + Point flagPosition = pos.center() - parent->pos.topLeft() - flagFrame->dimensions() / 2; - if(flip) - flagPosition += Point(61, 39); + if(defender) + flagPosition += Point(-4, -41); else - flagPosition += Point(72, 39); - - - auto heroFrame = animation->getImage(currentFrame, phase, true); + flagPosition += Point(4, -41); canvas.draw(flagFrame, flagPosition); - canvas.draw(heroFrame, pos.topLeft()); + canvas.draw(heroFrame, heroPosition); - if(++animCount >= 4) + flagCurrentFrame += currentSpeed; + currentFrame += currentSpeed; + + if(flagCurrentFrame >= flagAnimation->size(0)) + flagCurrentFrame -= flagAnimation->size(0); + + if(currentFrame >= animation->size(groupIndex)) { - animCount = 0; - if(++flagAnim >= flagAnimation->size(0)) - flagAnim = 0; - - if(++currentFrame >= lastFrame) - switchToNextPhase(); + currentFrame -= animation->size(groupIndex); + switchToNextPhase(); } } -void BattleHero::setPhase(int newPhase) +void BattleHero::pause() +{ + currentSpeed = 0.f; +} + +void BattleHero::play() +{ + //FIXME: un-hardcode speed + currentSpeed = 0.25f; +} + +float BattleHero::getFrame() const +{ + return currentFrame; +} + +void BattleHero::collectRenderableObjects(BattleRenderer & renderer) +{ + auto hex = defender ? BattleHex(GameConstants::BFIELD_WIDTH-1) : BattleHex(0); + + renderer.insert(EBattleFieldLayer::HEROES, hex, [this](BattleRenderer::RendererRef canvas) + { + render(canvas); + }); +} + +void BattleHero::onPhaseFinished(const std::function & callback) +{ + phaseFinishedCallback = callback; +} + +void BattleHero::setPhase(EHeroAnimType newPhase) { nextPhase = newPhase; switchToNextPhase(); //immediately switch to next phase and then restore idling phase - nextPhase = 0; + nextPhase = EHeroAnimType::HOLDING; } void BattleHero::hover(bool on) { - //TODO: Make lines below work properly + //TODO: BROKEN CODE if (on) - CCS->curh->changeGraphic(ECursor::COMBAT, 5); + CCS->curh->set(Cursor::Combat::HERO); else - CCS->curh->changeGraphic(ECursor::COMBAT, 0); + CCS->curh->set(Cursor::Combat::POINTER); } void BattleHero::clickLeft(tribool down, bool previousState) { - if(myOwner->actionsController->spellcastingModeActive()) //we are casting a spell + if(owner.actionsController->spellcastingModeActive()) //we are casting a spell return; if(boost::logic::indeterminate(down)) return; - if(!myHero || down || !myOwner->myTurn) + if(!hero || down || !owner.myTurn) return; - if(myOwner->getCurrentPlayerInterface()->cb->battleCanCastSpell(myHero, spells::Mode::HERO) == ESpellCastProblem::OK) //check conditions + if(owner.getCurrentPlayerInterface()->cb->battleCanCastSpell(hero, spells::Mode::HERO) == ESpellCastProblem::OK) //check conditions { - BattleHex hoveredHex = myOwner->fieldController->getHoveredHex(); + BattleHex hoveredHex = owner.fieldController->getHoveredHex(); //do nothing when any hex is hovered - hero's animation overlaps battlefield if ( hoveredHex != BattleHex::INVALID ) return; - CCS->curh->changeGraphic(ECursor::ADVENTURE, 0); + CCS->curh->set(Cursor::Map::POINTER); - GH.pushIntT(myHero, myOwner->getCurrentPlayerInterface()); + GH.pushIntT(hero, owner.getCurrentPlayerInterface()); } } @@ -239,13 +308,13 @@ void BattleHero::clickRight(tribool down, bool previousState) return; Point windowPosition; - windowPosition.x = (!flip) ? myOwner->pos.topLeft().x + 1 : myOwner->pos.topRight().x - 79; - windowPosition.y = myOwner->pos.y + 135; + windowPosition.x = (!defender) ? owner.fieldController->pos.topLeft().x + 1 : owner.fieldController->pos.topRight().x - 79; + windowPosition.y = owner.fieldController->pos.y + 135; InfoAboutHero targetHero; - if(down && (myOwner->myTurn || settings["session"]["spectate"].Bool())) + if(down && (owner.myTurn || settings["session"]["spectate"].Bool())) { - auto h = flip ? myOwner->defendingHeroInstance : myOwner->attackingHeroInstance; + auto h = defender ? owner.defendingHeroInstance : owner.attackingHeroInstance; targetHero.initFromHero(h, InfoAboutHero::EInfoLevel::INBATTLE); GH.pushIntT(targetHero, &windowPosition); } @@ -253,43 +322,57 @@ void BattleHero::clickRight(tribool down, bool previousState) void BattleHero::switchToNextPhase() { - if(phase != nextPhase) - { - phase = nextPhase; + phase = nextPhase; + currentFrame = 0.f; - firstFrame = 0; - - lastFrame = static_cast(animation->size(phase)); - } - - currentFrame = firstFrame; + auto copy = phaseFinishedCallback; + phaseFinishedCallback.clear(); + copy(); } -BattleHero::BattleHero(const std::string & animationPath, bool flipG, PlayerColor player, const CGHeroInstance * hero, const BattleInterface & owner): - flip(flipG), - myHero(hero), - myOwner(&owner), - phase(1), - nextPhase(0), - flagAnim(0), - animCount(0) +BattleHero::BattleHero(const BattleInterface & owner, const CGHeroInstance * hero, bool defender): + defender(defender), + hero(hero), + owner(owner), + phase(EHeroAnimType::HOLDING), + nextPhase(EHeroAnimType::HOLDING), + currentSpeed(0.f), + currentFrame(0.f), + flagCurrentFrame(0.f) { + std::string animationPath; + + if(!hero->type->battleImage.empty()) + animationPath = hero->type->battleImage; + else + if(hero->sex) + animationPath = hero->type->heroClass->imageBattleFemale; + else + animationPath = hero->type->heroClass->imageBattleMale; + animation = std::make_shared(animationPath); animation->preload(); - if(flipG) + + pos.w = 64; + pos.h = 136; + pos.x = owner.fieldController->pos.x + (defender ? (owner.fieldController->pos.w - pos.w) : 0); + pos.y = owner.fieldController->pos.y; + + if(defender) animation->verticalFlip(); - if(flip) + if(defender) flagAnimation = std::make_shared("CMFLAGR"); else flagAnimation = std::make_shared("CMFLAGL"); flagAnimation->preload(); - flagAnimation->playerColored(player); + flagAnimation->playerColored(hero->tempOwner); addUsedEvents(LCLICK | RCLICK | HOVER); switchToNextPhase(); + play(); } HeroInfoWindow::HeroInfoWindow(const InfoAboutHero & hero, Point * position) @@ -584,7 +667,7 @@ void BattleResultWindow::bExitf() close(); - if(dynamic_cast(GH.topInt().get())) + if(dynamic_cast(GH.topInt().get())) GH.popInts(1); //pop battle interface if present //Result window and battle interface are gone. We requested all dialogs to be closed before opening the battle, @@ -599,7 +682,7 @@ void ClickableHex::hover(bool on) //Hoverable::hover(on); if(!on && setAlterText) { - myInterface->controlPanel->console->clear(); + GH.statusbar->clear(); setAlterText = false; } } @@ -623,13 +706,13 @@ void ClickableHex::mouseMoved(const SDL_MouseMotionEvent &sEvent) MetaString text; text.addTxt(MetaString::GENERAL_TXT, 220); attackedStack->addNameReplacement(text); - myInterface->controlPanel->console->write(text.toString()); + GH.statusbar->write(text.toString()); setAlterText = true; } } else if(setAlterText) { - myInterface->controlPanel->console->clear(); + GH.statusbar->clear(); setAlterText = false; } } @@ -638,7 +721,7 @@ void ClickableHex::clickLeft(tribool down, bool previousState) { if(!down && hovered && strictHovered) //we've been really clicked! { - myInterface->hexLclicked(myNumber); + myInterface->actionsController->handleHex(myNumber, LCLICK); } } @@ -662,10 +745,10 @@ StackQueue::StackQueue(bool Embedded, BattleInterface & owner) OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); if(embedded) { - pos.w = QUEUE_SIZE * 37; - pos.h = 46; - pos.x = screen->w/2 - pos.w/2; - pos.y = (screen->h - 600)/2 + 10; + pos.w = QUEUE_SIZE * 41; + pos.h = 49; + pos.x += parent->pos.w/2 - pos.w/2; + pos.y += 10; icons = std::make_shared("CPRSMALL"); stateIcons = std::make_shared("VCMI/BATTLEQUEUE/STATESSMALL"); @@ -674,6 +757,8 @@ StackQueue::StackQueue(bool Embedded, BattleInterface & owner) { pos.w = 800; pos.h = 85; + pos.x += 0; + pos.y -= pos.h; background = std::make_shared("DIBOXBCK", Rect(0, 0, pos.w, pos.h)); @@ -688,10 +773,17 @@ StackQueue::StackQueue(bool Embedded, BattleInterface & owner) for (int i = 0; i < stackBoxes.size(); i++) { stackBoxes[i] = std::make_shared(this); - stackBoxes[i]->moveBy(Point(1 + (embedded ? 36 : 80) * i, 0)); + stackBoxes[i]->moveBy(Point(1 + (embedded ? 41 : 80) * i, 0)); } } +void StackQueue::show(SDL_Surface * to) +{ + if (embedded) + showAll(to); + CIntObject::show(to); +} + void StackQueue::update() { std::vector queueData; diff --git a/client/battle/BattleInterfaceClasses.h b/client/battle/BattleInterfaceClasses.h index 4b4df51c0..5413a1399 100644 --- a/client/battle/BattleInterfaceClasses.h +++ b/client/battle/BattleInterfaceClasses.h @@ -9,7 +9,9 @@ */ #pragma once +#include "BattleConstants.h" #include "../gui/CIntObject.h" +#include "../../lib/FunctionList.h" #include "../../lib/battle/BattleHex.h" #include "../windows/CWindowObject.h" @@ -38,11 +40,14 @@ class CLabel; class CTextBox; class CAnimImage; class CPlayerInterface; +class BattleRenderer; /// Class which shows the console at the bottom of the battle screen and manages the text of the console class BattleConsole : public CIntObject, public IStatusBar { private: + std::shared_ptr background; + /// List of all texts added during battle, essentially - log of entire battle std::vector< std::string > logEntries; @@ -57,8 +62,14 @@ private: /// if true then we are currently entering console text bool enteringText; + + /// splits text into individual strings for battle log + std::vector splitText(const std::string &text); + + /// select line(s) that will be visible in UI + std::vector getVisibleText(); public: - BattleConsole(const Rect & position); + BattleConsole(std::shared_ptr backgroundSource, const Point & objectPos, const Point & imagePos, const Point &size); void showAll(SDL_Surface * to) override; void deactivate() override; @@ -78,27 +89,43 @@ public: /// Hero battle animation class BattleHero : public CIntObject { - void switchToNextPhase(); -public: - bool flip; //false if it's attacking hero, true otherwise + bool defender; + + CFunctionList phaseFinishedCallback; std::shared_ptr animation; std::shared_ptr flagAnimation; - const CGHeroInstance * myHero; //this animation's hero instance - const BattleInterface * myOwner; //battle interface to which this animation is assigned - int phase; //stage of animation - int nextPhase; //stage of animation to be set after current phase is fully displayed - int currentFrame, firstFrame, lastFrame; //frame of animation + const CGHeroInstance * hero; //this animation's hero instance + const BattleInterface & owner; //battle interface to which this animation is assigned + + EHeroAnimType phase; //stage of animation + EHeroAnimType nextPhase; //stage of animation to be set after current phase is fully displayed + + float currentSpeed; + float currentFrame; //frame of animation + float flagCurrentFrame; + + void switchToNextPhase(); - size_t flagAnim; - ui8 animCount; //for flag animation void render(Canvas & canvas); //prints next frame of animation to to - void setPhase(int newPhase); //sets phase of hero animation +public: + const CGHeroInstance * instance(); + + void setPhase(EHeroAnimType newPhase); //sets phase of hero animation + + void collectRenderableObjects(BattleRenderer & renderer); + + float getFrame() const; + void onPhaseFinished(const std::function &); + + void pause(); + void play(); + void hover(bool on) override; void clickLeft(tribool down, bool previousState) override; //call-in void clickRight(tribool down, bool previousState) override; //call-in - BattleHero(const std::string & animationPath, bool filpG, PlayerColor player, const CGHeroInstance * hero, const BattleInterface & owner); + BattleHero(const BattleInterface & owner, const CGHeroInstance * hero, bool defender); }; class HeroInfoWindow : public CWindowObject @@ -193,4 +220,6 @@ public: StackQueue(bool Embedded, BattleInterface & owner); void update(); + + void show(SDL_Surface * to) override; }; diff --git a/client/battle/BattleObstacleController.cpp b/client/battle/BattleObstacleController.cpp index 292ee393b..ba2485d13 100644 --- a/client/battle/BattleObstacleController.cpp +++ b/client/battle/BattleObstacleController.cpp @@ -15,17 +15,22 @@ #include "BattleAnimationClasses.h" #include "BattleStacksController.h" #include "BattleRenderer.h" +#include "CreatureAnimation.h" +#include "../CMusicHandler.h" +#include "../CGameInfo.h" #include "../CPlayerInterface.h" #include "../gui/CAnimation.h" #include "../gui/Canvas.h" +#include "../gui/CGuiHandler.h" #include "../../CCallback.h" #include "../../lib/battle/CObstacleInstance.h" #include "../../lib/ObstacleHandler.h" BattleObstacleController::BattleObstacleController(BattleInterface & owner): - owner(owner) + owner(owner), + timePassed(0.f) { auto obst = owner.curInt->cb->battleGetAllObstacles(); for(auto & elem : obst) @@ -72,10 +77,6 @@ void BattleObstacleController::loadObstacleImage(const CObstacleInstance & oi) void BattleObstacleController::obstaclePlaced(const std::vector> & obstacles) { - assert(obstaclesBeingPlaced.empty()); - for (auto const & oi : obstacles) - obstaclesBeingPlaced.push_back(oi->uniqueID); - for (auto const & oi : obstacles) { auto spellObstacle = dynamic_cast(oi.get()); @@ -83,7 +84,6 @@ void BattleObstacleController::obstaclePlaced(const std::vectorerror("I don't know how to animate appearing obstacle of type %d", (int)oi->obstacleType); - obstaclesBeingPlaced.erase(obstaclesBeingPlaced.begin()); continue; } @@ -92,29 +92,22 @@ void BattleObstacleController::obstaclePlaced(const std::vectorgetImage(0, 0); if(!first) - { - obstaclesBeingPlaced.erase(obstaclesBeingPlaced.begin()); continue; - } - - //TODO: sound - //soundBase::QUIKSAND - //soundBase::LANDMINE //we assume here that effect graphics have the same size as the usual obstacle image // -> if we know how to blit obstacle, let's blit the effect in the same place Point whereTo = getObstaclePosition(first, *oi); - owner.stacksController->addNewAnim(new CPointEffectAnimation(owner, soundBase::invalid, spellObstacle->appearAnimation, whereTo, oi->pos, CPointEffectAnimation::WAIT_FOR_SOUND)); + CCS->soundh->playSound( spellObstacle->appearSound ); + owner.stacksController->addNewAnim(new EffectAnimation(owner, spellObstacle->appearAnimation, whereTo, oi->pos)); //so when multiple obstacles are added, they show up one after another - owner.waitForAnims(); + owner.waitForAnimationCondition(EAnimationEvents::ACTION, false); - obstaclesBeingPlaced.erase(obstaclesBeingPlaced.begin()); loadObstacleImage(*spellObstacle); } } -void BattleObstacleController::showAbsoluteObstacles(Canvas & canvas, const Point & offset) +void BattleObstacleController::showAbsoluteObstacles(Canvas & canvas) { //Blit absolute obstacles for(auto & oi : owner.curInt->cb->battleGetAllObstacles()) @@ -123,7 +116,7 @@ void BattleObstacleController::showAbsoluteObstacles(Canvas & canvas, const Poin { auto img = getObstacleImage(*oi); if(img) - canvas.draw(img, Point(offset.x + oi->getInfo().width, offset.y + oi->getInfo().height)); + canvas.draw(img, Point(oi->getInfo().width, oi->getInfo().height)); } } } @@ -149,31 +142,26 @@ void BattleObstacleController::collectRenderableObjects(BattleRenderer & rendere } } +void BattleObstacleController::update() +{ + timePassed += GH.mainFPSmng->getElapsedMilliseconds() / 1000.f; +} + std::shared_ptr BattleObstacleController::getObstacleImage(const CObstacleInstance & oi) { - int frameIndex = (owner.animCount+1) *25 / owner.getAnimSpeed(); + int framesCount = timePassed * AnimationControls::getObstaclesSpeed(); std::shared_ptr animation; + // obstacle is not loaded yet, don't show anything if (obstacleAnimations.count(oi.uniqueID) == 0) - { - if (boost::range::find(obstaclesBeingPlaced, oi.uniqueID) != obstaclesBeingPlaced.end()) - { - // obstacle is not loaded yet, don't show anything - return nullptr; - } - else - { - assert(0); // how? - loadObstacleImage(oi); - } - } + return nullptr; animation = obstacleAnimations[oi.uniqueID]; assert(animation); if(animation) { - frameIndex %= animation->size(0); + int frameIndex = framesCount % animation->size(0); return animation->getImage(frameIndex, 0); } return nullptr; @@ -183,7 +171,7 @@ Point BattleObstacleController::getObstaclePosition(std::shared_ptr imag { int offset = obstacle.getAnimationYOffset(image->height()); - Rect r = owner.fieldController->hexPositionAbsolute(obstacle.pos); + Rect r = owner.fieldController->hexPositionLocal(obstacle.pos); r.y += 42 - image->height() + offset; return r.topLeft(); diff --git a/client/battle/BattleObstacleController.h b/client/battle/BattleObstacleController.h index 93afa3eeb..688beea57 100644 --- a/client/battle/BattleObstacleController.h +++ b/client/battle/BattleObstacleController.h @@ -29,16 +29,15 @@ class BattleObstacleController { BattleInterface & owner; + /// total time, in seconds, since start of battle. Used for animating obstacles + float timePassed; + /// cached animations of all obstacles in current battle std::map> animationsCache; /// list of all obstacles that are currently being rendered std::map> obstacleAnimations; - /// semi-debug member, contains obstacles that should not yet be visible due to ongoing placement animation - /// used only for sanity checks to ensure that there are no invisible obstacles - std::vector obstaclesBeingPlaced; - void loadObstacleImage(const CObstacleInstance & oi); std::shared_ptr getObstacleImage(const CObstacleInstance & oi); @@ -47,11 +46,14 @@ class BattleObstacleController public: BattleObstacleController(BattleInterface & owner); + /// called every frame + void update(); + /// call-in from network pack, add newly placed obstacles with any required animations void obstaclePlaced(const std::vector> & oi); /// renders all "absolute" obstacles - void showAbsoluteObstacles(Canvas & canvas, const Point & offset); + void showAbsoluteObstacles(Canvas & canvas); /// adds all non-"absolute" visible obstacles for rendering void collectRenderableObjects(BattleRenderer & renderer); diff --git a/client/battle/BattleProjectileController.cpp b/client/battle/BattleProjectileController.cpp index 6932a9a06..571aca45a 100644 --- a/client/battle/BattleProjectileController.cpp +++ b/client/battle/BattleProjectileController.cpp @@ -48,22 +48,21 @@ static double calculateCatapultParabolaY(const Point & from, const Point & dest, void ProjectileMissile::show(Canvas & canvas) { - logAnim->info("Projectile rendering, %d / %d", step, steps); size_t group = reverse ? 1 : 0; auto image = animation->getImage(frameNum, group, true); if(image) { - float progress = float(step) / steps; - Point pos { - CSDL_Ext::lerp(from.x, dest.x, progress) - image->width() / 2, - CSDL_Ext::lerp(from.y, dest.y, progress) - image->height() / 2, + vstd::lerp(from.x, dest.x, progress) - image->width() / 2, + vstd::lerp(from.y, dest.y, progress) - image->height() / 2, }; canvas.draw(image, pos); } - ++step; + + float timePassed = GH.mainFPSmng->getElapsedMilliseconds() / 1000.f; + progress += timePassed * speed; } void ProjectileAnimatedMissile::show(Canvas & canvas) @@ -83,9 +82,7 @@ void ProjectileCatapult::show(Canvas & canvas) if(image) { - float progress = float(step) / steps; - - int posX = CSDL_Ext::lerp(from.x, dest.x, progress); + int posX = vstd::lerp(from.x, dest.x, progress); int posY = calculateCatapultParabolaY(from, dest, posX); Point pos(posX, posY); @@ -93,16 +90,16 @@ void ProjectileCatapult::show(Canvas & canvas) frameNum = (frameNum + 1) % animation->size(0); } - ++step; + + float timePassed = GH.mainFPSmng->getElapsedMilliseconds() / 1000.f; + progress += timePassed * speed; } void ProjectileRay::show(Canvas & canvas) { - float progress = float(step) / steps; - Point curr { - CSDL_Ext::lerp(from.x, dest.x, progress), - CSDL_Ext::lerp(from.y, dest.y, progress), + vstd::lerp(from.x, dest.x, progress), + vstd::lerp(from.y, dest.y, progress), }; Point length = curr - from; @@ -143,7 +140,9 @@ void ProjectileRay::show(Canvas & canvas) canvas.drawLine(Point(x1 + i, y1), Point(x2 + i, y2), beginColor, endColor); } } - ++step; + + float timePassed = GH.mainFPSmng->getElapsedMilliseconds() / 1000.f; + progress += timePassed * speed; } BattleProjectileController::BattleProjectileController(BattleInterface & owner): @@ -232,17 +231,17 @@ void BattleProjectileController::showProjectiles(Canvas & canvas) } vstd::erase_if(projectiles, [&](const std::shared_ptr & projectile){ - return projectile->step > projectile->steps; + return projectile->progress > 1.0f; }); } -bool BattleProjectileController::hasActiveProjectile(const CStack * stack) const +bool BattleProjectileController::hasActiveProjectile(const CStack * stack, bool emittedOnly) const { int stackID = stack ? stack->ID : -1; for(auto const & instance : projectiles) { - if(instance->shooterID == stackID) + if(instance->shooterID == stackID && (instance->playing || !emittedOnly)) { return true; } @@ -250,15 +249,14 @@ bool BattleProjectileController::hasActiveProjectile(const CStack * stack) const return false; } -int BattleProjectileController::computeProjectileFlightTime( Point from, Point dest, double animSpeed) +float BattleProjectileController::computeProjectileFlightTime( Point from, Point dest, double animSpeed) { - double distanceSquared = (dest.x - from.x) * (dest.x - from.x) + (dest.y - from.y) * (dest.y - from.y); - double distance = sqrt(distanceSquared); - int steps = std::round(distance / animSpeed); + float distanceSquared = (dest.x - from.x) * (dest.x - from.x) + (dest.y - from.y) * (dest.y - from.y); + float distance = sqrt(distanceSquared); - if (steps > 0) - return steps; - return 1; + assert(distance > 1.f); + + return animSpeed / std::max( 1.f, distance); } int BattleProjectileController::computeProjectileFrameID( Point from, Point dest, const CStack * stack) @@ -298,12 +296,11 @@ void BattleProjectileController::createCatapultProjectile(const CStack * shooter catapultProjectile->animation = getProjectileImage(shooter); catapultProjectile->frameNum = 0; - catapultProjectile->step = 0; - catapultProjectile->steps = computeProjectileFlightTime(from, dest, AnimationControls::getCatapultSpeed()); + catapultProjectile->progress = 0; + catapultProjectile->speed = computeProjectileFlightTime(from, dest, AnimationControls::getCatapultSpeed()); catapultProjectile->from = from; catapultProjectile->dest = dest; catapultProjectile->shooterID = shooter->ID; - catapultProjectile->step = 0; catapultProjectile->playing = false; projectiles.push_back(std::shared_ptr(catapultProjectile)); @@ -336,11 +333,11 @@ void BattleProjectileController::createProjectile(const CStack * shooter, Point missileProjectile->frameNum = computeProjectileFrameID(from, dest, shooter); } - projectile->steps = computeProjectileFlightTime(from, dest, AnimationControls::getProjectileSpeed()); + projectile->speed = computeProjectileFlightTime(from, dest, AnimationControls::getProjectileSpeed()); projectile->from = from; projectile->dest = dest; projectile->shooterID = shooter->ID; - projectile->step = 0; + projectile->progress = 0; projectile->playing = false; projectiles.push_back(projectile); @@ -364,8 +361,8 @@ void BattleProjectileController::createSpellProjectile(const CStack * shooter, P projectile->from = from; projectile->dest = dest; projectile->shooterID = shooter ? shooter->ID : -1; - projectile->step = 0; - projectile->steps = computeProjectileFlightTime(from, dest, AnimationControls::getSpellEffectSpeed()); + projectile->progress = 0; + projectile->speed = computeProjectileFlightTime(from, dest, AnimationControls::getProjectileSpeed()); projectile->playing = false; projectiles.push_back(std::shared_ptr(projectile)); diff --git a/client/battle/BattleProjectileController.h b/client/battle/BattleProjectileController.h index aea9c290f..8b6be2937 100644 --- a/client/battle/BattleProjectileController.h +++ b/client/battle/BattleProjectileController.h @@ -33,10 +33,10 @@ struct ProjectileBase Point from; // initial position on the screen Point dest; // target position on the screen - int step; // current step counter - int steps; // total number of steps/frames to show - int shooterID; // ID of shooter stack - bool playing; // if set to true, projectile animation is playing, e.g. flying to target + float progress; // current position of projectile on from->dest line + float speed; // how much progress is gained per second + int shooterID; // ID of shooter stack + bool playing; // if set to true, projectile animation is playing, e.g. flying to target }; /// Projectile for most shooters - render pre-selected frame moving in straight line from origin to destination @@ -97,7 +97,7 @@ class BattleProjectileController const CCreature & getShooter(const CStack * stack) const; int computeProjectileFrameID( Point from, Point dest, const CStack * stack); - int computeProjectileFlightTime( Point from, Point dest, double speed); + float computeProjectileFlightTime( Point from, Point dest, double speed); public: BattleProjectileController(BattleInterface & owner); @@ -106,7 +106,7 @@ public: void showProjectiles(Canvas & canvas); /// returns true if stack has projectile that is yet to hit target - bool hasActiveProjectile(const CStack * stack) const; + bool hasActiveProjectile(const CStack * stack, bool emittedOnly) const; /// starts rendering previously created projectile void emitStackProjectile(const CStack * stack); diff --git a/client/battle/BattleRenderer.cpp b/client/battle/BattleRenderer.cpp index 9679d6a40..9e5971749 100644 --- a/client/battle/BattleRenderer.cpp +++ b/client/battle/BattleRenderer.cpp @@ -11,19 +11,24 @@ #include "BattleRenderer.h" #include "BattleInterface.h" +#include "BattleInterfaceClasses.h" #include "BattleEffectsController.h" +#include "BattleWindow.h" #include "BattleSiegeController.h" #include "BattleStacksController.h" #include "BattleObstacleController.h" void BattleRenderer::collectObjects() { - owner.collectRenderableObjects(*this); owner.effectsController->collectRenderableObjects(*this); owner.obstacleController->collectRenderableObjects(*this); owner.stacksController->collectRenderableObjects(*this); if (owner.siegeController) owner.siegeController->collectRenderableObjects(*this); + if (owner.defendingHero) + owner.defendingHero->collectRenderableObjects(*this); + if (owner.attackingHero) + owner.attackingHero->collectRenderableObjects(*this); } void BattleRenderer::sortObjects() diff --git a/client/battle/BattleRenderer.h b/client/battle/BattleRenderer.h index 28dbee1b9..193972493 100644 --- a/client/battle/BattleRenderer.h +++ b/client/battle/BattleRenderer.h @@ -19,11 +19,11 @@ enum class EBattleFieldLayer { OBSTACLES = 0, CORPSES = 0, WALLS = 1, - HEROES = 1, - STACKS = 1, // after corpses, obstacles - BATTLEMENTS = 2, // after stacks - STACK_AMOUNTS = 2, // after stacks, obstacles, corpses - EFFECTS = 3, // after obstacles, battlements + HEROES = 2, + STACKS = 2, // after corpses, obstacles, walls + BATTLEMENTS = 3, // after stacks + STACK_AMOUNTS = 3, // after stacks, obstacles, corpses + EFFECTS = 4, // after obstacles, battlements }; class BattleRenderer diff --git a/client/battle/BattleSiegeController.cpp b/client/battle/BattleSiegeController.cpp index 436cec9f2..0d6b76895 100644 --- a/client/battle/BattleSiegeController.cpp +++ b/client/battle/BattleSiegeController.cpp @@ -106,13 +106,13 @@ std::string BattleSiegeController::getWallPieceImageName(EWallVisual::EWallVisua } } -void BattleSiegeController::showWallPiece(Canvas & canvas, EWallVisual::EWallVisual what, const Point & offset) +void BattleSiegeController::showWallPiece(Canvas & canvas, EWallVisual::EWallVisual what) { auto & ci = town->town->clientInfo; auto const & pos = ci.siegePositions[what]; if ( wallPieceImages[what]) - canvas.draw(wallPieceImages[what], offset + Point(pos.x, pos.y)); + canvas.draw(wallPieceImages[what], Point(pos.x, pos.y)); } std::string BattleSiegeController::getBattleBackgroundName() const @@ -146,7 +146,7 @@ BattleHex BattleSiegeController::getWallPiecePosition(EWallVisual::EWallVisual w BattleHex::HEX_AFTER_ALL, // BOTTOM_TOWER, 182, // BOTTOM_WALL, 130, // WALL_BELLOW_GATE, - 78, // WALL_OVER_GATE, + 62, // WALL_OVER_GATE, 12, // UPPER_WALL, BattleHex::HEX_BEFORE_ALL, // UPPER_TOWER, BattleHex::HEX_BEFORE_ALL, // GATE, // 94 @@ -205,10 +205,10 @@ Point BattleSiegeController::getTurretCreaturePosition( BattleHex position ) con if (posID != 0) { - Point result = owner.pos.topLeft(); - result.x += town->town->clientInfo.siegePositions[posID].x; - result.y += town->town->clientInfo.siegePositions[posID].y; - return result; + return { + town->town->clientInfo.siegePositions[posID].x, + town->town->clientInfo.siegePositions[posID].y + }; } assert(0); @@ -249,13 +249,13 @@ void BattleSiegeController::gateStateChanged(const EGateState state) CCS->soundh->playSound(soundBase::DRAWBRG); } -void BattleSiegeController::showAbsoluteObstacles(Canvas & canvas, const Point & offset) +void BattleSiegeController::showAbsoluteObstacles(Canvas & canvas) { if (getWallPieceExistance(EWallVisual::MOAT)) - showWallPiece(canvas, EWallVisual::MOAT, offset); + showWallPiece(canvas, EWallVisual::MOAT); if (getWallPieceExistance(EWallVisual::MOAT_BANK)) - showWallPiece(canvas, EWallVisual::MOAT_BANK, offset); + showWallPiece(canvas, EWallVisual::MOAT_BANK); } BattleHex BattleSiegeController::getTurretBattleHex(EWallVisual::EWallVisual wallPiece) const @@ -301,11 +301,11 @@ void BattleSiegeController::collectRenderableObjects(BattleRenderer & renderer) owner.stacksController->showStack(canvas, getTurretStack(wallPiece)); }); renderer.insert( EBattleFieldLayer::BATTLEMENTS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){ - showWallPiece(canvas, wallPiece, owner.pos.topLeft()); + showWallPiece(canvas, wallPiece); }); } renderer.insert( EBattleFieldLayer::WALLS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){ - showWallPiece(canvas, wallPiece, owner.pos.topLeft()); + showWallPiece(canvas, wallPiece); }); @@ -327,12 +327,14 @@ bool BattleSiegeController::isAttackableByCatapult(BattleHex hex) const void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca) { + assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false); + if (ca.attacker != -1) { const CStack *stack = owner.curInt->cb->battleGetStackByID(ca.attacker); for (auto attackInfo : ca.attackedParts) { - owner.stacksController->addNewAnim(new CCatapultAnimation(owner, stack, attackInfo.destinationTile, nullptr, attackInfo.damageDealt)); + owner.stacksController->addNewAnim(new CatapultAnimation(owner, stack, attackInfo.destinationTile, nullptr, attackInfo.damageDealt)); } } else @@ -343,11 +345,12 @@ void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca) for (auto attackInfo : ca.attackedParts) positions.push_back(owner.stacksController->getStackPositionAtHex(attackInfo.destinationTile, nullptr) + Point(99, 120)); - - owner.stacksController->addNewAnim(new CPointEffectAnimation(owner, soundBase::WALLHIT, "SGEXPL.DEF", positions)); + CCS->soundh->playSound( "WALLHIT" ); + owner.stacksController->addNewAnim(new EffectAnimation(owner, "SGEXPL.DEF", positions)); } - owner.waitForAnims(); + owner.waitForAnimationCondition(EAnimationEvents::ACTION, false); + owner.setAnimationCondition(EAnimationEvents::HIT, false); for (auto attackInfo : ca.attackedParts) { diff --git a/client/battle/BattleSiegeController.h b/client/battle/BattleSiegeController.h index 62a175ba7..90e304c46 100644 --- a/client/battle/BattleSiegeController.h +++ b/client/battle/BattleSiegeController.h @@ -84,7 +84,7 @@ class BattleSiegeController /// returns true if chosen wall piece should be present in current battle bool getWallPieceExistance(EWallVisual::EWallVisual what) const; - void showWallPiece(Canvas & canvas, EWallVisual::EWallVisual what, const Point & offset); + void showWallPiece(Canvas & canvas, EWallVisual::EWallVisual what); BattleHex getTurretBattleHex(EWallVisual::EWallVisual wallPiece) const; const CStack * getTurretStack(EWallVisual::EWallVisual wallPiece) const; @@ -97,7 +97,7 @@ public: void stackIsCatapulting(const CatapultAttack & ca); /// call-ins from other battle controllers - void showAbsoluteObstacles(Canvas & canvas, const Point & offset); + void showAbsoluteObstacles(Canvas & canvas); void collectRenderableObjects(BattleRenderer & renderer); /// queries from other battle controllers diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index 0b3b15ff3..811726c5f 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -13,11 +13,12 @@ #include "BattleSiegeController.h" #include "BattleInterfaceClasses.h" #include "BattleInterface.h" +#include "BattleActionsController.h" #include "BattleAnimationClasses.h" #include "BattleFieldController.h" #include "BattleEffectsController.h" #include "BattleProjectileController.h" -#include "BattleControlPanel.h" +#include "BattleWindow.h" #include "BattleRenderer.h" #include "CreatureAnimation.h" @@ -27,6 +28,7 @@ #include "../gui/CAnimation.h" #include "../gui/CGuiHandler.h" #include "../gui/Canvas.h" +#include "../../lib/spells/ISpellMechanics.h" #include "../../CCallback.h" #include "../../lib/battle/BattleHex.h" @@ -40,20 +42,26 @@ static void onAnimationFinished(const CStack *stack, std::weak_ptrisFrozen() && animation->getType() == ECreatureAnimType::FROZEN) + animation->setType(ECreatureAnimType::HOLDING); + if (animation->isIdle()) { const CCreature *creature = stack->getCreature(); - if (animation->framesInGroup(CCreatureAnim::MOUSEON) > 0) + if (stack->isFrozen()) + animation->setType(ECreatureAnimType::FROZEN); + else + if (animation->framesInGroup(ECreatureAnimType::MOUSEON) > 0) { if (CRandomGenerator::getDefault().nextDouble(99.0) < creature->animation.timeBetweenFidgets *10) - animation->playOnce(CCreatureAnim::MOUSEON); + animation->playOnce(ECreatureAnimType::MOUSEON); else - animation->setType(CCreatureAnim::HOLDING); + animation->setType(ECreatureAnimType::HOLDING); } else { - animation->setType(CCreatureAnim::HOLDING); + animation->setType(ECreatureAnimType::HOLDING); } } // always reset callback @@ -63,7 +71,6 @@ static void onAnimationFinished(const CStack *stack, std::weak_ptradjustPalette(&shifterNormal); - amountPositive->adjustPalette(&shifterPositive); - amountNegative->adjustPalette(&shifterNegative); - amountEffNeutral->adjustPalette(&shifterNeutral); + amountNormal->adjustPalette(shifterNormal); + amountPositive->adjustPalette(shifterPositive); + amountNegative->adjustPalette(shifterNegative); + amountEffNeutral->adjustPalette(shifterNeutral); + + //Restore border color {255, 231, 132, 255} to its original state + amountNormal->resetPalette(26); + amountPositive->resetPalette(26); + amountNegative->resetPalette(26); + amountEffNeutral->resetPalette(26); std::vector stacks = owner.curInt->cb->battleGetAllStacks(true); for(const CStack * s : stacks) { - stackAdded(s); + stackAdded(s, true); } } @@ -98,7 +111,7 @@ BattleHex BattleStacksController::getStackCurrentPosition(const CStack * stack) if ( !stackAnimation.at(stack->ID)->isMoving()) return stack->getPosition(); - if (stack->hasBonusOfType(Bonus::FLYING)) + if (stack->hasBonusOfType(Bonus::FLYING) && stackAnimation.at(stack->ID)->getType() == ECreatureAnimType::MOVING ) return BattleHex::HEX_AFTER_ALL; for (auto & anim : currentAnimations) @@ -107,10 +120,10 @@ BattleHex BattleStacksController::getStackCurrentPosition(const CStack * stack) // stack position will be updated only *after* movement is finished // before this - stack is always at its initial position. Thus we need to find // its current position. Which can be found only in this class - if (CStackMoveAnimation *move = dynamic_cast(anim)) + if (StackMoveAnimation *move = dynamic_cast(anim)) { if (move->stack == stack) - return move->currentHex; + return std::max(move->prevHex, move->nextHex); } } return stack->getPosition(); @@ -126,7 +139,7 @@ void BattleStacksController::collectRenderableObjects(BattleRenderer & renderer) continue; //FIXME: hack to ignore ghost stacks - if ((stackAnimation[stack->ID]->getType() == CCreatureAnim::DEAD || stackAnimation[stack->ID]->getType() == CCreatureAnim::HOLDING) && stack->isGhost()) + if ((stackAnimation[stack->ID]->getType() == ECreatureAnimType::DEAD || stackAnimation[stack->ID]->getType() == ECreatureAnimType::HOLDING) && stack->isGhost()) continue; auto layer = stackAnimation[stack->ID]->isDead() ? EBattleFieldLayer::CORPSES : EBattleFieldLayer::STACKS; @@ -147,6 +160,11 @@ void BattleStacksController::collectRenderableObjects(BattleRenderer & renderer) void BattleStacksController::stackReset(const CStack * stack) { + assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false); + + //reset orientation? + //stackFacingRight[stack->ID] = stack->side == BattleSide::ATTACKER; + auto iter = stackAnimation.find(stack->ID); if(iter == stackAnimation.end()) @@ -158,22 +176,18 @@ void BattleStacksController::stackReset(const CStack * stack) auto animation = iter->second; if(stack->alive() && animation->isDeadOrDying()) - animation->setType(CCreatureAnim::HOLDING); - - static const ColorShifterMultiplyAndAdd shifterClone ({255, 255, 0, 255}, {0, 0, 255, 0}); - - if (stack->isClone()) { - animation->shiftColor(&shifterClone); + owner.executeOnAnimationCondition(EAnimationEvents::HIT, true, [=]() + { + addNewAnim(new ResurrectionAnimation(owner, stack)); + }); } - - //TODO: handle more cases } -void BattleStacksController::stackAdded(const CStack * stack) +void BattleStacksController::stackAdded(const CStack * stack, bool instant) { // Tower shooters have only their upper half visible - static const int turretCreatureAnimationHeight = 235; + static const int turretCreatureAnimationHeight = 225; stackFacingRight[stack->ID] = stack->side == BattleSide::ATTACKER; // must be set before getting stack position @@ -199,7 +213,21 @@ void BattleStacksController::stackAdded(const CStack * stack) stackAnimation[stack->ID]->pos.x = coords.x; stackAnimation[stack->ID]->pos.y = coords.y; stackAnimation[stack->ID]->pos.w = stackAnimation[stack->ID]->getWidth(); - stackAnimation[stack->ID]->setType(CCreatureAnim::HOLDING); + stackAnimation[stack->ID]->setType(ECreatureAnimType::HOLDING); + + if (!instant) + { + // immediately make stack transparent, giving correct shifter time to start + auto shifterFade = ColorFilter::genAlphaShifter(0); + setStackColorFilter(shifterFade, stack, nullptr, true); + + owner.executeOnAnimationCondition(EAnimationEvents::HIT, true, [=]() + { + addNewAnim(new ColorTransformAnimation(owner, stack, "summonFadeIn", nullptr)); + if (stack->isClone()) + addNewAnim(new ColorTransformAnimation(owner, stack, "cloning", SpellID(SpellID::CLONE).toSpell() )); + }); + } } void BattleStacksController::setActiveStack(const CStack *stack) @@ -212,31 +240,7 @@ void BattleStacksController::setActiveStack(const CStack *stack) if (activeStack) // update UI stackAnimation[activeStack->ID]->setBorderColor(AnimationControls::getGoldBorder()); - owner.controlPanel->blockUI(activeStack == nullptr); -} - -void BattleStacksController::setHoveredStack(const CStack *stack) -{ - if ( stack == mouseHoveredStack ) - return; - - if (mouseHoveredStack) - stackAnimation[mouseHoveredStack->ID]->setBorderColor(AnimationControls::getNoBorder()); - - // stack must be alive and not active (which uses gold border instead) - if (stack && stack->alive() && stack != activeStack) - { - mouseHoveredStack = stack; - - if (mouseHoveredStack) - { - stackAnimation[mouseHoveredStack->ID]->setBorderColor(AnimationControls::getBlueBorder()); - if (stackAnimation[mouseHoveredStack->ID]->framesInGroup(CCreatureAnim::MOUSEON) > 0) - stackAnimation[mouseHoveredStack->ID]->playOnce(CCreatureAnim::MOUSEON); - } - } - else - mouseHoveredStack = nullptr; + owner.windowObject->blockUI(activeStack == nullptr); } bool BattleStacksController::stackNeedsAmountBox(const CStack * stack) const @@ -249,39 +253,25 @@ bool BattleStacksController::stackNeedsAmountBox(const CStack * stack) const currentActionTarget = target.at(0).hexValue; } - if(stack->hasBonusOfType(Bonus::SIEGE_WEAPON) && stack->getCount() == 1) //do not show box for singular war machines, stacked war machines with box shown are supported as extension feature + //do not show box for singular war machines, stacked war machines with box shown are supported as extension feature + if(stack->hasBonusOfType(Bonus::SIEGE_WEAPON) && stack->getCount() == 1) return false; - if (!owner.battleActionsStarted) // do not perform any further checks since they are related to actions that will only occur after intro music - return true; - if(!stack->alive()) return false; - if(stack->getCount() == 0) //hide box when target is going to die anyway - do not display "0 creatures" + //hide box when target is going to die anyway - do not display "0 creatures" + if(stack->getCount() == 0) return false; - for(auto anim : currentAnimations) //no matter what other conditions below are, hide box when creature is playing hit animation + // if stack has any ongoing animation - hide the box + for(auto anim : currentAnimations) { - auto hitAnimation = dynamic_cast(anim); - if(hitAnimation && (hitAnimation->stack->ID == stack->ID)) + auto stackAnimation = dynamic_cast(anim); + if(stackAnimation && (stackAnimation->stack->ID == stack->ID)) return false; } - if(owner.curInt->curAction) - { - if(owner.curInt->curAction->stackNumber == stack->ID) //stack is currently taking action (is not a target of another creature's action etc) - { - if(owner.curInt->curAction->actionType == EActionType::WALK || owner.curInt->curAction->actionType == EActionType::SHOOT) //hide when stack walks or shoots - return false; - - else if(owner.curInt->curAction->actionType == EActionType::WALK_AND_ATTACK && currentActionTarget != stack->getPosition()) //when attacking, hide until walk phase finished - return false; - } - - if(owner.curInt->curAction->actionType == EActionType::SHOOT && currentActionTarget == stack->getPosition()) //hide if we are ranged attack target - return false; - } return true; } @@ -332,46 +322,65 @@ void BattleStacksController::showStackAmountBox(Canvas & canvas, const CStack * void BattleStacksController::showStack(Canvas & canvas, const CStack * stack) { - stackAnimation[stack->ID]->nextFrame(canvas, facingRight(stack)); // do actual blit + ColorFilter fullFilter = ColorFilter::genEmptyShifter(); + for (auto const & filter : stackFilterEffects) + { + if (filter.target == stack) + fullFilter = ColorFilter::genCombined(fullFilter, filter.effect); + } + + bool stackHasProjectile = owner.projectilesController->hasActiveProjectile(stack, true); + + if (stackHasProjectile) + stackAnimation[stack->ID]->pause(); + else + stackAnimation[stack->ID]->play(); + + stackAnimation[stack->ID]->nextFrame(canvas, fullFilter, facingRight(stack)); // do actual blit stackAnimation[stack->ID]->incrementFrame(float(GH.mainFPSmng->getElapsedMilliseconds()) / 1000); } +void BattleStacksController::update() +{ + updateHoveredStacks(); + updateBattleAnimations(); +} + +void BattleStacksController::initializeBattleAnimations() +{ + auto copiedVector = currentAnimations; + for (auto & elem : copiedVector) + if (elem && !elem->isInitialized()) + elem->tryInitialize(); +} + +void BattleStacksController::stepFrameBattleAnimations() +{ + // operate on copy - to prevent potential iterator invalidation due to push_back's + // FIXME? : remove remaining calls to addNewAnim from BattleAnimation::nextFrame (only Catapult explosion at the time of writing) + auto copiedVector = currentAnimations; + for (auto & elem : copiedVector) + if (elem && elem->isInitialized()) + elem->nextFrame(); +} + void BattleStacksController::updateBattleAnimations() { - for (auto & elem : currentAnimations) - { - if (!elem) - continue; - - if (elem->isInitialized()) - elem->nextFrame(); - else - elem->tryInitialize(); - } - bool hadAnimations = !currentAnimations.empty(); + initializeBattleAnimations(); + stepFrameBattleAnimations(); vstd::erase(currentAnimations, nullptr); if (hadAnimations && currentAnimations.empty()) - { - //anims ended - owner.controlPanel->blockUI(activeStack == nullptr); - owner.animsAreDisplayed.setn(false); - } + owner.setAnimationCondition(EAnimationEvents::ACTION, false); + + initializeBattleAnimations(); } -void BattleStacksController::addNewAnim(CBattleAnimation *anim) +void BattleStacksController::addNewAnim(BattleAnimation *anim) { currentAnimations.push_back(anim); - owner.animsAreDisplayed.setn(true); -} - -void BattleStacksController::stackActivated(const CStack *stack) //TODO: check it all before game state is changed due to abilities -{ - stackToActivate = stack; - owner.waitForAnims(); - if (stackToActivate) //during waiting stack may have gotten activated through show - owner.activateStack(); + owner.setAnimationCondition(EAnimationEvents::ACTION, true); } void BattleStacksController::stackRemoved(uint32_t stackID) @@ -389,44 +398,275 @@ void BattleStacksController::stackRemoved(uint32_t stackID) void BattleStacksController::stacksAreAttacked(std::vector attackedInfos) { + owner.executeOnAnimationCondition(EAnimationEvents::HIT, true, [=](){ + // remove any potentially erased petrification effect + removeExpiredColorFilters(); + }); + for(auto & attackedInfo : attackedInfos) { - //if (!attackedInfo.cloneKilled) //FIXME: play dead animation for cloned creature before it vanishes - addNewAnim(new CDefenceAnimation(attackedInfo, owner)); + if (!attackedInfo.attacker) + continue; - if(attackedInfo.rebirth) + // In H3, attacked stack will not reverse on ranged attack + if (attackedInfo.indirectAttack) + continue; + + // Another type of indirect attack - dragon breath + if (!CStack::isMeleeAttackPossible(attackedInfo.attacker, attackedInfo.defender)) + continue; + + // defender need to face in direction opposited to out attacker + bool needsReverse = shouldAttackFacingRight(attackedInfo.attacker, attackedInfo.defender) == facingRight(attackedInfo.defender); + + // FIXME: this check is better, however not usable since stacksAreAttacked is called after net pack is applyed - petrification is already removed + // if (needsReverse && !attackedInfo.defender->isFrozen()) + if (needsReverse && stackAnimation[attackedInfo.defender->ID]->getType() != ECreatureAnimType::FROZEN) { - owner.effectsController->displayEffect(EBattleEffect::RESURRECT, soundBase::RESURECT, attackedInfo.defender->getPosition()); //TODO: play reverse death animation + owner.executeOnAnimationCondition(EAnimationEvents::MOVEMENT, true, [=]() + { + addNewAnim(new ReverseAnimation(owner, attackedInfo.defender, attackedInfo.defender->getPosition())); + }); } } - owner.waitForAnims(); + + for(auto & attackedInfo : attackedInfos) + { + bool useDeathAnim = attackedInfo.killed; + bool useDefenceAnim = attackedInfo.defender->defendingAnim && !attackedInfo.indirectAttack && !attackedInfo.killed; + + EAnimationEvents usedEvent = useDefenceAnim ? EAnimationEvents::ATTACK : EAnimationEvents::HIT; + + owner.executeOnAnimationCondition(usedEvent, true, [=]() + { + if (useDeathAnim) + addNewAnim(new DeathAnimation(owner, attackedInfo.defender, attackedInfo.indirectAttack)); + else if(useDefenceAnim) + addNewAnim(new DefenceAnimation(owner, attackedInfo.defender)); + else + addNewAnim(new HittedAnimation(owner, attackedInfo.defender)); + + if (attackedInfo.fireShield) + owner.effectsController->displayEffect(EBattleEffect::FIRE_SHIELD, "FIRESHIE", attackedInfo.attacker->getPosition()); + + if (attackedInfo.spellEffect != SpellID::NONE) + { + auto spell = attackedInfo.spellEffect.toSpell(); + if (!spell->getCastSound().empty()) + CCS->soundh->playSound(spell->getCastSound()); + + + owner.displaySpellEffect(spell, attackedInfo.defender->getPosition()); + } + }); + } for (auto & attackedInfo : attackedInfos) { if (attackedInfo.rebirth) - stackAnimation[attackedInfo.defender->ID]->setType(CCreatureAnim::HOLDING); - if (attackedInfo.cloneKilled) - stackRemoved(attackedInfo.defender->ID); + { + owner.executeOnAnimationCondition(EAnimationEvents::AFTER_HIT, true, [=](){ + owner.effectsController->displayEffect(EBattleEffect::RESURRECT, "RESURECT", attackedInfo.defender->getPosition()); + addNewAnim(new ResurrectionAnimation(owner, attackedInfo.defender)); + }); + } + + if (attackedInfo.killed && attackedInfo.defender->summoned) + { + owner.executeOnAnimationCondition(EAnimationEvents::AFTER_HIT, true, [=](){ + addNewAnim(new ColorTransformAnimation(owner, attackedInfo.defender, "summonFadeOut", nullptr)); + stackRemoved(attackedInfo.defender->ID); + }); + } } + executeAttackAnimations(); +} + +void BattleStacksController::stackTeleported(const CStack *stack, std::vector destHex, int distance) +{ + assert(destHex.size() > 0); + assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false); + + owner.executeOnAnimationCondition(EAnimationEvents::HIT, true, [=](){ + addNewAnim( new ColorTransformAnimation(owner, stack, "teleportFadeOut", nullptr) ); + }); + + owner.executeOnAnimationCondition(EAnimationEvents::AFTER_HIT, true, [=](){ + stackAnimation[stack->ID]->pos.moveTo(getStackPositionAtHex(destHex.back(), stack)); + addNewAnim( new ColorTransformAnimation(owner, stack, "teleportFadeIn", nullptr) ); + }); + + // animations will be executed by spell } void BattleStacksController::stackMoved(const CStack *stack, std::vector destHex, int distance) { - addNewAnim(new CMovementAnimation(owner, stack, destHex, distance)); - owner.waitForAnims(); + assert(destHex.size() > 0); + assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false); + + bool stackTeleports = stack->hasBonus(Selector::typeSubtype(Bonus::FLYING, 1)); + owner.setAnimationCondition(EAnimationEvents::MOVEMENT, true); + + auto enqueMoveEnd = [&](){ + addNewAnim(new MovementEndAnimation(owner, stack, destHex.back())); + owner.executeOnAnimationCondition(EAnimationEvents::ACTION, false, [&](){ + owner.setAnimationCondition(EAnimationEvents::MOVEMENT, false); + }); + }; + + auto enqueMove = [&](){ + if (!stackTeleports) + { + addNewAnim(new MovementAnimation(owner, stack, destHex, distance)); + owner.executeOnAnimationCondition(EAnimationEvents::ACTION, false, enqueMoveEnd); + } + else + enqueMoveEnd(); + }; + + auto enqueMoveStart = [&](){ + addNewAnim(new MovementStartAnimation(owner, stack)); + owner.executeOnAnimationCondition(EAnimationEvents::ACTION, false, enqueMove); + }; + + if(shouldRotate(stack, stack->getPosition(), destHex[0])) + { + addNewAnim(new ReverseAnimation(owner, stack, stack->getPosition())); + owner.executeOnAnimationCondition(EAnimationEvents::ACTION, false, enqueMoveStart); + } + else + enqueMoveStart(); + + owner.waitForAnimationCondition(EAnimationEvents::MOVEMENT, false); } -void BattleStacksController::stackAttacking( const CStack *attacker, BattleHex dest, const CStack *attacked, bool shooting ) +bool BattleStacksController::shouldAttackFacingRight(const CStack * attacker, const CStack * defender) { - if (shooting) + bool mustReverse = owner.curInt->cb->isToReverse( + attacker->getPosition(), + attacker, + defender); + + if (attacker->side == BattleSide::ATTACKER) + return !mustReverse; + else + return mustReverse; +} + +void BattleStacksController::stackAttacking( const StackAttackInfo & info ) +{ + assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false); + + auto attacker = info.attacker; + auto defender = info.defender; + auto tile = info.tile; + auto spellEffect = info.spellEffect; + auto multiAttack = !info.secondaryDefender.empty(); + bool needsReverse = false; + + if (info.indirectAttack) { - addNewAnim(new CShootingAnimation(owner, attacker, dest, attacked)); + needsReverse = shouldRotate(attacker, attacker->position, info.tile); } else { - addNewAnim(new CMeleeAttackAnimation(owner, attacker, dest, attacked)); + needsReverse = shouldAttackFacingRight(attacker, defender) != facingRight(attacker); } - //waitForAnims(); + + if (needsReverse) + { + owner.executeOnAnimationCondition(EAnimationEvents::MOVEMENT, true, [=]() + { + addNewAnim(new ReverseAnimation(owner, attacker, attacker->getPosition())); + }); + } + + if(info.lucky) + { + owner.executeOnAnimationCondition(EAnimationEvents::BEFORE_HIT, true, [=]() { + owner.appendBattleLog(info.attacker->formatGeneralMessage(-45)); + owner.effectsController->displayEffect(EBattleEffect::GOOD_LUCK, "GOODLUCK", attacker->getPosition()); + }); + } + + if(info.unlucky) + { + owner.executeOnAnimationCondition(EAnimationEvents::BEFORE_HIT, true, [=]() { + owner.appendBattleLog(info.attacker->formatGeneralMessage(-44)); + owner.effectsController->displayEffect(EBattleEffect::BAD_LUCK, "BADLUCK", attacker->getPosition()); + }); + } + + if(info.deathBlow) + { + owner.executeOnAnimationCondition(EAnimationEvents::BEFORE_HIT, true, [=]() { + owner.appendBattleLog(info.attacker->formatGeneralMessage(365)); + owner.effectsController->displayEffect(EBattleEffect::DEATH_BLOW, "DEATHBLO", defender->getPosition()); + }); + + for(auto elem : info.secondaryDefender) + { + owner.executeOnAnimationCondition(EAnimationEvents::BEFORE_HIT, true, [=]() { + owner.effectsController->displayEffect(EBattleEffect::DEATH_BLOW, elem->getPosition()); + }); + } + } + + owner.executeOnAnimationCondition(EAnimationEvents::ATTACK, true, [=]() + { + if (info.indirectAttack) + { + addNewAnim(new ShootingAnimation(owner, attacker, tile, defender)); + } + else + { + addNewAnim(new MeleeAttackAnimation(owner, attacker, tile, defender, multiAttack)); + } + }); + + if (info.spellEffect != SpellID::NONE) + { + owner.executeOnAnimationCondition(EAnimationEvents::HIT, true, [=]() + { + owner.displaySpellHit(spellEffect.toSpell(), tile); + }); + } + + if (info.lifeDrain) + { + owner.executeOnAnimationCondition(EAnimationEvents::AFTER_HIT, true, [=]() + { + owner.effectsController->displayEffect(EBattleEffect::DRAIN_LIFE, "DRAINLIF", attacker->getPosition()); + }); + } + + //return, animation playback will be handled by stacksAreAttacked +} + +void BattleStacksController::executeAttackAnimations() +{ + owner.setAnimationCondition(EAnimationEvents::MOVEMENT, true); + owner.waitForAnimationCondition(EAnimationEvents::ACTION, false); + owner.setAnimationCondition(EAnimationEvents::MOVEMENT, false); + + owner.setAnimationCondition(EAnimationEvents::BEFORE_HIT, true); + owner.waitForAnimationCondition(EAnimationEvents::ACTION, false); + owner.setAnimationCondition(EAnimationEvents::BEFORE_HIT, false); + + owner.setAnimationCondition(EAnimationEvents::ATTACK, true); + owner.waitForAnimationCondition(EAnimationEvents::ACTION, false); + owner.setAnimationCondition(EAnimationEvents::ATTACK, false); + + // Note that HIT event can also be emitted by attack animation + owner.setAnimationCondition(EAnimationEvents::HIT, true); + owner.waitForAnimationCondition(EAnimationEvents::ACTION, false); + owner.setAnimationCondition(EAnimationEvents::HIT, false); + + owner.setAnimationCondition(EAnimationEvents::AFTER_HIT, true); + owner.waitForAnimationCondition(EAnimationEvents::ACTION, false); + owner.setAnimationCondition(EAnimationEvents::AFTER_HIT, false); + + owner.waitForAnimationCondition(EAnimationEvents::ACTION, false); } bool BattleStacksController::shouldRotate(const CStack * stack, const BattleHex & oldPos, const BattleHex & nextHex) const @@ -442,11 +682,11 @@ bool BattleStacksController::shouldRotate(const CStack * stack, const BattleHex return false; } - void BattleStacksController::endAction(const BattleAction* action) { + assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false); + //check if we should reverse stacks - //for some strange reason, it's not enough TStacks stacks = owner.curInt->cb->battleGetStacks(CBattleCallback::MINE_AND_ENEMY); for (const CStack *s : stacks) @@ -455,29 +695,32 @@ void BattleStacksController::endAction(const BattleAction* action) if (s && facingRight(s) != shouldFaceRight && s->alive() && stackAnimation[s->ID]->isIdle()) { - addNewAnim(new CReverseAnimation(owner, s, s->getPosition(), false)); + addNewAnim(new ReverseAnimation(owner, s, s->getPosition())); } } + owner.waitForAnimationCondition(EAnimationEvents::ACTION, false); + + //Ensure that all animation flags were reset + assert(owner.getAnimationCondition(EAnimationEvents::OPENING) == false); + assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false); + assert(owner.getAnimationCondition(EAnimationEvents::MOVEMENT) == false); + assert(owner.getAnimationCondition(EAnimationEvents::ATTACK) == false); + assert(owner.getAnimationCondition(EAnimationEvents::HIT) == false); + + owner.windowObject->blockUI(activeStack == nullptr); + removeExpiredColorFilters(); } void BattleStacksController::startAction(const BattleAction* action) { - const CStack *stack = owner.curInt->cb->battleGetStackByID(action->stackNumber); - setHoveredStack(nullptr); + removeExpiredColorFilters(); +} - auto actionTarget = action->getTarget(owner.curInt->cb.get()); - - if(action->actionType == EActionType::WALK - || (action->actionType == EActionType::WALK_AND_ATTACK && actionTarget.at(0).hexValue != stack->getPosition())) - { - assert(stack); - owner.moveStarted = true; - if (stackAnimation[action->stackNumber]->framesInGroup(CCreatureAnim::MOVE_START)) - addNewAnim(new CMovementStartAnimation(owner, stack)); - - //if(shouldRotate(stack, stack->getPosition(), actionTarget.at(0).hexValue)) - // addNewAnim(new CReverseAnimation(owner, stack, stack->getPosition(), true)); - } +void BattleStacksController::stackActivated(const CStack *stack) +{ + stackToActivate = stack; + owner.waitForAnimationCondition(EAnimationEvents::ACTION, false); + owner.activateStack(); } void BattleStacksController::activateStack() @@ -584,6 +827,115 @@ Point BattleStacksController::getStackPositionAtHex(BattleHex hexNum, const CSta } } //returning - return ret + owner.pos.topLeft(); - + return ret; +} + +void BattleStacksController::setStackColorFilter(const ColorFilter & effect, const CStack * target, const CSpell * source, bool persistent) +{ + for (auto & filter : stackFilterEffects) + { + if (filter.target == target && filter.source == source) + { + filter.effect = effect; + filter.persistent = persistent; + return; + } + } + stackFilterEffects.push_back({ effect, target, source, persistent }); +} + +void BattleStacksController::removeExpiredColorFilters() +{ + vstd::erase_if(stackFilterEffects, [&](const BattleStackFilterEffect & filter) + { + if (!filter.persistent) + { + if (filter.source && !filter.target->hasBonus(Selector::source(Bonus::SPELL_EFFECT, filter.source->id), Selector::all)) + return true; + if (filter.effect == ColorFilter::genEmptyShifter()) + return true; + } + return false; + }); +} + +void BattleStacksController::updateHoveredStacks() +{ + auto newStacks = selectHoveredStacks(); + + for (auto const * stack : mouseHoveredStacks) + { + if (vstd::contains(newStacks, stack)) + continue; + + if (stack == activeStack) + stackAnimation[stack->ID]->setBorderColor(AnimationControls::getGoldBorder()); + else + stackAnimation[stack->ID]->setBorderColor(AnimationControls::getNoBorder()); + } + + for (auto const * stack : newStacks) + { + if (vstd::contains(mouseHoveredStacks, stack)) + continue; + + stackAnimation[stack->ID]->setBorderColor(AnimationControls::getBlueBorder()); + if (stackAnimation[stack->ID]->framesInGroup(ECreatureAnimType::MOUSEON) > 0 && stack->alive() && !stack->isFrozen()) + stackAnimation[stack->ID]->playOnce(ECreatureAnimType::MOUSEON); + + } + + mouseHoveredStacks = newStacks; +} + +std::vector BattleStacksController::selectHoveredStacks() +{ + // only allow during our turn - do not try to highlight creatures while they are in the middle of actions + if (!activeStack) + return {}; + + if(owner.getAnimationCondition(EAnimationEvents::ACTION) == true) + return {}; + + auto hoveredHex = owner.fieldController->getHoveredHex(); + + if (!hoveredHex.isValid()) + return {}; + + const spells::Caster *caster = nullptr; + const CSpell *spell = nullptr; + + spells::Mode mode = spells::Mode::HERO; + + if(owner.actionsController->spellcastingModeActive())//hero casts spell + { + spell = owner.actionsController->selectedSpell().toSpell(); + caster = owner.getActiveHero(); + } + else if(owner.stacksController->activeStackSpellToCast() != SpellID::NONE)//stack casts spell + { + spell = SpellID(owner.stacksController->activeStackSpellToCast()).toSpell(); + caster = owner.stacksController->getActiveStack(); + mode = spells::Mode::CREATURE_ACTIVE; + } + + if(caster && spell) //when casting spell + { + spells::Target target; + target.emplace_back(hoveredHex); + + spells::BattleCast event(owner.curInt->cb.get(), caster, mode, spell); + auto mechanics = spell->battleMechanics(&event); + return mechanics->getAffectedStacks(target); + } + + if(hoveredHex.isValid()) + { + const CStack * const stack = owner.curInt->cb->battleGetStackByPos(hoveredHex, true); + + if (stack) + return {stack}; + } + + return {}; } diff --git a/client/battle/BattleStacksController.h b/client/battle/BattleStacksController.h index 9c14c227b..0e3113c25 100644 --- a/client/battle/BattleStacksController.h +++ b/client/battle/BattleStacksController.h @@ -10,26 +10,38 @@ #pragma once #include "../gui/Geometries.h" +#include "../gui/ColorFilter.h" VCMI_LIB_NAMESPACE_BEGIN struct BattleHex; class BattleAction; class CStack; +class CSpell; class SpellID; VCMI_LIB_NAMESPACE_END struct StackAttackedInfo; +struct StackAttackInfo; +class ColorFilter; class Canvas; class BattleInterface; -class CBattleAnimation; +class BattleAnimation; class CreatureAnimation; -class CBattleAnimation; +class BattleAnimation; class BattleRenderer; class IImage; +struct BattleStackFilterEffect +{ + ColorFilter effect; + const CStack * target; + const CSpell * source; + bool persistent; +}; + /// Class responsible for handling stacks in battle /// Handles ordering of stacks animation /// As well as rendering of stacks, their amount boxes @@ -44,7 +56,10 @@ class BattleStacksController std::shared_ptr amountEffNeutral; /// currently displayed animations - std::vector currentAnimations; + std::vector currentAnimations; + + /// currently active color effects on stacks, in order of their addition (which corresponds to their apply order) + std::vector stackFilterEffects; /// animations of creatures from fighting armies (order by BattleInfo's stacks' ID) std::map> stackAnimation; @@ -52,11 +67,11 @@ class BattleStacksController /// //TODO: move it to battle callback std::map stackFacingRight; - /// number of active stack; nullptr - no one + /// currently active stack; nullptr - no one const CStack *activeStack; - /// stack below mouse pointer, used for border animation - const CStack *mouseHoveredStack; + /// stacks below mouse pointer (multiple stacks possible while spellcasting), used for border animation + std::vector mouseHoveredStacks; ///when animation is playing, we should wait till the end to make the next stack active; nullptr of none const CStack *stackToActivate; @@ -77,6 +92,19 @@ class BattleStacksController std::shared_ptr getStackAmountBox(const CStack * stack); + void executeAttackAnimations(); + void removeExpiredColorFilters(); + + void initializeBattleAnimations(); + void stepFrameBattleAnimations(); + + void updateBattleAnimations(); + void updateHoveredStacks(); + + std::vector selectHoveredStacks(); + + bool shouldAttackFacingRight(const CStack * attacker, const CStack * defender); + public: BattleStacksController(BattleInterface & owner); @@ -84,12 +112,13 @@ public: bool facingRight(const CStack * stack) const; void stackReset(const CStack * stack); - void stackAdded(const CStack * stack); //new stack appeared on battlefield + void stackAdded(const CStack * stack, bool instant); //new stack appeared on battlefield void stackRemoved(uint32_t stackID); //stack disappeared from batlefiled void stackActivated(const CStack *stack); //active stack has been changed void stackMoved(const CStack *stack, std::vector destHex, int distance); //stack with id number moved to destHex + void stackTeleported(const CStack *stack, std::vector destHex, int distance); //stack with id number moved to destHex void stacksAreAttacked(std::vector attackedInfos); //called when a certain amount of stacks has been attacked - void stackAttacking(const CStack *attacker, BattleHex dest, const CStack *attacked, bool shooting); //called when stack with id ID is attacking something on hex dest + void stackAttacking(const StackAttackInfo & info); //called when stack with id ID is attacking something on hex dest void startAction(const BattleAction* action); void endAction(const BattleAction* action); @@ -100,7 +129,6 @@ public: void activateStack(); //sets activeStack to stackToActivate etc. //FIXME: No, it's not clear at all void setActiveStack(const CStack *stack); - void setHoveredStack(const CStack *stack); void setSelectedStack(const CStack *stack); void showAliveStack(Canvas & canvas, const CStack * stack); @@ -108,14 +136,19 @@ public: void collectRenderableObjects(BattleRenderer & renderer); - void addNewAnim(CBattleAnimation *anim); //adds new anim to pendingAnims - void updateBattleAnimations(); + /// Adds new color filter effect targeting stack + /// Effect will last as long as stack is affected by specified spell (unless effect is persistent) + /// If effect from same (target, source) already exists, it will be updated + void setStackColorFilter(const ColorFilter & effect, const CStack * target, const CSpell *source, bool persistent); + void addNewAnim(BattleAnimation *anim); //adds new anim to pendingAnims const CStack* getActiveStack() const; const CStack* getSelectedStack() const; + void update(); + /// returns position of animation needed to place stack in specific hex Point getStackPositionAtHex(BattleHex hexNum, const CStack * creature) const; - friend class CBattleAnimation; // for exposing pendingAnims/creAnims/creDir to animations + friend class BattleAnimation; // for exposing pendingAnims/creAnims/creDir to animations }; diff --git a/client/battle/BattleControlPanel.cpp b/client/battle/BattleWindow.cpp similarity index 59% rename from client/battle/BattleControlPanel.cpp rename to client/battle/BattleWindow.cpp index f78299d76..76b57776b 100644 --- a/client/battle/BattleControlPanel.cpp +++ b/client/battle/BattleWindow.cpp @@ -1,5 +1,5 @@ /* - * BattleControlPanel.cpp, part of VCMI engine + * BattleWindow.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -8,19 +8,24 @@ * */ #include "StdInc.h" -#include "BattleControlPanel.h" +#include "BattleWindow.h" #include "BattleInterface.h" #include "BattleInterfaceClasses.h" +#include "BattleFieldController.h" #include "BattleStacksController.h" #include "BattleActionsController.h" #include "../CGameInfo.h" +#include "../CMessage.h" #include "../CPlayerInterface.h" +#include "../CMusicHandler.h" +#include "../gui/Canvas.h" #include "../gui/CCursorHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/CAnimation.h" #include "../windows/CSpellWindow.h" +#include "../widgets/AdventureMapClasses.h" #include "../widgets/Buttons.h" #include "../widgets/Images.h" @@ -31,97 +36,208 @@ #include "../../lib/CConfigHandler.h" #include "../../lib/filesystem/ResourceID.h" -BattleControlPanel::BattleControlPanel(BattleInterface & owner, const Point & position): +BattleWindow::BattleWindow(BattleInterface & owner): owner(owner) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - REGISTER_BUILDER("battleConsole", &BattleControlPanel::buildBattleConsole); + pos.w = 800; + pos.h = 600; + pos = center(); + + REGISTER_BUILDER("battleConsole", &BattleWindow::buildBattleConsole); - pos += position; + const JsonNode config(ResourceID("config/widgets/BattleWindow.json")); - const JsonNode config(ResourceID("config/widgets/battleControlPanel.json")); - - addCallback("options", std::bind(&BattleControlPanel::bOptionsf, this)); - addCallback("surrender", std::bind(&BattleControlPanel::bSurrenderf, this)); - addCallback("flee", std::bind(&BattleControlPanel::bFleef, this)); - addCallback("autofight", std::bind(&BattleControlPanel::bAutofightf, this)); - addCallback("spellbook", std::bind(&BattleControlPanel::bSpellf, this)); - addCallback("wait", std::bind(&BattleControlPanel::bWaitf, this)); - addCallback("defence", std::bind(&BattleControlPanel::bDefencef, this)); - addCallback("consoleUp", std::bind(&BattleControlPanel::bConsoleUpf, this)); - addCallback("consoleDown", std::bind(&BattleControlPanel::bConsoleDownf, this)); - addCallback("tacticNext", std::bind(&BattleControlPanel::bTacticNextStack, this)); - addCallback("tacticEnd", std::bind(&BattleControlPanel::bTacticPhaseEnd, this)); - addCallback("alternativeAction", std::bind(&BattleControlPanel::bSwitchActionf, this)); + addCallback("options", std::bind(&BattleWindow::bOptionsf, this)); + addCallback("surrender", std::bind(&BattleWindow::bSurrenderf, this)); + addCallback("flee", std::bind(&BattleWindow::bFleef, this)); + addCallback("autofight", std::bind(&BattleWindow::bAutofightf, this)); + addCallback("spellbook", std::bind(&BattleWindow::bSpellf, this)); + addCallback("wait", std::bind(&BattleWindow::bWaitf, this)); + addCallback("defence", std::bind(&BattleWindow::bDefencef, this)); + addCallback("consoleUp", std::bind(&BattleWindow::bConsoleUpf, this)); + addCallback("consoleDown", std::bind(&BattleWindow::bConsoleDownf, this)); + addCallback("tacticNext", std::bind(&BattleWindow::bTacticNextStack, this)); + addCallback("tacticEnd", std::bind(&BattleWindow::bTacticPhaseEnd, this)); + addCallback("alternativeAction", std::bind(&BattleWindow::bSwitchActionf, this)); build(config); console = widget("console"); + GH.statusbar = console; + owner.console = console; + + owner.fieldController.reset( new BattleFieldController(owner)); + owner.fieldController->createHeroes(); + + //create stack queue and adjust our own position + bool embedQueue; + std::string queueSize = settings["battle"]["queueSize"].String(); + + if(queueSize == "auto") + embedQueue = screen->h < 700; + else + embedQueue = screen->h < 700 || queueSize == "small"; + + queue = std::make_shared(embedQueue, owner); + if(!embedQueue && settings["battle"]["showQueue"].Bool()) + { + //re-center, taking into account stack queue position + pos.y -= queue->pos.h; + pos.h += queue->pos.h; + pos = center(); + } if ( owner.tacticsMode ) tacticPhaseStarted(); else tacticPhaseEnded(); + + addUsedEvents(RCLICK | KEYBOARD); } -std::shared_ptr BattleControlPanel::buildBattleConsole(const JsonNode & config) const +BattleWindow::~BattleWindow() { - return std::make_shared(readRect(config["rect"])); + CPlayerInterface::battleInt = nullptr; } -void BattleControlPanel::show(SDL_Surface * to) +std::shared_ptr BattleWindow::buildBattleConsole(const JsonNode & config) const { - //show menu before all other elements to keep it in background - if(auto w = widget("menu")) - w->show(to); - CIntObject::show(to); + auto rect = readRect(config["rect"]); + auto offset = readPosition(config["imagePosition"]); + auto background = widget("menuBattle"); + return std::make_shared(background, rect.topLeft(), offset, rect.dimensions() ); } -void BattleControlPanel::showAll(SDL_Surface * to) +void BattleWindow::hideQueue() { - //show menu before all other elements to keep it in background - if(auto w = widget("menu")) - w->showAll(to); - CIntObject::showAll(to); -} + Settings showQueue = settings.write["battle"]["showQueue"]; + showQueue->Bool() = false; + queue->disable(); -void BattleControlPanel::tacticPhaseStarted() -{ - build(variables["tacticItems"]); - - if(auto w = widget("menu")) + if (!queue->embedded) { - w->colorize(owner.curInt->playerID); - w->recActions &= ~(SHOWALL | UPDATE); - } -} -void BattleControlPanel::tacticPhaseEnded() -{ - deleteWidget("tacticNext"); - deleteWidget("tacticEnd"); - - build(variables["battleItems"]); - - if(auto w = widget("menu")) - { - w->colorize(owner.curInt->playerID); - w->recActions &= ~(SHOWALL | UPDATE); + //re-center, taking into account stack queue position + pos.y += queue->pos.h; + pos.h -= queue->pos.h; + pos = center(); + GH.totalRedraw(); } } -void BattleControlPanel::bOptionsf() +void BattleWindow::showQueue() +{ + Settings showQueue = settings.write["battle"]["showQueue"]; + showQueue->Bool() = true; + + queue->enable(); + + if (!queue->embedded) + { + //re-center, taking into account stack queue position + pos.y -= queue->pos.h; + pos.h += queue->pos.h; + pos = center(); + GH.totalRedraw(); + } +} + +void BattleWindow::updateQueue() +{ + queue->update(); +} + +void BattleWindow::activate() +{ + GH.statusbar = console; + CIntObject::activate(); + LOCPLINT->cingconsole->activate(); +} + +void BattleWindow::deactivate() +{ + CIntObject::deactivate(); + LOCPLINT->cingconsole->deactivate(); +} + +void BattleWindow::keyPressed(const SDL_KeyboardEvent & key) +{ + if(key.keysym.sym == SDLK_q && key.state == SDL_PRESSED) + { + if(settings["battle"]["showQueue"].Bool()) //hide queue + hideQueue(); + else + showQueue(); + + } + else if(key.keysym.sym == SDLK_f && key.state == SDL_PRESSED) + { + owner.actionsController->enterCreatureCastingMode(); + } + else if(key.keysym.sym == SDLK_ESCAPE) + { + if(owner.getAnimationCondition(EAnimationEvents::OPENING) == true) + CCS->soundh->stopSound(owner.battleIntroSoundChannel); + else + owner.actionsController->endCastingSpell(); + } +} + +void BattleWindow::clickRight(tribool down, bool previousState) +{ + if (!down) + owner.actionsController->endCastingSpell(); +} + +void BattleWindow::tacticPhaseStarted() +{ + auto menuBattle = widget("menuBattle"); + auto console = widget("console"); + auto menuTactics = widget("menuTactics"); + auto tacticNext = widget("tacticNext"); + auto tacticEnd = widget("tacticEnd"); + + menuBattle->disable(); + console->disable(); + + menuTactics->enable(); + tacticNext->enable(); + tacticEnd->enable(); + + redraw(); +} + +void BattleWindow::tacticPhaseEnded() +{ + auto menuBattle = widget("menuBattle"); + auto console = widget("console"); + auto menuTactics = widget("menuTactics"); + auto tacticNext = widget("tacticNext"); + auto tacticEnd = widget("tacticEnd"); + + menuBattle->enable(); + console->enable(); + + menuTactics->disable(); + tacticNext->disable(); + tacticEnd->disable(); + + redraw(); +} + +void BattleWindow::bOptionsf() { if (owner.actionsController->spellcastingModeActive()) return; - CCS->curh->changeGraphic(ECursor::ADVENTURE,0); + CCS->curh->set(Cursor::Map::POINTER); GH.pushIntT(owner); } -void BattleControlPanel::bSurrenderf() +void BattleWindow::bSurrenderf() { if (owner.actionsController->spellcastingModeActive()) return; @@ -141,14 +257,14 @@ void BattleControlPanel::bSurrenderf() } } -void BattleControlPanel::bFleef() +void BattleWindow::bFleef() { if (owner.actionsController->spellcastingModeActive()) return; if ( owner.curInt->cb->battleCanFlee() ) { - CFunctionList ony = std::bind(&BattleControlPanel::reallyFlee,this); + CFunctionList ony = std::bind(&BattleWindow::reallyFlee,this); owner.curInt->showYesNoDialog(CGI->generaltexth->allTexts[28], ony, nullptr); //Are you sure you want to retreat? } else @@ -170,13 +286,13 @@ void BattleControlPanel::bFleef() } } -void BattleControlPanel::reallyFlee() +void BattleWindow::reallyFlee() { owner.giveCommand(EActionType::RETREAT); - CCS->curh->changeGraphic(ECursor::ADVENTURE, 0); + CCS->curh->set(Cursor::Map::POINTER); } -void BattleControlPanel::reallySurrender() +void BattleWindow::reallySurrender() { if (owner.curInt->cb->getResourceAmount(Res::GOLD) < owner.curInt->cb->battleGetSurrenderCost()) { @@ -185,11 +301,11 @@ void BattleControlPanel::reallySurrender() else { owner.giveCommand(EActionType::SURRENDER); - CCS->curh->changeGraphic(ECursor::ADVENTURE, 0); + CCS->curh->set(Cursor::Map::POINTER); } } -void BattleControlPanel::showAlternativeActionIcon(PossiblePlayerBattleAction action) +void BattleWindow::showAlternativeActionIcon(PossiblePlayerBattleAction action) { auto w = widget("alternativeAction"); if(!w) @@ -228,7 +344,7 @@ void BattleControlPanel::showAlternativeActionIcon(PossiblePlayerBattleAction ac w->setImage(anim, false); } -void BattleControlPanel::setAlternativeActions(const std::list & actions) +void BattleWindow::setAlternativeActions(const std::list & actions) { alternativeActions = actions; defaultAction = PossiblePlayerBattleAction::INVALID; @@ -240,7 +356,7 @@ void BattleControlPanel::setAlternativeActions(const std::listspellcastingModeActive()) return; @@ -267,7 +383,7 @@ void BattleControlPanel::bAutofightf() } } -void BattleControlPanel::bSpellf() +void BattleWindow::bSpellf() { if (owner.actionsController->spellcastingModeActive()) return; @@ -279,7 +395,7 @@ void BattleControlPanel::bSpellf() if(!myHero) return; - CCS->curh->changeGraphic(ECursor::ADVENTURE,0); + CCS->curh->set(Cursor::Map::POINTER); ESpellCastProblem::ESpellCastProblem spellCastProblem = owner.curInt->cb->battleCanCastSpell(myHero, spells::Mode::HERO); @@ -309,7 +425,7 @@ void BattleControlPanel::bSpellf() } } -void BattleControlPanel::bSwitchActionf() +void BattleWindow::bSwitchActionf() { if(alternativeActions.empty()) return; @@ -336,7 +452,7 @@ void BattleControlPanel::bSwitchActionf() alternativeActions.pop_front(); } -void BattleControlPanel::bWaitf() +void BattleWindow::bWaitf() { if (owner.actionsController->spellcastingModeActive()) return; @@ -345,7 +461,7 @@ void BattleControlPanel::bWaitf() owner.giveCommand(EActionType::WAIT); } -void BattleControlPanel::bDefencef() +void BattleWindow::bDefencef() { if (owner.actionsController->spellcastingModeActive()) return; @@ -354,7 +470,7 @@ void BattleControlPanel::bDefencef() owner.giveCommand(EActionType::DEFEND); } -void BattleControlPanel::bConsoleUpf() +void BattleWindow::bConsoleUpf() { if (owner.actionsController->spellcastingModeActive()) return; @@ -362,7 +478,7 @@ void BattleControlPanel::bConsoleUpf() console->scrollUp(); } -void BattleControlPanel::bConsoleDownf() +void BattleWindow::bConsoleDownf() { if (owner.actionsController->spellcastingModeActive()) return; @@ -370,17 +486,17 @@ void BattleControlPanel::bConsoleDownf() console->scrollDown(); } -void BattleControlPanel::bTacticNextStack() +void BattleWindow::bTacticNextStack() { owner.tacticNextStack(nullptr); } -void BattleControlPanel::bTacticPhaseEnd() +void BattleWindow::bTacticPhaseEnd() { owner.tacticPhaseEnd(); } -void BattleControlPanel::blockUI(bool on) +void BattleWindow::blockUI(bool on) { bool canCastSpells = false; auto hero = owner.curInt->cb->battleGetMyHero(); @@ -433,3 +549,24 @@ void BattleControlPanel::blockUI(bool on) } } } + +void BattleWindow::showAll(SDL_Surface *to) +{ + CIntObject::showAll(to); + + if (screen->w != 800 || screen->h !=600) + CMessage::drawBorder(owner.curInt->playerID, to, pos.w+28, pos.h+29, pos.x-14, pos.y-15); +} + +void BattleWindow::show(SDL_Surface *to) +{ + CIntObject::show(to); + LOCPLINT->cingconsole->show(to); +} + +void BattleWindow::close() +{ + if(GH.topInt().get() != this) + logGlobal->error("Only top interface must be closed"); + GH.popInts(1); +} diff --git a/client/battle/BattleControlPanel.h b/client/battle/BattleWindow.h similarity index 69% rename from client/battle/BattleControlPanel.h rename to client/battle/BattleWindow.h index 31a940b0e..696f92a85 100644 --- a/client/battle/BattleControlPanel.h +++ b/client/battle/BattleWindow.h @@ -1,5 +1,5 @@ /* - * BattleControlPanel.h, part of VCMI engine + * BattleWindow.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -21,13 +21,16 @@ VCMI_LIB_NAMESPACE_END class CButton; class BattleInterface; class BattleConsole; +class BattleRenderer; +class StackQueue; /// GUI object that handles functionality of panel at the bottom of combat screen -class BattleControlPanel : public InterfaceObjectConfigurable -{ +class BattleWindow : public InterfaceObjectConfigurable +{ BattleInterface & owner; - - std::shared_ptr buildBattleConsole(const JsonNode &) const; + + std::shared_ptr queue; + std::shared_ptr console; /// button press handling functions void bOptionsf(); @@ -52,24 +55,40 @@ class BattleControlPanel : public InterfaceObjectConfigurable PossiblePlayerBattleAction defaultAction; void showAlternativeActionIcon(PossiblePlayerBattleAction); + /// Toggle StackQueue visibility + void hideQueue(); + void showQueue(); + + std::shared_ptr buildBattleConsole(const JsonNode &) const; + public: - std::shared_ptr console; + BattleWindow(BattleInterface & owner ); + ~BattleWindow(); + + /// Closes window once battle finished + void close(); /// block all UI elements when player is not allowed to act, e.g. during enemy turn void blockUI(bool on); - void show(SDL_Surface * to) override; - void showAll(SDL_Surface * to) override; + /// Refresh queue after turn order changes + void updateQueue(); + + void activate() override; + void deactivate() override; + void keyPressed(const SDL_KeyboardEvent & key) override; + void clickRight(tribool down, bool previousState) override; + void show(SDL_Surface *to) override; + void showAll(SDL_Surface *to) override; /// Toggle UI to displaying tactics phase void tacticPhaseStarted(); /// Toggle UI to displaying battle log in place of tactics UI void tacticPhaseEnded(); - + /// Set possible alternative options. If more than 1 - the last will be considered as default option void setAlternativeActions(const std::list &); - BattleControlPanel(BattleInterface & owner, const Point & position); }; diff --git a/client/battle/CreatureAnimation.cpp b/client/battle/CreatureAnimation.cpp index d91dca19e..b83a4f202 100644 --- a/client/battle/CreatureAnimation.cpp +++ b/client/battle/CreatureAnimation.cpp @@ -14,11 +14,17 @@ #include "../../lib/CCreatureHandler.h" #include "../gui/Canvas.h" +#include "../gui/ColorFilter.h" static const SDL_Color creatureBlueBorder = { 0, 255, 255, 255 }; static const SDL_Color creatureGoldBorder = { 255, 255, 0, 255 }; static const SDL_Color creatureNoBorder = { 0, 0, 0, 0 }; +static SDL_Color genShadow(ui8 alpha) +{ + return CSDL_Ext::makeColor(0, 0, 0, alpha); +} + SDL_Color AnimationControls::getBlueBorder() { return creatureBlueBorder; @@ -40,10 +46,8 @@ std::shared_ptr AnimationControls::getAnimation(const CCreatu return std::make_shared(creature->animDefName, func); } -float AnimationControls::getCreatureAnimationSpeed(const CCreature * creature, const CreatureAnimation * anim, size_t group) +float AnimationControls::getCreatureAnimationSpeed(const CCreature * creature, const CreatureAnimation * anim, ECreatureAnimType type) { - CCreatureAnim::EAnimType type = CCreatureAnim::EAnimType(group); - assert(creature->animation.walkAnimationTime != 0); assert(creature->animation.attackAnimationTime != 0); assert(anim->framesInGroup(type) != 0); @@ -58,49 +62,50 @@ float AnimationControls::getCreatureAnimationSpeed(const CCreature * creature, c switch (type) { - case CCreatureAnim::MOVING: + case ECreatureAnimType::MOVING: return static_cast(speed * 2 * creature->animation.walkAnimationTime / anim->framesInGroup(type)); - case CCreatureAnim::MOUSEON: + case ECreatureAnimType::MOUSEON: return baseSpeed; - case CCreatureAnim::HOLDING: + case ECreatureAnimType::HOLDING: return static_cast(baseSpeed * creature->animation.idleAnimationTime / anim->framesInGroup(type)); - case CCreatureAnim::SHOOT_UP: - case CCreatureAnim::SHOOT_FRONT: - case CCreatureAnim::SHOOT_DOWN: - case CCreatureAnim::CAST_UP: - case CCreatureAnim::CAST_FRONT: - case CCreatureAnim::CAST_DOWN: - case CCreatureAnim::VCMI_CAST_DOWN: - case CCreatureAnim::VCMI_CAST_FRONT: - case CCreatureAnim::VCMI_CAST_UP: + case ECreatureAnimType::SHOOT_UP: + case ECreatureAnimType::SHOOT_FRONT: + case ECreatureAnimType::SHOOT_DOWN: + case ECreatureAnimType::SPECIAL_UP: + case ECreatureAnimType::SPECIAL_FRONT: + case ECreatureAnimType::SPECIAL_DOWN: + case ECreatureAnimType::CAST_DOWN: + case ECreatureAnimType::CAST_FRONT: + case ECreatureAnimType::CAST_UP: return static_cast(speed * 4 * creature->animation.attackAnimationTime / anim->framesInGroup(type)); // as strange as it looks like "attackAnimationTime" does not affects melee attacks // necessary because length of these animations must be same for all creatures for synchronization - case CCreatureAnim::ATTACK_UP: - case CCreatureAnim::ATTACK_FRONT: - case CCreatureAnim::ATTACK_DOWN: - case CCreatureAnim::HITTED: - case CCreatureAnim::DEFENCE: - case CCreatureAnim::DEATH: - case CCreatureAnim::DEATH_RANGED: - case CCreatureAnim::VCMI_2HEX_DOWN: - case CCreatureAnim::VCMI_2HEX_FRONT: - case CCreatureAnim::VCMI_2HEX_UP: + case ECreatureAnimType::ATTACK_UP: + case ECreatureAnimType::ATTACK_FRONT: + case ECreatureAnimType::ATTACK_DOWN: + case ECreatureAnimType::HITTED: + case ECreatureAnimType::DEFENCE: + case ECreatureAnimType::DEATH: + case ECreatureAnimType::DEATH_RANGED: + case ECreatureAnimType::RESURRECTION: + case ECreatureAnimType::GROUP_ATTACK_DOWN: + case ECreatureAnimType::GROUP_ATTACK_FRONT: + case ECreatureAnimType::GROUP_ATTACK_UP: return speed * 3 / anim->framesInGroup(type); - case CCreatureAnim::TURN_L: - case CCreatureAnim::TURN_R: + case ECreatureAnimType::TURN_L: + case ECreatureAnimType::TURN_R: return speed / 3; - case CCreatureAnim::MOVE_START: - case CCreatureAnim::MOVE_END: + case ECreatureAnimType::MOVE_START: + case ECreatureAnimType::MOVE_END: return speed / 3; - case CCreatureAnim::DEAD: - case CCreatureAnim::DEAD_RANGED: + case ECreatureAnimType::DEAD: + case ECreatureAnimType::DEAD_RANGED: return speed; default: @@ -110,12 +115,12 @@ float AnimationControls::getCreatureAnimationSpeed(const CCreature * creature, c float AnimationControls::getProjectileSpeed() { - return static_cast(settings["battle"]["animationSpeed"].Float() * 100); + return static_cast(settings["battle"]["animationSpeed"].Float() * 4000); } float AnimationControls::getCatapultSpeed() { - return static_cast(settings["battle"]["animationSpeed"].Float() * 20); + return static_cast(settings["battle"]["animationSpeed"].Float() * 1000); } float AnimationControls::getSpellEffectSpeed() @@ -133,12 +138,22 @@ float AnimationControls::getFlightDistance(const CCreature * creature) return static_cast(creature->animation.flightAnimationDistance * 200); } -CCreatureAnim::EAnimType CreatureAnimation::getType() const +float AnimationControls::getFadeInDuration() +{ + return 1.0f / settings["battle"]["animationSpeed"].Float(); +} + +float AnimationControls::getObstaclesSpeed() +{ + return 10.0;// does not seems to be affected by animaiton speed settings +} + +ECreatureAnimType CreatureAnimation::getType() const { return type; } -void CreatureAnimation::setType(CCreatureAnim::EAnimType type) +void CreatureAnimation::setType(ECreatureAnimType type) { this->type = type; currentFrame = 0; @@ -147,21 +162,13 @@ void CreatureAnimation::setType(CCreatureAnim::EAnimType type) play(); } -void CreatureAnimation::shiftColor(const ColorShifter* shifter) -{ - if(forward) - forward->shiftColor(shifter); - - if(reverse) - reverse->shiftColor(shifter); -} - CreatureAnimation::CreatureAnimation(const std::string & name_, TSpeedController controller) : name(name_), speed(0.1f), + shadowAlpha(128), currentFrame(0), elapsedTime(0), - type(CCreatureAnim::HOLDING), + type(ECreatureAnimType::HOLDING), border(CSDL_Ext::makeColor(0, 0, 0, 0)), speedController(controller), once(false) @@ -174,20 +181,37 @@ CreatureAnimation::CreatureAnimation(const std::string & name_, TSpeedController reverse->preload(); // if necessary, add one frame into vcmi-only group DEAD - if(forward->size(CCreatureAnim::DEAD) == 0) + if(forward->size(size_t(ECreatureAnimType::DEAD)) == 0) { - forward->duplicateImage(CCreatureAnim::DEATH, forward->size(CCreatureAnim::DEATH)-1, CCreatureAnim::DEAD); - reverse->duplicateImage(CCreatureAnim::DEATH, reverse->size(CCreatureAnim::DEATH)-1, CCreatureAnim::DEAD); + forward->duplicateImage(size_t(ECreatureAnimType::DEATH), forward->size(size_t(ECreatureAnimType::DEATH))-1, size_t(ECreatureAnimType::DEAD)); + reverse->duplicateImage(size_t(ECreatureAnimType::DEATH), reverse->size(size_t(ECreatureAnimType::DEATH))-1, size_t(ECreatureAnimType::DEAD)); } - if(forward->size(CCreatureAnim::DEAD_RANGED) == 0 && forward->size(CCreatureAnim::DEATH_RANGED) != 0) + if(forward->size(size_t(ECreatureAnimType::DEAD_RANGED)) == 0 && forward->size(size_t(ECreatureAnimType::DEATH_RANGED)) != 0) { - forward->duplicateImage(CCreatureAnim::DEATH_RANGED, forward->size(CCreatureAnim::DEATH_RANGED)-1, CCreatureAnim::DEAD_RANGED); - reverse->duplicateImage(CCreatureAnim::DEATH_RANGED, reverse->size(CCreatureAnim::DEATH_RANGED)-1, CCreatureAnim::DEAD_RANGED); + forward->duplicateImage(size_t(ECreatureAnimType::DEATH_RANGED), forward->size(size_t(ECreatureAnimType::DEATH_RANGED))-1, size_t(ECreatureAnimType::DEAD_RANGED)); + reverse->duplicateImage(size_t(ECreatureAnimType::DEATH_RANGED), reverse->size(size_t(ECreatureAnimType::DEATH_RANGED))-1, size_t(ECreatureAnimType::DEAD_RANGED)); + } + + if(forward->size(size_t(ECreatureAnimType::FROZEN)) == 0) + { + forward->duplicateImage(size_t(ECreatureAnimType::HOLDING), 0, size_t(ECreatureAnimType::FROZEN)); + reverse->duplicateImage(size_t(ECreatureAnimType::HOLDING), 0, size_t(ECreatureAnimType::FROZEN)); + } + + if(forward->size(size_t(ECreatureAnimType::RESURRECTION)) == 0) + { + for (size_t i = 0; i < forward->size(size_t(ECreatureAnimType::DEATH)); ++i) + { + size_t current = forward->size(size_t(ECreatureAnimType::DEATH)) - 1 - i; + + forward->duplicateImage(size_t(ECreatureAnimType::DEATH), current, size_t(ECreatureAnimType::RESURRECTION)); + reverse->duplicateImage(size_t(ECreatureAnimType::DEATH), current, size_t(ECreatureAnimType::RESURRECTION)); + } } //TODO: get dimensions form CAnimation - auto first = forward->getImage(0, type, true); + auto first = forward->getImage(0, size_t(type), true); if(!first) { @@ -228,7 +252,7 @@ bool CreatureAnimation::incrementFrame(float timePassed) currentFrame -= framesNumber; if(once) - setType(CCreatureAnim::HOLDING); + setType(ECreatureAnimType::HOLDING); endAnimation(); return true; @@ -256,7 +280,7 @@ float CreatureAnimation::getCurrentFrame() const return currentFrame; } -void CreatureAnimation::playOnce( CCreatureAnim::EAnimType type ) +void CreatureAnimation::playOnce( ECreatureAnimType type ) { setType(type); once = true; @@ -269,11 +293,6 @@ inline int getBorderStrength(float time) return static_cast(borderStrength * 155 + 100); // scale to 0-255 } -static SDL_Color genShadow(ui8 alpha) -{ - return CSDL_Ext::makeColor(0, 0, 0, alpha); -} - static SDL_Color genBorderColor(ui8 alpha, const SDL_Color & base) { return CSDL_Ext::makeColor(base.r, base.g, base.b, ui8(base.a * alpha / 256)); @@ -294,80 +313,89 @@ static SDL_Color addColors(const SDL_Color & base, const SDL_Color & over) ); } -void CreatureAnimation::genBorderPalette(IImage::BorderPallete & target) +void CreatureAnimation::genSpecialPalette(IImage::SpecialPalette & target) { - target[0] = genBorderColor(getBorderStrength(elapsedTime), border); - target[1] = addColors(genShadow(128), genBorderColor(getBorderStrength(elapsedTime), border)); - target[2] = addColors(genShadow(64), genBorderColor(getBorderStrength(elapsedTime), border)); + target[0] = genShadow(shadowAlpha / 2); + target[1] = genShadow(shadowAlpha / 2); + target[2] = genShadow(shadowAlpha); + target[3] = genShadow(shadowAlpha); + target[4] = genBorderColor(getBorderStrength(elapsedTime), border); + target[5] = addColors(genShadow(shadowAlpha), genBorderColor(getBorderStrength(elapsedTime), border)); + target[6] = addColors(genShadow(shadowAlpha / 2), genBorderColor(getBorderStrength(elapsedTime), border)); } -void CreatureAnimation::nextFrame(Canvas & canvas, bool facingRight) +void CreatureAnimation::nextFrame(Canvas & canvas, const ColorFilter & shifter, bool facingRight) { + SDL_Color shadowTest = shifter.shiftColor(genShadow(128)); + shadowAlpha = shadowTest.a; + size_t frame = static_cast(floor(currentFrame)); std::shared_ptr image; if(facingRight) - image = forward->getImage(frame, type); + image = forward->getImage(frame, size_t(type)); else - image = reverse->getImage(frame, type); + image = reverse->getImage(frame, size_t(type)); if(image) { - IImage::BorderPallete borderPallete; - genBorderPalette(borderPallete); + IImage::SpecialPalette SpecialPalette; + genSpecialPalette(SpecialPalette); - image->setBorderPallete(borderPallete); + image->setSpecialPallete(SpecialPalette); + image->adjustPalette(shifter); canvas.draw(image, pos.topLeft(), Rect(0, 0, pos.w, pos.h)); + } } -int CreatureAnimation::framesInGroup(CCreatureAnim::EAnimType group) const +int CreatureAnimation::framesInGroup(ECreatureAnimType group) const { - return static_cast(forward->size(group)); + return static_cast(forward->size(size_t(group))); } bool CreatureAnimation::isDead() const { - return getType() == CCreatureAnim::DEAD - || getType() == CCreatureAnim::DEAD_RANGED; + return getType() == ECreatureAnimType::DEAD + || getType() == ECreatureAnimType::DEAD_RANGED; } bool CreatureAnimation::isDying() const { - return getType() == CCreatureAnim::DEATH - || getType() == CCreatureAnim::DEATH_RANGED; + return getType() == ECreatureAnimType::DEATH + || getType() == ECreatureAnimType::DEATH_RANGED; } bool CreatureAnimation::isDeadOrDying() const { - return getType() == CCreatureAnim::DEAD - || getType() == CCreatureAnim::DEATH - || getType() == CCreatureAnim::DEAD_RANGED - || getType() == CCreatureAnim::DEATH_RANGED; + return getType() == ECreatureAnimType::DEAD + || getType() == ECreatureAnimType::DEATH + || getType() == ECreatureAnimType::DEAD_RANGED + || getType() == ECreatureAnimType::DEATH_RANGED; } bool CreatureAnimation::isIdle() const { - return getType() == CCreatureAnim::HOLDING - || getType() == CCreatureAnim::MOUSEON; + return getType() == ECreatureAnimType::HOLDING + || getType() == ECreatureAnimType::MOUSEON; } bool CreatureAnimation::isMoving() const { - return getType() == CCreatureAnim::MOVE_START - || getType() == CCreatureAnim::MOVING - || getType() == CCreatureAnim::MOVE_END - || getType() == CCreatureAnim::TURN_L - || getType() == CCreatureAnim::TURN_R; + return getType() == ECreatureAnimType::MOVE_START + || getType() == ECreatureAnimType::MOVING + || getType() == ECreatureAnimType::MOVE_END + || getType() == ECreatureAnimType::TURN_L + || getType() == ECreatureAnimType::TURN_R; } bool CreatureAnimation::isShooting() const { - return getType() == CCreatureAnim::SHOOT_UP - || getType() == CCreatureAnim::SHOOT_FRONT - || getType() == CCreatureAnim::SHOOT_DOWN; + return getType() == ECreatureAnimType::SHOOT_UP + || getType() == ECreatureAnimType::SHOOT_FRONT + || getType() == ECreatureAnimType::SHOOT_DOWN; } void CreatureAnimation::pause() @@ -379,6 +407,6 @@ void CreatureAnimation::play() { //logAnim->trace("Play %s group %d at %d:%d", name, static_cast(getType()), pos.x, pos.y); speed = 0; - if(speedController(this, type) != 0) - speed = 1 / speedController(this, type); + if(speedController(this, type) != 0) + speed = 1 / speedController(this, type); } diff --git a/client/battle/CreatureAnimation.h b/client/battle/CreatureAnimation.h index efa94e198..cdead9c79 100644 --- a/client/battle/CreatureAnimation.h +++ b/client/battle/CreatureAnimation.h @@ -29,13 +29,12 @@ namespace AnimationControls std::shared_ptr getAnimation(const CCreature * creature); /// returns animation speed of specific group, taking in mind game setting (in frames per second) - float getCreatureAnimationSpeed(const CCreature * creature, const CreatureAnimation * anim, size_t groupID); + float getCreatureAnimationSpeed(const CCreature * creature, const CreatureAnimation * anim, ECreatureAnimType groupID); - /// returns how far projectile should move each frame - /// TODO: make it time-based + /// returns how far projectile should move per second float getProjectileSpeed(); - /// returns speed of catapult projectile + /// returns speed of catapult projectile, in pixels per second (horizontal axis only) float getCatapultSpeed(); /// returns speed of any spell effects, including any special effects like morale (in frames per second) @@ -46,6 +45,12 @@ namespace AnimationControls /// Returns distance on which flying creatures should during one animation loop float getFlightDistance(const CCreature * creature); + + /// Returns total time for full fade-in effect on newly summoned creatures, in seconds + float getFadeInDuration(); + + /// Returns animation speed for obstacles, in frames per second + float getObstaclesSpeed(); } /// Class which manages animations of creatures/units inside battles @@ -53,7 +58,7 @@ namespace AnimationControls class CreatureAnimation : public CIntObject { public: - typedef std::function TSpeedController; + typedef std::function TSpeedController; private: std::string name; @@ -78,7 +83,10 @@ private: float elapsedTime; ///type of animation being displayed - CCreatureAnim::EAnimType type; + ECreatureAnimType type; + + /// current value of shadow transparency + uint8_t shadowAlpha; /// border color, disabled if alpha = 0 SDL_Color border; @@ -90,7 +98,7 @@ private: void endAnimation(); - void genBorderPalette(IImage::BorderPallete & target); + void genSpecialPalette(IImage::SpecialPalette & target); public: /// function(s) that will be called when animation ends, after reset to 1st frame @@ -107,29 +115,26 @@ public: CreatureAnimation(const std::string & name_, TSpeedController speedController); /// sets type of animation and resets framecount - void setType(CCreatureAnim::EAnimType type); + void setType(ECreatureAnimType type); /// returns currently rendered type of animation - CCreatureAnim::EAnimType getType() const; + ECreatureAnimType getType() const; - void nextFrame(Canvas & canvas, bool facingRight); + void nextFrame(Canvas & canvas, const ColorFilter & shifter, bool facingRight); /// should be called every frame, return true when animation was reset to beginning bool incrementFrame(float timePassed); void setBorderColor(SDL_Color palette); - /// apply color tint effect - void shiftColor(const ColorShifter * shifter); - /// Gets the current frame ID within current group. float getCurrentFrame() const; /// plays once given type of animation, then resets to idle - void playOnce(CCreatureAnim::EAnimType type); + void playOnce(ECreatureAnimType type); /// returns number of frames in selected animation type - int framesInGroup(CCreatureAnim::EAnimType type) const; + int framesInGroup(ECreatureAnimType group) const; void pause(); void play(); diff --git a/client/gui/CAnimation.cpp b/client/gui/CAnimation.cpp index 42cf6916b..ef1b92115 100644 --- a/client/gui/CAnimation.cpp +++ b/client/gui/CAnimation.cpp @@ -12,6 +12,7 @@ #include "SDL_Extensions.h" #include "SDL_Pixels.h" +#include "ColorFilter.h" #include "../CBitmapHandler.h" #include "../Graphics.h" @@ -33,6 +34,7 @@ class CDefFile { private: + PACKED_STRUCT_BEGIN struct SSpriteDef { ui32 size; @@ -43,7 +45,7 @@ private: ui32 height; si32 leftMargin; si32 topMargin; - } PACKED_STRUCT; + } PACKED_STRUCT_END; //offset[group][frame] - offset of frame data in file std::map > offset; @@ -105,10 +107,11 @@ public: void verticalFlip() override; void shiftPalette(int from, int howMany) override; - void adjustPalette(const ColorShifter * shifter) override; + void adjustPalette(const ColorFilter & shifter) override; + void resetPalette(int colorID) override; void resetPalette() override; - void setBorderPallete(const BorderPallete & borderPallete) override; + void setSpecialPallete(const SpecialPalette & SpecialPalette) override; friend class SDLImageLoader; @@ -212,32 +215,17 @@ CDefFile::CDefFile(std::string Name): data(nullptr), palette(nullptr) { - - #if 0 - static SDL_Color H3_ORIG_PALETTE[8] = - { - { 0, 255, 255, SDL_ALPHA_OPAQUE}, - {255, 150, 255, SDL_ALPHA_OPAQUE}, - {255, 100, 255, SDL_ALPHA_OPAQUE}, - {255, 50, 255, SDL_ALPHA_OPAQUE}, - {255, 0, 255, SDL_ALPHA_OPAQUE}, - {255, 255, 0, SDL_ALPHA_OPAQUE}, - {180, 0, 255, SDL_ALPHA_OPAQUE}, - { 0, 255, 0, SDL_ALPHA_OPAQUE} - }; - #endif // 0 - //First 8 colors in def palette used for transparency static SDL_Color H3Palette[8] = { - { 0, 0, 0, 0},// 100% - transparency - { 0, 0, 0, 32},// 75% - shadow border, - { 0, 0, 0, 64},// TODO: find exact value - { 0, 0, 0, 128},// TODO: for transparency - { 0, 0, 0, 128},// 50% - shadow body - { 0, 0, 0, 0},// 100% - selection highlight - { 0, 0, 0, 128},// 50% - shadow body below selection - { 0, 0, 0, 64} // 75% - shadow border below selection + { 0, 0, 0, 0},// transparency ( used in most images ) + { 0, 0, 0, 64},// shadow border ( used in battle, adventure map def's ) + { 0, 0, 0, 64},// shadow border ( used in fog-of-war def's ) + { 0, 0, 0, 128},// shadow body ( used in fog-of-war def's ) + { 0, 0, 0, 128},// shadow body ( used in battle, adventure map def's ) + { 0, 0, 0, 0},// selection ( used in battle def's ) + { 0, 0, 0, 128},// shadow body below selection ( used in battle def's ) + { 0, 0, 0, 64} // shadow border below selection ( used in battle def's ) }; data = animationCache.getCachedFile(ResourceID(std::string("SPRITES/") + Name, EResType::ANIMATION)); @@ -804,7 +792,7 @@ void SDLImage::shiftPalette(int from, int howMany) } } -void SDLImage::adjustPalette(const ColorShifter * shifter) +void SDLImage::adjustPalette(const ColorFilter & shifter) { if(originalPalette == nullptr) return; @@ -814,7 +802,7 @@ void SDLImage::adjustPalette(const ColorShifter * shifter) // Note: here we skip the first 8 colors in the palette that predefined in H3Palette for(int i = 8; i < palette->ncolors; i++) { - palette->colors[i] = shifter->shiftColor(originalPalette->colors[i]); + palette->colors[i] = shifter.shiftColor(originalPalette->colors[i]); } } @@ -827,11 +815,20 @@ void SDLImage::resetPalette() SDL_SetPaletteColors(surf->format->palette, originalPalette->colors, 0, originalPalette->ncolors); } -void SDLImage::setBorderPallete(const IImage::BorderPallete & borderPallete) +void SDLImage::resetPalette( int colorID ) +{ + if(originalPalette == nullptr) + return; + + // Always keept the original palette not changed, copy a new palette to assign to surface + SDL_SetPaletteColors(surf->format->palette, originalPalette->colors + colorID, colorID, 1); +} + +void SDLImage::setSpecialPallete(const IImage::SpecialPalette & SpecialPalette) { if(surf->format->palette) { - SDL_SetColors(surf, const_cast(borderPallete.data()), 5, 3); + SDL_SetColors(surf, const_cast(SpecialPalette.data()), 1, 7); } } @@ -1094,18 +1091,6 @@ void CAnimation::duplicateImage(const size_t sourceGroup, const size_t sourceFra load(index, targetGroup); } -void CAnimation::shiftColor(const ColorShifter * shifter) -{ - for(auto groupIter = images.begin(); groupIter != images.end(); groupIter++) - { - for(auto frameIter = groupIter->second.begin(); frameIter != groupIter->second.end(); frameIter++) - { - std::shared_ptr image = frameIter->second; - image->adjustPalette(shifter); - } - } -} - void CAnimation::setCustom(std::string filename, size_t frame, size_t group) { if (source[group].size() <= frame) diff --git a/client/gui/CAnimation.h b/client/gui/CAnimation.h index c88f4e2a9..7263f3ee2 100644 --- a/client/gui/CAnimation.h +++ b/client/gui/CAnimation.h @@ -29,7 +29,7 @@ VCMI_LIB_NAMESPACE_END struct SDL_Surface; class CDefFile; -class ColorShifter; +class ColorFilter; /* * Base class for images, can be used for non-animation pictures as well @@ -37,7 +37,7 @@ class ColorShifter; class IImage { public: - using BorderPallete = std::array; + using SpecialPalette = std::array; //draws image on surface "where" at position virtual void draw(SDL_Surface * where, int posX = 0, int posY = 0, const Rect * src = nullptr, ui8 alpha = 255) const=0; @@ -62,11 +62,12 @@ public: //only indexed bitmaps, 16 colors maximum virtual void shiftPalette(int from, int howMany) = 0; - virtual void adjustPalette(const ColorShifter * shifter) = 0; + virtual void adjustPalette(const ColorFilter & shifter) = 0; + virtual void resetPalette(int colorID) = 0; virtual void resetPalette() = 0; - //only indexed bitmaps, colors 5,6,7 must be special - virtual void setBorderPallete(const BorderPallete & borderPallete) = 0; + //only indexed bitmaps with 7 special colors + virtual void setSpecialPallete(const SpecialPalette & SpecialPalette) = 0; virtual void horizontalFlip() = 0; virtual void verticalFlip() = 0; @@ -121,9 +122,6 @@ public: //and loads it if animation is preloaded void duplicateImage(const size_t sourceGroup, const size_t sourceFrame, const size_t targetGroup); - // adjust the color of the animation, used in battle spell effects, e.g. Cloned objects - void shiftColor(const ColorShifter * shifter); - //add custom surface to the selected position. void setCustom(std::string filename, size_t frame, size_t group=0); diff --git a/client/gui/CCursorHandler.cpp b/client/gui/CCursorHandler.cpp index 902ccef2b..bd9726a49 100644 --- a/client/gui/CCursorHandler.cpp +++ b/client/gui/CCursorHandler.cpp @@ -38,13 +38,18 @@ void CCursorHandler::replaceBuffer(CIntObject * payload) updateBuffer(payload); } -void CCursorHandler::initCursor() +CCursorHandler::CCursorHandler() + : needUpdate(true) + , buffer(nullptr) + , cursorLayer(nullptr) + , frameTime(0.f) + , showing(false) { cursorLayer = SDL_CreateTexture(mainRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, 40, 40); SDL_SetTextureBlendMode(cursorLayer, SDL_BLENDMODE_BLEND); xpos = ypos = 0; - type = ECursor::DEFAULT; + type = Cursor::Type::DEFAULT; dndObject = nullptr; cursors = @@ -55,17 +60,22 @@ void CCursorHandler::initCursor() std::make_unique("CRSPELL", 0) }; - currentCursor = cursors.at(int(ECursor::DEFAULT)).get(); + currentCursor = cursors.at(static_cast(Cursor::Type::DEFAULT)).get(); buffer = CSDL_Ext::newSurface(40,40); SDL_SetSurfaceBlendMode(buffer, SDL_BLENDMODE_NONE); SDL_ShowCursor(SDL_DISABLE); - changeGraphic(ECursor::ADVENTURE, 0); + set(Cursor::Map::POINTER); } -void CCursorHandler::changeGraphic(ECursor::ECursorTypes type, int index) +Point CCursorHandler::position() const +{ + return Point(xpos, ypos); +} + +void CCursorHandler::changeGraphic(Cursor::Type type, size_t index) { assert(dndObject == nullptr); @@ -73,7 +83,7 @@ void CCursorHandler::changeGraphic(ECursor::ECursorTypes type, int index) { this->type = type; this->frame = index; - currentCursor = cursors.at(int(type)).get(); + currentCursor = cursors.at(static_cast(type)).get(); currentCursor->setFrame(index); } else if(index != this->frame) @@ -85,6 +95,27 @@ void CCursorHandler::changeGraphic(ECursor::ECursorTypes type, int index) replaceBuffer(currentCursor); } +void CCursorHandler::set(Cursor::Default index) +{ + changeGraphic(Cursor::Type::DEFAULT, static_cast(index)); +} + +void CCursorHandler::set(Cursor::Map index) +{ + changeGraphic(Cursor::Type::ADVENTURE, static_cast(index)); +} + +void CCursorHandler::set(Cursor::Combat index) +{ + changeGraphic(Cursor::Type::COMBAT, static_cast(index)); +} + +void CCursorHandler::set(Cursor::Spellcast index) +{ + //Note: this is animated cursor, ignore specified frame and only change type + changeGraphic(Cursor::Type::SPELLBOOK, frame); +} + void CCursorHandler::dragAndDropCursor(std::unique_ptr object) { dndObject = std::move(object); @@ -102,54 +133,57 @@ void CCursorHandler::cursorMove(const int & x, const int & y) void CCursorHandler::shiftPos( int &x, int &y ) { - if(( type == ECursor::COMBAT && frame != ECursor::COMBAT_POINTER) || type == ECursor::SPELLBOOK) + if(( type == Cursor::Type::COMBAT && frame != static_cast(Cursor::Combat::POINTER)) || type == Cursor::Type::SPELLBOOK) { x-=16; y-=16; // Properly align the melee attack cursors. - if (type == ECursor::COMBAT) + if (type == Cursor::Type::COMBAT) { - switch (frame) + switch (static_cast(frame)) { - case 7: // Bottom left + case Cursor::Combat::HIT_NORTHEAST: x -= 6; y += 16; break; - case 8: // Left + case Cursor::Combat::HIT_EAST: x -= 16; y += 10; break; - case 9: // Top left + case Cursor::Combat::HIT_SOUTHEAST: x -= 6; y -= 6; break; - case 10: // Top right + case Cursor::Combat::HIT_SOUTHWEST: x += 16; y -= 6; break; - case 11: // Right + case Cursor::Combat::HIT_WEST: x += 16; y += 11; break; - case 12: // Bottom right + case Cursor::Combat::HIT_NORTHWEST: x += 16; y += 16; break; - case 13: // Below + case Cursor::Combat::HIT_NORTH: x += 9; y += 16; break; - case 14: // Above + case Cursor::Combat::HIT_SOUTH: x += 9; y -= 15; break; } } } - else if(type == ECursor::ADVENTURE) + else if(type == Cursor::Type::ADVENTURE) { - if (frame == 0); //to exclude + if (frame == 0) + { + //no-op + } else if(frame == 2) { x -= 12; @@ -221,6 +255,27 @@ void CCursorHandler::render() if(!showing) return; + if (type == Cursor::Type::SPELLBOOK) + { + static const float frameDisplayDuration = 0.1f; + + frameTime += GH.mainFPSmng->getElapsedMilliseconds() / 1000.f; + size_t newFrame = frame; + + while (frameTime > frameDisplayDuration) + { + frameTime -= frameDisplayDuration; + newFrame++; + } + + auto & animation = cursors.at(static_cast(type)); + + while (newFrame > animation->size()) + newFrame -= animation->size(); + + changeGraphic(Cursor::Type::SPELLBOOK, newFrame); + } + //the must update texture in the main (renderer) thread, but changes to cursor type may come from other threads updateTexture(); @@ -252,15 +307,6 @@ void CCursorHandler::updateTexture() } } -CCursorHandler::CCursorHandler() - : needUpdate(true), - buffer(nullptr), - cursorLayer(nullptr), - showing(false) -{ - -} - CCursorHandler::~CCursorHandler() { if(buffer) diff --git a/client/gui/CCursorHandler.h b/client/gui/CCursorHandler.h index 30ef0c65b..82082a93d 100644 --- a/client/gui/CCursorHandler.h +++ b/client/gui/CCursorHandler.h @@ -12,16 +12,97 @@ class CIntObject; class CAnimImage; struct SDL_Surface; struct SDL_Texture; +struct Point; -namespace ECursor +namespace Cursor { - enum ECursorTypes { ADVENTURE, COMBAT, DEFAULT, SPELLBOOK }; + enum class Type { + ADVENTURE, // set of various cursors for adventure map + COMBAT, // set of various cursors for combat + DEFAULT, // default arrow and hourglass cursors + SPELLBOOK // animated cursor for spellcasting + }; - enum EBattleCursors { COMBAT_BLOCKED, COMBAT_MOVE, COMBAT_FLY, COMBAT_SHOOT, - COMBAT_HERO, COMBAT_QUERY, COMBAT_POINTER, - //various attack frames - COMBAT_SHOOT_PENALTY = 15, COMBAT_SHOOT_CATAPULT, COMBAT_HEAL, - COMBAT_SACRIFICE, COMBAT_TELEPORT}; + enum class Default { + POINTER = 0, + //ARROW_COPY = 1, // probably unused + HOURGLASS = 2, + }; + + enum class Combat { + INVALID = -1, + + BLOCKED = 0, + MOVE = 1, + FLY = 2, + SHOOT = 3, + HERO = 4, + QUERY = 5, + POINTER = 6, + HIT_NORTHEAST = 7, + HIT_EAST = 8, + HIT_SOUTHEAST = 9, + HIT_SOUTHWEST = 10, + HIT_WEST = 11, + HIT_NORTHWEST = 12, + HIT_NORTH = 13, + HIT_SOUTH = 14, + SHOOT_PENALTY = 15, + SHOOT_CATAPULT = 16, + HEAL = 17, + SACRIFICE = 18, + TELEPORT = 19 + }; + + enum class Map { + POINTER = 0, + HOURGLASS = 1, + HERO = 2, + TOWN = 3, + T1_MOVE = 4, + T1_ATTACK = 5, + T1_SAIL = 6, + T1_DISEMBARK = 7, + T1_EXCHANGE = 8, + T1_VISIT = 9, + T2_MOVE = 10, + T2_ATTACK = 11, + T2_SAIL = 12, + T2_DISEMBARK = 13, + T2_EXCHANGE = 14, + T2_VISIT = 15, + T3_MOVE = 16, + T3_ATTACK = 17, + T3_SAIL = 18, + T3_DISEMBARK = 19, + T3_EXCHANGE = 20, + T3_VISIT = 21, + T4_MOVE = 22, + T4_ATTACK = 23, + T4_SAIL = 24, + T4_DISEMBARK = 25, + T4_EXCHANGE = 26, + T4_VISIT = 27, + T1_SAIL_VISIT = 28, + T2_SAIL_VISIT = 29, + T3_SAIL_VISIT = 30, + T4_SAIL_VISIT = 31, + SCROLL_NORTH = 32, + SCROLL_NORTHEAST = 33, + SCROLL_EAST = 34, + SCROLL_SOUTHEAST = 35, + SCROLL_SOUTH = 36, + SCROLL_SOUTHWEST = 37, + SCROLL_WEST = 38, + SCROLL_NORTHWEST = 39, + //POINTER_COPY = 40, // probably unused + TELEPORT = 41, + SCUTTLE_BOAT = 42 + }; + + enum class Spellcast { + SPELL = 0, + }; } /// handles mouse cursor @@ -45,19 +126,20 @@ class CCursorHandler final void shiftPos( int &x, int &y ); void updateTexture(); -public: + + /// Current cursor + Cursor::Type type; + size_t frame; + float frameTime; + + void changeGraphic(Cursor::Type type, size_t index); + /// position of cursor int xpos, ypos; - /// Current cursor - ECursor::ECursorTypes type; - size_t frame; - - /// inits cursorHandler - run only once, it's not memleak-proof (rev 1333) - void initCursor(); - - /// changes cursor graphic for type type (0 - adventure, 1 - combat, 2 - default, 3 - spellbook) and frame index (not used for type 3) - void changeGraphic(ECursor::ECursorTypes type, int index); +public: + CCursorHandler(); + ~CCursorHandler(); /** * Replaces the cursor with a custom image. @@ -67,6 +149,27 @@ public: */ void dragAndDropCursor (std::unique_ptr image); + /// Returns current position of the cursor + Point position() const; + + /// Changes cursor to specified index + void set(Cursor::Default index); + void set(Cursor::Map index); + void set(Cursor::Combat index); + void set(Cursor::Spellcast index); + + /// Returns current index of cursor + template + Index get() + { + assert((std::is_same::value )|| type != Cursor::Type::DEFAULT ); + assert((std::is_same::value )|| type != Cursor::Type::ADVENTURE ); + assert((std::is_same::value )|| type != Cursor::Type::COMBAT ); + assert((std::is_same::value )|| type != Cursor::Type::SPELLBOOK ); + + return static_cast(frame); + } + void render(); void hide() { showing=false; }; @@ -77,6 +180,4 @@ public: /// Move cursor to screen center void centerCursor(); - CCursorHandler(); - ~CCursorHandler(); }; diff --git a/client/gui/CGuiHandler.cpp b/client/gui/CGuiHandler.cpp index bff44534f..060ce0570 100644 --- a/client/gui/CGuiHandler.cpp +++ b/client/gui/CGuiHandler.cpp @@ -121,7 +121,7 @@ void CGuiHandler::pushInt(std::shared_ptr newInt) if(!listInt.empty()) listInt.front()->deactivate(); listInt.push_front(newInt); - CCS->curh->changeGraphic(ECursor::ADVENTURE, 0); + CCS->curh->set(Cursor::Map::POINTER); newInt->activate(); objsToBlit.push_back(newInt); totalRedraw(); @@ -238,7 +238,7 @@ void CGuiHandler::handleCurrentEvent() break; case SDLK_F9: - //not working yet since CClient::run remain locked after CBattleInterface removal + //not working yet since CClient::run remain locked after BattleInterface removal // if(LOCPLINT->battleInt) // { // GH.popInts(1); @@ -451,7 +451,7 @@ void CGuiHandler::renderFrame() bool acquiredTheLockOnPim = false; //for tracking whether pim mutex locking succeeded while(!terminate_cond->get() && !(acquiredTheLockOnPim = CPlayerInterface::pim->try_lock())) //try acquiring long until it succeeds or we are told to terminate - boost::this_thread::sleep(boost::posix_time::milliseconds(15)); + boost::this_thread::sleep(boost::posix_time::milliseconds(1)); if(acquiredTheLockOnPim) { @@ -489,7 +489,7 @@ CGuiHandler::CGuiHandler() statusbar = nullptr; // Creates the FPS manager and sets the framerate to 48 which is doubled the value of the original Heroes 3 FPS rate - mainFPSmng = new CFramerateManager(48); + mainFPSmng = new CFramerateManager(60); //do not init CFramerateManager here --AVS terminate_cond = new CondSh(false); @@ -623,8 +623,8 @@ void CFramerateManager::framerateDelay() currentTicks = SDL_GetTicks(); // recalculate timeElapsed for external calls via getElapsed() - // limit it to 1000 ms to avoid breaking animation in case of huge lag (e.g. triggered breakpoint) - timeElapsed = std::min(currentTicks - lastticks, 1000); + // limit it to 100 ms to avoid breaking animation in case of huge lag (e.g. triggered breakpoint) + timeElapsed = std::min(currentTicks - lastticks, 100); lastticks = SDL_GetTicks(); diff --git a/client/gui/CIntObject.cpp b/client/gui/CIntObject.cpp index 2d9158864..033dc46b5 100644 --- a/client/gui/CIntObject.cpp +++ b/client/gui/CIntObject.cpp @@ -267,7 +267,7 @@ void CIntObject::addChild(CIntObject * child, bool adjustPosition) children.push_back(child); child->parent_m = this; if(adjustPosition) - child->pos += pos; + child->pos += pos.topLeft(); if (!active && child->active) child->deactivate(); @@ -289,7 +289,7 @@ void CIntObject::removeChild(CIntObject * child, bool adjustPosition) children -= child; child->parent_m = nullptr; if(adjustPosition) - child->pos -= pos; + child->pos -= pos.topLeft(); } void CIntObject::redraw() diff --git a/client/gui/Canvas.cpp b/client/gui/Canvas.cpp index 8a643a1db..de300973f 100644 --- a/client/gui/Canvas.cpp +++ b/client/gui/Canvas.cpp @@ -17,24 +17,42 @@ #include "../Graphics.h" Canvas::Canvas(SDL_Surface * surface): - surface(surface) + surface(surface), + renderOffset(0,0) { surface->refcount++; } Canvas::Canvas(Canvas & other): - surface(other.surface) + surface(other.surface), + renderOffset(other.renderOffset) { surface->refcount++; } -Canvas::Canvas(const Point & size) +Canvas::Canvas(Canvas & other, const Rect & newClipRect): + Canvas(other) +{ + clipRect.emplace(); + SDL_GetClipRect(surface, clipRect.get_ptr()); + + Rect currClipRect = newClipRect + renderOffset; + SDL_SetClipRect(surface, &currClipRect); + + renderOffset += newClipRect.topLeft(); +} + +Canvas::Canvas(const Point & size): + renderOffset(0,0) { surface = CSDL_Ext::newSurface(size.x, size.y); } Canvas::~Canvas() { + if (clipRect) + SDL_SetClipRect(surface, clipRect.get_ptr()); + SDL_FreeSurface(surface); } @@ -42,33 +60,40 @@ void Canvas::draw(std::shared_ptr image, const Point & pos) { assert(image); if (image) - image->draw(surface, pos.x, pos.y); + image->draw(surface, renderOffset.x + pos.x, renderOffset.y + pos.y); } void Canvas::draw(std::shared_ptr image, const Point & pos, const Rect & sourceRect) { assert(image); if (image) - image->draw(surface, pos.x, pos.y, &sourceRect); + image->draw(surface, renderOffset.x + pos.x, renderOffset.y + pos.y, &sourceRect); +} + +void Canvas::draw(std::shared_ptr image, const Point & pos, const Rect & sourceRect, uint8_t alpha) +{ + assert(image); + if (image) + image->draw(surface, renderOffset.x + pos.x, renderOffset.y + pos.y, &sourceRect, alpha); } void Canvas::draw(Canvas & image, const Point & pos) { - blitAt(image.surface, pos.x, pos.y, surface); + blitAt(image.surface, renderOffset.x + pos.x, renderOffset.y + pos.y, surface); } void Canvas::drawLine(const Point & from, const Point & dest, const SDL_Color & colorFrom, const SDL_Color & colorDest) { - CSDL_Ext::drawLine(surface, from.x, from.y, dest.x, dest.y, colorFrom, colorDest); + CSDL_Ext::drawLine(surface, renderOffset.x + from.x, renderOffset.y + from.y, renderOffset.x + dest.x, renderOffset.y + dest.y, colorFrom, colorDest); } void Canvas::drawText(const Point & position, const EFonts & font, const SDL_Color & colorDest, ETextAlignment alignment, const std::string & text ) { switch (alignment) { - case ETextAlignment::TOPLEFT: return graphics->fonts[font]->renderTextLeft (surface, text, colorDest, position); - case ETextAlignment::CENTER: return graphics->fonts[font]->renderTextCenter(surface, text, colorDest, position); - case ETextAlignment::BOTTOMRIGHT: return graphics->fonts[font]->renderTextRight (surface, text, colorDest, position); + case ETextAlignment::TOPLEFT: return graphics->fonts[font]->renderTextLeft (surface, text, colorDest, renderOffset + position); + case ETextAlignment::CENTER: return graphics->fonts[font]->renderTextCenter(surface, text, colorDest, renderOffset + position); + case ETextAlignment::BOTTOMRIGHT: return graphics->fonts[font]->renderTextRight (surface, text, colorDest, renderOffset + position); } } @@ -76,9 +101,9 @@ void Canvas::drawText(const Point & position, const EFonts & font, const SDL_Col { switch (alignment) { - case ETextAlignment::TOPLEFT: return graphics->fonts[font]->renderTextLinesLeft (surface, text, colorDest, position); - case ETextAlignment::CENTER: return graphics->fonts[font]->renderTextLinesCenter(surface, text, colorDest, position); - case ETextAlignment::BOTTOMRIGHT: return graphics->fonts[font]->renderTextLinesRight (surface, text, colorDest, position); + case ETextAlignment::TOPLEFT: return graphics->fonts[font]->renderTextLinesLeft (surface, text, colorDest, renderOffset + position); + case ETextAlignment::CENTER: return graphics->fonts[font]->renderTextLinesCenter(surface, text, colorDest, renderOffset + position); + case ETextAlignment::BOTTOMRIGHT: return graphics->fonts[font]->renderTextLinesRight (surface, text, colorDest, renderOffset + position); } } diff --git a/client/gui/Canvas.h b/client/gui/Canvas.h index 46c24c87b..2a156ea3a 100644 --- a/client/gui/Canvas.h +++ b/client/gui/Canvas.h @@ -19,8 +19,15 @@ enum EFonts : int; /// Class that represents surface for drawing on class Canvas { + /// Target surface SDL_Surface * surface; + /// Clip rect that was in use on surface originally and needs to be restored on destruction + boost::optional clipRect; + + /// Current rendering area offset, all rendering operations will be moved into selected area + Point renderOffset; + Canvas & operator = (Canvas & other) = delete; public: @@ -30,6 +37,9 @@ public: /// copy contructor Canvas(Canvas & other); + /// creates canvas that only covers specified subsection of a surface + Canvas(Canvas & other, const Rect & clipRect); + /// constructs canvas of specified size Canvas(const Point & size); @@ -41,6 +51,10 @@ public: /// renders section of image bounded by sourceRect at specified position void draw(std::shared_ptr image, const Point & pos, const Rect & sourceRect); + /// renders section of image bounded by sourceRect at specified position at specific transparency value + void draw(std::shared_ptr image, const Point & pos, const Rect & sourceRect, uint8_t alpha); + + /// renders another canvas onto this canvas void draw(Canvas & image, const Point & pos); diff --git a/client/gui/ColorFilter.cpp b/client/gui/ColorFilter.cpp new file mode 100644 index 000000000..52713d8af --- /dev/null +++ b/client/gui/ColorFilter.cpp @@ -0,0 +1,162 @@ +/* + * Canvas.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "ColorFilter.h" + +#include + +#include "../../lib/JsonNode.h" + +SDL_Color ColorFilter::shiftColor(const SDL_Color & in) const +{ + int r_out = in.r * r.r + in.g * r.g + in.b * r.b + 255 * r.a; + int g_out = in.r * g.r + in.g * g.g + in.b * g.b + 255 * g.a; + int b_out = in.r * b.r + in.g * b.g + in.b * b.b + 255 * b.a; + int a_out = in.a * a; + + vstd::abetween(r_out, 0, 255); + vstd::abetween(g_out, 0, 255); + vstd::abetween(b_out, 0, 255); + vstd::abetween(a_out, 0, 255); + + return { + static_cast(r_out), + static_cast(g_out), + static_cast(b_out), + static_cast(a_out) + }; +} + +bool ColorFilter::operator != (const ColorFilter & other) const +{ + return !(this->operator==(other)); +} + +bool ColorFilter::operator == (const ColorFilter & other) const +{ + return + r.r == other.r.r && r.g && other.r.g && r.b == other.r.b && r.a == other.r.a && + g.r == other.g.r && g.g && other.g.g && g.b == other.g.b && g.a == other.g.a && + b.r == other.b.r && b.g && other.b.g && b.b == other.b.b && b.a == other.b.a && + a == other.a; +} + +ColorFilter ColorFilter::genEmptyShifter( ) +{ + return genAlphaShifter( 1.f); +} + +ColorFilter ColorFilter::genAlphaShifter( float alpha ) +{ + return genMuxerShifter( + { 1.f, 0.f, 0.f, 0.f }, + { 0.f, 1.f, 0.f, 0.f }, + { 0.f, 0.f, 1.f, 0.f }, + alpha); +} + +ColorFilter ColorFilter::genRangeShifter( float minR, float minG, float minB, float maxR, float maxG, float maxB ) +{ + return genMuxerShifter( + { maxR - minR, 0.f, 0.f, minR }, + { 0.f, maxG - minG, 0.f, minG }, + { 0.f, 0.f, maxB - minB, minB }, + 1.f); +} + +ColorFilter ColorFilter::genMuxerShifter( ChannelMuxer r, ChannelMuxer g, ChannelMuxer b, float a ) +{ + return ColorFilter(r, g, b, a); +} + +ColorFilter ColorFilter::genInterpolated(const ColorFilter & left, const ColorFilter & right, float power) +{ + auto lerpMuxer = [=]( const ChannelMuxer & left, const ChannelMuxer & right ) -> ChannelMuxer + { + return { + vstd::lerp(left.r, right.r, power), + vstd::lerp(left.g, right.g, power), + vstd::lerp(left.b, right.b, power), + vstd::lerp(left.a, right.a, power) + }; + }; + + return genMuxerShifter( + lerpMuxer(left.r, right.r), + lerpMuxer(left.g, right.g), + lerpMuxer(left.b, right.b), + vstd::lerp(left.a, right.a, power) + ); +} + +ColorFilter ColorFilter::genCombined(const ColorFilter & left, const ColorFilter & right) +{ + // matrix multiplication + ChannelMuxer r{ + left.r.r * right.r.r + left.g.r * right.r.g + left.b.r * right.r.b, + left.r.g * right.r.r + left.g.g * right.r.g + left.b.g * right.r.b, + left.r.b * right.r.r + left.g.b * right.r.g + left.b.b * right.r.b, + left.r.a * right.r.r + left.g.a * right.r.g + left.b.a * right.r.b + 1.f * right.r.a, + }; + + ChannelMuxer g{ + left.r.r * right.g.r + left.g.r * right.g.g + left.b.r * right.g.b, + left.r.g * right.g.r + left.g.g * right.g.g + left.b.g * right.g.b, + left.r.b * right.g.r + left.g.b * right.g.g + left.b.b * right.g.b, + left.r.a * right.g.r + left.g.a * right.g.g + left.b.a * right.g.b + 1.f * right.g.a, + }; + + ChannelMuxer b{ + left.r.r * right.b.r + left.g.r * right.b.g + left.b.r * right.b.b, + left.r.g * right.b.r + left.g.g * right.b.g + left.b.g * right.b.b, + left.r.b * right.b.r + left.g.b * right.b.g + left.b.b * right.b.b, + left.r.a * right.b.r + left.g.a * right.b.g + left.b.a * right.b.b + 1.f * right.b.a, + }; + + float a = left.a * right.a; + return genMuxerShifter(r,g,b,a); +} + +ColorFilter ColorFilter::genFromJson(const JsonNode & entry) +{ + ChannelMuxer r{ 1.f, 0.f, 0.f, 0.f }; + ChannelMuxer g{ 0.f, 1.f, 0.f, 0.f }; + ChannelMuxer b{ 0.f, 0.f, 1.f, 0.f }; + float a{ 1.0}; + + if (!entry["red"].isNull()) + { + r.r = entry["red"].Vector()[0].Float(); + r.g = entry["red"].Vector()[1].Float(); + r.b = entry["red"].Vector()[2].Float(); + r.a = entry["red"].Vector()[3].Float(); + } + + if (!entry["red"].isNull()) + { + g.r = entry["green"].Vector()[0].Float(); + g.g = entry["green"].Vector()[1].Float(); + g.b = entry["green"].Vector()[2].Float(); + g.a = entry["green"].Vector()[3].Float(); + } + + if (!entry["red"].isNull()) + { + b.r = entry["blue"].Vector()[0].Float(); + b.g = entry["blue"].Vector()[1].Float(); + b.b = entry["blue"].Vector()[2].Float(); + b.a = entry["blue"].Vector()[3].Float(); + } + + if (!entry["alpha"].isNull()) + a = entry["alpha"].Float(); + + return genMuxerShifter(r,g,b,a); +} diff --git a/client/gui/ColorFilter.h b/client/gui/ColorFilter.h new file mode 100644 index 000000000..bfca82695 --- /dev/null +++ b/client/gui/ColorFilter.h @@ -0,0 +1,66 @@ +/* + * ColorFilter.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 + +struct SDL_Color; + +VCMI_LIB_NAMESPACE_BEGIN +class JsonNode; +VCMI_LIB_NAMESPACE_END + +/// Base class for applying palette transformation on images +class ColorFilter +{ + struct ChannelMuxer { + float r, g, b, a; + }; + + ChannelMuxer r; + ChannelMuxer g; + ChannelMuxer b; + float a; + + ColorFilter(ChannelMuxer r, ChannelMuxer g, ChannelMuxer b, float a): + r(r), g(g), b(b), a(a) + {} +public: + SDL_Color shiftColor(const SDL_Color & in) const; + + bool operator == (const ColorFilter & other) const; + bool operator != (const ColorFilter & other) const; + + /// Generates empty object that has no effect on image + static ColorFilter genEmptyShifter(); + + /// Generates object that changes alpha (transparency) of the image + static ColorFilter genAlphaShifter( float alpha ); + + /// Generates object that transforms each channel independently + static ColorFilter genRangeShifter( float minR, float minG, float minB, float maxR, float maxG, float maxB ); + + /// Generates object that performs arbitrary mixing between any channels + static ColorFilter genMuxerShifter( ChannelMuxer r, ChannelMuxer g, ChannelMuxer b, float a ); + + /// Combines 2 mixers into a single object + static ColorFilter genCombined(const ColorFilter & left, const ColorFilter & right); + + /// Scales down strength of a shifter to a specified factor + static ColorFilter genInterpolated(const ColorFilter & left, const ColorFilter & right, float power); + + /// Generates object using supplied Json config + static ColorFilter genFromJson(const JsonNode & entry); +}; + +struct ColorMuxerEffect +{ + std::vector filters; + std::vector timePoints; +}; diff --git a/client/gui/Geometries.cpp b/client/gui/Geometries.cpp index e040f7d11..04a31e7c1 100644 --- a/client/gui/Geometries.cpp +++ b/client/gui/Geometries.cpp @@ -11,6 +11,11 @@ #include "Geometries.h" #include "../CMT.h" #include +#include "../../lib/int3.h" + +Point::Point(const int3 &a) + :x(a.x),y(a.y) +{} Point::Point(const SDL_MouseMotionEvent &a) :x(a.x),y(a.y) @@ -21,7 +26,7 @@ Rect Rect::createCentered( int w, int h ) return Rect(screen->w/2 - w/2, screen->h/2 - h/2, w, h); } -Rect Rect::around(const Rect &r, int width) /*creates rect around another */ +Rect Rect::around(const Rect &r, int width) { return Rect(r.x - width, r.y - width, r.w + width * 2, r.h + width * 2); } @@ -30,3 +35,4 @@ Rect Rect::centerIn(const Rect &r) { return Rect(r.x + (r.w - w) / 2, r.y + (r.h - h) / 2, w, h); } + diff --git a/client/gui/Geometries.h b/client/gui/Geometries.h index b2dfd462f..b1e05d672 100644 --- a/client/gui/Geometries.h +++ b/client/gui/Geometries.h @@ -9,13 +9,16 @@ */ #pragma once -#include -#include "../../lib/int3.h" +#include enum class ETextAlignment {TOPLEFT, CENTER, BOTTOMRIGHT}; struct SDL_MouseMotionEvent; +VCMI_LIB_NAMESPACE_BEGIN +class int3; +VCMI_LIB_NAMESPACE_END + // A point with x/y coordinate, used mostly for graphic rendering struct Point { @@ -26,13 +29,14 @@ struct Point { x = y = 0; }; + Point(int X, int Y) :x(X),y(Y) {}; - Point(const int3 &a) - :x(a.x),y(a.y) - {} - Point(const SDL_MouseMotionEvent &a); + + Point(const int3 &a); + + explicit Point(const SDL_MouseMotionEvent &a); template Point operator+(const T &b) const @@ -73,10 +77,7 @@ struct Point y -= b.y; return *this; } - bool operator<(const Point &b) const //product order - { - return x < b.x && y < b.y; - } + template Point& operator=(const T &t) { x = t.x; @@ -96,7 +97,7 @@ struct Point /// Rectangle class, which have a position and a size struct Rect : public SDL_Rect { - Rect()//default c-tor + Rect() { x = y = w = h = -1; } @@ -121,60 +122,59 @@ struct Rect : public SDL_Rect w = r.w; h = r.h; } - Rect(const Rect& r) : Rect(static_cast(r)) - {} - explicit Rect(const SDL_Surface * const &surf) - { - x = y = 0; - w = surf->w; - h = surf->h; - } + Rect(const Rect& r) = default; Rect centerIn(const Rect &r); static Rect createCentered(int w, int h); - static Rect around(const Rect &r, int width = 1); //creates rect around another + static Rect around(const Rect &r, int width = 1); - bool isIn(int qx, int qy) const //determines if given point lies inside rect + bool isIn(int qx, int qy) const { if (qx > x && qxy && qy Rect operator-(const T &t) - { - return Rect(x - t.x, y - t.y, w, h); - } + Rect operator&(const Rect &p) const //rect intersection { bool intersect = true; diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index a67570c15..cb9b6854e 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -13,6 +13,7 @@ #include "InterfaceObjectConfigurable.h" #include "../CGameInfo.h" +#include "../CPlayerInterface.h" #include "../gui/CAnimation.h" #include "../gui/CGuiHandler.h" #include "../widgets/CComponent.h" @@ -106,27 +107,9 @@ std::string InterfaceObjectConfigurable::readText(const JsonNode & config) const if(config.isNull()) return ""; - if(config.isNumber()) - { - logGlobal->debug("Reading text from generaltext handler id:%d", config.Integer()); - return CGI->generaltexth->allTexts[config.Integer()]; - } - - const std::string delimiter = "/"; std::string s = config.String(); logGlobal->debug("Reading text from translations by key: %s", s); - JsonNode translated = CGI->generaltexth->localizedTexts; - for(size_t p = s.find(delimiter); p != std::string::npos; p = s.find(delimiter)) - { - translated = translated[s.substr(0, p)]; - s.erase(0, p + delimiter.length()); - } - if(s == config.String()) - { - logGlobal->warn("Reading non-translated text: %s", s); - return s; - } - return translated[s].String(); + return CGI->generaltexth->translate(s); } Point InterfaceObjectConfigurable::readPosition(const JsonNode & config) const @@ -211,12 +194,6 @@ std::pair InterfaceObjectConfigurable::readHintText(co std::pair result; if(!config.isNull()) { - if(config.isNumber()) - { - logGlobal->debug("Reading hint text (zelp) from generaltext handler id:%d", config.Integer()); - return CGI->generaltexth->zelp[config.Integer()]; - } - if(config.getType() == JsonNode::JsonType::DATA_STRUCT) { result.first = readText(config["hover"]); @@ -225,8 +202,9 @@ std::pair InterfaceObjectConfigurable::readHintText(co } if(config.getType() == JsonNode::JsonType::DATA_STRING) { - logGlobal->debug("Reading non-translated hint: %s", config.String()); - result.first = result.second = config.String(); + logGlobal->debug("Reading hint text (help) from generaltext handler:%sd", config.String()); + result.first = CGI->generaltexth->translate( config.String(), "hover"); + result.second = CGI->generaltexth->translate( config.String(), "help"); } } return result; @@ -257,6 +235,9 @@ std::shared_ptr InterfaceObjectConfigurable::buildPicture(const JsonNo auto pic = std::make_shared(image, position.x, position.y); if(!config["visible"].isNull()) pic->visible = config["visible"].Bool(); + + if ( config["playerColored"].Bool() && LOCPLINT) + pic->colorize(LOCPLINT->playerID); return pic; } @@ -299,8 +280,8 @@ std::shared_ptr InterfaceObjectConfigurable::buildToggleButton(co logGlobal->debug("Building widget CToggleButton"); auto position = readPosition(config["position"]); auto image = config["image"].String(); - auto zelp = readHintText(config["zelp"]); - auto button = std::make_shared(position, image, zelp); + auto help = readHintText(config["help"]); + auto button = std::make_shared(position, image, help); if(!config["selected"].isNull()) button->setSelected(config["selected"].Bool()); if(!config["imageOrder"].isNull()) @@ -319,8 +300,8 @@ std::shared_ptr InterfaceObjectConfigurable::buildButton(const JsonNode logGlobal->debug("Building widget CButton"); auto position = readPosition(config["position"]); auto image = config["image"].String(); - auto zelp = readHintText(config["zelp"]); - auto button = std::make_shared(position, image, zelp); + auto help = readHintText(config["help"]); + auto button = std::make_shared(position, image, help); if(!config["items"].isNull()) { for(const auto & item : config["items"].Vector()) diff --git a/client/gui/SDL_Extensions.cpp b/client/gui/SDL_Extensions.cpp index 7eb4d9a8f..8629181f0 100644 --- a/client/gui/SDL_Extensions.cpp +++ b/client/gui/SDL_Extensions.cpp @@ -349,12 +349,12 @@ static void drawLineX(SDL_Surface * sur, int x1, int y1, int x2, int y2, const S for(int x = x1; x <= x2; x++) { float f = float(x - x1) / float(x2 - x1); - int y = CSDL_Ext::lerp(y1, y2, f); + int y = vstd::lerp(y1, y2, f); - uint8_t r = CSDL_Ext::lerp(color1.r, color2.r, f); - uint8_t g = CSDL_Ext::lerp(color1.g, color2.g, f); - uint8_t b = CSDL_Ext::lerp(color1.b, color2.b, f); - uint8_t a = CSDL_Ext::lerp(color1.a, color2.a, f); + uint8_t r = vstd::lerp(color1.r, color2.r, f); + uint8_t g = vstd::lerp(color1.g, color2.g, f); + uint8_t b = vstd::lerp(color1.b, color2.b, f); + uint8_t a = vstd::lerp(color1.a, color2.a, f); Uint8 *p = CSDL_Ext::getPxPtr(sur, x, y); ColorPutter<4, 0>::PutColor(p, r,g,b,a); @@ -366,12 +366,12 @@ static void drawLineY(SDL_Surface * sur, int x1, int y1, int x2, int y2, const S for(int y = y1; y <= y2; y++) { float f = float(y - y1) / float(y2 - y1); - int x = CSDL_Ext::lerp(x1, x2, f); + int x = vstd::lerp(x1, x2, f); - uint8_t r = CSDL_Ext::lerp(color1.r, color2.r, f); - uint8_t g = CSDL_Ext::lerp(color1.g, color2.g, f); - uint8_t b = CSDL_Ext::lerp(color1.b, color2.b, f); - uint8_t a = CSDL_Ext::lerp(color1.a, color2.a, f); + uint8_t r = vstd::lerp(color1.r, color2.r, f); + uint8_t g = vstd::lerp(color1.g, color2.g, f); + uint8_t b = vstd::lerp(color1.b, color2.b, f); + uint8_t a = vstd::lerp(color1.a, color2.a, f); Uint8 *p = CSDL_Ext::getPxPtr(sur, x, y); ColorPutter<4, 0>::PutColor(p, r,g,b,a); @@ -919,7 +919,6 @@ void CSDL_Ext::setDefaultColorKeyPresize(SDL_Surface * surface) } - template SDL_Surface * CSDL_Ext::createSurfaceWithBpp<2>(int, int); template SDL_Surface * CSDL_Ext::createSurfaceWithBpp<3>(int, int); template SDL_Surface * CSDL_Ext::createSurfaceWithBpp<4>(int, int); diff --git a/client/gui/SDL_Extensions.h b/client/gui/SDL_Extensions.h index dee447645..be7926ca6 100644 --- a/client/gui/SDL_Extensions.h +++ b/client/gui/SDL_Extensions.h @@ -49,12 +49,6 @@ inline bool isShiftKeyDown() } namespace CSDL_Ext { - template - Int lerp(Int a, Int b, float f) - { - return a + std::round((b - a) * f); - } - //todo: should this better be assignment operator? STRONG_INLINE void colorAssign(SDL_Color & dest, const SDL_Color & source) { @@ -158,57 +152,6 @@ struct ColorPutter typedef void (*BlitterWithRotationVal)(SDL_Surface *src,SDL_Rect srcRect, SDL_Surface * dst, SDL_Rect dstRect, ui8 rotation); -/// Base class for applying palette transformation on images -class ColorShifter -{ -public: - ~ColorShifter() = default; - virtual SDL_Color shiftColor(SDL_Color input) const = 0; -}; - -/// Generic class for palette transformation -/// formula: -/// result = input * factor + added -class ColorShifterMultiplyAndAdd : public ColorShifter -{ - SDL_Color added; - SDL_Color factor; - -public: - ColorShifterMultiplyAndAdd(SDL_Color factor, SDL_Color added) : - factor(factor), - added(added) - {} - - SDL_Color shiftColor(SDL_Color input) const override - { - return { - uint8_t(std::min(255.f, std::round(input.r * float(factor.r) / 255.f + added.r))), - uint8_t(std::min(255.f, std::round(input.g * float(factor.g) / 255.f + added.g))), - uint8_t(std::min(255.f, std::round(input.b * float(factor.b) / 255.f + added.b))), - uint8_t(std::min(255.f, std::round(input.a * float(factor.a) / 255.f + added.a))) - }; - } -}; - -/// Color shifter that allows to specify color to be excempt from changes -class ColorShifterMultiplyAndAddExcept : public ColorShifterMultiplyAndAdd -{ - SDL_Color ignored; -public: - ColorShifterMultiplyAndAddExcept(SDL_Color factor, SDL_Color added, SDL_Color ignored) : - ColorShifterMultiplyAndAdd(factor, added), - ignored(ignored) - {} - - SDL_Color shiftColor(SDL_Color input) const override - { - if ( input.r == ignored.r && input.g == ignored.g && input.b == ignored.b && input.a == ignored.a) - return input; - return ColorShifterMultiplyAndAdd::shiftColor(input); - } -}; - namespace CSDL_Ext { /// helper that will safely set and un-set ClipRect for SDL_Surface diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp index 8fd5b0cc0..d9210285c 100644 --- a/client/lobby/CBonusSelection.cpp +++ b/client/lobby/CBonusSelection.cpp @@ -92,7 +92,8 @@ CBonusSelection::CBonusSelection() flagbox = std::make_shared(Rect(486, 407, 335, 23)); std::vector difficulty; - boost::split(difficulty, CGI->generaltexth->allTexts[492], boost::is_any_of(" ")); + std::string difficultyString = CGI->generaltexth->allTexts[492]; + boost::split(difficulty, difficultyString, boost::is_any_of(" ")); labelDifficulty = std::make_shared(689, 432, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, difficulty.back()); for(size_t b = 0; b < difficultyIcons.size(); ++b) @@ -141,8 +142,6 @@ void CBonusSelection::loadPositionsOfGraphics() idx++; } - - assert(idx == CGI->generaltexth->campaignMapNames.size()); } void CBonusSelection::createBonusesIcons() diff --git a/client/lobby/CLobbyScreen.cpp b/client/lobby/CLobbyScreen.cpp index 22a26e244..dbe54e0cb 100644 --- a/client/lobby/CLobbyScreen.cpp +++ b/client/lobby/CLobbyScreen.cpp @@ -136,10 +136,10 @@ void CLobbyScreen::startScenario(bool allowOnlyAI) logGlobal->error("Exception during startScenario: %s", e.what()); if(std::string(e.what()) == "ExceptionNoHuman") - CInfoWindow::showInfoDialog(std::ref(CGI->generaltexth->allTexts[530]), CInfoWindow::TCompsInfo(), PlayerColor(1)); + CInfoWindow::showInfoDialog(CGI->generaltexth->allTexts[530], CInfoWindow::TCompsInfo(), PlayerColor(1)); if(std::string(e.what()) == "ExceptionNoTemplate") - CInfoWindow::showInfoDialog(std::ref(CGI->generaltexth->allTexts[751]), CInfoWindow::TCompsInfo(), PlayerColor(1)); + CInfoWindow::showInfoDialog(CGI->generaltexth->allTexts[751], CInfoWindow::TCompsInfo(), PlayerColor(1)); } catch(...) { diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index 2edcaf50c..71576115b 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -307,7 +307,7 @@ CChatBox::CChatBox(const Rect & rect) : CIntObject(KEYBOARD | TEXTINPUT) { OBJ_CONSTRUCTION; - pos += rect; + pos += rect.topLeft(); captureAllKeys = true; type |= REDRAW_PARENT; @@ -341,7 +341,7 @@ void CChatBox::addNewMessage(const std::string & text) CFlagBox::CFlagBox(const Rect & rect) : CIntObject(RCLICK) { - pos += rect; + pos += rect.topLeft(); pos.w = rect.w; pos.h = rect.h; OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index 6a60d5a14..b268b764b 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -410,9 +410,7 @@ TemplatesDropBox::TemplatesDropBox(RandomMapTab & randomMapTab, int3 size): addCallback("sliderMove", std::bind(&TemplatesDropBox::sliderMove, this, std::placeholders::_1)); OBJ_CONSTRUCTION; - pos = randomMapTab.pos.topLeft(); - pos.w = randomMapTab.pos.w; - pos.h = randomMapTab.pos.h; + pos = randomMapTab.pos; build(config); diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 870627580..d8d89d8d0 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -99,8 +99,8 @@ bool mapSorter::operator()(const std::shared_ptr aaa, const std::share switch(sortBy) { case _numOfMaps: //by number of maps in campaign - return CGI->generaltexth->campaignRegionNames[aaa->campaignHeader->mapVersion].size() < - CGI->generaltexth->campaignRegionNames[bbb->campaignHeader->mapVersion].size(); + return CGI->generaltexth->getCampaignLength(aaa->campaignHeader->mapVersion) < + CGI->generaltexth->getCampaignLength(bbb->campaignHeader->mapVersion); break; case _name: //by name return boost::ilexicographical_compare(aaa->campaignHeader->name, bbb->campaignHeader->name); @@ -660,7 +660,7 @@ void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool sel iconLossCondition->disable(); labelNumberOfCampaignMaps->enable(); std::ostringstream ostr(std::ostringstream::out); - ostr << CGI->generaltexth->campaignRegionNames[info->campaignHeader->mapVersion].size(); + ostr << CGI->generaltexth->getCampaignLength(info->campaignHeader->mapVersion); labelNumberOfCampaignMaps->setText(ostr.str()); labelNumberOfCampaignMaps->setColor(color); } diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 99aab3d59..3b265d0cd 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -205,7 +205,7 @@ static std::function genCommand(CMenuScreen * menu, std::vectorgeneraltexth->allTexts[69]), std::vector>(), do_quit, 0, PlayerColor(1)); + return std::bind(CInfoWindow::showYesNoDialog, CGI->generaltexth->allTexts[69], std::vector>(), do_quit, 0, PlayerColor(1)); } break; case 5: //highscores diff --git a/client/widgets/AdventureMapClasses.cpp b/client/widgets/AdventureMapClasses.cpp index 8c4098b5d..bd1923d0f 100644 --- a/client/widgets/AdventureMapClasses.cpp +++ b/client/widgets/AdventureMapClasses.cpp @@ -237,7 +237,7 @@ void CHeroList::CHeroItem::open() void CHeroList::CHeroItem::showTooltip() { - CRClickPopup::createAndPush(hero, GH.current->motion); + CRClickPopup::createAndPush(hero, Point(GH.current->motion)); } std::string CHeroList::CHeroItem::getHoverText() @@ -329,7 +329,7 @@ void CTownList::CTownItem::open() void CTownList::CTownItem::showTooltip() { - CRClickPopup::createAndPush(town, GH.current->motion); + CRClickPopup::createAndPush(town, Point(GH.current->motion)); } std::string CTownList::CTownItem::getHoverText() @@ -975,11 +975,8 @@ void CInGameConsole::show(SDL_Surface * to) boost::unique_lock lock(texts_mx); for(auto it = texts.begin(); it != texts.end(); ++it, ++number) { - Point leftBottomCorner(0, screen->h); - if(LOCPLINT->battleInt) - { - leftBottomCorner = LOCPLINT->battleInt->pos.bottomLeft(); - } + Point leftBottomCorner(0, pos.h); + graphics->fonts[FONT_MEDIUM]->renderTextLeft(to, it->first, Colors::GREEN, Point(leftBottomCorner.x + 50, leftBottomCorner.y - (int)texts.size() * 20 - 80 + number*20)); diff --git a/client/widgets/Buttons.cpp b/client/widgets/Buttons.cpp index 98bb638d7..aace018d3 100644 --- a/client/widgets/Buttons.cpp +++ b/client/widgets/Buttons.cpp @@ -22,6 +22,7 @@ #include "../gui/CGuiHandler.h" #include "../windows/InfoWindows.h" #include "../../lib/CConfigHandler.h" +#include "../../lib/CGeneralTextHandler.h" void CButton::update() { @@ -285,9 +286,12 @@ std::pair CButton::tooltip() return std::pair(); } -std::pair CButton::tooltip(const JsonNode & localizedTexts) +std::pair CButton::tooltipLocalized(const std::string & key) { - return std::make_pair(localizedTexts["label"].String(), localizedTexts["help"].String()); + return std::make_pair( + CGI->generaltexth->translate(key, "hover"), + CGI->generaltexth->translate(key, "help") + ); } std::pair CButton::tooltip(const std::string & hover, const std::string & help) @@ -457,10 +461,10 @@ int CToggleGroup::getSelected() const return selectedID; } -CVolumeSlider::CVolumeSlider(const Point & position, const std::string & defName, const int value, const std::pair * const help) +CVolumeSlider::CVolumeSlider(const Point & position, const std::string & defName, const int value, ETooltipMode mode) : CIntObject(LCLICK | RCLICK | WHEEL), value(value), - helpHandlers(help) + mode(mode) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); animImage = std::make_shared(std::make_shared(defName), 0, 0, position.x, position.y), @@ -517,8 +521,10 @@ void CVolumeSlider::clickRight(tribool down, bool previousState) { double px = GH.current->motion.x - pos.x; int index = static_cast(px / static_cast(pos.w) * animImage->size()); - std::string hoverText = helpHandlers[index].first; - std::string helpBox = helpHandlers[index].second; + + size_t helpIndex = index + (mode == MUSIC ? 326 : 336); + std::string helpBox = CGI->generaltexth->translate("core.help", helpIndex, "help" ); + if(!helpBox.empty()) CRClickPopup::createAndPush(helpBox); if(GH.statusbar) diff --git a/client/widgets/Buttons.h b/client/widgets/Buttons.h index 872cc448e..4c7a261f3 100644 --- a/client/widgets/Buttons.h +++ b/client/widgets/Buttons.h @@ -115,7 +115,7 @@ public: /// generates tooltip that can be passed into constructor static std::pair tooltip(); - static std::pair tooltip(const JsonNode & localizedTexts); + static std::pair tooltipLocalized(const std::string & key); static std::pair tooltip(const std::string & hover, const std::string & help = ""); }; @@ -191,19 +191,25 @@ public: /// A typical slider for volume with an animated indicator class CVolumeSlider : public CIntObject { +public: + enum ETooltipMode + { + MUSIC, + SOUND + }; + +private: int value; CFunctionList onChange; std::shared_ptr animImage; - const std::pair * const helpHandlers; + ETooltipMode mode; void setVolume(const int v); public: - /// @param position coordinates of slider /// @param defName name of def animation for slider /// @param value initial value for volume - /// @param help pointer to first helptext of slider - CVolumeSlider(const Point & position, const std::string & defName, const int value, - const std::pair * const help); + /// @param mode that determines tooltip texts + CVolumeSlider(const Point & position, const std::string & defName, const int value, ETooltipMode mode); void moveTo(int id); void addCallback(std::function callback); diff --git a/client/widgets/CArtifactHolder.cpp b/client/widgets/CArtifactHolder.cpp index 5b4690ed7..64fb70f80 100644 --- a/client/widgets/CArtifactHolder.cpp +++ b/client/widgets/CArtifactHolder.cpp @@ -126,7 +126,7 @@ void CHeroArtPlace::clickLeft(tribool down, bool previousState) if(ourArt && !down && previousState && !ourOwner->commonInfo->src.AOH) { if(ourArt->artType->id == ArtifactID::SPELLBOOK) - GH.pushIntT(ourOwner->curHero, LOCPLINT, LOCPLINT->battleInt); + GH.pushIntT(ourOwner->curHero, LOCPLINT, LOCPLINT->battleInt.get()); } if (!down && previousState) @@ -1008,7 +1008,7 @@ CCommanderArtPlace::CCommanderArtPlace(Point position, const CGHeroInstance * co void CCommanderArtPlace::clickLeft(tribool down, bool previousState) { if (ourArt && text.size() && down) - LOCPLINT->showYesNoDialog(CGI->generaltexth->localizedTexts["commanderWindow"]["artifactMessage"].String(), [this](){ returnArtToHeroCallback(); }, [](){}); + LOCPLINT->showYesNoDialog(CGI->generaltexth->translate("vcmi.commanderWindow.artifactMessage"), [this](){ returnArtToHeroCallback(); }, [](){}); } void CCommanderArtPlace::clickRight(tribool down, bool previousState) diff --git a/client/widgets/CComponent.cpp b/client/widgets/CComponent.cpp index d6dcf2287..14e166963 100644 --- a/client/widgets/CComponent.cpp +++ b/client/widgets/CComponent.cpp @@ -443,7 +443,7 @@ CComponentBox::CComponentBox(std::vector> _component components(_components) { type |= REDRAW_PARENT; - pos = position + pos; + pos = position + pos.topLeft(); placeComponents(false); } @@ -452,7 +452,7 @@ CComponentBox::CComponentBox(std::vector> onSelect(_onSelect) { type |= REDRAW_PARENT; - pos = position + pos; + pos = position + pos.topLeft(); placeComponents(true); assert(!components.empty()); diff --git a/client/widgets/Images.cpp b/client/widgets/Images.cpp index 84d9fb63b..db4c7925e 100644 --- a/client/widgets/Images.cpp +++ b/client/widgets/Images.cpp @@ -167,7 +167,7 @@ void CPicture::scaleTo(Point size) void CPicture::createSimpleRect(const Rect &r, bool screenFormat, ui32 color) { - pos += r; + pos += r.topLeft(); pos.w = r.w; pos.h = r.h; if(screenFormat) @@ -466,8 +466,8 @@ void CShowableAnim::rotate(bool on, bool vertical) flags &= ~flag; } -CCreatureAnim::CCreatureAnim(int x, int y, std::string name, ui8 flags, EAnimType type): - CShowableAnim(x,y,name,flags,4,type) +CCreatureAnim::CCreatureAnim(int x, int y, std::string name, ui8 flags, ECreatureAnimType type): + CShowableAnim(x,y,name,flags,4,size_t(type)) { xOffset = 0; yOffset = 0; @@ -475,45 +475,60 @@ CCreatureAnim::CCreatureAnim(int x, int y, std::string name, ui8 flags, EAnimTyp void CCreatureAnim::loopPreview(bool warMachine) { - std::vector available; + std::vector available; + + static const ECreatureAnimType creaPreviewList[] = { + ECreatureAnimType::HOLDING, + ECreatureAnimType::HITTED, + ECreatureAnimType::DEFENCE, + ECreatureAnimType::ATTACK_FRONT, + ECreatureAnimType::SPECIAL_FRONT + }; + static const ECreatureAnimType machPreviewList[] = { + ECreatureAnimType::HOLDING, + ECreatureAnimType::MOVING, + ECreatureAnimType::SHOOT_UP, + ECreatureAnimType::SHOOT_FRONT, + ECreatureAnimType::SHOOT_DOWN + }; - static const EAnimType creaPreviewList[] = {HOLDING, HITTED, DEFENCE, ATTACK_FRONT, CAST_FRONT}; - static const EAnimType machPreviewList[] = {HOLDING, MOVING, SHOOT_UP, SHOOT_FRONT, SHOOT_DOWN}; auto & previewList = warMachine ? machPreviewList : creaPreviewList; for (auto & elem : previewList) - if (anim->size(elem)) + if (anim->size(size_t(elem))) available.push_back(elem); size_t rnd = CRandomGenerator::getDefault().nextInt((int)available.size() * 2 - 1); if (rnd >= available.size()) { - EAnimType type; - if ( anim->size(MOVING) == 0 )//no moving animation present - type = HOLDING; + ECreatureAnimType type; + if ( anim->size(size_t(ECreatureAnimType::MOVING)) == 0 )//no moving animation present + type = ECreatureAnimType::HOLDING; else - type = MOVING; + type = ECreatureAnimType::MOVING; //display this anim for ~1 second (time is random, but it looks good) - for (size_t i=0; i< 12/anim->size(type) + 1; i++) + for (size_t i=0; i< 12/anim->size(size_t(type)) + 1; i++) addLast(type); } else addLast(available[rnd]); } -void CCreatureAnim::addLast(EAnimType newType) +void CCreatureAnim::addLast(ECreatureAnimType newType) { - if (type != MOVING && newType == MOVING)//starting moving - play init sequence + auto currType = ECreatureAnimType(group); + + if (currType != ECreatureAnimType::MOVING && newType == ECreatureAnimType::MOVING)//starting moving - play init sequence { - queue.push( MOVE_START ); + queue.push( ECreatureAnimType::MOVE_START ); } - else if (type == MOVING && newType != MOVING )//previous anim was moving - finish it + else if (currType == ECreatureAnimType::MOVING && newType != ECreatureAnimType::MOVING )//previous anim was moving - finish it { - queue.push( MOVE_END ); + queue.push( ECreatureAnimType::MOVE_END ); } - if (newType == TURN_L || newType == TURN_R) + if (newType == ECreatureAnimType::TURN_L || newType == ECreatureAnimType::TURN_R) queue.push(newType); queue.push(newType); @@ -522,28 +537,28 @@ void CCreatureAnim::addLast(EAnimType newType) void CCreatureAnim::reset() { //if we are in the middle of rotation - set flag - if (type == TURN_L && !queue.empty() && queue.front() == TURN_L) + if (group == size_t(ECreatureAnimType::TURN_L) && !queue.empty() && queue.front() == ECreatureAnimType::TURN_L) rotate(true); - if (type == TURN_R && !queue.empty() && queue.front() == TURN_R) + if (group == size_t(ECreatureAnimType::TURN_R) && !queue.empty() && queue.front() == ECreatureAnimType::TURN_R) rotate(false); while (!queue.empty()) { - EAnimType at = queue.front(); + ECreatureAnimType at = queue.front(); queue.pop(); - if (set(at)) + if (set(size_t(at))) return; } if (callback) callback(); while (!queue.empty()) { - EAnimType at = queue.front(); + ECreatureAnimType at = queue.front(); queue.pop(); - if (set(at)) + if (set(size_t(at))) return; } - set(HOLDING); + set(size_t(ECreatureAnimType::HOLDING)); } void CCreatureAnim::startPreview(bool warMachine) @@ -551,9 +566,9 @@ void CCreatureAnim::startPreview(bool warMachine) callback = std::bind(&CCreatureAnim::loopPreview, this, warMachine); } -void CCreatureAnim::clearAndSet(EAnimType type) +void CCreatureAnim::clearAndSet(ECreatureAnimType type) { while (!queue.empty()) queue.pop(); - set(type); + set(size_t(type)); } diff --git a/client/widgets/Images.h b/client/widgets/Images.h index 84f069f38..cf0b64855 100644 --- a/client/widgets/Images.h +++ b/client/widgets/Images.h @@ -11,6 +11,7 @@ #include "../gui/CIntObject.h" #include "../gui/SDL_Extensions.h" +#include "../battle/BattleConstants.h" struct SDL_Surface; struct Rect; @@ -165,56 +166,9 @@ public: /// Creature-dependend animations like attacking, moving,... class CCreatureAnim: public CShowableAnim { -public: - - enum EHeroAnimType - { - HERO_HOLDING = 0, - HERO_IDLE = 1, // idling movement that happens from time to time - HERO_DEFEAT = 2, // played when army loses stack or on friendly fire - HERO_VICTORY = 3, // when enemy stack killed or huge damage is dealt - HERO_CAST_SPELL = 4 // spellcasting - }; - - enum EAnimType // list of creature animations, numbers were taken from def files - { - MOVING=0, - MOUSEON=1, - HOLDING=2, - HITTED=3, - DEFENCE=4, - DEATH=5, - DEATH_RANGED=6, - TURN_L=7, - TURN_R=8, //same - //TURN_L2=9, //identical to previous? - //TURN_R2=10, - ATTACK_UP=11, - ATTACK_FRONT=12, - ATTACK_DOWN=13, - SHOOT_UP=14, - SHOOT_FRONT=15, - SHOOT_DOWN=16, - CAST_UP=17, - CAST_FRONT=18, - CAST_DOWN=19, - MOVE_START=20, - MOVE_END=21, - - DEAD = 22, // new group, used to show dead stacks. If empty - last frame from "DEATH" will be copied here - DEAD_RANGED = 23, // new group, used to show dead stacks (if DEATH_RANGED was used). If empty - last frame from "DEATH_RANGED" will be copied here - - VCMI_CAST_UP = 30, - VCMI_CAST_FRONT = 31, - VCMI_CAST_DOWN = 32, - VCMI_2HEX_UP = 40, - VCMI_2HEX_FRONT = 41, - VCMI_2HEX_DOWN = 42 - }; - private: //queue of animations waiting to be displayed - std::queue queue; + std::queue queue; //this function is used as callback if preview flag was set during construction void loopPreview(bool warMachine); @@ -224,13 +178,13 @@ public: void reset() override; //add sequence to the end of queue - void addLast(EAnimType newType); + void addLast(ECreatureAnimType newType); void startPreview(bool warMachine); //clear queue and set animation to this sequence - void clearAndSet(EAnimType type); + void clearAndSet(ECreatureAnimType type); - CCreatureAnim(int x, int y, std::string name, ui8 flags = 0, EAnimType = HOLDING); + CCreatureAnim(int x, int y, std::string name, ui8 flags = 0, ECreatureAnimType = ECreatureAnimType::HOLDING); }; diff --git a/client/widgets/MiscWidgets.cpp b/client/widgets/MiscWidgets.cpp index f57d09b49..e11f9ab95 100644 --- a/client/widgets/MiscWidgets.cpp +++ b/client/widgets/MiscWidgets.cpp @@ -68,7 +68,7 @@ LRClickableAreaWText::LRClickableAreaWText() LRClickableAreaWText::LRClickableAreaWText(const Rect &Pos, const std::string &HoverText, const std::string &ClickText) { init(); - pos = Pos + pos; + pos = Pos + pos.topLeft(); hoverText = HoverText; text = ClickText; } @@ -430,7 +430,7 @@ MoraleLuckBox::MoraleLuckBox(bool Morale, const Rect &r, bool Small) small(Small) { bonusValue = 0; - pos = r + pos; + pos = r + pos.topLeft(); defActions = 255-DISPOSE; } diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index d5eba1de8..7b0c3bfa4 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -478,7 +478,7 @@ CTextInput::CTextInput(const Rect & Pos, EFonts font, const CFunctionList & CB) :cb(CB), CFocusable(std::make_shared(this)) { - pos += Pos; + pos += Pos.topLeft(); pos.h = Pos.h; pos.w = Pos.w; @@ -495,7 +495,7 @@ CTextInput::CTextInput(const Rect & Pos, const Point & bgOffset, const std::stri CTextInput::CTextInput(const Rect & Pos, SDL_Surface * srf) :CFocusable(std::make_shared(this)) { - pos += Pos; + pos += Pos.topLeft(); captureAllKeys = true; OBJ_CONSTRUCTION; background = std::make_shared(Pos, 0, true); diff --git a/client/windows/CAdvmapInterface.cpp b/client/windows/CAdvmapInterface.cpp index 9dc791c6f..d41b01b4a 100644 --- a/client/windows/CAdvmapInterface.cpp +++ b/client/windows/CAdvmapInterface.cpp @@ -64,25 +64,25 @@ static void setScrollingCursor(ui8 direction) if(direction & CAdvMapInt::RIGHT) { if(direction & CAdvMapInt::UP) - CCS->curh->changeGraphic(ECursor::ADVENTURE, 33); + CCS->curh->set(Cursor::Map::SCROLL_NORTHEAST); else if(direction & CAdvMapInt::DOWN) - CCS->curh->changeGraphic(ECursor::ADVENTURE, 35); + CCS->curh->set(Cursor::Map::SCROLL_SOUTHEAST); else - CCS->curh->changeGraphic(ECursor::ADVENTURE, 34); + CCS->curh->set(Cursor::Map::SCROLL_EAST); } else if(direction & CAdvMapInt::LEFT) { if(direction & CAdvMapInt::UP) - CCS->curh->changeGraphic(ECursor::ADVENTURE, 39); + CCS->curh->set(Cursor::Map::SCROLL_NORTHWEST); else if(direction & CAdvMapInt::DOWN) - CCS->curh->changeGraphic(ECursor::ADVENTURE, 37); + CCS->curh->set(Cursor::Map::SCROLL_SOUTHWEST); else - CCS->curh->changeGraphic(ECursor::ADVENTURE, 38); + CCS->curh->set(Cursor::Map::SCROLL_WEST); } else if(direction & CAdvMapInt::UP) - CCS->curh->changeGraphic(ECursor::ADVENTURE, 32); + CCS->curh->set(Cursor::Map::SCROLL_NORTH); else if(direction & CAdvMapInt::DOWN) - CCS->curh->changeGraphic(ECursor::ADVENTURE, 36); + CCS->curh->set(Cursor::Map::SCROLL_SOUTH); } CTerrainRect::CTerrainRect() @@ -227,7 +227,7 @@ void CTerrainRect::handleHover(const SDL_MouseMotionEvent &sEvent) if(tHovered != pom) //tile outside the map { - CCS->curh->changeGraphic(ECursor::ADVENTURE, 0); + CCS->curh->set(Cursor::Map::POINTER); return; } @@ -243,7 +243,7 @@ void CTerrainRect::hover(bool on) if (!on) { adventureInt->statusbar->clear(); - CCS->curh->changeGraphic(ECursor::ADVENTURE,0); + CCS->curh->set(Cursor::Map::POINTER); } //Hoverable::hover(on); } @@ -932,7 +932,10 @@ void CAdvMapInt::activate() GH.statusbar = statusbar; if(LOCPLINT) + { LOCPLINT->cingconsole->activate(); + LOCPLINT->cingconsole->pos = this->pos; + } if(!duringAITurn) { @@ -959,7 +962,7 @@ void CAdvMapInt::deactivate() { scrollingDir = 0; - CCS->curh->changeGraphic(ECursor::ADVENTURE,0); + CCS->curh->set(Cursor::Map::POINTER); activeMapPanel->deactivate(); if (mode == EAdvMapMode::NORMAL) { @@ -1117,7 +1120,7 @@ void CAdvMapInt::handleMapScrollingUpdate() } else if(scrollingState) { - CCS->curh->changeGraphic(ECursor::ADVENTURE, 0); + CCS->curh->set(Cursor::Map::POINTER); scrollingState = false; } } @@ -1130,7 +1133,7 @@ void CAdvMapInt::handleSwipeUpdate() auto fixedPos = LOCPLINT->repairScreenPos(swipeTargetPosition); position.x = fixedPos.x; position.y = fixedPos.y; - CCS->curh->changeGraphic(ECursor::DEFAULT, 0); + CCS->curh->set(Cursor::Map::POINTER); updateScreen = true; minimap.redraw(); swipeMovementRequested = false; @@ -1215,7 +1218,7 @@ void CAdvMapInt::keyPressed(const SDL_KeyboardEvent & key) if(itr != LOCPLINT->towns.end()) LOCPLINT->showThievesGuildWindow(*itr); else - LOCPLINT->showInfoDialog(CGI->generaltexth->localizedTexts["adventureMap"]["noTownWithTavern"].String()); + LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithTavern")); } return; case SDLK_i: @@ -1247,7 +1250,7 @@ void CAdvMapInt::keyPressed(const SDL_KeyboardEvent & key) case SDLK_r: if(isActive() && LOCPLINT->ctrlPressed()) { - LOCPLINT->showYesNoDialog(CGI->generaltexth->localizedTexts["adventureMap"]["confirmRestartGame"].String(), + LOCPLINT->showYesNoDialog(CGI->generaltexth->translate("vcmi.adventureMap.confirmRestartGame"), [](){ LOCPLINT->sendCustomEvent(EUserEvent::RESTART_GAME); }, nullptr); } return; @@ -1306,7 +1309,7 @@ void CAdvMapInt::keyPressed(const SDL_KeyboardEvent & key) if(townWithMarket) //if any town has marketplace, open window GH.pushIntT(townWithMarket); else //if not - complain - LOCPLINT->showInfoDialog(CGI->generaltexth->localizedTexts["adventureMap"]["noTownWithMarket"].String()); + LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket")); } else if(isActive()) //no ctrl, advmapint is on the top => switch to town { @@ -1648,7 +1651,7 @@ void CAdvMapInt::tileHovered(const int3 &mapPos) return; if(!LOCPLINT->cb->isVisible(mapPos)) { - CCS->curh->changeGraphic(ECursor::ADVENTURE, 0); + CCS->curh->set(Cursor::Map::POINTER); statusbar->clear(); return; } @@ -1674,18 +1677,18 @@ void CAdvMapInt::tileHovered(const int3 &mapPos) { case SpellID::SCUTTLE_BOAT: if(objAtTile && objAtTile->ID == Obj::BOAT) - CCS->curh->changeGraphic(ECursor::ADVENTURE, 42); + CCS->curh->set(Cursor::Map::SCUTTLE_BOAT); else - CCS->curh->changeGraphic(ECursor::ADVENTURE, 0); + CCS->curh->set(Cursor::Map::POINTER); return; case SpellID::DIMENSION_DOOR: { const TerrainTile * t = LOCPLINT->cb->getTile(mapPos, false); int3 hpos = selection->getSightCenter(); if((!t || t->isClear(LOCPLINT->cb->getTile(hpos))) && isInScreenRange(hpos, mapPos)) - CCS->curh->changeGraphic(ECursor::ADVENTURE, 41); + CCS->curh->set(Cursor::Map::TELEPORT); else - CCS->curh->changeGraphic(ECursor::ADVENTURE, 0); + CCS->curh->set(Cursor::Map::POINTER); return; } } @@ -1696,17 +1699,25 @@ void CAdvMapInt::tileHovered(const int3 &mapPos) if(objAtTile) { if(objAtTile->ID == Obj::TOWN && objRelations != PlayerRelations::ENEMIES) - CCS->curh->changeGraphic(ECursor::ADVENTURE, 3); + CCS->curh->set(Cursor::Map::TOWN); else if(objAtTile->ID == Obj::HERO && objRelations == PlayerRelations::SAME_PLAYER) - CCS->curh->changeGraphic(ECursor::ADVENTURE, 2); + CCS->curh->set(Cursor::Map::HERO); else - CCS->curh->changeGraphic(ECursor::ADVENTURE, 0); + CCS->curh->set(Cursor::Map::POINTER); } else - CCS->curh->changeGraphic(ECursor::ADVENTURE, 0); + CCS->curh->set(Cursor::Map::POINTER); } else if(const CGHeroInstance * h = curHero()) { + std::array cursorMove = { Cursor::Map::T1_MOVE, Cursor::Map::T2_MOVE, Cursor::Map::T3_MOVE, Cursor::Map::T4_MOVE, }; + std::array cursorAttack = { Cursor::Map::T1_ATTACK, Cursor::Map::T2_ATTACK, Cursor::Map::T3_ATTACK, Cursor::Map::T4_ATTACK, }; + std::array cursorSail = { Cursor::Map::T1_SAIL, Cursor::Map::T2_SAIL, Cursor::Map::T3_SAIL, Cursor::Map::T4_SAIL, }; + std::array cursorDisembark = { Cursor::Map::T1_DISEMBARK, Cursor::Map::T2_DISEMBARK, Cursor::Map::T3_DISEMBARK, Cursor::Map::T4_DISEMBARK, }; + std::array cursorExchange = { Cursor::Map::T1_EXCHANGE, Cursor::Map::T2_EXCHANGE, Cursor::Map::T3_EXCHANGE, Cursor::Map::T4_EXCHANGE, }; + std::array cursorVisit = { Cursor::Map::T1_VISIT, Cursor::Map::T2_VISIT, Cursor::Map::T3_VISIT, Cursor::Map::T4_VISIT, }; + std::array cursorSailVisit = { Cursor::Map::T1_SAIL_VISIT, Cursor::Map::T2_SAIL_VISIT, Cursor::Map::T3_SAIL_VISIT, Cursor::Map::T4_SAIL_VISIT, }; + const CGPathNode * pnode = LOCPLINT->cb->getPathsInfo(h)->getPathInfo(mapPos); assert(pnode); @@ -1717,9 +1728,9 @@ void CAdvMapInt::tileHovered(const int3 &mapPos) case CGPathNode::NORMAL: case CGPathNode::TELEPORT_NORMAL: if(pnode->layer == EPathfindingLayer::LAND) - CCS->curh->changeGraphic(ECursor::ADVENTURE, 4 + turns*6); + CCS->curh->set(cursorMove[turns]); else - CCS->curh->changeGraphic(ECursor::ADVENTURE, 28 + turns); + CCS->curh->set(cursorSailVisit[turns]); break; case CGPathNode::VISIT: @@ -1728,48 +1739,48 @@ void CAdvMapInt::tileHovered(const int3 &mapPos) if(objAtTile && objAtTile->ID == Obj::HERO) { if(selection == objAtTile) - CCS->curh->changeGraphic(ECursor::ADVENTURE, 2); + CCS->curh->set(Cursor::Map::HERO); else - CCS->curh->changeGraphic(ECursor::ADVENTURE, 8 + turns*6); + CCS->curh->set(cursorExchange[turns]); } else if(pnode->layer == EPathfindingLayer::LAND) - CCS->curh->changeGraphic(ECursor::ADVENTURE, 9 + turns*6); + CCS->curh->set(cursorVisit[turns]); else - CCS->curh->changeGraphic(ECursor::ADVENTURE, 28 + turns); + CCS->curh->set(cursorSailVisit[turns]); break; case CGPathNode::BATTLE: case CGPathNode::TELEPORT_BATTLE: - CCS->curh->changeGraphic(ECursor::ADVENTURE, 5 + turns*6); + CCS->curh->set(cursorAttack[turns]); break; case CGPathNode::EMBARK: - CCS->curh->changeGraphic(ECursor::ADVENTURE, 6 + turns*6); + CCS->curh->set(cursorSail[turns]); break; case CGPathNode::DISEMBARK: - CCS->curh->changeGraphic(ECursor::ADVENTURE, 7 + turns*6); + CCS->curh->set(cursorDisembark[turns]); break; default: if(objAtTile && objRelations != PlayerRelations::ENEMIES) { if(objAtTile->ID == Obj::TOWN) - CCS->curh->changeGraphic(ECursor::ADVENTURE, 3); + CCS->curh->set(Cursor::Map::TOWN); else if(objAtTile->ID == Obj::HERO && objRelations == PlayerRelations::SAME_PLAYER) - CCS->curh->changeGraphic(ECursor::ADVENTURE, 2); + CCS->curh->set(Cursor::Map::HERO); else - CCS->curh->changeGraphic(ECursor::ADVENTURE, 0); + CCS->curh->set(Cursor::Map::POINTER); } else - CCS->curh->changeGraphic(ECursor::ADVENTURE, 0); + CCS->curh->set(Cursor::Map::POINTER); break; } } if(ourInaccessibleShipyard(objAtTile)) { - CCS->curh->changeGraphic(ECursor::ADVENTURE, 6); + CCS->curh->set(Cursor::Map::T1_SAIL); } } @@ -1802,7 +1813,7 @@ void CAdvMapInt::tileRClicked(const int3 &mapPos) return; } - CRClickPopup::createAndPush(obj, GH.current->motion, ETextAlignment::CENTER); + CRClickPopup::createAndPush(obj, Point(GH.current->motion), ETextAlignment::CENTER); } void CAdvMapInt::enterCastingMode(const CSpell * sp) @@ -1849,7 +1860,9 @@ const IShipyard * CAdvMapInt::ourInaccessibleShipyard(const CGObjectInstance *ob { const IShipyard *ret = IShipyard::castFrom(obj); - if(!ret || obj->tempOwner != player || CCS->curh->type || (CCS->curh->frame != 6 && CCS->curh->frame != 0)) + if(!ret || + obj->tempOwner != player || + (CCS->curh->get() != Cursor::Map::T1_SAIL && CCS->curh->get() != Cursor::Map::POINTER)) return nullptr; return ret; diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index e4b774795..31ba43873 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -54,7 +54,7 @@ CBuildingRect::CBuildingRect(CCastleBuildings * Par, const CGTownInstance * Town pos.y += str->pos.y; if(!str->borderName.empty()) - border = BitmapHandler::loadBitmap(str->borderName, true); + border = BitmapHandler::loadBitmap(str->borderName); else border = nullptr; @@ -851,7 +851,7 @@ void CCastleBuildings::enterToTheQuickRecruitmentWindow() if(hasSomeoneToRecruit) GH.pushIntT(town, pos); else - CInfoWindow::showInfoDialog(CGI->generaltexth->localizedTexts["townHall"]["noCreaturesToRecruit"].String(), {}); + CInfoWindow::showInfoDialog(CGI->generaltexth->translate("vcmi.townHall.noCreaturesToRecruit"), {}); } void CCastleBuildings::enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID::EBuildingID upgrades) @@ -870,8 +870,8 @@ void CCastleBuildings::enterFountain(const BuildingID & building, BuildingSubID: { auto buildingName = town->town->getSpecialBuilding(subID)->Name(); - hasNotProduced = std::string(CGI->generaltexth->localizedTexts["townHall"]["hasNotProduced"].String()); - hasProduced = std::string(CGI->generaltexth->localizedTexts["townHall"]["hasProduced"].String()); + hasNotProduced = std::string(CGI->generaltexth->translate("vcmi.townHall.hasNotProduced")); + hasProduced = std::string(CGI->generaltexth->translate("vcmi.townHall.hasProduced")); boost::algorithm::replace_first(hasNotProduced, "%s", buildingName); boost::algorithm::replace_first(hasProduced, "%s", buildingName); } @@ -1468,7 +1468,7 @@ std::string CBuildWindow::getTextForState(int state) } case EBuildingState::MISSING_BASE: { - std::string msg = CGI->generaltexth->localizedTexts["townHall"]["missingBase"].String(); + std::string msg = CGI->generaltexth->translate("vcmi.townHall.missingBase"); ret = boost::str(boost::format(msg) % town->town->buildings.at(building->upgrade)->Name()); break; } diff --git a/client/windows/CCreatureWindow.cpp b/client/windows/CCreatureWindow.cpp index acf2b9773..0c769b54d 100644 --- a/client/windows/CCreatureWindow.cpp +++ b/client/windows/CCreatureWindow.cpp @@ -348,8 +348,8 @@ CStackWindow::ButtonsSection::ButtonsSection(CStackWindow * owner, int yOffset) parent->redraw(); // FIXME: enable/disable don't redraw screen themselves }; - const JsonNode & text = VLC->generaltexth->localizedTexts["creatureWindow"][btnIDs[buttonIndex]]; - parent->switchButtons[buttonIndex] = std::make_shared(Point(302 + (int)buttonIndex*40, 5), "stackWindow/upgradeButton", CButton::tooltip(text), onSwitch); + std::string tooltipText = "vcmi.creatureWindow." + btnIDs[buttonIndex]; + parent->switchButtons[buttonIndex] = std::make_shared(Point(302 + (int)buttonIndex*40, 5), "stackWindow/upgradeButton", CButton::tooltipLocalized(tooltipText), onSwitch); parent->switchButtons[buttonIndex]->addOverlay(std::make_shared("stackWindow/switchModeIcons", buttonIndex)); } parent->switchButtons[parent->activeTab]->disable(); @@ -377,10 +377,7 @@ CStackWindow::CommanderMainSection::CommanderMainSection(CStackWindow * owner, i auto getSkillDescription = [this](int skillIndex) -> std::string { - if(CGI->generaltexth->znpc00.size() == 0) - return ""; - - return CGI->generaltexth->znpc00[151 + (12 * skillIndex) + (parent->info->commander->secondarySkills[skillIndex] * 2)]; + return CGI->generaltexth->znpc00[152 + (12 * skillIndex) + (parent->info->commander->secondarySkills[skillIndex] * 2)]; }; for(int index = ECommander::ATTACK; index <= ECommander::SPELL_POWER; ++index) @@ -601,13 +598,12 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s parent->stackArtifactIcon = std::make_shared("ARTIFACT", art->artType->iconIndex, 0, pos.x, pos.y); parent->stackArtifactHelp = std::make_shared(Rect(pos, Point(44, 44)), CComponent::artifact); parent->stackArtifactHelp->type = art->artType->id; - const JsonNode & text = VLC->generaltexth->localizedTexts["creatureWindow"]["returnArtifact"]; if(parent->info->owner) { parent->stackArtifactButton = std::make_shared( Point(pos.x - 2 , pos.y + 46), "stackWindow/cancelButton", - CButton::tooltip(text), [=]() + CButton::tooltipLocalized("vcmi.creatureWindow.returnArtifact"), [=]() { parent->removeStackArtifact(ArtifactPosition::CREATURE_SLOT); }); @@ -869,9 +865,9 @@ std::string CStackWindow::generateStackExpDescription() if (!vstd::iswithin(tier, 1, 7)) tier = 0; int number; - std::string expText = CGI->generaltexth->zcrexp[325]; + std::string expText = CGI->generaltexth->translate("vcmi.stackExperience.description"); boost::replace_first(expText, "%s", creature->namePl); - boost::replace_first(expText, "%s", CGI->generaltexth->zcrexp[rank]); + boost::replace_first(expText, "%s", CGI->generaltexth->translate("vcmi.stackExperience.rank", rank)); boost::replace_first(expText, "%i", boost::lexical_cast(rank)); boost::replace_first(expText, "%i", boost::lexical_cast(stack->experience)); number = static_cast(CGI->creh->expRanks[tier][rank] - stack->experience); @@ -906,13 +902,10 @@ void CStackWindow::setSelection(si32 newSkill, std::shared_ptr std::string { - if(CGI->generaltexth->znpc00.size() == 0) - return ""; - if(selected) - return CGI->generaltexth->znpc00[151 + (12 * skillIndex) + ((info->commander->secondarySkills[skillIndex] + 1) * 2)]; //upgrade description + return CGI->generaltexth->znpc00[152 + (12 * skillIndex) + ((info->commander->secondarySkills[skillIndex] + 1) * 2)]; //upgrade description else - return CGI->generaltexth->znpc00[151 + (12 * skillIndex) + (info->commander->secondarySkills[skillIndex] * 2)]; + return CGI->generaltexth->znpc00[152 + (12 * skillIndex) + (info->commander->secondarySkills[skillIndex] * 2)]; }; auto getSkillImage = [this](int skillIndex) -> std::string diff --git a/client/windows/CHeroWindow.cpp b/client/windows/CHeroWindow.cpp index 102819aa5..62e8d0f11 100644 --- a/client/windows/CHeroWindow.cpp +++ b/client/windows/CHeroWindow.cpp @@ -128,8 +128,7 @@ CHeroWindow::CHeroWindow(const CGHeroInstance * hero) if(hero->commander) { - auto texts = CGI->generaltexth->localizedTexts["heroWindow"]["openCommander"]; - commanderButton = std::make_shared(Point(317, 18), "buttons/commander", CButton::tooltip(texts), [&](){ commanderWindow(); }, SDLK_c); + commanderButton = std::make_shared(Point(317, 18), "buttons/commander", CButton::tooltipLocalized("vcmi.heroWindow.openCommander"), [&](){ commanderWindow(); }, SDLK_c); } //right list of heroes diff --git a/client/windows/CKingdomInterface.cpp b/client/windows/CKingdomInterface.cpp index d91842b70..c7d19c3f0 100644 --- a/client/windows/CKingdomInterface.cpp +++ b/client/windows/CKingdomInterface.cpp @@ -594,7 +594,7 @@ void CKingdomInterface::generateMinesList(const std::vector(minesCount[i]); - auto data = std::make_shared(value, "", "OVMINES", i, CGI->generaltexth->mines[i].first); + auto data = std::make_shared(value, "", "OVMINES", i, CGI->generaltexth->translate("core.minename", i)); minesBox[i] = std::make_shared(Point(20+i*80, 31+footerPos), InfoBox::POS_INSIDE, InfoBox::SIZE_SMALL, data); minesBox[i]->removeUsedEvents(LCLICK|RCLICK); //fixes #890 - mines boxes ignore clicks } diff --git a/client/windows/CQuestLog.cpp b/client/windows/CQuestLog.cpp index 8bc8775bf..2295433ba 100644 --- a/client/windows/CQuestLog.cpp +++ b/client/windows/CQuestLog.cpp @@ -126,15 +126,13 @@ CQuestLog::CQuestLog (const std::vector & Quests) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - const JsonNode & texts = CGI->generaltexth->localizedTexts["questLog"]; - minimap = std::make_shared(Rect(12, 12, 169, 169)); // TextBox have it's own 4 pixel padding from top at least for English. To achieve 10px from both left and top only add 6px margin description = std::make_shared("", Rect(205, 18, 385, DESCRIPTION_HEIGHT_MAX), CSlider::BROWN, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE); ok = std::make_shared(Point(539, 398), "IOKAY.DEF", CGI->generaltexth->zelp[445], std::bind(&CQuestLog::close, this), SDLK_RETURN); // Both button and lable are shifted to -2px by x and y to not make them actually look like they're on same line with quests list and ok button - hideCompleteButton = std::make_shared(Point(10, 396), "sysopchk.def", CButton::tooltip(texts["hideComplete"]), std::bind(&CQuestLog::toggleComplete, this, _1)); - hideCompleteLabel = std::make_shared(46, 398, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, texts["hideComplete"]["label"].String()); + hideCompleteButton = std::make_shared(Point(10, 396), "sysopchk.def", CButton::tooltipLocalized("vcmi.questLog.hideComplete"), std::bind(&CQuestLog::toggleComplete, this, _1)); + hideCompleteLabel = std::make_shared(46, 398, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("vcmi.questLog.hideComplete.hover")); slider = std::make_shared(Point(166, 195), 191, std::bind(&CQuestLog::sliderMoved, this, _1), QUEST_COUNT, 0, false, CSlider::BROWN); recreateLabelList(); diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index 959e2c61b..b7d6c7f9f 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -562,7 +562,7 @@ void CSpellWindow::SpellArea::clickLeft(tribool down, bool previousState) if(!texts.empty()) owner->myInt->showInfoDialog(texts.front()); else - owner->myInt->showInfoDialog(CGI->generaltexth->localizedTexts["adventureMap"]["spellUnknownProblem"].String()); + owner->myInt->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.spellUnknownProblem")); } } else //adventure spell @@ -658,7 +658,7 @@ void CSpellWindow::SpellArea::setSpell(const CSpell * spell) { boost::format fmt("%s/%s"); fmt % CGI->generaltexth->allTexts[171 + mySpell->level]; - fmt % CGI->generaltexth->levels.at(3+(schoolLevel-1));//lines 4-6 + fmt % CGI->generaltexth->levels[3+(schoolLevel-1)];//lines 4-6 level->setText(fmt.str()); } else diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 8ceaed783..cf3978479 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -449,14 +449,12 @@ CSystemOptionsWindow::CSystemOptionsWindow() OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); title = std::make_shared(242, 32, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[568]); - const JsonNode & texts = CGI->generaltexth->localizedTexts["systemOptions"]; - //left window section leftGroup = std::make_shared(FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW); leftGroup->add(122, 64, CGI->generaltexth->allTexts[569]); leftGroup->add(122, 130, CGI->generaltexth->allTexts[570]); leftGroup->add(122, 196, CGI->generaltexth->allTexts[571]); - leftGroup->add(122, 262, texts["resolutionButton"]["label"].String()); + leftGroup->add(122, 262, CGI->generaltexth->translate("vcmi.systemOptions.resolutionButton.hover")); leftGroup->add(122, 347, CGI->generaltexth->allTexts[394]); leftGroup->add(122, 412, CGI->generaltexth->allTexts[395]); @@ -466,8 +464,7 @@ CSystemOptionsWindow::CSystemOptionsWindow() rightGroup->add(282, 89, CGI->generaltexth->allTexts[573]); rightGroup->add(282, 121, CGI->generaltexth->allTexts[574]); rightGroup->add(282, 153, CGI->generaltexth->allTexts[577]); - rightGroup->add(282, 185, texts["creatureWindowButton"]["label"].String()); - rightGroup->add(282, 217, texts["fullscreenButton"]["label"].String()); + rightGroup->add(282, 217, CGI->generaltexth->translate("vcmi.systemOptions.fullscreenButton.hover")); //setting up buttons load = std::make_shared(Point(246, 298), "SOLOAD.DEF", CGI->generaltexth->zelp[321], [&](){ bloadf(); }, SDLK_l); @@ -520,10 +517,10 @@ CSystemOptionsWindow::CSystemOptionsWindow() mapScrollSpeed->setSelected((int)settings["adventure"]["scrollSpeed"].Float()); mapScrollSpeed->addCallback(std::bind(&setIntSetting, "adventure", "scrollSpeed", _1)); - musicVolume = std::make_shared(Point(29, 359), "syslb.def", CCS->musich->getVolume(), &CGI->generaltexth->zelp[326]); + musicVolume = std::make_shared(Point(29, 359), "syslb.def", CCS->musich->getVolume(), CVolumeSlider::MUSIC); musicVolume->addCallback(std::bind(&setIntSetting, "general", "music", _1)); - effectsVolume = std::make_shared(Point(29, 425), "syslb.def", CCS->soundh->getVolume(), &CGI->generaltexth->zelp[336]); + effectsVolume = std::make_shared(Point(29, 425), "syslb.def", CCS->soundh->getVolume(), CVolumeSlider::SOUND); effectsVolume->addCallback(std::bind(&setIntSetting, "general", "sound", _1)); showReminder = std::make_shared(Point(246, 87), "sysopchk.def", CGI->generaltexth->zelp[361], [&](bool value) @@ -544,7 +541,7 @@ CSystemOptionsWindow::CSystemOptionsWindow() }); spellbookAnim->setSelected(settings["video"]["spellbookAnimation"].Bool()); - fullscreen = std::make_shared(Point(246, 215), "sysopchk.def", CButton::tooltip(texts["fullscreenButton"]), [&](bool value) + fullscreen = std::make_shared(Point(246, 215), "sysopchk.def", CButton::tooltipLocalized("vcmi.systemOptions.fullscreenButton"), [&](bool value) { setBoolSetting("video", "fullscreen", value); }); @@ -552,7 +549,7 @@ CSystemOptionsWindow::CSystemOptionsWindow() onFullscreenChanged([&](const JsonNode &newState){ fullscreen->setSelected(newState.Bool());}); - gameResButton = std::make_shared(Point(28, 275),"buttons/resolution", CButton::tooltip(texts["resolutionButton"]), std::bind(&CSystemOptionsWindow::selectGameRes, this), SDLK_g); + gameResButton = std::make_shared(Point(28, 275),"buttons/resolution", CButton::tooltipLocalized("vcmi.systemOptions.resolutionButton"), std::bind(&CSystemOptionsWindow::selectGameRes, this), SDLK_g); const auto & screenRes = settings["video"]["screenRes"]; gameResLabel = std::make_shared(170, 292, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, resolutionToString(screenRes["width"].Integer(), screenRes["height"].Integer())); @@ -561,7 +558,6 @@ CSystemOptionsWindow::CSystemOptionsWindow() void CSystemOptionsWindow::selectGameRes() { std::vector items; - const JsonNode & texts = CGI->generaltexth->localizedTexts["systemOptions"]["resolutionMenu"]; #ifndef VCMI_IOS SDL_Rect displayBounds; @@ -585,7 +581,11 @@ void CSystemOptionsWindow::selectGameRes() ++i; } - GH.pushIntT(items, nullptr, texts["label"].String(), texts["help"].String(), std::bind(&CSystemOptionsWindow::setGameRes, this, _1), currentResolutionIndex); + GH.pushIntT(items, nullptr, + CGI->generaltexth->translate("vcmi.systemOptions.resolutionMenu.hover"), + CGI->generaltexth->translate("vcmi.systemOptions.resolutionMenu.help"), + std::bind(&CSystemOptionsWindow::setGameRes, this, _1), + currentResolutionIndex); } void CSystemOptionsWindow::setGameRes(int index) @@ -1217,7 +1217,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, garr->addSplitBtn(std::make_shared( Point( 10, qeLayout ? 122 : 132), "TSBTNS.DEF", CButton::tooltip(CGI->generaltexth->tcommands[3]), splitButtonCallback)); garr->addSplitBtn(std::make_shared( Point(744, qeLayout ? 122 : 132), "TSBTNS.DEF", CButton::tooltip(CGI->generaltexth->tcommands[3]), splitButtonCallback)); - if (qeLayout && (CGI->generaltexth->qeModCommands.size() >= 5)) + if(qeLayout) { moveAllGarrButtonLeft = std::make_shared(Point(325, 118), QUICK_EXCHANGE_MOD_PREFIX + "/armRight.DEF", CButton::tooltip(CGI->generaltexth->qeModCommands[1]), controller.onMoveArmyToRight()); echangeGarrButton = std::make_shared(Point(377, 118), QUICK_EXCHANGE_MOD_PREFIX + "/swapAll.DEF", CButton::tooltip(CGI->generaltexth->qeModCommands[2]), controller.onSwapArmy()); diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index 90370d26f..0c3266f48 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -139,6 +139,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/spells/effects/Catapult.cpp ${MAIN_LIB_DIR}/spells/effects/Clone.cpp ${MAIN_LIB_DIR}/spells/effects/Damage.cpp + ${MAIN_LIB_DIR}/spells/effects/DemonSummon.cpp ${MAIN_LIB_DIR}/spells/effects/Dispel.cpp ${MAIN_LIB_DIR}/spells/effects/Effect.cpp ${MAIN_LIB_DIR}/spells/effects/Effects.cpp @@ -374,6 +375,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/spells/effects/Catapult.h ${MAIN_LIB_DIR}/spells/effects/Clone.h ${MAIN_LIB_DIR}/spells/effects/Damage.h + ${MAIN_LIB_DIR}/spells/effects/DemonSummon.h ${MAIN_LIB_DIR}/spells/effects/Dispel.h ${MAIN_LIB_DIR}/spells/effects/Effect.h ${MAIN_LIB_DIR}/spells/effects/Effects.h diff --git a/config/artifacts.json b/config/artifacts.json index 2983e79ed..e028a28d1 100644 --- a/config/artifacts.json +++ b/config/artifacts.json @@ -2240,12 +2240,6 @@ "val" : 0, "valueType" : "BASE_NUMBER" }, - { - "subtype" : "creature.vampireLord", - "type" : "DAEMON_SUMMONING", - "val" : 10, - "valueType" : "BASE_NUMBER" - }, { "addInfo" : 2, "subtype" : "spell.lightningBolt", diff --git a/config/battleEffects.json b/config/battleEffects.json new file mode 100644 index 000000000..089d8dbef --- /dev/null +++ b/config/battleEffects.json @@ -0,0 +1,113 @@ +{ + "colorMuxers" : { + "example" : [ + { + // Required. Time point at which this effect will be applied at full puwer. + // Must be greater than time point of previous effect + // During playback, effects will be played with smooth transition + // Effect will end once game reaches time point of the final filter + // Effect of final step will be applied to stack permanently for duration of the spell + // Note that actual speed of animation is subject to changes from in-game animation speed setting + "time" : 0.0, + + // Optional. Transformation filter for red, green and blue components of a color + // Applies transformation with specified parameters to each channel. Formula: + // result = red * (value 1) + green * (value 2) + blue * (value 3) + (value 4) + "red" : [ 1.0, 0.0, 0.0, 0.0 ], + "green" : [ 0.0, 1.0, 0.0, 0.0 ], + "blue" : [ 0.0, 0.0, 1.0, 0.0 ], + + /// Optional. Transparency filter, makes stack appear semi-transparent, used mostly for fade-in effects + /// Value 0 = full transparency, 1 = fully opaque + "alpha" : 1.0 + } + ], + + "petrification" : [ + { + "time" : 0.0 + }, + { + "time" : 1.0, + // Conversion to grayscale, using human eye perception factor for channels + "red" : [ 0.299, 0.587, 0.114, 0.0 ], + "green" : [ 0.299, 0.587, 0.114, 0.0 ], + "blue" : [ 0.299, 0.587, 0.114, 0.0 ], + } + ], + "cloning" : [ + { + // No fade in - will be handled by summonFadeIn effect + "time" : 0.0, + "red" : [ 0.5, 0.0, 0.0, 0.0 ], + "green" : [ 0.0, 0.5, 0.0, 0.0 ], + "blue" : [ 0.0, 0.0, 0.5, 0.5 ], + } + ], + "summonFadeIn" : [ + { + "time" : 0.0, + "alpha" : 0.0 + }, + { + "time" : 1.0 + }, + ], + "summonFadeOut" : [ + { + "time" : 0.0 + }, + { + "time" : 1.0, + "alpha" : 0.0 + }, + ], + "teleportFadeIn" : [ + { + "time" : 0.0, + "alpha" : 0.0 + }, + { + "time" : 0.2 + }, + ], + "teleportFadeOut" : [ + { + "time" : 0.0 + }, + { + "time" : 0.2, + "alpha" : 0.0 + }, + ], + "bloodlust" : [ + { + "time" : 0.0 + }, + { + "time" : 0.2, + "red" : [ 0.5, 0.0, 0.5, 0.4 ], + "green" : [ 0.0, 1.0, 0.0, 0.0 ], + "blue" : [ 0.0, 0.0, 1.0, 0.0 ], + "alpha" : 1.0 + }, + { + "time" : 0.4, + "red" : [ 0.6, 0.6, 0.6, 0.0 ], + "green" : [ 0.0, 0.5, 0.0, 0.0 ], + "blue" : [ 0.0, 0.0, 0.5, 0.0 ], + "alpha" : 1.0 + }, + { + "time" : 0.6, + "red" : [ 0.5, 0.0, 0.5, 0.4 ], + "green" : [ 0.0, 1.0, 0.0, 0.0 ], + "blue" : [ 0.0, 0.0, 1.0, 0.0 ], + "alpha" : 1.0 + }, + { + "time" : 0.8, + }, + ], + } +} diff --git a/config/bonuses.json b/config/bonuses.json index b8c4807a5..a02880722 100644 --- a/config/bonuses.json +++ b/config/bonuses.json @@ -84,14 +84,6 @@ } }, - "DAEMON_SUMMONING": - { - "graphics": - { - "icon": "zvs/Lib1.res/RiseDemons" - } - }, - "DARKNESS": { }, diff --git a/config/bonuses_texts.json b/config/bonuses_texts.json index d0d273f88..eb24ba01f 100644 --- a/config/bonuses_texts.json +++ b/config/bonuses_texts.json @@ -72,12 +72,6 @@ "description": "Immune to Champion charge" }, - "DAEMON_SUMMONING": - { - "name": "Summoner (${subtype.creature})", - "description": "Can rise creatures from corpses" - }, - "DARKNESS": { "name": "Darkness cover", diff --git a/config/creatures/inferno.json b/config/creatures/inferno.json index 87539d4a6..998e9a2e0 100755 --- a/config/creatures/inferno.json +++ b/config/creatures/inferno.json @@ -216,12 +216,18 @@ "faction": "inferno", "abilities": { - "demonSummon" : + "summons50HP" : { - "type" : "DAEMON_SUMMONING", - "subtype" : "creature.demon", + "type" : "SPECIFIC_SPELL_POWER", + "subtype" : "spell.summonDemons", "val" : 50 }, + "resurrects" : + { + "type" : "SPELLCASTER", + "subtype" : "spell.summonDemons", + "val" : 0 + }, "castsAmount" : { "type" : "CASTS", diff --git a/config/creatures/rampart.json b/config/creatures/rampart.json index ea105a901..157fe24ae 100644 --- a/config/creatures/rampart.json +++ b/config/creatures/rampart.json @@ -274,7 +274,8 @@ { "type" : "SPELL_AFTER_ATTACK", "subtype" : "spell.blind", - "val" : 20 + "val" : 20, + "addInfo" : [3,0] } }, "upgrades": ["warUnicorn"], @@ -307,7 +308,8 @@ { "type" : "SPELL_AFTER_ATTACK", "subtype" : "spell.blind", - "val" : 20 + "val" : 20, + "addInfo" : [3,0] } }, "graphics" : diff --git a/config/defaultMods.json b/config/defaultMods.json index 4aad3fb71..7b2e37197 100644 --- a/config/defaultMods.json +++ b/config/defaultMods.json @@ -27,10 +27,10 @@ "WINNING_HERO_WITH_NO_TROOPS_RETREATS": true, "BLACK_MARKET_MONTHLY_ARTIFACTS_CHANGE": true, "NO_RANDOM_SPECIAL_WEEKS_AND_MONTHS": false, - "ATTACK_POINT_DMG_MULTIPLIER": 0.05, - "ATTACK_POINTS_DMG_MULTIPLIER_CAP": 4.0, - "DEFENSE_POINT_DMG_MULTIPLIER": 0.025, - "DEFENSE_POINTS_DMG_MULTIPLIER_CAP": 0.7 + "ATTACK_POINT_DMG_MULTIPLIER": 0.05, //every 1 attack point damage influence in battle when attack points > defense points during creature attack + "ATTACK_POINTS_DMG_MULTIPLIER_CAP": 4.0, //limit of damage increase that can be achieved by overpowering attack points + "DEFENSE_POINT_DMG_MULTIPLIER": 0.025, //every 1 defense point damage influence in battle when defense points > attack points during creature attack + "DEFENSE_POINTS_DMG_MULTIPLIER_CAP": 0.7 //limit of damage reduction that can be achieved by overpowering defense points }, "modules": { diff --git a/config/schemas/settings.json b/config/schemas/settings.json index 9baa638c6..0c78608b1 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -17,7 +17,7 @@ "type" : "object", "default": {}, "additionalProperties" : false, - "required" : [ "playerName", "showfps", "music", "sound", "encoding", "swipe", "saveRandomMaps", "saveFrequency", "notifications", "extraDump" ], + "required" : [ "playerName", "showfps", "music", "sound", "encoding", "language", "swipe", "saveRandomMaps", "saveFrequency", "notifications", "extraDump" ], "properties" : { "playerName" : { "type":"string", @@ -51,6 +51,10 @@ "type":"string", "default" : "Maps/Arrogance" }, + "language" : { + "type":"string", + "default" : "english" + }, "lastSave" : { "type":"string", "default" : "NEWGAME" @@ -356,7 +360,7 @@ "properties" : { "format" : { "type" : "string", - "default" : "%l %n [%t] - %m" + "default" : "[%c] %l %n - %m" } } }, diff --git a/config/spells/ability.json b/config/spells/ability.json index 26123e475..45d19490c 100644 --- a/config/spells/ability.json +++ b/config/spells/ability.json @@ -3,7 +3,9 @@ "index" : 70, "targetType": "NO_TARGET", "animation":{ - //need special animation + "affect":[ { + "effectName" : "petrification" + } ] }, "sounds": { "cast": "PARALYZE" @@ -428,5 +430,52 @@ "bonus.DIRECT_DAMAGE_IMMUNITY" : "normal" } } - } + }, + "summonDemons" : { + "type": "ability", + "targetType" : "CREATURE", + "name": "Summon Demons", + "school" : {}, + "level": 2, + "power": 50, + "defaultGainChance": 0, + "gainChance": {}, + "animation":{ + }, + "sounds": { + "cast": "RESURECT" + }, + "levels" : { + "base": { + "description" : "", + "aiValue" : 0, + "power" : 40, + "cost" : 1, + "range" : "0", + "battleEffects":{ + "demonSummon":{ + "id":"demon", + "permanent":true, + "type":"core:demonSummon" + } + } + }, + "none" :{}, + "basic" :{}, + "advanced" :{}, + "expert" :{} + }, + "flags" : { + "rising": true, + "positive": true + }, + "targetCondition" : { + "noneOf" : { + "bonus.NON_LIVING" : "absolute", + "bonus.SIEGE_WEAPON" : "absolute", + "bonus.UNDEAD" : "absolute", + "bonus.GARGOYLE" : "absolute" + } + } + }, } diff --git a/config/spells/other.json b/config/spells/other.json index 16c6f2e41..aede514ca 100644 --- a/config/spells/other.json +++ b/config/spells/other.json @@ -21,11 +21,13 @@ "attacker" :{ "animation" : "C17SPE1", "appearAnimation" : "C17SPE0", + "appearSound" : "QUIKSAND", "offsetY" : -42 }, "defender" :{ "animation" : "C17SPE1", "appearAnimation" : "C17SPE0", + "appearSound" : "QUIKSAND", "offsetY" : -42 } } @@ -72,11 +74,17 @@ "turnsRemaining" : -1, "attacker" :{ "animation" : "C09SPF1", - "appearAnimation" : "C09SPF0" + "appearAnimation" : "C09SPF0", + "appearSound" : "LANDMINE", + "triggerAnimation" : "C09SPF3", + "triggerSound" : "LANDKILL" }, "defender" :{ "animation" : "C09SPF1", - "appearAnimation" : "C09SPF0" + "appearAnimation" : "C09SPF0", + "appearSound" : "LANDMINE", + "triggerAnimation" : "C09SPF3", + "triggerSound" : "LANDKILL" } }, "damage":{ @@ -588,7 +596,6 @@ "index" : 65, "targetType" : "CREATURE", "animation":{ - "cast":[2] }, "sounds": { "cast": "CLONE" @@ -632,7 +639,6 @@ "index" : 66, "targetType" : "NO_TARGET", "animation":{ - "cast":[2] }, "sounds": { "cast": "SUMNELM" @@ -658,7 +664,6 @@ "index" : 67, "targetType" : "NO_TARGET", "animation":{ - "cast":[2] }, "sounds": { "cast": "SUMNELM" @@ -684,7 +689,6 @@ "index" : 68, "targetType" : "NO_TARGET", "animation":{ - "cast":[2] }, "sounds": { "cast": "SUMNELM" @@ -710,7 +714,6 @@ "index" : 69, "targetType" : "NO_TARGET", "animation":{ - "cast":[2] }, "sounds": { "cast": "SUMNELM" diff --git a/config/spells/timed.json b/config/spells/timed.json index 63d4977b4..66ea44fdd 100644 --- a/config/spells/timed.json +++ b/config/spells/timed.json @@ -69,11 +69,8 @@ "affect":["C05SPF0"] }, "sounds": { - "cast": "FIRESHIE" + "cast": "FIRESHLD" }, - // It looks that fireshield has two separate sounds - // "soundfile":"FIRESHLD.wav" - // "levels" : { "base":{ "range" : "0", @@ -440,7 +437,9 @@ "targetType" : "CREATURE", "animation":{ - "affect":["SP12_"] //??? + "affect":[ { + "effectName" : "bloodlust" + } ] }, "sounds": { "cast": "BLOODLUS" @@ -541,7 +540,7 @@ "targetType" : "CREATURE", "animation":{ - "affect":["C0ACID"] + "affect":["C17SPW0"] }, "sounds": { "cast": "WEAKNESS" @@ -1200,7 +1199,7 @@ "effects" : { "attacksNearestCreature" : { "type" : "ATTACKS_NEAREST_CREATURE", - "duration" : "STACK_GETS_TURN" + "duration" : "UNTIL_ATTACK" } } }, diff --git a/config/translate.json b/config/translate.json index 35d36f18d..c2a6f0c71 100644 --- a/config/translate.json +++ b/config/translate.json @@ -1,127 +1,84 @@ -// This file contains all translateable strings added in VCMI { - "adventureMap": - { - "monsterThreat" : - { - "title" : "\n\n Threat: ", - "levels" : - [ - "Effortless", - "Very Weak", - "Weak", - "A bit weaker", - "Equal", - "A bit stronger", - "Strong", - "Very Strong", - "Challenging", - "Overpowering", - "Deadly", - "Impossible" - ] - }, - "confirmRestartGame" : "Are you sure you want to restart game?", - "noTownWithMarket": "No available marketplace!", - "noTownWithTavern": "No available town with tavern!", - "spellUnknownProblem": "Unknown problem with this spell, no more information available.", - "playerAttacked" : "Player has been attacked: %s" - }, - "server" : - { - "errors" : - { - "existingProcess" : "Another vcmiserver process is running, please terminate it first", - "modsIncompatibility" : "Required mods to load game:" - }, - "confirmReconnect" : "Connect to the last session?" - }, - "systemOptions" : - { - "fullscreenButton" : - { - "label" : "Fullscreen", - "help" : "{Fullscreen}\n\n If selected, VCMI will run in fullscreen mode, othervice VCMI will run in window" - }, - "resolutionButton" : - { - "label" : "Resolution", - "help" : "{Select resolution}\n\n Change in-game screen resolution. Game restart required to apply new resolution." - }, - "resolutionMenu" : - { - "label" : "Select resolution", - "help" : "Change in-game screen resolution." - } - }, - "townHall" : - { - "missingBase" : "Base building %s must be built first", - "noCreaturesToRecruit" : "There are no creatures to recruit!", - "greetingManaVortex" : "As you near the %s your body is filled with new energy. You have doubled your normal spell points.", - "greetingKnowledge" : "You study the glyphs on the %s and gain insight into the workings of various magics (+1 Knowledge).", - "greetingSpellPower" : "The %s teaches you new ways to focus your magical powers (+1 Power).", - "greetingExperience" : "A visit to the %s teaches you many new skills (+1000 Experience).", - "greetingAttack" : "Some time spent at the %s allows you to learn more effective combat skills (+1 Attack Skill).", - "greetingDefence" : "Spending time in the %s, the experienced warriors therein teach you additional defensive skills (+1 Defense).", - "hasNotProduced" : "The %s has not produced anything yet.", - "hasProduced" : "The %s produced %d %s this week.", - "greetingCustomBonus" : "%s gives you +%d %s%s", - "greetingCustomUntil" : " until next battle.", - "greetingInTownMagicWell" : "%s has restored your spell points to maximum." - }, - "logicalExpressions" : - { - "anyOf" : "Any of the following:", - "allOf" : "All of the following:", - "noneOf" : "None of the following:" - }, - "heroWindow" : - { - "openCommander" : - { - "label" : "Open commander window", - "help" : "Displays information about commander of this hero" - } - }, - "commanderWindow": - { - "artifactMessage": "Do you want to give this artifact back to hero?" - }, - "creatureWindow" : - { - "showBonuses" : - { - "label" : "Switch to bonuses view", - "help" : "Displays all active bonuses of the commander" - }, - "showSkills" : - { - "label" : "Switch to skills view", - "help" : "Displays all learned skills of the commander" - }, - "returnArtifact" : - { - "label" : "Give back artifact", - "help" : "Use this button to return stack artifact back into hero backpack" - } - }, - "questLog" : - { - "hideComplete" : - { - "label" : "Hide complete quests", - "help" : "Hide all quests that already completed" - } - }, - "randomMapTab": - { - "widgets": - { - "defaultTemplate": "default", - "templateLabel": "Template", - "teamAlignmentsButton": "Setup...", - "teamAlignmentsLabel": "Team alignments" - } - } + "vcmi.adventureMap.monsterThreat.title" : "\n\n Threat: ", + "vcmi.adventureMap.monsterThreat.levels.0" : "Effortless", + "vcmi.adventureMap.monsterThreat.levels.1" : "Very Weak", + "vcmi.adventureMap.monsterThreat.levels.2" : "Weak", + "vcmi.adventureMap.monsterThreat.levels.3" : "A bit weaker", + "vcmi.adventureMap.monsterThreat.levels.4" : "Equal", + "vcmi.adventureMap.monsterThreat.levels.5" : "A bit stronger", + "vcmi.adventureMap.monsterThreat.levels.6" : "Strong", + "vcmi.adventureMap.monsterThreat.levels.7" : "Very Strong", + "vcmi.adventureMap.monsterThreat.levels.8" : "Challenging", + "vcmi.adventureMap.monsterThreat.levels.9" : "Overpowering", + "vcmi.adventureMap.monsterThreat.levels.10" : "Deadly", + "vcmi.adventureMap.monsterThreat.levels.11" : "Impossible", + + "vcmi.adventureMap.confirmRestartGame" : "Are you sure you want to restart game?", + "vcmi.adventureMap.noTownWithMarket" : "No available marketplace!", + "vcmi.adventureMap.noTownWithTavern" : "No available town with tavern!", + "vcmi.adventureMap.spellUnknownProblem" : "Unknown problem with this spell, no more information available.", + "vcmi.adventureMap.playerAttacked" : "Player has been attacked: %s", + + "vcmi.server.errors.existingProcess" : "Another vcmiserver process is running, please terminate it first", + "vcmi.server.errors.modsIncompatibility" : "Required mods to load game:", + "vcmi.server.confirmReconnect" : "Connect to the last session?", + + "vcmi.systemOptions.fullscreenButton.hover" : "Fullscreen", + "vcmi.systemOptions.fullscreenButton.help" : "{Fullscreen}\n\n If selected, VCMI will run in fullscreen mode, othervice VCMI will run in window", + "vcmi.systemOptions.resolutionButton.hover" : "Resolution", + "vcmi.systemOptions.resolutionButton.help" : "{Select resolution}\n\n Change in-game screen resolution. Game restart required to apply new resolution.", + "vcmi.systemOptions.resolutionMenu.hover" : "Select resolution", + "vcmi.systemOptions.resolutionMenu.help" : "Change in-game screen resolution.", + + "vcmi.townHall.missingBase" : "Base building %s must be built first", + "vcmi.townHall.noCreaturesToRecruit" : "There are no creatures to recruit!", + "vcmi.townHall.greetingManaVortex" : "As you near the %s your body is filled with new energy. You have doubled your normal spell points.", + "vcmi.townHall.greetingKnowledge" : "You study the glyphs on the %s and gain insight into the workings of various magics (+1 Knowledge).", + "vcmi.townHall.greetingSpellPower" : "The %s teaches you new ways to focus your magical powers (+1 Power).", + "vcmi.townHall.greetingExperience" : "A visit to the %s teaches you many new skills (+1000 Experience).", + "vcmi.townHall.greetingAttack" : "Some time spent at the %s allows you to learn more effective combat skills (+1 Attack Skill).", + "vcmi.townHall.greetingDefence" : "Spending time in the %s, the experienced warriors therein teach you additional defensive skills (+1 Defense).", + "vcmi.townHall.hasNotProduced" : "The %s has not produced anything yet.", + "vcmi.townHall.hasProduced" : "The %s produced %d %s this week.", + "vcmi.townHall.greetingCustomBonus" : "%s gives you +%d %s%s", + "vcmi.townHall.greetingCustomUntil" : " until next battle.", + "vcmi.townHall.greetingInTownMagicWell" : "%s has restored your spell points to maximum.", + + "vcmi.logicalExpressions.anyOf" : "Any of the following:", + "vcmi.logicalExpressions.allOf" : "All of the following:", + "vcmi.logicalExpressions.noneOf" : "None of the following:", + + "vcmi.heroWindow.openCommander.hover" : "Open commander window", + "vcmi.heroWindow.openCommander.help" : "Displays information about commander of this hero", + + "vcmi.commanderWindow.artifactMessage" : "Do you want to give this artifact back to hero?", + + "vcmi.creatureWindow.showBonuses.hover" : "Switch to bonuses view", + "vcmi.creatureWindow.showBonuses.help" : "Displays all active bonuses of the commander", + "vcmi.creatureWindow.showSkills.hover" : "Switch to skills view", + "vcmi.creatureWindow.showSkills.help" : "Displays all learned skills of the commander", + "vcmi.creatureWindow.returnArtifact.hover" : "Give back artifact", + "vcmi.creatureWindow.returnArtifact.help" : "Use this button to return stack artifact back into hero backpack", + + "vcmi.questLog.hideComplete.hover" : "Hide complete quests", + "vcmi.questLog.hideComplete.help" : "Hide all quests that already completed", + + "vcmi.randomMapTab.widgets.defaultTemplate" : "default", + "vcmi.randomMapTab.widgets.templateLabel" : "Template", + "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Setup...", + "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Team alignments", + + // few strings from WoG used by vcmi + "vcmi.stackExperience.description" : "» S t a c k E x p e r i e n c e D e t a i l s «\n\nCreature Type ................... : %s\nExperience Rank ................. : %s (%i)\nExperience Points ............... : %i\nExperience Points to Next Rank .. : %i\nMaximum Experience per Battle ... : %i%% (%i)\nNumber of Creatures in stack .... : %i\nMaximum New Recruits\n without losing current Rank .... : %i\nExperience Multiplier ........... : %.2f\nUpgrade Multiplier .............. : %.2f\nExperience after Rank 10 ........ : %i\nMaximum New Recruits to remain at\n Rank 10 if at Maximum Experience : %i", + "vcmi.stackExperience.rank.1" : "Basic", + "vcmi.stackExperience.rank.2" : "Novice", + "vcmi.stackExperience.rank.3" : "Trained", + "vcmi.stackExperience.rank.4" : "Skilled", + "vcmi.stackExperience.rank.5" : "Proven", + "vcmi.stackExperience.rank.6" : "Veteran", + "vcmi.stackExperience.rank.7" : "Adept", + "vcmi.stackExperience.rank.8" : "Expert", + "vcmi.stackExperience.rank.9" : "Elite", + "vcmi.stackExperience.rank.10" : "Master", + "vcmi.stackExperience.rank.11" : "Ace" } diff --git a/config/widgets/battleControlPanel.json b/config/widgets/battleControlPanel.json deleted file mode 100644 index 2bcb3aead..000000000 --- a/config/widgets/battleControlPanel.json +++ /dev/null @@ -1,144 +0,0 @@ -{ - "items": - [ - { - "type": "button", - "name": "options", - "position": {"x": 3, "y": 5}, - "image": "icm003", - "zelp": 381, - "callback": "options", - "hotkey": "o" - }, - - { - "type": "button", - "name": "surrender", - "position": {"x": 54, "y": 5}, - "image": "icm001", - "zelp": 379, - "callback": "surrender", - "hotkey": "s" - }, - - { - "type": "button", - "name": "flee", - "position": {"x": 105, "y": 5}, - "image": "icm002", - "zelp": 380, - "callback": "flee", - "hotkey": "r" - }, - - { - "type": "button", - "name": "autofight", - "position": {"x": 157, "y": 5}, - "image": "icm004", - "zelp": 382, - "callback": "autofight", - "hotkey": "a" - }, - - { - "type": "button", - "name": "cast", - "position": {"x": 645, "y": 5}, - "image": "icm005", - "zelp": 385, - "callback": "spellbook", - "hotkey": "c" - }, - - { - "type": "button", - "name": "wait", - "position": {"x": 696, "y": 5}, - "image": "icm006", - "zelp": 386, - "callback": "wait", - "hotkey": "w" - }, - - { - "type": "button", - "name": "defence", - "position": {"x": 747, "y": 5}, - "image": "icm007", - "zelp": 387, - "callback": "defence", - "hotkey": ["d", "space"] - }, - - { - "type": "button", - "name": "consoleUp", - "position": {"x": 624, "y": 5}, - "image": "ComSlide", - "zelp": "", - "callback": "consoleUp", - "imageOrder": [0, 1, 0, 0], - "hotkey": "up" - }, - - { - "type": "button", - "name": "consoleDown", - "position": {"x": 624, "y": 24}, - "image": "ComSlide", - "zelp": "", - "callback": "consoleDown", - "imageOrder": [2, 3, 2, 2], - "hotkey": "down" - }, - - { - "type": "battleConsole", - "name": "console", - "rect": {"x": 211, "y": 4, "w": 406, "h": 38} - } - ], - "variables": - { - "tacticItems": - [ - { - "type": "button", - "name": "tacticNext", - "position": {"x": 213, "y": 4}, - "image": "icm011", - "zelp": "", - "callback": "tacticNext", - "hotkey": "space" - }, - - { - "type": "button", - "name": "tacticEnd", - "position": {"x": 419, "y": 4}, - "image": "icm012", - "zelp": "", - "callback": "tacticEnd", - "hotkey": "enter" - }, - - { - "type": "picture", - "name": "menu", - "position": {"x": 0, "y": 0}, - "image": "COPLACBR.bmp" - } - ], - - "battleItems": - [ - { - "type": "picture", - "name": "menu", - "position": {"x": 0, "y": 0}, - "image": "CBAR.bmp" - } - ] - } -} diff --git a/config/widgets/battleWindow.json b/config/widgets/battleWindow.json new file mode 100644 index 000000000..21261f32e --- /dev/null +++ b/config/widgets/battleWindow.json @@ -0,0 +1,133 @@ +{ + "items": + [ + { + "type": "picture", + "name": "menuTactics", + "playerColored" : true, + "position": {"x": 0, "y": 556}, + "image": "COPLACBR.bmp" + }, + { + "type": "picture", + "name": "menuBattle", + "playerColored" : true, + "position": {"x": 1, "y": 555}, + "image": "CBAR.bmp" + }, + { + "type": "button", + "name": "options", + "position": {"x": 4, "y": 560}, + "image": "icm003", + "help": "core.help.381", + "callback": "options", + "hotkey": "o" + }, + + { + "type": "button", + "name": "surrender", + "position": {"x": 55, "y": 560}, + "image": "icm001", + "help": "core.help.379", + "callback": "surrender", + "hotkey": "s" + }, + + { + "type": "button", + "name": "flee", + "position": {"x": 106, "y": 560}, + "image": "icm002", + "help": "core.help.380", + "callback": "flee", + "hotkey": "r" + }, + + { + "type": "button", + "name": "autofight", + "position": {"x": 157, "y": 560}, + "image": "icm004", + "help": "core.help.382", + "callback": "autofight", + "hotkey": "a" + }, + + { + "type": "button", + "name": "cast", + "position": {"x": 646, "y": 560}, + "image": "icm005", + "help": "core.help.385", + "callback": "spellbook", + "hotkey": "c" + }, + + { + "type": "button", + "name": "wait", + "position": {"x": 697, "y": 560}, + "image": "icm006", + "help": "core.help.386", + "callback": "wait", + "hotkey": "w" + }, + + { + "type": "button", + "name": "defence", + "position": {"x": 748, "y": 560}, + "image": "icm007", + "help": "core.help.387", + "callback": "defence", + "hotkey": ["d", "space"] + }, + + { + "type": "button", + "name": "consoleUp", + "position": {"x": 625, "y": 560}, + "image": "ComSlide", + "callback": "consoleUp", + "imageOrder": [0, 1, 0, 0], + "hotkey": "up" + }, + + { + "type": "button", + "name": "consoleDown", + "position": {"x": 625, "y": 579}, + "image": "ComSlide", + "callback": "consoleDown", + "imageOrder": [2, 3, 2, 2], + "hotkey": "down" + }, + + { + "type": "battleConsole", + "name": "console", + "imagePosition" : { "x" : 214, "y" : 7 }, + "rect": {"x": 215, "y": 562, "w": 400, "h": 32} + }, + + { + "type": "button", + "name": "tacticNext", + "position": {"x": 213, "y": 560}, + "image": "icm011", + "callback": "tacticNext", + "hotkey": "space" + }, + + { + "type": "button", + "name": "tacticEnd", + "position": {"x": 419, "y": 560}, + "image": "icm012", + "callback": "tacticEnd", + "hotkey": "enter" + } + ] +} diff --git a/config/widgets/randomMapTab.json b/config/widgets/randomMapTab.json index a1741c121..fde414156 100644 --- a/config/widgets/randomMapTab.json +++ b/config/widgets/randomMapTab.json @@ -14,7 +14,7 @@ "font": "big", "alignment": "center", "color": "yellow", - "text": 738, + "text": "core.genrltxt.738", "position": {"x": 222, "y": 36} }, @@ -24,7 +24,7 @@ "font": "small", "alignment": "center", "color": "white", - "text": 739, + "text": "core.genrltxt.739", "position": {"x": 222, "y": 56} }, @@ -34,7 +34,7 @@ "font": "small", "alignment": "center", "color": "white", - "text": 752, + "text": "core.genrltxt.752", "position": {"x": 104, "y": 97} }, @@ -48,7 +48,7 @@ "index": 0, "type": "toggleButton", "image": "RANSIZS", - "zelp": 198, + "help": "core.help.198", "imageOrder": [0, 1, 1, 3], "position": {"x": 0, "y": 0}, }, @@ -56,7 +56,7 @@ { "type": "toggleButton", "image": "RANSIZM", - "zelp": 199, + "help": "core.help.199", "imageOrder": [0, 1, 1, 3], "position": {"x": 47, "y": 0}, }, @@ -64,7 +64,7 @@ { "type": "toggleButton", "image": "RANSIZL", - "zelp": 200, + "help": "core.help.200", "imageOrder": [0, 1, 1, 3], "position": {"x": 94, "y": 0}, }, @@ -72,7 +72,7 @@ { "type": "toggleButton", "image": "RANSIZX", - "zelp": 201, + "help": "core.help.201", "imageOrder": [0, 1, 1, 3], "position": {"x": 141, "y": 0} } @@ -100,56 +100,56 @@ "index": 1, "type": "toggleButton", "image": "RANNUM1", - "zelp": 204, + "help": "core.help.204", "imageOrder": [0, 1, 1, 3], "position": {"x": 0, "y": 0} }, { "type": "toggleButton", "image": "RANNUM2", - "zelp": 205, + "help": "core.help.205", "imageOrder": [0, 1, 1, 3], "position": {"x": 32, "y": 0} }, { "type": "toggleButton", "image": "RANNUM3", - "zelp": 206, + "help": "core.help.206", "imageOrder": [0, 1, 1, 3], "position": {"x": 64, "y": 0} }, { "type": "toggleButton", "image": "RANNUM4", - "zelp": 207, + "help": "core.help.207", "imageOrder": [0, 1, 1, 3], "position": {"x": 96, "y": 0} }, { "type": "toggleButton", "image": "RANNUM5", - "zelp": 208, + "help": "core.help.208", "imageOrder": [0, 1, 1, 3], "position": {"x": 128, "y": 0} }, { "type": "toggleButton", "image": "RANNUM6", - "zelp": 209, + "help": "core.help.209", "imageOrder": [0, 1, 1, 3], "position": {"x": 160, "y": 0} }, { "type": "toggleButton", "image": "RANNUM7", - "zelp": 210, + "help": "core.help.210", "imageOrder": [0, 1, 1, 3], "position": {"x": 192, "y": 0} }, { "type": "toggleButton", "image": "RANNUM8", - "zelp": 211, + "help": "core.help.211", "imageOrder": [0, 1, 1, 3], "position": {"x": 224, "y": 0} }, @@ -157,7 +157,7 @@ "index": -1, "type": "toggleButton", "image": "RANRAND", - "zelp": 212, + "help": "core.help.212", "position": {"x": 256, "y": 0}, } ], @@ -175,56 +175,56 @@ "index": 0, "type": "toggleButton", "image": "RANNUM0", - "zelp": 214, + "help": "core.help.214", "imageOrder": [0, 1, 1, 3], "position": {"x": 0, "y": 0} }, { "type": "toggleButton", "image": "RANNUM1", - "zelp": 215, + "help": "core.help.215", "imageOrder": [0, 1, 1, 3], "position": {"x": 32, "y": 0} }, { "type": "toggleButton", "image": "RANNUM2", - "zelp": 216, + "help": "core.help.216", "imageOrder": [0, 1, 1, 3], "position": {"x": 64, "y": 0} }, { "type": "toggleButton", "image": "RANNUM3", - "zelp": 217, + "help": "core.help.217", "imageOrder": [0, 1, 1, 3], "position": {"x": 96, "y": 0} }, { "type": "toggleButton", "image": "RANNUM4", - "zelp": 218, + "help": "core.help.218", "imageOrder": [0, 1, 1, 3], "position": {"x": 128, "y": 0} }, { "type": "toggleButton", "image": "RANNUM5", - "zelp": 219, + "help": "core.help.219", "imageOrder": [0, 1, 1, 3], "position": {"x": 160, "y": 0} }, { "type": "toggleButton", "image": "RANNUM6", - "zelp": 220, + "help": "core.help.220", "imageOrder": [0, 1, 1, 3], "position": {"x": 192, "y": 0} }, { "type": "toggleButton", "image": "RANNUM7", - "zelp": 221, + "help": "core.help.221", "imageOrder": [0, 1, 1, 3], "position": {"x": 224, "y": 0} }, @@ -232,7 +232,7 @@ "index": -1, "type": "toggleButton", "image": "RANRAND", - "zelp": 222, + "help": "core.help.222", "position": {"x": 256, "y": 0}, } ], @@ -250,56 +250,56 @@ "index": 0, "type": "toggleButton", "image": "RANNUM0", - "zelp": 224, + "help": "core.help.224", "imageOrder": [0, 1, 1, 3], "position": {"x": 0, "y": 0} }, { "type": "toggleButton", "image": "RANNUM1", - "zelp": 225, + "help": "core.help.225", "imageOrder": [0, 1, 1, 3], "position": {"x": 32, "y": 0} }, { "type": "toggleButton", "image": "RANNUM2", - "zelp": 226, + "help": "core.help.226", "imageOrder": [0, 1, 1, 3], "position": {"x": 64, "y": 0} }, { "type": "toggleButton", "image": "RANNUM3", - "zelp": 227, + "help": "core.help.227", "imageOrder": [0, 1, 1, 3], "position": {"x": 96, "y": 0} }, { "type": "toggleButton", "image": "RANNUM4", - "zelp": 228, + "help": "core.help.228", "imageOrder": [0, 1, 1, 3], "position": {"x": 128, "y": 0} }, { "type": "toggleButton", "image": "RANNUM5", - "zelp": 229, + "help": "core.help.229", "imageOrder": [0, 1, 1, 3], "position": {"x": 160, "y": 0} }, { "type": "toggleButton", "image": "RANNUM6", - "zelp": 230, + "help": "core.help.230", "imageOrder": [0, 1, 1, 3], "position": {"x": 192, "y": 0} }, { "type": "toggleButton", "image": "RANNUM7", - "zelp": 231, + "help": "core.help.231", "imageOrder": [0, 1, 1, 3], "position": {"x": 224, "y": 0} }, @@ -307,7 +307,7 @@ "index": -1, "type": "toggleButton", "image": "RANRAND", - "zelp": 232, + "help": "core.help.232", "position": {"x": 256, "y": 0}, } ], @@ -325,49 +325,49 @@ "index": 0, "type": "toggleButton", "image": "RANNUM0", - "zelp": 234, + "help": 234, "imageOrder": [0, 1, 1, 3], "position": {"x": 0, "y": 0} }, { "type": "toggleButton", "image": "RANNUM1", - "zelp": 235, + "help": "core.help.235", "imageOrder": [0, 1, 1, 3], "position": {"x": 32, "y": 0} }, { "type": "toggleButton", "image": "RANNUM2", - "zelp": 236, + "help": "core.help.236", "imageOrder": [0, 1, 1, 3], "position": {"x": 64, "y": 0} }, { "type": "toggleButton", "image": "RANNUM3", - "zelp": 237, + "help": "core.help.237", "imageOrder": [0, 1, 1, 3], "position": {"x": 96, "y": 0} }, { "type": "toggleButton", "image": "RANNUM4", - "zelp": 238, + "help": "core.help.238", "imageOrder": [0, 1, 1, 3], "position": {"x": 128, "y": 0} }, { "type": "toggleButton", "image": "RANNUM5", - "zelp": 239, + "help": "core.help.239", "imageOrder": [0, 1, 1, 3], "position": {"x": 160, "y": 0} }, { "type": "toggleButton", "image": "RANNUM6", - "zelp": 240, + "help": "core.help.240", "imageOrder": [0, 1, 1, 3], "position": {"x": 192, "y": 0} }, @@ -375,7 +375,7 @@ "index": -1, "type": "toggleButton", "image": "RANRAND", - "zelp": 241, + "help": "core.help.241", "position": {"x": 256, "y": 0}, } ], @@ -393,21 +393,21 @@ "index": 0, "type": "toggleButton", "image": "RANNONE", - "zelp": 243, + "help": "core.help.243", "imageOrder": [0, 1, 1, 3], "position": {"x": 0, "y": 0} }, { "type": "toggleButton", "image": "RANNORM", - "zelp": 244, + "help": "core.help.244", "imageOrder": [0, 1, 1, 3], "position": {"x": 85, "y": 0} }, { "type": "toggleButton", "image": "RANISLD", - "zelp": 245, + "help": "core.help.245", "imageOrder": [0, 1, 1, 3], "position": {"x": 170, "y": 0} }, @@ -415,7 +415,7 @@ "index": -1, "type": "toggleButton", "image": "RANRAND", - "zelp": 246, + "help": "core.help.246", "position": {"x": 256, "y": 0}, } ], @@ -433,7 +433,7 @@ "index": 2, "type": "toggleButton", "image": "RANWEAK", - "zelp": 248, + "help": "core.help.248", "imageOrder": [0, 1, 1, 3], "position": {"x": 0, "y": 0} }, @@ -441,7 +441,7 @@ "index": 3, "type": "toggleButton", "image": "RANNORM", - "zelp": 249, + "help": "core.help.249", "imageOrder": [0, 1, 1, 3], "position": {"x": 85, "y": 0} }, @@ -449,7 +449,7 @@ "index": 4, "type": "toggleButton", "image": "RANSTRG", - "zelp": 250, + "help": "core.help.250", "imageOrder": [0, 1, 1, 3], "position": {"x": 170, "y": 0} }, @@ -457,7 +457,7 @@ "index": -2, "type": "toggleButton", "image": "RANRAND", - "zelp": 251, + "help": "core.help.251", "position": {"x": 256, "y": 0}, } ], @@ -470,7 +470,7 @@ "type": "button", "position": {"x": 54, "y": 535}, "image": "RANSHOW", - "zelp": 252 + "help": "core.help.252" }, { @@ -482,27 +482,27 @@ [ { "position": {"x": 68, "y": 133}, - "text": 753 + "text": "core.genrltxt.753" }, { "position": {"x": 68, "y": 199}, - "text": 754 + "text": "core.genrltxt.754" }, { "position": {"x": 68, "y": 265}, - "text": 755 + "text": "core.genrltxt.755" }, { "position": {"x": 68, "y": 331}, - "text": 756 + "text": "core.genrltxt.756" }, { "position": {"x": 68, "y": 398}, - "text": 757 + "text": "core.genrltxt.757" }, { "position": {"x": 68, "y": 465}, - "text": 758 + "text": "core.genrltxt.758" } ] } diff --git a/lib/CGameInterface.cpp b/lib/CGameInterface.cpp index bbbb602dc..6c49fa0ab 100644 --- a/lib/CGameInterface.cpp +++ b/lib/CGameInterface.cpp @@ -174,9 +174,9 @@ void CAdventureAI::battleStart(const CCreatureSet * army1, const CCreatureSet * battleAI->battleStart(army1, army2, tile, hero1, hero2, side); } -void CAdventureAI::battleStacksAttacked(const std::vector & bsa) +void CAdventureAI::battleStacksAttacked(const std::vector & bsa, bool ranged) { - battleAI->battleStacksAttacked(bsa); + battleAI->battleStacksAttacked(bsa, ranged); } void CAdventureAI::actionStarted(const BattleAction & action) @@ -204,9 +204,9 @@ void CAdventureAI::battleObstaclesChanged(const std::vector & o battleAI->battleObstaclesChanged(obstacles); } -void CAdventureAI::battleStackMoved(const CStack * stack, std::vector dest, int distance) +void CAdventureAI::battleStackMoved(const CStack * stack, std::vector dest, int distance, bool teleport) { - battleAI->battleStackMoved(stack, dest, distance); + battleAI->battleStackMoved(stack, dest, distance, teleport); } void CAdventureAI::battleAttack(const BattleAttack * ba) @@ -225,9 +225,9 @@ void CAdventureAI::battleEnd(const BattleResult * br) battleAI.reset(); } -void CAdventureAI::battleUnitsChanged(const std::vector & units, const std::vector & customEffects) +void CAdventureAI::battleUnitsChanged(const std::vector & units) { - battleAI->battleUnitsChanged(units, customEffects); + battleAI->battleUnitsChanged(units); } BattleAction CAdventureAI::activeStack(const CStack * stack) diff --git a/lib/CGameInterface.h b/lib/CGameInterface.h index 973ee1eef..f07a9548a 100644 --- a/lib/CGameInterface.h +++ b/lib/CGameInterface.h @@ -153,17 +153,17 @@ public: virtual void battleNewRound(int round) override; virtual void battleCatapultAttacked(const CatapultAttack & ca) override; virtual void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override; - virtual void battleStacksAttacked(const std::vector & bsa) override; + virtual void battleStacksAttacked(const std::vector & bsa, bool ranged) override; virtual void actionStarted(const BattleAction &action) override; virtual void battleNewRoundFirst(int round) override; virtual void actionFinished(const BattleAction &action) override; virtual void battleStacksEffectsSet(const SetStackEffect & sse) override; virtual void battleObstaclesChanged(const std::vector & obstacles) override; - virtual void battleStackMoved(const CStack * stack, std::vector dest, int distance) override; + virtual void battleStackMoved(const CStack * stack, std::vector dest, int distance, bool teleport) override; virtual void battleAttack(const BattleAttack *ba) override; virtual void battleSpellCast(const BattleSpellCast *sc) override; virtual void battleEnd(const BattleResult *br) override; - virtual void battleUnitsChanged(const std::vector & units, const std::vector & customEffects) override; + virtual void battleUnitsChanged(const std::vector & units) override; virtual void saveGame(BinarySerializer & h, const int version) override; virtual void loadGame(BinaryDeserializer & h, const int version) override; diff --git a/lib/CGameState.cpp b/lib/CGameState.cpp index eb129b066..8bf972536 100644 --- a/lib/CGameState.cpp +++ b/lib/CGameState.cpp @@ -112,11 +112,11 @@ void MetaString::getLocalString(const std::pair &txt, std::string &dst } else if(type == MINE_NAMES) { - dst = VLC->generaltexth->mines[ser].first; + dst = VLC->generaltexth->translate("core.minename", ser); } else if(type == MINE_EVNTS) { - dst = VLC->generaltexth->mines[ser].second; + dst = VLC->generaltexth->translate("core.mineevnt", ser); } else if(type == SPELL_NAME) { @@ -136,48 +136,40 @@ void MetaString::getLocalString(const std::pair &txt, std::string &dst } else { - std::vector *vec; switch(type) { case GENERAL_TXT: - vec = &VLC->generaltexth->allTexts; + dst = VLC->generaltexth->translate("core.genrltxt", ser); break; case XTRAINFO_TXT: - vec = &VLC->generaltexth->xtrainfo; + dst = VLC->generaltexth->translate("core.xtrainfo", ser); break; case RES_NAMES: - vec = &VLC->generaltexth->restypes; + dst = VLC->generaltexth->translate("core.restypes", ser); break; case ARRAY_TXT: - vec = &VLC->generaltexth->arraytxt; + dst = VLC->generaltexth->translate("core.arraytxt", ser); break; case CREGENS: - vec = &VLC->generaltexth->creGens; + dst = VLC->generaltexth->translate("core.crgen1", ser); break; case CREGENS4: - vec = &VLC->generaltexth->creGens4; + dst = VLC->generaltexth->translate("core.crgen4", ser); break; case ADVOB_TXT: - vec = &VLC->generaltexth->advobtxt; + dst = VLC->generaltexth->translate("core.advevent", ser); break; case COLOR: - vec = &VLC->generaltexth->capColors; + dst = VLC->generaltexth->translate("vcmi.capitalColors", ser); break; case JK_TXT: - vec = &VLC->generaltexth->jktexts; + dst = VLC->generaltexth->translate("core.jktext", ser); break; default: logGlobal->error("Failed string substitution because type is %d", type); dst = "#@#"; return; } - if(vec->size() <= ser) - { - logGlobal->error("Failed string substitution with type %d because index %d is out of bounds!", type, ser); - dst = "#!#"; - } - else - dst = (*vec)[ser]; } } @@ -2182,11 +2174,8 @@ void CGameState::updateRumor() FALLTHROUGH case RumorState::TYPE_RAND: - do - { - rumorId = rand.nextInt((int)VLC->generaltexth->tavernRumors.size() - 1); - } - while(!VLC->generaltexth->tavernRumors[rumorId].length()); + auto vector = VLC->generaltexth->findStringsWithPrefix("core.randtvrn"); + rumorId = rand.nextInt((int)vector.size() - 1); break; } diff --git a/lib/CGeneralTextHandler.cpp b/lib/CGeneralTextHandler.cpp index 7d389bbdd..eed81242e 100644 --- a/lib/CGeneralTextHandler.cpp +++ b/lib/CGeneralTextHandler.cpp @@ -16,6 +16,7 @@ #include "CConfigHandler.h" #include "CModHandler.h" #include "GameConstants.h" +#include "mapObjects/CQuest.h" #include "VCMI_Lib.h" #include "Terrain.h" @@ -300,99 +301,153 @@ bool CLegacyConfigParser::endLine() return curr < end; } -void CGeneralTextHandler::readToVector(std::string sourceName, std::vector &dest) +void CGeneralTextHandler::readToVector(std::string const & sourceID, std::string const & sourceName) { CLegacyConfigParser parser(sourceName); + size_t index = 0; do { - dest.push_back(parser.readString()); + registerString({sourceID, index}, parser.readString()); + index += 1; } while (parser.endLine()); } -CGeneralTextHandler::CGeneralTextHandler() +const std::string & CGeneralTextHandler::serialize(const std::string & identifier) const { - std::vector h3mTerrainNames; - readToVector("DATA/VCDESC.TXT", victoryConditions); - readToVector("DATA/LCDESC.TXT", lossCondtions); - readToVector("DATA/TCOMMAND.TXT", tcommands); - readToVector("DATA/HALLINFO.TXT", hcommands); - readToVector("DATA/CASTINFO.TXT", fcommands); - readToVector("DATA/ADVEVENT.TXT", advobtxt); - readToVector("DATA/XTRAINFO.TXT", xtrainfo); - readToVector("DATA/RESTYPES.TXT", restypes); - readToVector("DATA/TERRNAME.TXT", h3mTerrainNames); - readToVector("DATA/RANDSIGN.TXT", randsign); - readToVector("DATA/CRGEN1.TXT", creGens); - readToVector("DATA/CRGEN4.TXT", creGens4); - readToVector("DATA/OVERVIEW.TXT", overview); - readToVector("DATA/ARRAYTXT.TXT", arraytxt); - readToVector("DATA/PRISKILL.TXT", primarySkillNames); - readToVector("DATA/JKTEXT.TXT", jktexts); - readToVector("DATA/TVRNINFO.TXT", tavernInfo); - readToVector("DATA/RANDTVRN.TXT", tavernRumors); - readToVector("DATA/TURNDUR.TXT", turnDurations); - readToVector("DATA/HEROSCRN.TXT", heroscrn); - readToVector("DATA/TENTCOLR.TXT", tentColors); - readToVector("DATA/SKILLLEV.TXT", levels); - - for(int i = 0; i < h3mTerrainNames.size(); ++i) - { - terrainNames[i] = h3mTerrainNames[i]; - } - for(const auto & terrain : VLC->terrainTypeHandler->terrains()) - { - if(!terrain.terrainText.empty()) - terrainNames[terrain.id] = terrain.terrainText; - } - + assert(stringsIdentifiers.count(identifier)); + return stringsIdentifiers.at(identifier); +} + +const std::string & CGeneralTextHandler::deserialize(const TextIdentifier & identifier) const +{ + if(stringsLocalizations.count(identifier.get())) + return stringsLocalizations.at(identifier.get()); + logGlobal->error("Unable to find localization for string '%s'", identifier.get()); + return identifier.get(); +} + +void CGeneralTextHandler::registerString(const TextIdentifier & UID, const std::string & localized) +{ + stringsIdentifiers[localized] = UID.get(); + stringsLocalizations[UID.get()] = localized; +} + +CGeneralTextHandler::CGeneralTextHandler(): + victoryConditions(*this, "core.vcdesc" ), + lossCondtions (*this, "core.lcdesc" ), + colors (*this, "core.plcolors" ), + tcommands (*this, "core.tcommand" ), + hcommands (*this, "core.hallinfo" ), + fcommands (*this, "core.castinfo" ), + advobtxt (*this, "core.advevent" ), + xtrainfo (*this, "core.xtrainfo" ), + restypes (*this, "core.restypes" ), + terrainNames (*this, "core.terrname" ), + randsign (*this, "core.randsign" ), + overview (*this, "core.overview" ), + arraytxt (*this, "core.arraytxt" ), + primarySkillNames(*this, "core.priskill" ), + jktexts (*this, "core.jktext" ), + tavernInfo (*this, "core.tvrninfo" ), + tavernRumors (*this, "core.randtvrn" ), + turnDurations (*this, "core.turndur" ), + heroscrn (*this, "core.heroscrn" ), + tentColors (*this, "core.tentcolr" ), + levels (*this, "core.skilllev" ), + zelp (*this, "core.help" ), + allTexts (*this, "core.genrltxt" ), + // pseudo-array, that don't have H3 file with same name + seerEmpty (*this, "core.seerhut.empty" ), + seerNames (*this, "core.seerhut.names" ), + capColors (*this, "vcmi.capitalColors" ), + znpc00 (*this, "vcmi.znpc00" ), // technically - wog + qeModCommands (*this, "vcmi.quickExchange" ) +{ + readToVector("core.vcdesc", "DATA/VCDESC.TXT" ); + readToVector("core.lcdesc", "DATA/LCDESC.TXT" ); + readToVector("core.tcommand", "DATA/TCOMMAND.TXT" ); + readToVector("core.hallinfo", "DATA/HALLINFO.TXT" ); + readToVector("core.castinfo", "DATA/CASTINFO.TXT" ); + readToVector("core.advevent", "DATA/ADVEVENT.TXT" ); + readToVector("core.xtrainfo", "DATA/XTRAINFO.TXT" ); + readToVector("core.restypes", "DATA/RESTYPES.TXT" ); + readToVector("core.terrname", "DATA/TERRNAME.TXT" ); + readToVector("core.randsign", "DATA/RANDSIGN.TXT" ); + readToVector("core.crgen1", "DATA/CRGEN1.TXT" ); + readToVector("core.crgen4", "DATA/CRGEN4.TXT" ); + readToVector("core.overview", "DATA/OVERVIEW.TXT" ); + readToVector("core.arraytxt", "DATA/ARRAYTXT.TXT" ); + readToVector("core.priskill", "DATA/PRISKILL.TXT" ); + readToVector("core.jktext", "DATA/JKTEXT.TXT" ); + readToVector("core.tvrninfo", "DATA/TVRNINFO.TXT" ); + readToVector("core.turndur", "DATA/TURNDUR.TXT" ); + readToVector("core.heroscrn", "DATA/HEROSCRN.TXT" ); + readToVector("core.tentcolr", "DATA/TENTCOLR.TXT" ); + readToVector("core.skilllev", "DATA/SKILLLEV.TXT" ); + readToVector("core.cmpmusic", "DATA/CMPMUSIC.TXT" ); + readToVector("core.minename", "DATA/MINENAME.TXT" ); + readToVector("core.mineevnt", "DATA/MINEEVNT.TXT" ); static const char * QE_MOD_COMMANDS = "DATA/QECOMMANDS.TXT"; if (CResourceHandler::get()->existsResource(ResourceID(QE_MOD_COMMANDS, EResType::TEXT))) - readToVector(QE_MOD_COMMANDS, qeModCommands); + readToVector("vcmi.quickExchange", QE_MOD_COMMANDS); - localizedTexts = JsonNode(ResourceID("config/translate.json", EResType::TEXT)); + auto vcmiTexts = JsonNode(ResourceID("config/translate.json", EResType::TEXT)); + for ( auto const & node : vcmiTexts.Struct()) + registerString(node.first, node.second.String()); + + { + CLegacyConfigParser parser("DATA/RANDTVRN.TXT"); + parser.endLine(); + size_t index = 0; + do + { + std::string line = parser.readString(); + if(!line.empty()) + { + registerString({"core.randtvrn", index}, line); + index += 1; + } + } + while (parser.endLine()); + } { CLegacyConfigParser parser("DATA/GENRLTXT.TXT"); parser.endLine(); + size_t index = 0; do { - allTexts.push_back(parser.readString()); + registerString({"core.genrltxt", index}, parser.readString()); + index += 1; } while (parser.endLine()); } { CLegacyConfigParser parser("DATA/HELP.TXT"); + size_t index = 0; do { std::string first = parser.readString(); std::string second = parser.readString(); - zelp.push_back(std::make_pair(first, second)); + registerString("core.help." + std::to_string(index) + ".hover", first); + registerString("core.help." + std::to_string(index) + ".help", second); + index += 1; } while (parser.endLine()); } - { - CLegacyConfigParser nameParser("DATA/MINENAME.TXT"); - CLegacyConfigParser eventParser("DATA/MINEEVNT.TXT"); - - do - { - std::string name = nameParser.readString(); - std::string event = eventParser.readString(); - mines.push_back(std::make_pair(name, event)); - } - while (nameParser.endLine() && eventParser.endLine()); - } { CLegacyConfigParser parser("DATA/PLCOLORS.TXT"); + size_t index = 0; do { std::string color = parser.readString(); - colors.push_back(color); + registerString({"core.plcolors", index}, color); color[0] = toupper(color[0]); - capColors.push_back(color); + registerString({"vcmi.capitalColors", index}, color); + index += 1; } while (parser.endLine()); } @@ -402,37 +457,41 @@ CGeneralTextHandler::CGeneralTextHandler() //skip header parser.endLine(); - for (int i = 0; i < 6; ++i) - seerEmpty.push_back(parser.readString()); + for (size_t i = 0; i < 6; ++i) + { + registerString({"core.seerhut.empty", i}, parser.readString()); + } parser.endLine(); - quests.resize(10); - for (int i = 0; i < 9; ++i) //9 types of quests + for (size_t i = 0; i < 9; ++i) //9 types of quests { - quests[i].resize(5); - for (int j = 0; j < 5; ++j) - { - parser.readString(); //front description - for (int k = 0; k < 6; ++k) - quests[i][j].push_back(parser.readString()); + std::string questName = CQuest::missionName(CQuest::Emission(1+i)); + for (size_t j = 0; j < 5; ++j) + { + std::string questState = CQuest::missionState(j); + + parser.readString(); //front description + for (size_t k = 0; k < 6; ++k) + { + registerString({"core.seerhut.quest", questName, questState, k}, parser.readString()); + } parser.endLine(); } } - quests[9].resize(1); - for (int k = 0; k < 6; ++k) //Time limit + for (size_t k = 0; k < 6; ++k) //Time limit { - quests[9][0].push_back(parser.readString()); + registerString({"core.seerhut.time", k}, parser.readString()); } parser.endLine(); parser.endLine(); // empty line parser.endLine(); // header - for (int i = 0; i < 48; ++i) + for (size_t i = 0; i < 48; ++i) { - seerNames.push_back(parser.readString()); + registerString({"core.seerhut.names", i}, parser.readString()); parser.endLine(); } } @@ -443,70 +502,46 @@ CGeneralTextHandler::CGeneralTextHandler() parser.endLine(); std::string text; + size_t campaignsCount = 0; do { text = parser.readString(); if (!text.empty()) - campaignMapNames.push_back(text); + { + registerString({"core.camptext.names", campaignsCount}, text); + campaignsCount += 1; + } } while (parser.endLine() && !text.empty()); - for (size_t i=0; i()); do { text = parser.readString(); if (!text.empty()) - campaignRegionNames.back().push_back(text); + { + registerString({"core.camptext.regions", std::to_string(campaign), region}, text); + region += 1; + } } while (parser.endLine() && !text.empty()); - } - } - if (VLC->modh->modules.STACK_EXP) - { - CLegacyConfigParser parser("DATA/ZCREXP.TXT"); - parser.endLine();//header - for (size_t iter=0; iter<325; iter++) - { - parser.readString(); //ignore 1st column with description - zcrexp.push_back(parser.readString()); - parser.endLine(); - } - // line 325 - some weird formatting here - zcrexp.push_back(parser.readString()); - parser.readString(); - parser.endLine(); - do // rest of file can be read normally - { - parser.readString(); //ignore 1st column with description - zcrexp.push_back(parser.readString()); + scenariosCountPerCampaign.push_back(region); } - while (parser.endLine()); } if (VLC->modh->modules.COMMANDERS) { - try - { - CLegacyConfigParser parser("DATA/ZNPC00.TXT"); - parser.endLine();//header - - do - { - znpc00.push_back(parser.readString()); - } while (parser.endLine()); - } - catch (const std::runtime_error &) - { - logGlobal->warn("WoG file ZNPC00.TXT containing commander texts was not found"); - } + if(CResourceHandler::get()->existsResource(ResourceID("DATA/ZNPC00.TXT", EResType::TEXT))) + readToVector("vcmi.znpc00", "DATA/ZNPC00.TXT" ); } } @@ -522,4 +557,67 @@ int32_t CGeneralTextHandler::pluralText(const int32_t textIndex, const int32_t c return textIndex + 1; } +void CGeneralTextHandler::dumpAllTexts() +{ + logGlobal->info("BEGIN TEXT EXPORT"); + for ( auto const & entry : stringsLocalizations) + { + auto cleanString = entry.second; + boost::replace_all(cleanString, "\\", "\\\\"); + boost::replace_all(cleanString, "\n", "\\n"); + boost::replace_all(cleanString, "\r", "\\r"); + boost::replace_all(cleanString, "\t", "\\t"); + boost::replace_all(cleanString, "\"", "\\\""); + + logGlobal->info("\"%s\" : \"%s\",", entry.first, cleanString); + } + logGlobal->info("END TEXT EXPORT"); +} + +size_t CGeneralTextHandler::getCampaignLength(size_t campaignID) const +{ + assert(campaignID < scenariosCountPerCampaign.size()); + + if(campaignID < scenariosCountPerCampaign.size()) + return scenariosCountPerCampaign[campaignID]; + return 0; +} + +std::vector CGeneralTextHandler::findStringsWithPrefix(std::string const & prefix) +{ + std::vector result; + + for (auto const & entry : stringsLocalizations) + { + if(boost::algorithm::starts_with(entry.first, prefix)) + result.push_back(entry.first); + } + + return result; +} + +LegacyTextContainer::LegacyTextContainer(CGeneralTextHandler & owner, std::string const & basePath): + owner(owner), + basePath(basePath) +{} + +std::string LegacyTextContainer::operator[](size_t index) const +{ + return owner.translate(basePath, index); +} + +LegacyHelpContainer::LegacyHelpContainer(CGeneralTextHandler & owner, std::string const & basePath): + owner(owner), + basePath(basePath) +{} + +std::pair LegacyHelpContainer::operator[](size_t index) const +{ + return { + owner.translate(basePath + "." + std::to_string(index) + ".hover"), + owner.translate(basePath + "." + std::to_string(index) + ".help") + }; +} + + VCMI_LIB_NAMESPACE_END diff --git a/lib/CGeneralTextHandler.h b/lib/CGeneralTextHandler.h index bc5dcc07b..bad392794 100644 --- a/lib/CGeneralTextHandler.h +++ b/lib/CGeneralTextHandler.h @@ -91,62 +91,139 @@ public: CLegacyConfigParser(const std::unique_ptr & input); }; -class DLL_LINKAGE CGeneralTextHandler //Handles general texts +class CGeneralTextHandler; + +/// Small wrapper that provides text access API compatible with old code +class DLL_LINKAGE LegacyTextContainer { + CGeneralTextHandler & owner; + std::string basePath; + public: - JsonNode localizedTexts; + LegacyTextContainer(CGeneralTextHandler & owner, std::string const & basePath); + std::string operator [](size_t index) const; +}; - std::vector allTexts; +/// Small wrapper that provides help text access API compatible with old code +class DLL_LINKAGE LegacyHelpContainer +{ + CGeneralTextHandler & owner; + std::string basePath; - std::vector arraytxt; - std::vector primarySkillNames; - std::vector jktexts; - std::vector heroscrn; - std::vector overview;//text for Kingdom Overview window - std::vector colors; //names of player colors ("red",...) - std::vector capColors; //names of player colors with first letter capitalized ("Red",...) - std::vector turnDurations; //turn durations for pregame (1 Minute ... Unlimited) +public: + LegacyHelpContainer(CGeneralTextHandler & owner, std::string const & basePath); + std::pair operator[](size_t index) const; +}; + +class TextIdentifier +{ + std::string identifier; +public: + std::string const & get() const + { + return identifier; + } + + TextIdentifier(const char * id): + identifier(id) + {} + + TextIdentifier(std::string const & id): + identifier(id) + {} + + template + TextIdentifier(std::string const & id, size_t index, T ... rest): + TextIdentifier(id + '.' + std::to_string(index), rest ... ) + {} + + template + TextIdentifier(std::string const & id, std::string const & id2, T ... rest): + TextIdentifier(id + '.' + id2, rest ... ) + {} +}; + +/// Handles all text-related data in game +class DLL_LINKAGE CGeneralTextHandler +{ + /// map identifier -> localization + std::unordered_map stringsLocalizations; + + /// map localization -> identifier + std::unordered_map stringsIdentifiers; + + void readToVector(const std::string & sourceID, const std::string & sourceName); + + /// number of scenarios in specific campaign. TODO: move to a better location + std::vector scenariosCountPerCampaign; +public: + /// add selected string to internal storage + void registerString(const TextIdentifier & UID, const std::string & localized); + + // returns true if identifier with such name was registered, even if not translated to current language + // not required right now, can be added if necessary + // bool identifierExists( const std::string identifier) const; + + /// returns translated version of a string that can be displayed to user + template + std::string translate(std::string arg1, Args ... args) const + { + TextIdentifier id(arg1, args ...); + return deserialize(id); + } + + /// converts translated string into locale-independent text that can be sent to another client + const std::string & serialize(const std::string & identifier) const; + + /// converts identifier into user-readable string + const std::string & deserialize(const TextIdentifier & identifier) const; + + /// Debug method, dumps all currently known texts into console using Json-like format + void dumpAllTexts(); + + LegacyTextContainer allTexts; + + LegacyTextContainer arraytxt; + LegacyTextContainer primarySkillNames; + LegacyTextContainer jktexts; + LegacyTextContainer heroscrn; + LegacyTextContainer overview;//text for Kingdom Overview window + LegacyTextContainer colors; //names of player colors ("red",...) + LegacyTextContainer capColors; //names of player colors with first letter capitalized ("Red",...) + LegacyTextContainer turnDurations; //turn durations for pregame (1 Minute ... Unlimited) //towns - std::vector tcommands, hcommands, fcommands; //texts for town screen, town hall screen and fort screen - std::vector tavernInfo; - std::vector tavernRumors; + LegacyTextContainer tcommands, hcommands, fcommands; //texts for town screen, town hall screen and fort screen + LegacyTextContainer tavernInfo; + LegacyTextContainer tavernRumors; - std::vector qeModCommands; + LegacyTextContainer qeModCommands; - std::vector> zelp; - std::vector lossCondtions; - std::vector victoryConditions; + LegacyHelpContainer zelp; + LegacyTextContainer lossCondtions; + LegacyTextContainer victoryConditions; //objects - std::vector creGens; //names of creatures' generators - std::vector creGens4; //names of multiple creatures' generators - std::vector advobtxt; - std::vector xtrainfo; - std::vector restypes; //names of resources - std::map terrainNames; - std::vector randsign; - std::vector> mines; //first - name; second - event description - std::vector seerEmpty; - std::vector>> quests; //[quest][type][index] - //type: quest, progress, complete, rollover, log OR time limit //index: 0-2 seer hut, 3-5 border guard - std::vector seerNames; - std::vector tentColors; + LegacyTextContainer advobtxt; + LegacyTextContainer xtrainfo; + LegacyTextContainer restypes; //names of resources + LegacyTextContainer terrainNames; + LegacyTextContainer randsign; + LegacyTextContainer seerEmpty; + LegacyTextContainer seerNames; + LegacyTextContainer tentColors; //sec skills - std::vector levels; - std::vector zcrexp; //more or less useful content of that file + LegacyTextContainer levels; //commanders - std::vector znpc00; //more or less useful content of that file + LegacyTextContainer znpc00; //more or less useful content of that file - //campaigns - std::vector campaignMapNames; - std::vector> campaignRegionNames; - - static void readToVector(std::string sourceName, std::vector &dest); + std::vector findStringsWithPrefix(std::string const & prefix); int32_t pluralText(const int32_t textIndex, const int32_t count) const; + size_t getCampaignLength(size_t campaignID) const; + CGeneralTextHandler(); CGeneralTextHandler(const CGeneralTextHandler&) = delete; CGeneralTextHandler operator=(const CGeneralTextHandler&) = delete; diff --git a/lib/CSoundBase.h b/lib/CSoundBase.h index d8b421494..99edd485b 100644 --- a/lib/CSoundBase.h +++ b/lib/CSoundBase.h @@ -1044,21 +1044,6 @@ public: VCMI_SOUND_LIST sound_after_last }; -#undef VCMI_SOUND_NAME -#define VCMI_SOUND_NAME(sequence) sounds.push_back("" #sequence ""); - - static std::vector & stringsList() - { - static std::vector sounds; - if(!sounds.size()) - { - sounds.push_back("invalid"); - sounds.push_back("sound_todo"); - VCMI_SOUND_LIST - sounds.push_back("sound_after_last"); - } - return sounds; - } #undef VCMI_SOUND_FILE #undef VCMI_SOUND_NAME }; diff --git a/lib/CStack.cpp b/lib/CStack.cpp index 682ed2b09..63046dfaa 100644 --- a/lib/CStack.cpp +++ b/lib/CStack.cpp @@ -201,6 +201,7 @@ void CStack::prepareAttacked(BattleStackAttacked & bsa, vstd::RNG & rand, std::s { auto initialCount = customState->getCount(); + // compute damage and update bsa.damageAmount customState->damage(bsa.damageAmount); bsa.killedAmount = initialCount - customState->getCount(); diff --git a/lib/CondSh.h b/lib/CondSh.h index 165e2a0c1..0120a36fa 100644 --- a/lib/CondSh.h +++ b/lib/CondSh.h @@ -18,6 +18,8 @@ template struct CondSh boost::condition_variable cond; boost::mutex mx; + CondSh() : data(T()) {} + CondSh(T t) : data(t) {} // set data diff --git a/lib/GameConstants.cpp b/lib/GameConstants.cpp index 38c00d67e..c569b8d4c 100644 --- a/lib/GameConstants.cpp +++ b/lib/GameConstants.cpp @@ -232,7 +232,6 @@ std::ostream & operator<<(std::ostream & os, const EActionType actionType) {EActionType::MONSTER_SPELL, "Monster spell"}, {EActionType::BAD_MORALE, "Bad morale"}, {EActionType::STACK_HEAL, "Stack heal"}, - {EActionType::DAEMON_SUMMONING, "Daemon summoning"} }; auto it = actionTypeToString.find(actionType); diff --git a/lib/GameConstants.h b/lib/GameConstants.h index 068fbfd90..b995bc089 100644 --- a/lib/GameConstants.h +++ b/lib/GameConstants.h @@ -911,7 +911,8 @@ enum class EActionType : int32_t INVALID = -1, NO_ACTION = 0, HERO_SPELL, - WALK, DEFEND, + WALK, + DEFEND, RETREAT, SURRENDER, WALK_AND_ATTACK, @@ -921,7 +922,6 @@ enum class EActionType : int32_t MONSTER_SPELL, BAD_MORALE, STACK_HEAL, - DAEMON_SUMMONING }; DLL_LINKAGE std::ostream & operator<<(std::ostream & os, const EActionType actionType); diff --git a/lib/HeroBonus.h b/lib/HeroBonus.h index ef256cc77..d120f8df9 100644 --- a/lib/HeroBonus.h +++ b/lib/HeroBonus.h @@ -237,7 +237,7 @@ public: BONUS_NAME(MANA_CHANNELING) /*value in %, eg. familiar*/ \ BONUS_NAME(SPELL_LIKE_ATTACK) /*subtype - spell, value - spell level; range is taken from spell, but damage from creature; eg. magog*/ \ BONUS_NAME(THREE_HEADED_ATTACK) /*eg. cerberus*/ \ - BONUS_NAME(DAEMON_SUMMONING) /*pit lord, subtype - type of creatures, val - hp per unit*/ \ + BONUS_NAME(UNUSED_BONUS_ENTRY) \ BONUS_NAME(FIRE_IMMUNITY) /*subtype 0 - all, 1 - all except positive, 2 - only damage spells*/ \ BONUS_NAME(WATER_IMMUNITY) \ BONUS_NAME(EARTH_IMMUNITY) \ diff --git a/lib/IGameEventsReceiver.h b/lib/IGameEventsReceiver.h index 9970b2c39..c237c15ac 100644 --- a/lib/IGameEventsReceiver.h +++ b/lib/IGameEventsReceiver.h @@ -49,7 +49,6 @@ struct CObstacleInstance; struct CPackForServer; class EVictoryLossCheckResult; struct MetaString; -struct CustomEffectInfo; class ObstacleChanges; class UnitChanges; @@ -59,18 +58,18 @@ public: virtual void actionFinished(const BattleAction &action){};//occurs AFTER every action taken by any stack or by the hero virtual void actionStarted(const BattleAction &action){};//occurs BEFORE every action taken by any stack or by the hero virtual void battleAttack(const BattleAttack *ba){}; //called when stack is performing attack - virtual void battleStacksAttacked(const std::vector & bsa){}; //called when stack receives damage (after battleAttack()) + virtual void battleStacksAttacked(const std::vector & bsa, bool ranged){}; //called when stack receives damage (after battleAttack()) virtual void battleEnd(const BattleResult *br){}; virtual void battleNewRoundFirst(int round){}; //called at the beginning of each turn before changes are applied; virtual void battleNewRound(int round){}; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn virtual void battleLogMessage(const std::vector & lines){}; - virtual void battleStackMoved(const CStack * stack, std::vector dest, int distance){}; + virtual void battleStackMoved(const CStack * stack, std::vector dest, int distance, bool teleport){}; virtual void battleSpellCast(const BattleSpellCast *sc){}; virtual void battleStacksEffectsSet(const SetStackEffect & sse){};//called when a specific effect is set to stacks virtual void battleTriggerEffect(const BattleTriggerEffect & bte){}; //called for various one-shot effects virtual void battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) {}; //called just before battle start virtual void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side){}; //called by engine when battle starts; side=0 - left, side=1 - right - virtual void battleUnitsChanged(const std::vector & units, const std::vector & customEffects){}; + virtual void battleUnitsChanged(const std::vector & units){}; virtual void battleObstaclesChanged(const std::vector & obstacles){}; virtual void battleCatapultAttacked(const CatapultAttack & ca){}; //called when catapult makes an attack virtual void battleGateStateChanged(const EGateState state){}; diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index a3e2a257f..97da86084 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -1022,20 +1022,6 @@ void JsonUtils::merge(JsonNode & dest, JsonNode & source, bool ignoreOverride, b return; } - bool hasNull = dest.isNull() || source.isNull(); - bool sameType = dest.getType() == source.getType(); - bool sourceNumeric = source.getType() == JsonNode::JsonType::DATA_FLOAT || source.getType() == JsonNode::JsonType::DATA_INTEGER; - bool destNumeric = dest.getType() == JsonNode::JsonType::DATA_FLOAT || dest.getType() == JsonNode::JsonType::DATA_INTEGER; - bool bothNumeric = sourceNumeric && destNumeric; - - MAYBE_UNUSED(hasNull); - MAYBE_UNUSED(sameType); - MAYBE_UNUSED(sourceNumeric); - MAYBE_UNUSED(destNumeric); - MAYBE_UNUSED(bothNumeric); - - assert( hasNull || sameType || bothNumeric ); - switch (source.getType()) { case JsonNode::JsonType::DATA_NULL: diff --git a/lib/LogicalExpression.cpp b/lib/LogicalExpression.cpp index e14891bf2..064d55c44 100644 --- a/lib/LogicalExpression.cpp +++ b/lib/LogicalExpression.cpp @@ -19,7 +19,7 @@ VCMI_LIB_NAMESPACE_BEGIN std::string LogicalExpressionDetail::getTextForOperator(std::string operation) { //placed in cpp mostly to avoid unnecessary includes in header - return VLC->generaltexth->localizedTexts["logicalExpressions"][operation].String(); + return VLC->generaltexth->translate("vcmi.logicalExpressions." + operation); } VCMI_LIB_NAMESPACE_END diff --git a/lib/NetPacks.h b/lib/NetPacks.h index b71499fb0..82df6ab91 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -1548,6 +1548,7 @@ struct BattleStackMoved : public CPackForClient h & stack; h & tilesToMove; h & distance; + h & teleporting; } }; @@ -1560,22 +1561,23 @@ struct BattleUnitsChanged : public CPackForClient void applyCl(CClient *cl); std::vector changedStacks; - std::vector customEffects; template void serialize(Handler & h, const int version) { h & changedStacks; - h & customEffects; } }; struct BattleStackAttacked { BattleStackAttacked(): - stackAttacked(0), attackerID(0), - killedAmount(0), damageAmount(0), + stackAttacked(0), + attackerID(0), + killedAmount(0), + damageAmount(0), newState(), - flags(0), effect(0), spellID(SpellID::NONE) + flags(0), + spellID(SpellID::NONE) {}; DLL_LINKAGE void applyGs(CGameState *gs); @@ -1585,9 +1587,8 @@ struct BattleStackAttacked ui32 killedAmount; int64_t damageAmount; UnitChanges newState; - enum EFlags {KILLED = 1, EFFECT = 2/*deprecated */, SECONDARY = 4, REBIRTH = 8, CLONE_KILLED = 16, SPELL_EFFECT = 32 /*, BONUS_EFFECT = 64 */}; + enum EFlags {KILLED = 1, SECONDARY = 2, REBIRTH = 4, CLONE_KILLED = 8, SPELL_EFFECT = 16, FIRE_SHIELD = 32, }; ui32 flags; //uses EFlags (above) - ui32 effect; //set only if flag EFFECT is set SpellID spellID; //only if flag SPELL_EFFECT is set bool killed() const//if target stack was killed @@ -1598,10 +1599,6 @@ struct BattleStackAttacked { return flags & CLONE_KILLED; } - bool isEffect() const//if stack has been attacked by a spell - { - return flags & EFFECT; - } bool isSecondary() const//if stack was not a primary target (receives no spell effects) { return flags & SECONDARY; @@ -1615,6 +1612,10 @@ struct BattleStackAttacked { return flags & REBIRTH; } + bool fireShield() const + { + return flags & FIRE_SHIELD; + } template void serialize(Handler &h, const int version) { h & stackAttacked; @@ -1623,7 +1624,6 @@ struct BattleStackAttacked h & flags; h & killedAmount; h & damageAmount; - h & effect; h & spellID; } bool operator<(const BattleStackAttacked &b) const @@ -1646,12 +1646,11 @@ struct BattleAttack : public CPackForClient std::vector bsa; ui32 stackAttacking; ui32 flags; //uses Eflags (below) - enum EFlags{SHOT = 1, COUNTER = 2, LUCKY = 4, UNLUCKY = 8, BALLISTA_DOUBLE_DMG = 16, DEATH_BLOW = 32, SPELL_LIKE = 64}; + enum EFlags{SHOT = 1, COUNTER = 2, LUCKY = 4, UNLUCKY = 8, BALLISTA_DOUBLE_DMG = 16, DEATH_BLOW = 32, SPELL_LIKE = 64, LIFE_DRAIN = 128}; + BattleHex tile; SpellID spellID; //for SPELL_LIKE - std::vector customEffects; - bool shot() const//distance attack - decrease number of shots { return flags & SHOT; @@ -1680,13 +1679,17 @@ struct BattleAttack : public CPackForClient { return flags & SPELL_LIKE; } + bool lifeDrain() const + { + return flags & LIFE_DRAIN; + } template void serialize(Handler &h, const int version) { h & bsa; h & stackAttacking; h & flags; + h & tile; h & spellID; - h & customEffects; h & attackerChanges; } }; @@ -1733,8 +1736,9 @@ struct BattleSpellCast : public CPackForClient SpellID spellID; //id of spell ui8 manaGained; //mana channeling ability BattleHex tile; //destination tile (may not be set in some global/mass spells - std::vector customEffects; std::set affectedCres; //ids of creatures affected by this spell, generally used if spell does not set any effect (like dispel or cure) + std::set resistedCres; // creatures that resisted the spell (e.g. Dwarves) + std::set reflectedCres; // creatures that reflected the spell (e.g. Magic Mirror spell) si32 casterStack;// -1 if not cated by creature, >=0 caster stack ID bool castByHero; //if true - spell has been cast by hero, otherwise by a creature @@ -1744,8 +1748,9 @@ struct BattleSpellCast : public CPackForClient h & spellID; h & manaGained; h & tile; - h & customEffects; h & affectedCres; + h & resistedCres; + h & reflectedCres; h & casterStack; h & castByHero; h & activeCast; diff --git a/lib/NetPacksBase.h b/lib/NetPacksBase.h index d14cf0eb4..eec1100d0 100644 --- a/lib/NetPacksBase.h +++ b/lib/NetPacksBase.h @@ -250,27 +250,6 @@ struct ArtifactLocation } }; -///custom effect (resistance, reflection, etc) -struct CustomEffectInfo -{ - CustomEffectInfo() - :effect(0), - sound(0), - stack(0) - { - } - /// WoG AC format - ui32 effect; - ui32 sound; - ui32 stack; - template void serialize(Handler & h, const int version) - { - h & effect; - h & sound; - h & stack; - } -}; - class EntityChanges { public: @@ -299,7 +278,9 @@ public: ADD, RESET_STATE, UPDATE, - REMOVE + REMOVE, + ACTIVATE_AND_UPDATE, + ACTIVATE_AND_REMOVE }; JsonNode data; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 4e052db10..5e4e869ea 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -1670,6 +1670,7 @@ DLL_LINKAGE void BattleObstaclesChanged::applyBattle(IBattleState * battleState) case BattleChanges::EOperation::ADD: battleState->addObstacle(change); break; + case BattleChanges::EOperation::ACTIVATE_AND_UPDATE: case BattleChanges::EOperation::UPDATE: battleState->updateObstacle(change); break; diff --git a/lib/ObstacleHandler.h b/lib/ObstacleHandler.h index f79ff6877..77ed0a078 100644 --- a/lib/ObstacleHandler.h +++ b/lib/ObstacleHandler.h @@ -32,7 +32,7 @@ public: Obstacle obstacle; si32 iconIndex; std::string identifier; - std::string appearAnimation, animation, dissapearAnimation; + std::string appearSound, appearAnimation, triggerAnimation, triggerSound, animation; std::vector allowedTerrains; std::vector allowedSpecialBfields; @@ -62,7 +62,12 @@ public: h & identifier; h & animation; h & appearAnimation; - h & dissapearAnimation; + h & triggerAnimation; + if (version > 806) + { + h & appearSound; + h & triggerSound; + } h & allowedTerrains; h & allowedSpecialBfields; h & isAbsoluteObstacle; diff --git a/lib/battle/BattleHex.cpp b/lib/battle/BattleHex.cpp index 7763fb335..a8a026af8 100644 --- a/lib/battle/BattleHex.cpp +++ b/lib/battle/BattleHex.cpp @@ -140,12 +140,23 @@ std::vector BattleHex::neighbouringTiles() const return ret; } -signed char BattleHex::mutualPosition(BattleHex hex1, BattleHex hex2) +std::vector BattleHex::allNeighbouringTiles() const +{ + std::vector ret; + ret.resize(6); + + for(EDir dir = EDir(0); dir <= EDir(5); dir = EDir(dir+1)) + ret[dir] = cloneInDirection(dir, false); + + return ret; +} + +BattleHex::EDir BattleHex::mutualPosition(BattleHex hex1, BattleHex hex2) { for(EDir dir = EDir(0); dir <= EDir(5); dir = EDir(dir+1)) if(hex2 == hex1.cloneInDirection(dir,false)) return dir; - return INVALID; + return NONE; } char BattleHex::getDistance(BattleHex hex1, BattleHex hex2) diff --git a/lib/battle/BattleHex.h b/lib/battle/BattleHex.h index 965644e9c..f3c92b97c 100644 --- a/lib/battle/BattleHex.h +++ b/lib/battle/BattleHex.h @@ -15,7 +15,7 @@ VCMI_LIB_NAMESPACE_BEGIN namespace BattleSide { - enum + enum Type { ATTACKER = 0, DEFENDER = 1 @@ -47,13 +47,18 @@ struct DLL_LINKAGE BattleHex //TODO: decide if this should be changed to class f static const si16 INVALID = -1; enum EDir { + NONE = -1, + TOP_LEFT, TOP_RIGHT, RIGHT, BOTTOM_RIGHT, BOTTOM_LEFT, LEFT, - NONE + + //Note: unused by BattleHex class, used by other code + TOP, + BOTTOM }; BattleHex(); @@ -74,8 +79,15 @@ struct DLL_LINKAGE BattleHex //TODO: decide if this should be changed to class f BattleHex& operator+=(EDir dir); BattleHex cloneInDirection(EDir dir, bool hasToBeValid = true) const; BattleHex operator+(EDir dir) const; + + /// returns all valid neighbouring tiles std::vector neighbouringTiles() const; - static signed char mutualPosition(BattleHex hex1, BattleHex hex2); + + /// returns all tiles, unavailable tiles will be set as invalid + /// order of returned tiles matches EDir enim + std::vector allNeighbouringTiles() const; + + static EDir mutualPosition(BattleHex hex1, BattleHex hex2); static char getDistance(BattleHex hex1, BattleHex hex2); static void checkAndPush(BattleHex tile, std::vector & ret); static BattleHex getClosestTile(ui8 side, BattleHex initialPos, std::set & possibilities); //TODO: vector or set? copying one to another is bad diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index cc5e4b275..808249246 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -231,8 +231,6 @@ std::vector CBattleInfoCallback::getClientActionsFor } if(stack->hasBonusOfType(Bonus::RANDOM_SPELLCASTER)) allowedActionList.push_back(PossiblePlayerBattleAction::RANDOM_GENIE_SPELL); - if(stack->hasBonusOfType(Bonus::DAEMON_SUMMONING)) - allowedActionList.push_back(PossiblePlayerBattleAction::RISE_DEMONS); } if(stack->canShoot()) allowedActionList.push_back(PossiblePlayerBattleAction::SHOOT); @@ -1380,17 +1378,17 @@ ReachabilityInfo CBattleInfoCallback::getFlyingReachability(const ReachabilityIn AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes (const battle::Unit* attacker, BattleHex destinationTile, BattleHex attackerPos) const { //does not return hex attacked directly - //TODO: apply rotation to two-hex attackers - bool isAttacker = attacker->unitSide() == BattleSide::ATTACKER; - AttackableTiles at; RETURN_IF_NOT_BATTLE(at); - const int WN = GameConstants::BFIELD_WIDTH; BattleHex hex = (attackerPos != BattleHex::INVALID) ? attackerPos : attacker->getPosition(); //real or hypothetical (cursor) position + auto defender = battleGetUnitByPos(destinationTile, true); + if (!defender) + return at; // can't attack thin air + //FIXME: dragons or cerbers can rotate before attack, making their base hex different (#1124) - bool reverse = isToReverse(hex, destinationTile, isAttacker, attacker->doubleWide(), isAttacker); + bool reverse = isToReverse(destinationTile, attacker, defender); if(reverse && attacker->doubleWide()) { hex = attacker->occupiedHex(hex); //the other hex stack stands on @@ -1433,34 +1431,17 @@ AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes (const battl } else if(attacker->hasBonusOfType(Bonus::TWO_HEX_ATTACK_BREATH)) { - int pos = BattleHex::mutualPosition(destinationTile, hex); - if(pos > -1) //only adjacent hexes are subject of dragon breath calculation + auto direction = BattleHex::mutualPosition(hex, destinationTile); + if(direction != BattleHex::NONE) //only adjacent hexes are subject of dragon breath calculation { - std::vector hexes; //only one, in fact - int pseudoVector = destinationTile.hex - hex; - switch(pseudoVector) - { - case 1: - case -1: - BattleHex::checkAndPush(destinationTile.hex + pseudoVector, hexes); - break; - case WN: //17 //left-down or right-down - case -WN: //-17 //left-up or right-up - case WN + 1: //18 //right-down - case -WN + 1: //-16 //right-up - BattleHex::checkAndPush(destinationTile.hex + pseudoVector + (((hex / WN) % 2) ? 1 : -1), hexes); - break; - case WN - 1: //16 //left-down - case -WN - 1: //-18 //left-up - BattleHex::checkAndPush(destinationTile.hex + pseudoVector + (((hex / WN) % 2) ? 1 : 0), hexes); - break; - } - for(BattleHex tile : hexes) + BattleHex nextHex = destinationTile.cloneInDirection(direction, false); + + if (nextHex.isValid()) { //friendly stacks can also be damaged by Dragon Breath - auto st = battleGetUnitByPos(tile, true); + auto st = battleGetUnitByPos(nextHex, true); if(st != nullptr) - at.friendlyCreaturePositions.insert(tile); + at.friendlyCreaturePositions.insert(nextHex); } } } @@ -1544,60 +1525,50 @@ std::set CBattleInfoCallback::getAttackedCreatures(const CStack* return attackedCres; } -//TODO: this should apply also to mechanics and cursor interface -bool CBattleInfoCallback::isToReverseHlp (BattleHex hexFrom, BattleHex hexTo, bool curDir) const +static bool isHexInFront(BattleHex hex, BattleHex testHex, BattleSide::Type side ) { - int fromX = hexFrom.getX(); - int fromY = hexFrom.getY(); - int toX = hexTo.getX(); - int toY = hexTo.getY(); + static const std::set rightDirs { BattleHex::BOTTOM_RIGHT, BattleHex::TOP_RIGHT, BattleHex::RIGHT }; + static const std::set leftDirs { BattleHex::BOTTOM_LEFT, BattleHex::TOP_LEFT, BattleHex::LEFT }; - if (curDir) // attacker, facing right - { - if (fromX < toX) - return false; - if (fromX > toX) - return true; + auto mutualPos = BattleHex::mutualPosition(hex, testHex); - if (fromY % 2 == 0 && toY % 2 == 1) - - return true; - return false; - } - else // defender, facing left - { - if(fromX < toX) - return true; - if(fromX > toX) - return false; - - if (fromY % 2 == 1 && toY % 2 == 0) - return true; - return false; - } + if (side == BattleSide::ATTACKER) + return rightDirs.count(mutualPos); + else + return leftDirs.count(mutualPos); } //TODO: this should apply also to mechanics and cursor interface -bool CBattleInfoCallback::isToReverse (BattleHex hexFrom, BattleHex hexTo, bool curDir, bool toDoubleWide, bool toDir) const +bool CBattleInfoCallback::isToReverse (BattleHex attackerHex, const battle::Unit * attacker, const battle::Unit * defender) const { - if (hexTo < 0 || hexFrom < 0) //turret + if (attackerHex < 0 ) //turret return false; - if (toDoubleWide) - { - if (isToReverseHlp (hexFrom, hexTo, curDir)) - { - if (toDir) - return isToReverseHlp (hexFrom, hexTo-1, curDir); - else - return isToReverseHlp (hexFrom, hexTo+1, curDir); - } + BattleHex defenderHex = defender->getPosition(); + + if (isHexInFront(attackerHex, defenderHex, BattleSide::Type(attacker->unitSide()))) return false; - } - else + + if (defender->doubleWide()) { - return isToReverseHlp(hexFrom, hexTo, curDir); + if (isHexInFront(attackerHex,defender->occupiedHex(), BattleSide::Type(attacker->unitSide()))) + return false; } + + if (attacker->doubleWide()) + { + if (isHexInFront(attacker->occupiedHex(), defenderHex, BattleSide::Type(attacker->unitSide()))) + return false; + } + + // a bit weird case since here defender is slightly behind attacker, so reversing seems preferable, + // but this is how H3 handles it which is important, e.g. for direction of dragon breath attacks + if (attacker->doubleWide() && defender->doubleWide()) + { + if (isHexInFront(attacker->occupiedHex(), defender->occupiedHex(), BattleSide::Type(attacker->unitSide()))) + return false; + } + return true; } ReachabilityInfo::TDistances CBattleInfoCallback::battleGetDistances(const battle::Unit * unit, BattleHex assumedPosition) const diff --git a/lib/battle/CBattleInfoCallback.h b/lib/battle/CBattleInfoCallback.h index db88b9f11..75fc6bf81 100644 --- a/lib/battle/CBattleInfoCallback.h +++ b/lib/battle/CBattleInfoCallback.h @@ -49,7 +49,7 @@ enum class PossiblePlayerBattleAction // actions performed at l-click MOVE_STACK, ATTACK, WALK_AND_ATTACK, ATTACK_AND_RETURN, SHOOT, //OPEN_GATE, //we can open castle gate during siege NO_LOCATION, ANY_LOCATION, OBSTACLE, TELEPORT, SACRIFICE, RANDOM_GENIE_SPELL, FREE_LOCATION, //used with Force Field and Fire Wall - all tiles affected by spell must be free - CATAPULT, HEAL, RISE_DEMONS, + CATAPULT, HEAL, AIMED_SPELL_CREATURE }; @@ -136,8 +136,7 @@ public: AttackableTiles getPotentiallyShootableHexes(const battle::Unit* attacker, BattleHex destinationTile, BattleHex attackerPos) const; std::vector getAttackedBattleUnits(const battle::Unit* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos = BattleHex::INVALID) const; //calculates range of multi-hex attacks std::set getAttackedCreatures(const CStack* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos = BattleHex::INVALID) const; //calculates range of multi-hex attacks - bool isToReverse(BattleHex hexFrom, BattleHex hexTo, bool curDir /*if true, creature is in attacker's direction*/, bool toDoubleWide, bool toDir) const; //determines if creature should be reversed (it stands on hexFrom and should 'see' hexTo) - bool isToReverseHlp(BattleHex hexFrom, BattleHex hexTo, bool curDir) const; //helper for isToReverse + bool isToReverse(BattleHex attackerHex, const battle::Unit *attacker, const battle::Unit *defender) const; //determines if attacker standing at attackerHex should reverse in order to attack defender ReachabilityInfo getReachability(const battle::Unit * unit) const; ReachabilityInfo getReachability(const ReachabilityInfo::Parameters & params) const; diff --git a/lib/battle/CObstacleInstance.cpp b/lib/battle/CObstacleInstance.cpp index 262fa0380..94e1d9d01 100644 --- a/lib/battle/CObstacleInstance.cpp +++ b/lib/battle/CObstacleInstance.cpp @@ -176,7 +176,10 @@ void SpellCreatedObstacle::serializeJson(JsonSerializeFormat & handler) handler.serializeBool("trap", trap); handler.serializeBool("removeOnTrigger", removeOnTrigger); + handler.serializeString("appearSound", appearSound); handler.serializeString("appearAnimation", appearAnimation); + handler.serializeString("triggerSound", triggerSound); + handler.serializeString("triggerAnimation", triggerAnimation); handler.serializeString("animation", animation); handler.serializeInt("animationYOffset", animationYOffset); diff --git a/lib/battle/CObstacleInstance.h b/lib/battle/CObstacleInstance.h index 1ca3d38fb..a4731d608 100644 --- a/lib/battle/CObstacleInstance.h +++ b/lib/battle/CObstacleInstance.h @@ -79,7 +79,10 @@ struct DLL_LINKAGE SpellCreatedObstacle : CObstacleInstance bool revealed; + std::string appearSound; std::string appearAnimation; + std::string triggerSound; + std::string triggerAnimation; std::string animation; int animationYOffset; diff --git a/lib/battle/CUnitState.cpp b/lib/battle/CUnitState.cpp index 770ce2b9d..0d97fea3d 100644 --- a/lib/battle/CUnitState.cpp +++ b/lib/battle/CUnitState.cpp @@ -547,6 +547,11 @@ bool CUnitState::isGhost() const return ghost; } +bool CUnitState::isFrozen() const +{ + return hasBonus(Selector::source(Bonus::SPELL_EFFECT, SpellID::STONE_GAZE), Selector::all); +} + bool CUnitState::isValidTarget(bool allowDead) const { return (alive() || (allowDead && isDead())) && getPosition().isValid() && !isTurret(); @@ -818,7 +823,7 @@ void CUnitState::damage(int64_t & amount) // block ability should not kill clone (0 damage) if(amount > 0) { - amount = 1;//TODO: what should be actual damage against clone? + amount = 0; health.reset(); } } diff --git a/lib/battle/CUnitState.h b/lib/battle/CUnitState.h index 5bd3e02e4..36a716299 100644 --- a/lib/battle/CUnitState.h +++ b/lib/battle/CUnitState.h @@ -195,6 +195,7 @@ public: bool ableToRetaliate() const override; bool alive() const override; bool isGhost() const override; + bool isFrozen() const override; bool isValidTarget(bool allowDead = false) const override; bool isClone() const override; diff --git a/lib/battle/Unit.h b/lib/battle/Unit.h index 656c7bd7a..5a0e858d7 100644 --- a/lib/battle/Unit.h +++ b/lib/battle/Unit.h @@ -43,6 +43,7 @@ public: virtual bool ableToRetaliate() const = 0; virtual bool alive() const = 0; virtual bool isGhost() const = 0; + virtual bool isFrozen() const = 0; bool isDead() const; bool isTurret() const; diff --git a/lib/logging/CLogger.cpp b/lib/logging/CLogger.cpp index 564e6a6de..c892eecd2 100644 --- a/lib/logging/CLogger.cpp +++ b/lib/logging/CLogger.cpp @@ -146,7 +146,7 @@ void CLogger::log(ELogLevel::ELogLevel level, const boost::format & fmt) const } catch(...) { - log(ELogLevel::ERROR, "Invalid log format!"); + log(ELogLevel::ERROR, "Invalid log format!"); } } @@ -295,6 +295,7 @@ std::string CLogFormatter::format(const LogRecord & record) const boost::algorithm::replace_first(message, "%n", record.domain.getName()); boost::algorithm::replace_first(message, "%t", record.threadId); boost::algorithm::replace_first(message, "%m", record.message); + boost::algorithm::replace_first(message, "%c", boost::posix_time::to_simple_string(record.timeStamp)); //return boost::to_string (boost::format("%d %d %d[%d] - %d") % dateStream.str() % level % record.domain.getName() % record.threadId % record.message); @@ -348,9 +349,9 @@ EConsoleTextColor::EConsoleTextColor CColorMapping::getColorFor(const CLoggerDom CLogConsoleTarget::CLogConsoleTarget(CConsoleHandler * console) : #ifndef VCMI_IOS - console(console), + console(console), #endif - threshold(ELogLevel::INFO), coloredOutputEnabled(true) + threshold(ELogLevel::INFO), coloredOutputEnabled(true) { formatter.setPattern("%m"); } @@ -363,7 +364,7 @@ void CLogConsoleTarget::write(const LogRecord & record) std::string message = formatter.format(record); #ifdef VCMI_ANDROID - __android_log_write(ELogLevel::toAndroid(record.level), ("VCMI-" + record.domain.getName()).c_str(), message.c_str()); + __android_log_write(ELogLevel::toAndroid(record.level), ("VCMI-" + record.domain.getName()).c_str(), message.c_str()); #elif defined(VCMI_IOS) os_log_type_t type; switch (record.level) diff --git a/lib/mapObjects/CBank.cpp b/lib/mapObjects/CBank.cpp index 7cbe050b1..ffdab4555 100644 --- a/lib/mapObjects/CBank.cpp +++ b/lib/mapObjects/CBank.cpp @@ -24,7 +24,7 @@ VCMI_LIB_NAMESPACE_BEGIN ///helpers -static std::string & visitedTxt(const bool visited) +static std::string visitedTxt(const bool visited) { int id = visited ? 352 : 353; return VLC->generaltexth->allTexts[id]; diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 74176223a..6b5301732 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -319,6 +319,9 @@ void CGHeroInstance::initHero(CRandomGenerator & rand) } assert(validTypes()); + if (patrol.patrolling) + patrol.initialPos = visitablePos(); + if(exp == 0xffffffff) { initExp(rand); diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index bb6a0bb8c..55b5ac447 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -1815,22 +1815,22 @@ const std::string CGTownBuilding::getVisitingBonusGreeting() const switch(bType) { case BuildingSubID::MANA_VORTEX: - bonusGreeting = std::string(VLC->generaltexth->localizedTexts["townHall"]["greetingManaVortex"].String()); + bonusGreeting = std::string(VLC->generaltexth->translate("vcmi.townHall.greetingManaVortex")); break; case BuildingSubID::KNOWLEDGE_VISITING_BONUS: - bonusGreeting = std::string(VLC->generaltexth->localizedTexts["townHall"]["greetingKnowledge"].String()); + bonusGreeting = std::string(VLC->generaltexth->translate("vcmi.townHall.greetingKnowledge")); break; case BuildingSubID::SPELL_POWER_VISITING_BONUS: - bonusGreeting = std::string(VLC->generaltexth->localizedTexts["townHall"]["greetingSpellPower"].String()); + bonusGreeting = std::string(VLC->generaltexth->translate("vcmi.townHall.greetingSpellPower")); break; case BuildingSubID::ATTACK_VISITING_BONUS: - bonusGreeting = std::string(VLC->generaltexth->localizedTexts["townHall"]["greetingAttack"].String()); + bonusGreeting = std::string(VLC->generaltexth->translate("vcmi.townHall.greetingAttack")); break; case BuildingSubID::EXPERIENCE_VISITING_BONUS: - bonusGreeting = std::string(VLC->generaltexth->localizedTexts["townHall"]["greetingExperience"].String()); + bonusGreeting = std::string(VLC->generaltexth->translate("vcmi.townHall.greetingExperience")); break; case BuildingSubID::DEFENSE_VISITING_BONUS: - bonusGreeting = std::string(VLC->generaltexth->localizedTexts["townHall"]["greetingDefence"].String()); + bonusGreeting = std::string(VLC->generaltexth->translate("vcmi.townHall.greetingDefence")); break; } auto buildingName = town->town->getSpecialBuilding(bType)->Name(); @@ -1849,12 +1849,12 @@ const std::string CGTownBuilding::getCustomBonusGreeting(const Bonus & bonus) co { if(bonus.type == Bonus::TOWN_MAGIC_WELL) { - auto bonusGreeting = std::string(VLC->generaltexth->localizedTexts["townHall"]["greetingInTownMagicWell"].String()); + auto bonusGreeting = std::string(VLC->generaltexth->translate("vcmi.townHall.greetingInTownMagicWell")); auto buildingName = town->town->getSpecialBuilding(bType)->Name(); boost::algorithm::replace_first(bonusGreeting, "%s", buildingName); return bonusGreeting; } - auto bonusGreeting = std::string(VLC->generaltexth->localizedTexts["townHall"]["greetingCustomBonus"].String()); //"%s gives you +%d %s%s" + auto bonusGreeting = std::string(VLC->generaltexth->translate("vcmi.townHall.greetingCustomBonus")); //"%s gives you +%d %s%s" std::string param = ""; std::string until = ""; @@ -1864,7 +1864,7 @@ const std::string CGTownBuilding::getCustomBonusGreeting(const Bonus & bonus) co param = VLC->generaltexth->allTexts[385]; until = bonus.duration == (ui16)Bonus::ONE_BATTLE - ? VLC->generaltexth->localizedTexts["townHall"]["greetingCustomUntil"].String() : "."; + ? VLC->generaltexth->translate("vcmi.townHall.greetingCustomUntil") : "."; boost::format fmt = boost::format(bonusGreeting) % bonus.description % bonus.val % param % until; std::string greeting = fmt.str(); diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 837841715..d945c5eb6 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -56,12 +56,48 @@ static void showInfoDialog(const CGHeroInstance* h, const ui32 txtID, const ui16 showInfoDialog(playerID,txtID,soundID); } -static std::string & visitedTxt(const bool visited) +static std::string visitedTxt(const bool visited) { int id = visited ? 352 : 353; return VLC->generaltexth->allTexts[id]; } +const std::string & CQuest::missionName(CQuest::Emission mission) +{ + static const std::array names = { + "empty", + "heroLevel", + "primarySkill", + "killHero", + "killCreature", + "bringArt", + "bringCreature", + "bringResources", + "bringHero", + "bringPlayer", + "keymaster" + }; + + if(static_cast(mission) < names.size()) + return names[static_cast(mission)]; + return names[0]; +} + +const std::string & CQuest::missionState(int state) +{ + static const std::array states = { + "receive", + "visit", + "complete", + "hover", + "description", + }; + + if(state < states.size()) + return states[state]; + return states[0]; +} + bool CQuest::checkMissionArmy(const CQuest * q, const CCreatureSet * army) { std::vector::const_iterator cre; @@ -264,7 +300,10 @@ void CQuest::getRolloverText(MetaString &ms, bool onHover) const if(onHover) ms << "\n\n"; - ms << VLC->generaltexth->quests[missionType-1][onHover ? 3 : 4][textOption]; + std::string questName = missionName(Emission(missionType-1)); + std::string questState = missionState(onHover ? 3 : 4); + + ms << VLC->generaltexth->translate("core.seerhut.quest", questName, questState,textOption); switch(missionType) { @@ -535,7 +574,9 @@ void CGSeerHut::setObjToKill() void CGSeerHut::init(CRandomGenerator & rand) { - seerName = *RandomGeneratorUtil::nextItem(VLC->generaltexth->seerNames, rand); + auto names = VLC->generaltexth->findStringsWithPrefix("core.seerhut.names"); + + seerName = *RandomGeneratorUtil::nextItem(names, rand); quest->textOption = rand.nextInt(2); quest->completedOption = rand.nextInt(1, 3); } @@ -547,12 +588,14 @@ void CGSeerHut::initObj(CRandomGenerator & rand) quest->progress = CQuest::NOT_ACTIVE; if(quest->missionType) { + std::string questName = quest->missionName(quest->missionType); + if(!quest->isCustomFirst) - quest->firstVisitText = VLC->generaltexth->quests[quest->missionType-1][0][quest->textOption]; + quest->firstVisitText = VLC->generaltexth->translate("core.seerhut.quest." + questName + "." + quest->missionState(0), quest->textOption); if(!quest->isCustomNext) - quest->nextVisitText = VLC->generaltexth->quests[quest->missionType-1][1][quest->textOption]; + quest->nextVisitText = VLC->generaltexth->translate("core.seerhut.quest." + questName + "." + quest->missionState(1), quest->textOption); if(!quest->isCustomComplete) - quest->completedText = VLC->generaltexth->quests[quest->missionType-1][2][quest->textOption]; + quest->completedText = VLC->generaltexth->translate("core.seerhut.quest." + questName + "." + quest->missionState(2), quest->textOption); } else { diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index 12312bac3..83c55bf96 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -24,9 +24,28 @@ class DLL_LINKAGE CQuest final mutable std::unordered_map artifactsRequirements; // artifact ID -> required count public: - enum Emission {MISSION_NONE = 0, MISSION_LEVEL = 1, MISSION_PRIMARY_STAT = 2, MISSION_KILL_HERO = 3, MISSION_KILL_CREATURE = 4, - MISSION_ART = 5, MISSION_ARMY = 6, MISSION_RESOURCES = 7, MISSION_HERO = 8, MISSION_PLAYER = 9, MISSION_KEYMASTER = 10}; - enum Eprogress {NOT_ACTIVE, IN_PROGRESS, COMPLETE}; + enum Emission { + MISSION_NONE = 0, + MISSION_LEVEL = 1, + MISSION_PRIMARY_STAT = 2, + MISSION_KILL_HERO = 3, + MISSION_KILL_CREATURE = 4, + MISSION_ART = 5, + MISSION_ARMY = 6, + MISSION_RESOURCES = 7, + MISSION_HERO = 8, + MISSION_PLAYER = 9, + MISSION_KEYMASTER = 10 + }; + + enum Eprogress { + NOT_ACTIVE, + IN_PROGRESS, + COMPLETE + }; + + static const std::string & missionName(Emission mission); + static const std::string & missionState(int index); si32 qid; //unique quest id for serialization / identification diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index e6452f53d..0b8e73f52 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -402,7 +402,7 @@ Component CRewardInfo::getDisplayedComponent(const CGHeroInstance * h) const } // FIXME: copy-pasted from CObjectHandler -static std::string & visitedTxt(const bool visited) +static std::string visitedTxt(const bool visited) { int id = visited ? 352 : 353; return VLC->generaltexth->allTexts[id]; diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 1200ddb1b..3d784a602 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -58,7 +58,7 @@ static void showInfoDialog(const CGHeroInstance* h, const ui32 txtID, const ui16 showInfoDialog(playerID,txtID,soundID); } -static std::string & visitedTxt(const bool visited) +static std::string visitedTxt(const bool visited) { int id = visited ? 352 : 353; return VLC->generaltexth->allTexts[id]; @@ -147,9 +147,8 @@ std::string CGCreature::getHoverText(const CGHeroInstance * hero) const hoverName = getHoverText(hero->tempOwner); } - const JsonNode & texts = VLC->generaltexth->localizedTexts["adventureMap"]["monsterThreat"]; + hoverName += VLC->generaltexth->translate("vcmi.adventureMap.monsterThreat.title"); - hoverName += texts["title"].String(); int choice; double ratio = ((double)getArmyStrength() / hero->getTotalStrength()); if (ratio < 0.1) choice = 0; @@ -164,7 +163,8 @@ std::string CGCreature::getHoverText(const CGHeroInstance * hero) const else if (ratio < 8) choice = 9; else if (ratio < 20) choice = 10; else choice = 11; - hoverName += texts["levels"].Vector()[choice].String(); + + hoverName += VLC->generaltexth->translate("vcmi.adventureMap.monsterThreat.levels." + std::to_string(choice)); return hoverName; } @@ -710,7 +710,7 @@ bool CGMine::isAbandoned() const std::string CGMine::getObjectName() const { - return VLC->generaltexth->mines.at(subID).first; + return VLC->generaltexth->translate("core.minename", subID); } std::string CGMine::getHoverText(PlayerColor player) const @@ -1686,7 +1686,9 @@ void CGSignBottle::initObj(CRandomGenerator & rand) //if no text is set than we pick random from the predefined ones if(message.empty()) { - message = *RandomGeneratorUtil::nextItem(VLC->generaltexth->randsign, rand); + auto vector = VLC->generaltexth->findStringsWithPrefix("core.randsign"); + std::string messageIdentifier = *RandomGeneratorUtil::nextItem(vector, rand); + message = VLC->generaltexth->translate(messageIdentifier); } if(ID == Obj::OCEAN_BOTTLE) diff --git a/lib/mapping/CCampaignHandler.cpp b/lib/mapping/CCampaignHandler.cpp index bf37f812a..eacdc82ad 100644 --- a/lib/mapping/CCampaignHandler.cpp +++ b/lib/mapping/CCampaignHandler.cpp @@ -79,7 +79,7 @@ std::unique_ptr CCampaignHandler::getCampaign( const std::string & na ret->header = readHeaderFromMemory(reader); ret->header.filename = name; - int howManyScenarios = static_cast(VLC->generaltexth->campaignRegionNames[ret->header.mapVersion].size()); + int howManyScenarios = static_cast(VLC->generaltexth->getCampaignLength(ret->header.mapVersion)); for(int g=0; gheader.version, ret->header.mapVersion); @@ -546,11 +546,7 @@ std::string CCampaignHandler::prologVideoName(ui8 index) std::string CCampaignHandler::prologMusicName(ui8 index) { std::vector music; - - VLC->generaltexth->readToVector("Data/CmpMusic.txt", music); - if(index < music.size()) - return music[index]; - return ""; + return VLC->generaltexth->translate("core.cmpmusic." + std::to_string(int(index))); } std::string CCampaignHandler::prologVoiceName(ui8 index) diff --git a/lib/mapping/CCampaignHandler.h b/lib/mapping/CCampaignHandler.h index ebd11d459..ac6d18ca6 100644 --- a/lib/mapping/CCampaignHandler.h +++ b/lib/mapping/CCampaignHandler.h @@ -229,6 +229,8 @@ public: class DLL_LINKAGE CCampaignHandler { + std::vector scenariosCountPerCampaign; + static CCampaignHeader readHeaderFromMemory(CBinaryReader & reader); static CCampaignScenario readScenarioFromMemory(CBinaryReader & reader, int version, int mapVersion ); static CScenarioTravel readScenarioTravelFromMemory(CBinaryReader & reader, int version); diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 7d5ba23c5..719a8eb1e 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1657,15 +1657,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(ObjectInstanceID idToBeGiven, const i nhi->formation = reader.readUInt8(); loadArtifactsOfHero(nhi); nhi->patrol.patrolRadius = reader.readUInt8(); - if(nhi->patrol.patrolRadius == 0xff) - { - nhi->patrol.patrolling = false; - } - else - { - nhi->patrol.patrolling = true; - nhi->patrol.initialPos = nhi->convertToVisitablePos(initialPos); - } + nhi->patrol.patrolling = (nhi->patrol.patrolRadius != 0xff); if(map->version > EMapFormat::ROE) { diff --git a/lib/serializer/CSerializer.h b/lib/serializer/CSerializer.h index bcfc6fed9..9ae6f1714 100644 --- a/lib/serializer/CSerializer.h +++ b/lib/serializer/CSerializer.h @@ -14,7 +14,7 @@ VCMI_LIB_NAMESPACE_BEGIN -const ui32 SERIALIZATION_VERSION = 806; +const ui32 SERIALIZATION_VERSION = 807; const ui32 MINIMAL_SERIALIZATION_VERSION = 805; const std::string SAVEGAME_MAGIC = "VCMISVG"; diff --git a/lib/spells/BattleSpellMechanics.cpp b/lib/spells/BattleSpellMechanics.cpp index 135c9eee2..125798b1e 100644 --- a/lib/spells/BattleSpellMechanics.cpp +++ b/lib/spells/BattleSpellMechanics.cpp @@ -330,8 +330,6 @@ void BattleSpellMechanics::cast(ServerCallback * server, const Target & target) for(auto & p : effectsToApply) p.first->apply(server, this, p.second); -// afterCast(); - if(sc.activeCast) { caster->spendMana(server, spellCost); @@ -342,6 +340,11 @@ void BattleSpellMechanics::cast(ServerCallback * server, const Target & target) otherHero->spendMana(server, -sc.manaGained); } } + + // send empty event to client + // temporary(?) workaround to force animations to trigger + StacksInjured fake_event; + server->apply(&fake_event); } void BattleSpellMechanics::beforeCast(BattleSpellCast & sc, vstd::RNG & rng, const Target & target) @@ -398,12 +401,12 @@ void BattleSpellMechanics::beforeCast(BattleSpellCast & sc, vstd::RNG & rng, con { if(caster->getCasterUnitId() >= 0) { - addCustomEffect(sc, caster->getCasterUnitId(), 3); + sc.reflectedCres.insert(caster->getCasterUnitId()); } } for(auto unit : resisted) - addCustomEffect(sc, unit, 78); + sc.resistedCres.insert(unit->unitId()); } void BattleSpellMechanics::castEval(ServerCallback * server, const Target & target) @@ -427,19 +430,6 @@ void BattleSpellMechanics::castEval(ServerCallback * server, const Target & targ p.first->apply(server, this, p.second); } -void BattleSpellMechanics::addCustomEffect(BattleSpellCast & sc, const battle::Unit * target, ui32 effect) -{ - addCustomEffect(sc, target->unitId(), effect); -} - -void BattleSpellMechanics::addCustomEffect(BattleSpellCast & sc, ui32 targetId, ui32 effect) -{ - CustomEffectInfo customEffect; - customEffect.effect = effect; - customEffect.stack = targetId; - sc.customEffects.push_back(customEffect); -} - std::set BattleSpellMechanics::collectTargets() const { std::set result; @@ -618,9 +608,9 @@ std::vector BattleSpellMechanics::getPossibleDestinations(size_t in Target tmp = current; tmp.emplace_back(dest); - detail::ProblemImpl ingored; + detail::ProblemImpl ignored; - if(canBeCastAt(tmp, ingored)) + if(canBeCastAt(tmp, ignored)) ret.emplace_back(dest); } } @@ -640,7 +630,7 @@ bool BattleSpellMechanics::isReceptive(const battle::Unit * target) const return targetCondition->isReceptive(this, target); } -std::vector BattleSpellMechanics::rangeInHexes(BattleHex centralHex, bool * outDroppedHexes) const +std::vector BattleSpellMechanics::rangeInHexes(BattleHex centralHex) const { if(isMassive() || !centralHex.isValid()) return std::vector(1, BattleHex::INVALID); diff --git a/lib/spells/BattleSpellMechanics.h b/lib/spells/BattleSpellMechanics.h index 3742f1b23..7909da805 100644 --- a/lib/spells/BattleSpellMechanics.h +++ b/lib/spells/BattleSpellMechanics.h @@ -27,22 +27,36 @@ public: BattleSpellMechanics(const IBattleCast * event, std::shared_ptr effects_, std::shared_ptr targetCondition_); virtual ~BattleSpellMechanics(); + // TODO: ??? (what's the difference compared to cast?) void applyEffects(ServerCallback * server, const Target & targets, bool indirect, bool ignoreImmunity) const override; + /// Returns false if spell can not be cast at all, e.g. due to not having any possible target on battlefield bool canBeCast(Problem & problem) const override; + + /// Returns false if spell can not be cast at specifid target bool canBeCastAt(const Target & target, Problem & problem) const override; + // TODO: ??? (what's the difference compared to applyEffects?) void cast(ServerCallback * server, const Target & target) override final; + // TODO: ??? (what's the difference compared to cast?) void castEval(ServerCallback * server, const Target & target) override final; + /// Returns list of affected stack using currently configured target std::vector getAffectedStacks(const Target & target) const override final; + /// Returns list of target types that can be targeted by spell std::vector getTargetTypes() const override final; + + /// Returns vector of all possible destinations for specified aim type + /// index - ??? + /// current - ??? std::vector getPossibleDestinations(size_t index, AimType aimType, const Target & current) const override final; + /// Returns true if spell can be cast on unit bool isReceptive(const battle::Unit * target) const override; - std::vector rangeInHexes(BattleHex centralHex, bool * outDroppedHexes = nullptr) const override; + /// Returns list of hexes that are affected by spell assuming cast at centralHex + std::vector rangeInHexes(BattleHex centralHex) const override; const Spell * getSpell() const override; @@ -57,9 +71,6 @@ private: void beforeCast(BattleSpellCast & sc, vstd::RNG & rng, const Target & target); - void addCustomEffect(BattleSpellCast & sc, const battle::Unit * target, ui32 effect); - void addCustomEffect(BattleSpellCast & sc, ui32 targetId, ui32 effect); - std::set collectTargets() const; static void doRemoveEffects(ServerCallback * server, const std::vector & targets, const CSelector & selector); diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index 9cf572727..7e20dbb92 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -522,8 +522,9 @@ void CSpell::serializeJson(JsonSerializeFormat & handler) } ///CSpell::AnimationInfo -CSpell::AnimationItem::AnimationItem() - :resourceName(""),verticalPosition(VerticalPosition::TOP),pause(0) +CSpell::AnimationItem::AnimationItem() : + verticalPosition(VerticalPosition::TOP), + pause(0) { } @@ -571,11 +572,6 @@ CSpell::TargetInfo::TargetInfo(const CSpell * spell, const int level, spells::Mo massive = levelInfo.range == "X"; clearAffected = levelInfo.clearAffected; clearTarget = levelInfo.clearTarget; - - if(mode == spells::Mode::CREATURE_ACTIVE) - { - massive = false;//FIXME: find better solution for Commander spells - } } bool DLL_LINKAGE isInScreenRange(const int3 & center, const int3 & pos) @@ -890,6 +886,7 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & else if(item.getType() == JsonNode::JsonType::DATA_STRUCT) { newItem.resourceName = item["defName"].String(); + newItem.effectName = item["effectName"].String(); auto vPosStr = item["verticalPosition"].String(); if("bottom" == vPosStr) diff --git a/lib/spells/CSpellHandler.h b/lib/spells/CSpellHandler.h index 53345fcfe..a9a3c0a68 100644 --- a/lib/spells/CSpellHandler.h +++ b/lib/spells/CSpellHandler.h @@ -76,6 +76,7 @@ public: struct AnimationItem { std::string resourceName; + std::string effectName; VerticalPosition verticalPosition; int pause; @@ -84,6 +85,8 @@ public: template void serialize(Handler & h, const int version) { h & resourceName; + if (version > 806) + h & effectName; h & verticalPosition; h & pause; } diff --git a/lib/spells/ISpellMechanics.cpp b/lib/spells/ISpellMechanics.cpp index 5709ef6d1..5e8f39287 100644 --- a/lib/spells/ISpellMechanics.cpp +++ b/lib/spells/ISpellMechanics.cpp @@ -701,9 +701,9 @@ PlayerColor BaseMechanics::getCasterColor() const std::vector BaseMechanics::getTargetTypes() const { std::vector ret; - detail::ProblemImpl ingored; + detail::ProblemImpl ignored; - if(canBeCast(ingored)) + if(canBeCast(ignored)) { auto spellTargetType = owner->getTargetType(); diff --git a/lib/spells/ISpellMechanics.h b/lib/spells/ISpellMechanics.h index 6206da9ba..e7163fa8e 100644 --- a/lib/spells/ISpellMechanics.h +++ b/lib/spells/ISpellMechanics.h @@ -183,7 +183,7 @@ public: virtual bool adaptProblem(ESpellCastProblem::ESpellCastProblem source, Problem & target) const = 0; virtual bool adaptGenericProblem(Problem & target) const = 0; - virtual std::vector rangeInHexes(BattleHex centralHex, bool * outDroppedHexes = nullptr) const = 0; + virtual std::vector rangeInHexes(BattleHex centralHex) const = 0; virtual std::vector getAffectedStacks(const Target & target) const = 0; virtual bool canBeCast(Problem & problem) const = 0; diff --git a/lib/spells/effects/Catapult.cpp b/lib/spells/effects/Catapult.cpp index 660be493e..62c9363f3 100644 --- a/lib/spells/effects/Catapult.cpp +++ b/lib/spells/effects/Catapult.cpp @@ -68,7 +68,7 @@ bool Catapult::applicable(Problem & problem, const Mechanics * m) const void Catapult::apply(ServerCallback * server, const Mechanics * m, const EffectTarget & /* eTarget */) const { //start with all destructible parts - static const std::set possibleTargets = + static const std::set potentialTargets = { EWallPart::KEEP, EWallPart::BOTTOM_TOWER, @@ -80,7 +80,20 @@ void Catapult::apply(ServerCallback * server, const Mechanics * m, const EffectT EWallPart::GATE }; - assert(possibleTargets.size() == EWallPart::PARTS_COUNT); + assert(potentialTargets.size() == EWallPart::PARTS_COUNT); + + std::set allowedTargets; + + for (auto const & target : potentialTargets) + { + auto state = m->battle()->battleGetWallState(target); + + if(state != EWallState::DESTROYED && state != EWallState::NONE) + allowedTargets.insert(target); + } + assert(!allowedTargets.empty()); + if (allowedTargets.empty()) + return; CatapultAttack ca; ca.attacker = -1; @@ -89,21 +102,31 @@ void Catapult::apply(ServerCallback * server, const Mechanics * m, const EffectT for(int i = 0; i < targetsToAttack; i++) { - //Any destructible part can be hit regardless of its HP. Multiple hit on same target is allowed. - EWallPart::EWallPart target = *RandomGeneratorUtil::nextItem(possibleTargets, *server->getRNG()); + // Hit on any existing, not destroyed targets are allowed + // Multiple hit on same target are allowed. + // Potential overshots (more hits on same targets than remaining HP) are allowed + EWallPart::EWallPart target = *RandomGeneratorUtil::nextItem(allowedTargets, *server->getRNG()); auto state = m->battle()->battleGetWallState(target); - if(state == EWallState::DESTROYED || state == EWallState::NONE) - continue; + auto attackInfo = ca.attackedParts.begin(); + for ( ; attackInfo != ca.attackedParts.end(); ++attackInfo) + if ( attackInfo->attackedPart == target ) + break; - CatapultAttack::AttackInfo attackInfo; - - attackInfo.damageDealt = 1; - attackInfo.attackedPart = target; - attackInfo.destinationTile = m->battle()->wallPartToBattleHex(target); - - ca.attackedParts.push_back(attackInfo); + if (attackInfo == ca.attackedParts.end()) // new part + { + CatapultAttack::AttackInfo newInfo; + newInfo.damageDealt = 1; + newInfo.attackedPart = target; + newInfo.destinationTile = m->battle()->wallPartToBattleHex(target); + ca.attackedParts.push_back(newInfo); + attackInfo = ca.attackedParts.end() - 1; + } + else // already damaged before, update damage + { + attackInfo->damageDealt += 1; + } //removing creatures in turrets / keep if one is destroyed BattleHex posRemove; @@ -111,17 +134,17 @@ void Catapult::apply(ServerCallback * server, const Mechanics * m, const EffectT switch(target) { case EWallPart::KEEP: - posRemove = -2; + posRemove = BattleHex::CASTLE_CENTRAL_TOWER; break; case EWallPart::BOTTOM_TOWER: - posRemove = -3; + posRemove = BattleHex::CASTLE_BOTTOM_TOWER; break; case EWallPart::UPPER_TOWER: - posRemove = -4; + posRemove = BattleHex::CASTLE_UPPER_TOWER; break; } - if(posRemove != BattleHex::INVALID && state - attackInfo.damageDealt <= 0) //HP enum subtraction not intuitive, consider using SiegeInfo::applyDamage + if(posRemove != BattleHex::INVALID && state - attackInfo->damageDealt <= 0) //HP enum subtraction not intuitive, consider using SiegeInfo::applyDamage { auto all = m->battle()->battleGetUnitsIf([=](const battle::Unit * unit) { @@ -130,11 +153,20 @@ void Catapult::apply(ServerCallback * server, const Mechanics * m, const EffectT for(auto & elem : all) { - if(elem->getPosition() == posRemove) + if(elem->getPosition() != posRemove) + continue; + + // if tower was hit multiple times, it may have been destroyed already + bool stackWasRemovedBefore = false; + for(auto & removed : removeUnits.changedStacks) { - removeUnits.changedStacks.emplace_back(elem->unitId(), UnitChanges::EOperation::REMOVE); - break; + if (removed.id == elem->unitId()) + stackWasRemovedBefore = true; } + + if (!stackWasRemovedBefore) + removeUnits.changedStacks.emplace_back(elem->unitId(), UnitChanges::EOperation::REMOVE); + break; } } } diff --git a/lib/spells/effects/Damage.cpp b/lib/spells/effects/Damage.cpp index 88b0d1848..944400f59 100644 --- a/lib/spells/effects/Damage.cpp +++ b/lib/spells/effects/Damage.cpp @@ -33,7 +33,6 @@ VCMI_REGISTER_SPELL_EFFECT(Damage, EFFECT_NAME); Damage::Damage() : UnitEffect(), - customEffectId(-1), killByPercentage(false), killByCount(false) { @@ -74,12 +73,6 @@ void Damage::apply(ServerCallback * server, const Mechanics * m, const EffectTar damageToDisplay += bsa.damageAmount; killed += bsa.killedAmount; } - if(customEffectId >= 0) - { - bsa.effect = 82; - bsa.flags |= BattleStackAttacked::EFFECT; - } - stacksInjured.stacks.push_back(bsa); } targetIndex++; @@ -116,7 +109,6 @@ bool Damage::isReceptive(const Mechanics * m, const battle::Unit * unit) const void Damage::serializeJsonUnitEffect(JsonSerializeFormat & handler) { - handler.serializeInt("customEffectId", customEffectId, -1); handler.serializeBool("killByPercentage", killByPercentage); handler.serializeBool("killByCount", killByCount); } @@ -179,7 +171,7 @@ void Damage::describeEffect(std::vector & log, const Mechanics * m, { MetaString line; //todo: handle newlines in metastring - std::string text = VLC->generaltexth->allTexts.at(343); //Does %d points of damage. + std::string text = VLC->generaltexth->allTexts[343]; //Does %d points of damage. boost::algorithm::trim(text); line << text; line.addReplacement((int)damage); //no more text afterwards @@ -190,32 +182,32 @@ void Damage::describeEffect(std::vector & log, const Mechanics * m, { { MetaString line; - line.addTxt(MetaString::GENERAL_TXT, 376); + line.addTxt(MetaString::GENERAL_TXT, 376); // Spell %s does %d damage line.addReplacement(MetaString::SPELL_NAME, m->getSpellIndex()); line.addReplacement((int)damage); log.push_back(line); } + if (kills > 0) { MetaString line; - const int textId = (kills > 1) ? 379 : 378; - line.addTxt(MetaString::GENERAL_TXT, textId); - - if(kills > 1) - line.addReplacement(kills); if(kills > 1) { + line.addTxt(MetaString::GENERAL_TXT, 379); // %d %s perishes + line.addReplacement(kills); + if(multiple) - line.addReplacement(MetaString::GENERAL_TXT, 43); + line.addReplacement(MetaString::GENERAL_TXT, 43); // creatures else firstTarget->addNameReplacement(line, true); } - else + else // single creature killed { + line.addTxt(MetaString::GENERAL_TXT, 378); // one %s perishes if(multiple) - line.addReplacement(MetaString::GENERAL_TXT, 42); + line.addReplacement(MetaString::GENERAL_TXT, 42); // creature else firstTarget->addNameReplacement(line, false); } diff --git a/lib/spells/effects/Damage.h b/lib/spells/effects/Damage.h index 699d5005d..34beeebea 100644 --- a/lib/spells/effects/Damage.h +++ b/lib/spells/effects/Damage.h @@ -39,7 +39,6 @@ protected: virtual void describeEffect(std::vector & log, const Mechanics * m, const battle::Unit * firstTarget, uint32_t kills, int64_t damage, bool multiple) const; private: - int32_t customEffectId; bool killByPercentage; bool killByCount; }; diff --git a/lib/spells/effects/DemonSummon.cpp b/lib/spells/effects/DemonSummon.cpp new file mode 100644 index 000000000..75f0008ff --- /dev/null +++ b/lib/spells/effects/DemonSummon.cpp @@ -0,0 +1,130 @@ +/* + * DemonSummon.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" + +#include "DemonSummon.h" +#include "Registry.h" +#include "../ISpellMechanics.h" +#include "../../NetPacks.h" +#include "../../battle/CBattleInfoCallback.h" +#include "../../battle/CUnitState.h" +#include "../../serializer/JsonSerializeFormat.h" + +VCMI_LIB_NAMESPACE_BEGIN + +static const std::string EFFECT_NAME = "core:demonSummon"; + +namespace spells +{ +namespace effects +{ + +VCMI_REGISTER_SPELL_EFFECT(DemonSummon, EFFECT_NAME); + +DemonSummon::DemonSummon() + : UnitEffect() + , creature(0) + , permanent(false) +{ +} + +DemonSummon::~DemonSummon() = default; + +void DemonSummon::apply(ServerCallback * server, const Mechanics * m, const EffectTarget & target) const +{ + BattleUnitsChanged pack; + + for(const Destination & dest : target) + { + const battle::Unit * targetStack = dest.unitValue; + + //we shall have all targets to be stacks + if(!targetStack || targetStack->alive() || targetStack->isGhost()) + { + server->complain("No corpse to demonize! Invalid effect target transformation."); + continue; + } + + auto hex = m->battle()->getAvaliableHex(targetStack->creatureId(), m->casterSide, targetStack->getPosition()); + + if(!hex.isValid()) + { + server->complain("No place to put new summon!"); + break; + } + + auto creatureType = creature.toCreature(m->creatures()); + + int32_t deadCount = targetStack->unitBaseAmount(); + int32_t deadTotalHealth = targetStack->getTotalHealth(); + int32_t raisedMaxHealth = creatureType->getMaxHealth(); + int32_t raisedTotalHealth = m->applySpellBonus(m->getEffectValue(), targetStack); + + // Can't raise stack with more HP than original stack + int32_t maxAmountFromHealth = deadTotalHealth / raisedMaxHealth; + // Can't raise stack with more creatures than original stack + int32_t maxAmountFromAmount = deadCount; + // Can't raise stack with more HP than our spellpower + int32_t maxAmountFromSpellpower = raisedTotalHealth / raisedMaxHealth; + + int32_t finalAmount = std::min( { maxAmountFromHealth, maxAmountFromAmount, maxAmountFromSpellpower } ); + + if(finalAmount < 1) + { + server->complain("Summoning didn't summon any!"); + continue; + } + + battle::UnitInfo info; + info.id = m->battle()->battleNextUnitId(); + info.count = finalAmount; + info.type = creature; + info.side = m->casterSide; + info.position = dest.hexValue; + info.summoned = !permanent; + + // add newly created creature + pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD); + info.save(pack.changedStacks.back().data); + + // and remove corpse to prevent second raising or resurrection + pack.changedStacks.emplace_back(targetStack->unitId(), UnitChanges::EOperation::REMOVE); + } + + if(!pack.changedStacks.empty()) + server->apply(&pack); +} + +bool DemonSummon::isValidTarget(const Mechanics * m, const battle::Unit * s) const +{ + if(!s->isDead()) + return false; + + if (s->isGhost()) + return false; + + auto creatureType = creature.toCreature(m->creatures()); + + if (s->getTotalHealth() < creatureType->getMaxHealth()) + return false; + + return m->isReceptive(s); +} + +void DemonSummon::serializeJsonUnitEffect(JsonSerializeFormat & handler) +{ + handler.serializeId("id", creature, CreatureID()); + handler.serializeBool("permanent", permanent, false); +} + +} +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/effects/DemonSummon.h b/lib/spells/effects/DemonSummon.h new file mode 100644 index 000000000..58a966a5a --- /dev/null +++ b/lib/spells/effects/DemonSummon.h @@ -0,0 +1,44 @@ +/* + * DemonSummon.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#pragma once + +#include "UnitEffect.h" +#include "../../GameConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +namespace spells +{ +namespace effects +{ + +class DemonSummon : public UnitEffect +{ +public: + DemonSummon(); + virtual ~DemonSummon(); + + void apply(ServerCallback * server, const Mechanics * m, const EffectTarget & target) const override; +protected: + bool isValidTarget(const Mechanics * m, const battle::Unit * s) const override; + + void serializeJsonUnitEffect(JsonSerializeFormat & handler) override final; + +private: + CreatureID creature; + + bool permanent; +}; + +} +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/effects/Effect.h b/lib/spells/effects/Effect.h index a8f6ec387..4500797da 100644 --- a/lib/spells/effects/Effect.h +++ b/lib/spells/effects/Effect.h @@ -51,19 +51,27 @@ public: Effect(); virtual ~Effect(); + // TODO: document me virtual void adjustTargetTypes(std::vector & types) const = 0; + /// Generates list of hexes affected by spell, if spell were to cast at specified target virtual void adjustAffectedHexes(std::set & hexes, const Mechanics * m, const Target & spellTarget) const = 0; + /// Returns whether effect has any valid targets on the battlefield virtual bool applicable(Problem & problem, const Mechanics * m) const; + + /// Returns whether effect is valid and can be applied onto selected target virtual bool applicable(Problem & problem, const Mechanics * m, const EffectTarget & target) const; virtual void apply(ServerCallback * server, const Mechanics * m, const EffectTarget & target) const = 0; + /// Processes input target and generates subset-result that contains only valid targets virtual EffectTarget filterTarget(const Mechanics * m, const EffectTarget & target) const = 0; + // TODO: document me virtual EffectTarget transformTarget(const Mechanics * m, const Target & aimPoint, const Target & spellTarget) const = 0; + /// Serializes (or deserializes) parameters of Effect void serializeJson(JsonSerializeFormat & handler); static std::shared_ptr create(const Registry * registry, const std::string & type); diff --git a/lib/spells/effects/Heal.cpp b/lib/spells/effects/Heal.cpp index b192315b8..4723d908b 100644 --- a/lib/spells/effects/Heal.cpp +++ b/lib/spells/effects/Heal.cpp @@ -63,14 +63,14 @@ bool Heal::isValidTarget(const Mechanics * m, const battle::Unit * unit) const if(!validInGenaral) return false; - auto insuries = unit->getTotalHealth() - unit->getAvailableHealth(); + auto injuries = unit->getTotalHealth() - unit->getAvailableHealth(); - if(insuries == 0) + if(injuries == 0) return false; if(minFullUnits > 0) { - auto hpGained = std::min(m->getEffectValue(), insuries); + auto hpGained = std::min(m->getEffectValue(), injuries); if(hpGained < minFullUnits * unit->MaxHealth()) return false; } diff --git a/lib/spells/effects/Obstacle.cpp b/lib/spells/effects/Obstacle.cpp index 5f8cbcc3e..4c381ce9c 100644 --- a/lib/spells/effects/Obstacle.cpp +++ b/lib/spells/effects/Obstacle.cpp @@ -41,7 +41,10 @@ void ObstacleSideOptions::serializeJson(JsonSerializeFormat & handler) serializeRelativeShape(handler, "shape", shape); serializeRelativeShape(handler, "range", range); + handler.serializeString("appearSound", appearSound); handler.serializeString("appearAnimation", appearAnimation); + handler.serializeString("triggerSound", triggerSound); + handler.serializeString("triggerAnimation", triggerAnimation); handler.serializeString("animation", animation); handler.serializeInt("offsetY", offsetY); @@ -57,7 +60,6 @@ void ObstacleSideOptions::serializeRelativeShape(JsonSerializeFormat & handler, "BR", "BL", "L", - "" }; { @@ -75,7 +77,9 @@ void ObstacleSideOptions::serializeRelativeShape(JsonSerializeFormat & handler, if(handler.saving) { - temp = EDirMap.at(value.at(outerIndex).at(innerIndex)); + auto index = value.at(outerIndex).at(innerIndex); + if (index < EDirMap.size()) + temp = EDirMap[index]; } inner.serializeString(innerIndex, temp); @@ -313,7 +317,10 @@ void Obstacle::placeObstacles(ServerCallback * server, const Mechanics * m, cons obstacle.trap = trap; obstacle.removeOnTrigger = removeOnTrigger; + obstacle.appearSound = options.appearSound; obstacle.appearAnimation = options.appearAnimation; + obstacle.triggerSound = options.triggerSound; + obstacle.triggerAnimation = options.triggerAnimation; obstacle.animation = options.animation; obstacle.animationYOffset = options.offsetY; diff --git a/lib/spells/effects/Obstacle.h b/lib/spells/effects/Obstacle.h index 5cc1fd215..5bf789f18 100644 --- a/lib/spells/effects/Obstacle.h +++ b/lib/spells/effects/Obstacle.h @@ -29,7 +29,10 @@ public: RelativeShape shape; //shape of single obstacle relative to obstacle position RelativeShape range; //position of obstacles relative to effect destination + std::string appearSound; std::string appearAnimation; + std::string triggerSound; + std::string triggerAnimation; std::string animation; int offsetY; diff --git a/lib/spells/effects/UnitEffect.cpp b/lib/spells/effects/UnitEffect.cpp index 665842750..2b7ec0eed 100644 --- a/lib/spells/effects/UnitEffect.cpp +++ b/lib/spells/effects/UnitEffect.cpp @@ -114,10 +114,11 @@ EffectTarget UnitEffect::transformTargetByRange(const Mechanics * m, const Targe Target spellTargetCopy(spellTarget); - //make sure that we have valid target with valid aim, even if spell have invalid range configured - //TODO: check than spell range is actually not valid - //also hackfix for banned creature massive spells - if(!aimPoint.empty()) + // make sure that we have valid target with valid aim, even if spell have invalid range configured + // TODO: check than spell range is actually not valid + // also hackfix for banned creature massive spells + // FIXME: potentially breaking change: aimPoint may NOT be in Target - example: frost ring + if(!aimPoint.empty() && spellTarget.empty()) spellTargetCopy.insert(spellTargetCopy.begin(), Destination(aimPoint.front())); std::set targets; diff --git a/lib/vcmi_endian.h b/lib/vcmi_endian.h index 45db01bfb..2686261f8 100644 --- a/lib/vcmi_endian.h +++ b/lib/vcmi_endian.h @@ -9,7 +9,7 @@ */ #pragma once -//FIXME:library file depends on SDL - make cause troubles +//FIXME:library file depends on SDL - may cause troubles #include VCMI_LIB_NAMESPACE_BEGIN @@ -20,11 +20,18 @@ VCMI_LIB_NAMESPACE_BEGIN * memory. On big endian machines, the value will be byteswapped. */ -#if (defined(linux) || defined(__linux) || defined(__linux__)) && (defined(sparc) || defined(__arm__)) -/* SPARC does not support unaligned memory access. Let gcc know when - * to emit the right code. */ -struct unaligned_Uint16 { ui16 val __attribute__(( packed )); }; -struct unaligned_Uint32 { ui32 val __attribute__(( packed )); }; +#if defined(__clang__) || defined(__GNUC__) || defined(_MSC_VER) + +#if defined(_MSC_VER) +#define PACKED_STRUCT_BEGIN __pragma( pack(push, 1) ) +#define PACKED_STRUCT_END __pragma( pack(pop) ) +#else +#define PACKED_STRUCT_BEGIN +#define PACKED_STRUCT_END __attribute__(( packed )) +#endif + +PACKED_STRUCT_BEGIN struct unaligned_Uint16 { ui16 val; } PACKED_STRUCT_END; +PACKED_STRUCT_BEGIN struct unaligned_Uint32 { ui32 val; } PACKED_STRUCT_END; static inline ui16 read_unaligned_u16(const void *p) { @@ -41,14 +48,15 @@ static inline ui32 read_unaligned_u32(const void *p) #define read_le_u16(p) (SDL_SwapLE16(read_unaligned_u16(p))) #define read_le_u32(p) (SDL_SwapLE32(read_unaligned_u32(p))) -#define PACKED_STRUCT __attribute__(( packed )) - #else +#warning UB: unaligned memory access + #define read_le_u16(p) (SDL_SwapLE16(* reinterpret_cast(p))) #define read_le_u32(p) (SDL_SwapLE32(* reinterpret_cast(p))) -#define PACKED_STRUCT +#define PACKED_STRUCT_BEGIN +#define PACKED_STRUCT_END #endif diff --git a/mapeditor/CMakeLists.txt b/mapeditor/CMakeLists.txt index 241c6a9fa..8f02f65f2 100644 --- a/mapeditor/CMakeLists.txt +++ b/mapeditor/CMakeLists.txt @@ -70,6 +70,13 @@ set(editor_FORMS inspector/questwidget.ui ) +set(editor_TS + translation/english.ts + translation/german.ts + translation/polish.ts + translation/russian.ts + translation/ukrainian.ts) + assign_source_group(${editor_SRCS} ${editor_HEADERS} mapeditor.rc) # Tell CMake to run moc when necessary: @@ -85,15 +92,22 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) if(TARGET Qt6::Core) qt_wrap_ui(editor_UI_HEADERS ${editor_FORMS}) + if(ENABLE_TRANSLATIONS) + qt_add_translation( editor_QM ${editor_TS} ) + endif() else() qt5_wrap_ui(editor_UI_HEADERS ${editor_FORMS}) + if(ENABLE_TRANSLATIONS) + set_source_files_properties(${editor_TS} PROPERTIES OUTPUT_LOCATION ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/mapeditor/translation) + qt5_add_translation( editor_QM ${editor_TS} ) + endif() endif() if(WIN32) set(editor_ICON mapeditor.rc) endif() -add_executable(vcmieditor WIN32 ${editor_SRCS} ${editor_HEADERS} ${editor_UI_HEADERS} ${editor_ICON}) +add_executable(vcmieditor WIN32 ${editor_QM} ${editor_SRCS} ${editor_HEADERS} ${editor_UI_HEADERS} ${editor_ICON}) if(WIN32) set_target_properties(vcmieditor @@ -132,6 +146,7 @@ add_custom_command(TARGET vcmieditor POST_BUILD install(TARGETS vcmieditor DESTINATION ${BIN_DIR}) # copy whole directory install(DIRECTORY icons DESTINATION ${DATA_DIR}/mapeditor) +install(DIRECTORY translation DESTINATION ${DATA_DIR}/mapeditor) # Install icons and desktop file on Linux if(NOT WIN32 AND NOT APPLE) install(FILES "vcmieditor.desktop" DESTINATION share/applications) diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index d02947db8..4b831be65 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -115,6 +115,31 @@ void MainWindow::parseCommandLine(ExtractionOptions & extractionOptions) parser.isSet("d")}}; } +void MainWindow::loadTranslation() +{ +#ifdef ENABLE_QT_TRANSLATIONS + std::string translationFile = settings["general"]["language"].String() + ".qm"; + + QVector searchPaths; + + for(auto const & string : VCMIDirs::get().dataPaths()) + searchPaths.push_back(pathToQString(string / "mapeditor" / "translation" / translationFile)); + searchPaths.push_back(pathToQString(VCMIDirs::get().userDataPath() / "mapeditor" / "translation" / translationFile)); + + for(auto const & string : boost::adaptors::reverse(searchPaths)) + { + if (translator.load(string)) + { + if (!qApp->installTranslator(&translator)) + logGlobal->error("Failed to install translator"); + return; + } + } + + logGlobal->error("Failed to find translation"); +#endif +} + MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow), @@ -128,10 +153,6 @@ MainWindow::MainWindow(QWidget* parent) : QDir::addSearchPath("icons", pathToQString(string / "mapeditor" / "icons")); QDir::addSearchPath("icons", pathToQString(VCMIDirs::get().userDataPath() / "mapeditor" / "icons")); - ui->setupUi(this); - loadUserSettings(); //For example window size - setTitle(); - ExtractionOptions extractionOptions; parseCommandLine(extractionOptions); @@ -169,6 +190,12 @@ MainWindow::MainWindow(QWidget* parent) : conf.init(); logGlobal->info("Loading settings"); + loadTranslation(); + + ui->setupUi(this); + loadUserSettings(); //For example window size + setTitle(); + init(); graphics = new Graphics(); // should be before curh->init() diff --git a/mapeditor/mainwindow.h b/mapeditor/mainwindow.h index ccd410aa8..f23590ca2 100644 --- a/mapeditor/mainwindow.h +++ b/mapeditor/mainwindow.h @@ -29,6 +29,10 @@ class MainWindow : public QMainWindow const QString mainWindowSizeSetting = "MainWindow/Size"; const QString mainWindowPositionSetting = "MainWindow/Position"; +#ifdef ENABLE_QT_TRANSLATIONS + QTranslator translator; +#endif + public: explicit MainWindow(QWidget *parent = nullptr); ~MainWindow(); @@ -48,6 +52,8 @@ public: MapController controller; + void loadTranslation(); + private slots: void on_actionOpen_triggered(); diff --git a/mapeditor/mainwindow.ui b/mapeditor/mainwindow.ui index 766114052..400277c35 100644 --- a/mapeditor/mainwindow.ui +++ b/mapeditor/mainwindow.ui @@ -114,7 +114,7 @@ - toolBar + TopToolBarArea @@ -387,7 +387,7 @@ false - 20 + 21 @@ -663,7 +663,7 @@ - O + O true @@ -737,7 +737,7 @@ 0 0 128 - 251 + 257 @@ -780,7 +780,7 @@ 0 0 128 - 251 + 257 @@ -816,7 +816,7 @@ 0 0 128 - 251 + 257 @@ -876,7 +876,7 @@ Open - Ctrl+O + Ctrl+O @@ -888,7 +888,7 @@ Save - Ctrl+S + Ctrl+S @@ -900,7 +900,7 @@ New - Ctrl+N + Ctrl+N @@ -923,7 +923,7 @@ View underground - U + U @@ -938,7 +938,7 @@ Pass - P + P @@ -950,7 +950,7 @@ Cut - Ctrl+X + Ctrl+X @@ -962,7 +962,7 @@ Copy - Ctrl+C + Ctrl+C @@ -974,7 +974,7 @@ Paste - Ctrl+V + Ctrl+V @@ -989,7 +989,7 @@ Fills the selection with obstacles - F + F @@ -1004,7 +1004,7 @@ Grid - G + G @@ -1017,6 +1017,9 @@ Map title and description + + + @@ -1025,6 +1028,9 @@ Players settings + + + @@ -1041,7 +1047,7 @@ Undo - Ctrl+Z + Ctrl+Z true @@ -1059,7 +1065,7 @@ Redo - Ctrl+Y + Ctrl+Y true @@ -1077,7 +1083,7 @@ Erase - Backspace, Del + Backspace, Del @@ -1088,13 +1094,16 @@ Neutral - Ctrl+0 + Ctrl+0 Validate + + + @@ -1103,6 +1112,9 @@ Update appearance + + + @@ -1111,6 +1123,9 @@ Recreate obstacles + + + @@ -1120,7 +1135,7 @@ Player 1 - Ctrl+1 + Ctrl+1 @@ -1131,7 +1146,7 @@ Player 2 - Ctrl+2 + Ctrl+2 @@ -1142,7 +1157,7 @@ Player 3 - Ctrl+3 + Ctrl+3 @@ -1153,7 +1168,7 @@ Player 4 - Ctrl+4 + Ctrl+4 @@ -1164,7 +1179,7 @@ Player 5 - Ctrl+5 + Ctrl+5 @@ -1175,7 +1190,7 @@ Player 6 - Ctrl+6 + Ctrl+6 @@ -1186,7 +1201,7 @@ Player 7 - Ctrl+7 + Ctrl+7 @@ -1197,7 +1212,7 @@ Player 8 - Ctrl+8 + Ctrl+8 diff --git a/mapeditor/playerparams.ui b/mapeditor/playerparams.ui index bd2444b8f..5eeee8832 100644 --- a/mapeditor/playerparams.ui +++ b/mapeditor/playerparams.ui @@ -23,7 +23,7 @@ - Form + @@ -47,7 +47,7 @@ - GroupBox + diff --git a/mapeditor/playersettings.ui b/mapeditor/playersettings.ui index 5d111ee04..697a87c0c 100644 --- a/mapeditor/playersettings.ui +++ b/mapeditor/playersettings.ui @@ -33,8 +33,8 @@ 0 0 - 628 - 187 + 634 + 201 @@ -66,39 +66,42 @@ + + 2 + - 2 + 2 - 3 + 3 - 4 + 4 - 5 + 5 - 6 + 6 - 7 + 7 - 8 + 8 diff --git a/mapeditor/translation/english.ts b/mapeditor/translation/english.ts new file mode 100644 index 000000000..dea684404 --- /dev/null +++ b/mapeditor/translation/english.ts @@ -0,0 +1,641 @@ + + + + + ArmyWidget + + + Army settings + + + + + Wide formation + + + + + Tight formation + + + + + GeneratorProgress + + + Generating map + + + + + MainWindow + + + VCMI Map Editor + + + + + File + + + + + Map + + + + + Edit + + + + + View + + + + + Player + + + + + Browser + + + + + Inspector + + + + + Property + + + + + Value + + + + + Brush + + + + + Terrains + + + + + Roads + + + + + Rivers + + + + + Open + + + + + Save + + + + + New + + + + + Save as + + + + + Ctrl+Shift+S + + + + + U/G + + + + + + View underground + + + + + Pass + + + + + Cut + + + + + Copy + + + + + Paste + + + + + Fill + + + + + Fills the selection with obstacles + + + + + Grid + + + + + General + + + + + Map title and description + + + + + Players settings + + + + + + Undo + + + + + Redo + + + + + Erase + + + + + Neutral + + + + + Validate + + + + + Update appearance + + + + + Recreate obstacles + + + + + Player 1 + + + + + Player 2 + + + + + Player 3 + + + + + Player 4 + + + + + Player 5 + + + + + Player 6 + + + + + Player 7 + + + + + Player 8 + + + + + Open map + + + + + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) + + + + + + Save map + + + + + + VCMI maps (*.vmap) + + + + + Type + + + + + View surface + + + + + MapSettings + + + Map settings + + + + + General + + + + + Map name + + + + + Map description + + + + + Abilities + + + + + Spells + + + + + Artifacts + + + + + Heroes + + + + + Ok + + + + + MessageWidget + + + Message + + + + + PlayerParams + + + No team + + + + + Human/CPU + + + + + CPU only + + + + + Team + + + + + Main town + + + + + Random faction + + + + + Generate hero at main + + + + + (default) + + + + + Player ID: %1 + + + + + PlayerSettings + + + Player settings + + + + + Players + + + + + Ok + + + + + QuestWidget + + + Mission goal + + + + + RewardsWidget + + + Rewards + + + + + Remove selected + + + + + Delete all + + + + + Add or change + + + + + TownBulidingsWidget + + + Buildings + + + + + Validator + + + Map validation results + + + + + Town %1 has undefined owner %2 + + + + + WindowNewMap + + + Create new map + + + + + Map size + + + + + Two level map + + + + + Height + + + + + Width + + + + + S (36x36) + + + + + M (72x72) + + + + + L (108x108) + + + + + XL (144x144) + + + + + Random map + + + + + Players + + + + + 0 + + + + + Human/Computer + + + + + + + + Random + + + + + Computer only + + + + + Monster strength + + + + + Weak + + + + + + Normal + + + + + Strong + + + + + Water content + + + + + None + + + + + Islands + + + + + Template + + + + + Custom seed + + + + + Generate random map + + + + + Ok + + + + + Cancel + + + + + main + + + Filepath of the map to open. + + + + + Extract original H3 archives into a separate folder. + + + + + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. + + + + + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. + + + + + Delete original files, for the ones splitted / converted. + + + + diff --git a/mapeditor/translation/german.ts b/mapeditor/translation/german.ts new file mode 100644 index 000000000..320f01f71 --- /dev/null +++ b/mapeditor/translation/german.ts @@ -0,0 +1,641 @@ + + + + + ArmyWidget + + + Army settings + Einstellungen der Armee + + + + Wide formation + Breite Formation + + + + Tight formation + Enge Formation + + + + GeneratorProgress + + + Generating map + Karte generieren + + + + MainWindow + + + VCMI Map Editor + VCMI-Karteneditor + + + + File + Datei + + + + Map + Karte + + + + Edit + Bearbeiten + + + + View + Ansicht + + + + Player + Spieler + + + + Browser + Browser + + + + Inspector + Inspektor + + + + Property + Eigenschaft + + + + Value + Wert + + + + Brush + Pinsel + + + + Terrains + Terrains + + + + Roads + Straßen + + + + Rivers + Flüsse + + + + Open + Öffnen + + + + Save + Speichern + + + + New + Neu + + + + Save as + Speichern unter + + + + Ctrl+Shift+S + Strg+Shift+S + + + + U/G + U/G + + + + + View underground + Ansicht Untergrund + + + + Pass + Passierbar + + + + Cut + Ausschneiden + + + + Copy + Kopieren + + + + Paste + Einfügen + + + + Fill + Füllen + + + + Fills the selection with obstacles + Füllt die Auswahl mit Hindernissen + + + + Grid + Raster + + + + General + Allgemein + + + + Map title and description + Titel und Beschreibung der Karte + + + + Players settings + Spieler-Einstellungen + + + + + Undo + Rückgängig + + + + Redo + Wiederholen + + + + Erase + Löschen + + + + Neutral + Neutral + + + + Validate + Validieren + + + + Update appearance + Aussehen aktualisieren + + + + Recreate obstacles + Hindernisse neu erschaffen + + + + Player 1 + Spieler 1 + + + + Player 2 + Spieler 2 + + + + Player 3 + Spieler 3 + + + + Player 4 + Spieler 4 + + + + Player 5 + Spieler 5 + + + + Player 6 + Spieler 6 + + + + Player 7 + Spieler 7 + + + + Player 8 + Spieler 8 + + + + Open map + Karte öffnen + + + + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) + Alle unterstützten Karten (*.vmap *.h3m);;VCMI-Karten (*.vmap);;HoMM3-Karten (*.h3m) + + + + + Save map + Karte speichern + + + + + VCMI maps (*.vmap) + VCMI-Karten (*.vmap) + + + + Type + Typ + + + + View surface + Oberfläche anzeigen + + + + MapSettings + + + Map settings + Karteneinstellungen + + + + General + Allgemein + + + + Map name + Kartenname + + + + Map description + Kartenbeschreibung + + + + Abilities + Fähigkeiten + + + + Spells + Zaubersprüche + + + + Artifacts + Artefakte + + + + Heroes + Helden + + + + Ok + Ok + + + + MessageWidget + + + Message + Nachricht + + + + PlayerParams + + + No team + Kein Team + + + + Human/CPU + Mensch/CPU + + + + CPU only + Nur CPU + + + + Team + Team + + + + Main town + Hauptstadt + + + + Random faction + Zufällige Fraktion + + + + Generate hero at main + Held am Hauptplatz generieren + + + + (default) + (Standard) + + + + Player ID: %1 + Spieler-ID: %1 + + + + PlayerSettings + + + Player settings + Spieler-Einstellungen + + + + Players + Spieler + + + + Ok + Ok + + + + QuestWidget + + + Mission goal + Missionsziel + + + + RewardsWidget + + + Rewards + Belohnungen + + + + Remove selected + Ausgewähltes entfernen + + + + Delete all + Alle löschen + + + + Add or change + Hinzufügen oder ändern + + + + TownBulidingsWidget + + + Buildings + Gebäude + + + + Validator + + + Map validation results + Ergebnisse der Kartenvalidierung + + + + Town %1 has undefined owner %2 + Stadt %1 hat undefinierten Besitzer %2 + + + + WindowNewMap + + + Create new map + Neue Karte erstellen + + + + Map size + Größe der Karte + + + + Two level map + Karte mit zwei Ebenen + + + + Height + Höhe + + + + Width + Breite + + + + S (36x36) + S (36x36) + + + + M (72x72) + M (72x72) + + + + L (108x108) + L (108x108) + + + + XL (144x144) + XL (144x144) + + + + Random map + Zufallsgenerierte Karte + + + + Players + Spieler + + + + 0 + 0 + + + + Human/Computer + Mensch/Computer + + + + + + + Random + Zufall + + + + Computer only + Nur Computer + + + + Monster strength + Monster-Stärke + + + + Weak + Schwach + + + + + Normal + Normal + + + + Strong + Stark + + + + Water content + Wasseranteil + + + + None + Keine + + + + Islands + Inseln + + + + Template + Vorlage + + + + Custom seed + Benutzerdefiniertes Seed + + + + Generate random map + Zufällige Karte generieren + + + + Ok + Ok + + + + Cancel + Abbrechen + + + + main + + + Filepath of the map to open. + Dateipfad der zu öffnenden Karte. + + + + Extract original H3 archives into a separate folder. + Extrahieren Sie die Original-H3-Archive in einen separaten Ordner. + + + + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. + Aus einem extrahierten Archiv zerlegt es TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 und Un44 in einzelne PNGs. + + + + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. + Aus einem extrahierten Archiv werden einzelne Bilder (aus dem Ordner "Images") von .pcx in png konvertiert. + + + + Delete original files, for the ones splitted / converted. + Löschen Sie die Originaldateien für die gesplitteten/konvertierten Dateien. + + + diff --git a/mapeditor/translation/polish.ts b/mapeditor/translation/polish.ts new file mode 100644 index 000000000..d0d19a15c --- /dev/null +++ b/mapeditor/translation/polish.ts @@ -0,0 +1,641 @@ + + + + + ArmyWidget + + + Army settings + Ustawienia armii + + + + Wide formation + Luźna formacja + + + + Tight formation + Zwarta formacja + + + + GeneratorProgress + + + Generating map + Trwa generowanie mapy + + + + MainWindow + + + VCMI Map Editor + Edytor map VCMI + + + + File + Plik + + + + Map + Mapa + + + + Edit + Edycja + + + + View + Widok + + + + Player + Gracz + + + + Browser + Przeglądarka + + + + Inspector + Inspektor + + + + Property + Właściwość + + + + Value + Wartość + + + + Brush + Pędzel + + + + Terrains + Tereny + + + + Roads + Drogi + + + + Rivers + Rzeki + + + + Open + Otwórz + + + + Save + Zapisz + + + + New + Nowy + + + + Save as + Zapisz jako + + + + Ctrl+Shift+S + Ctrl+Shift+S + + + + U/G + Podziemia + + + + + View underground + Pokaż podziemia + + + + Pass + Przejścia + + + + Cut + Wytnij + + + + Copy + Kopiuj + + + + Paste + Wklej + + + + Fill + Wypełnij + + + + Fills the selection with obstacles + Wypełnia zaznaczony obszar przeszkodami + + + + Grid + Siatka + + + + General + Ogólne + + + + Map title and description + Nazwa i opis mapy + + + + Players settings + Ustawienia graczy + + + + + Undo + Cofnij + + + + Redo + Przywróć + + + + Erase + Wymaż + + + + Neutral + Neutralny + + + + Validate + Sprawdź + + + + Update appearance + Aktualizuj wygląd + + + + Recreate obstacles + Powtórnie stwórz przeszkody + + + + Player 1 + Gracz 1 + + + + Player 2 + Gracz 2 + + + + Player 3 + Gracz 3 + + + + Player 4 + Gracz 4 + + + + Player 5 + Gracz 5 + + + + Player 6 + Gracz 6 + + + + Player 7 + Gracz 7 + + + + Player 8 + Gracz 8 + + + + Open map + Otwórz mapę + + + + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) + Wszystkie wspierane mapy (*.vmap *.h3m);;Mapy VCMI(*.vmap);;Mapy HoMM3(*.h3m) + + + + + Save map + Zapisz mapę + + + + + VCMI maps (*.vmap) + Mapy VCMI (*.vmap) + + + + Type + Typ + + + + View surface + Pokaż powierzchnię + + + + MapSettings + + + Map settings + Ustawienia mapy + + + + General + Ogólne + + + + Map name + Nazwa mapy + + + + Map description + Opis mapy + + + + Abilities + Umiejętności + + + + Spells + Zaklęcia + + + + Artifacts + Artefakty + + + + Heroes + Bohaterowie + + + + Ok + Ok + + + + MessageWidget + + + Message + Wiadomość + + + + PlayerParams + + + No team + Brak drużyny + + + + Human/CPU + Człowiek/Komputer + + + + CPU only + Tylko komputer + + + + Team + Drużyna + + + + Main town + Główne miasto + + + + Random faction + Losowe miasto + + + + Generate hero at main + Generuj bohatera w głównym + + + + (default) + (domyślny) + + + + Player ID: %1 + ID gracza: %1 + + + + PlayerSettings + + + Player settings + Ustawienia gracza + + + + Players + Gracze + + + + Ok + Ok + + + + QuestWidget + + + Mission goal + Cel misji + + + + RewardsWidget + + + Rewards + Nagrody + + + + Remove selected + Usuń wybrane + + + + Delete all + Usuń wszystkie + + + + Add or change + Dodaj lub zmień + + + + TownBulidingsWidget + + + Buildings + Budynki + + + + Validator + + + Map validation results + Wynik sprawdzenia mapy + + + + Town %1 has undefined owner %2 + Miasto %1 ma niezdefiniowanego właściciela %2 + + + + WindowNewMap + + + Create new map + Stwórz nową mapę + + + + Map size + Rozmiar mapy + + + + Two level map + Mapa dwupoziomowa + + + + Height + Wysokość + + + + Width + Szerokość + + + + S (36x36) + S (36x36) + + + + M (72x72) + M (72x72) + + + + L (108x108) + L (108x108) + + + + XL (144x144) + XL (144x144) + + + + Random map + Mapa losowa + + + + Players + Gracze + + + + 0 + 0 + + + + Human/Computer + Człowiek/Komputer + + + + + + + Random + Losowo + + + + Computer only + Tylko komputer + + + + Monster strength + Siła potworów + + + + Weak + Słaba + + + + + Normal + Normalna + + + + Strong + Silna + + + + Water content + Powierzchnia wody + + + + None + Brak + + + + Islands + Wyspy + + + + Template + Szablon + + + + Custom seed + Własny seed + + + + Generate random map + Generuj mapę losową + + + + Ok + Ok + + + + Cancel + Anuluj + + + + main + + + Filepath of the map to open. + Lokalizacja pliku mapy do otworzenia. + + + + Extract original H3 archives into a separate folder. + Wyodrębnij oryginalne archiwa H3 do osobnego folderu. + + + + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. + Z wyodrębnionego archiwum, rozdzielenie TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 i Un44 do poszczególnych plików PNG. + + + + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. + Z wyodrębnionego archiwum, konwersja pojedynczych obrazków (znalezionych w folderze Images) z .pcx do .png. + + + + Delete original files, for the ones splitted / converted. + Usuń oryginalne pliki, dla już rozdzielonych / skonwertowanych. + + + diff --git a/mapeditor/translation/russian.ts b/mapeditor/translation/russian.ts new file mode 100644 index 000000000..4a0bc3525 --- /dev/null +++ b/mapeditor/translation/russian.ts @@ -0,0 +1,641 @@ + + + + + ArmyWidget + + + Army settings + + + + + Wide formation + + + + + Tight formation + + + + + GeneratorProgress + + + Generating map + + + + + MainWindow + + + VCMI Map Editor + + + + + File + + + + + Map + + + + + Edit + + + + + View + + + + + Player + + + + + Browser + + + + + Inspector + + + + + Property + + + + + Value + + + + + Brush + + + + + Terrains + + + + + Roads + + + + + Rivers + + + + + Open + + + + + Save + + + + + New + + + + + Save as + + + + + Ctrl+Shift+S + + + + + U/G + + + + + + View underground + + + + + Pass + + + + + Cut + + + + + Copy + + + + + Paste + + + + + Fill + + + + + Fills the selection with obstacles + + + + + Grid + + + + + General + + + + + Map title and description + + + + + Players settings + + + + + + Undo + + + + + Redo + + + + + Erase + + + + + Neutral + + + + + Validate + + + + + Update appearance + + + + + Recreate obstacles + + + + + Player 1 + + + + + Player 2 + + + + + Player 3 + + + + + Player 4 + + + + + Player 5 + + + + + Player 6 + + + + + Player 7 + + + + + Player 8 + + + + + Open map + + + + + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) + + + + + + Save map + + + + + + VCMI maps (*.vmap) + + + + + Type + + + + + View surface + + + + + MapSettings + + + Map settings + + + + + General + + + + + Map name + + + + + Map description + + + + + Abilities + + + + + Spells + + + + + Artifacts + + + + + Heroes + + + + + Ok + + + + + MessageWidget + + + Message + + + + + PlayerParams + + + No team + + + + + Human/CPU + + + + + CPU only + + + + + Team + + + + + Main town + + + + + Random faction + + + + + Generate hero at main + + + + + (default) + + + + + Player ID: %1 + + + + + PlayerSettings + + + Player settings + + + + + Players + + + + + Ok + + + + + QuestWidget + + + Mission goal + + + + + RewardsWidget + + + Rewards + + + + + Remove selected + + + + + Delete all + + + + + Add or change + + + + + TownBulidingsWidget + + + Buildings + + + + + Validator + + + Map validation results + + + + + Town %1 has undefined owner %2 + + + + + WindowNewMap + + + Create new map + + + + + Map size + + + + + Two level map + + + + + Height + + + + + Width + + + + + S (36x36) + + + + + M (72x72) + + + + + L (108x108) + + + + + XL (144x144) + + + + + Random map + + + + + Players + + + + + 0 + + + + + Human/Computer + + + + + + + + Random + + + + + Computer only + + + + + Monster strength + + + + + Weak + + + + + + Normal + + + + + Strong + + + + + Water content + + + + + None + + + + + Islands + + + + + Template + + + + + Custom seed + + + + + Generate random map + + + + + Ok + + + + + Cancel + + + + + main + + + Filepath of the map to open. + + + + + Extract original H3 archives into a separate folder. + + + + + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. + + + + + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. + + + + + Delete original files, for the ones splitted / converted. + + + + diff --git a/mapeditor/translation/ukrainian.ts b/mapeditor/translation/ukrainian.ts new file mode 100644 index 000000000..7737a95ac --- /dev/null +++ b/mapeditor/translation/ukrainian.ts @@ -0,0 +1,641 @@ + + + + + ArmyWidget + + + Army settings + Налаштування армії + + + + Wide formation + Широка формація + + + + Tight formation + Щільна формація + + + + GeneratorProgress + + + Generating map + Побудова мапи + + + + MainWindow + + + VCMI Map Editor + Редактор мап VCMI + + + + File + Файл + + + + Map + Мапа + + + + Edit + Редагування + + + + View + Вигляд + + + + Player + Гравець + + + + Browser + Навігатор + + + + Inspector + Інспектор + + + + Property + Властивість + + + + Value + Значення + + + + Brush + Кисть + + + + Terrains + Землі + + + + Roads + Шляхи + + + + Rivers + Річки + + + + Open + Відкрити + + + + Save + Зберегти + + + + New + Створити + + + + Save as + Зберегти як + + + + Ctrl+Shift+S + Ctrl+Shift+S + + + + U/G + П/З + + + + + View underground + Дивитись підземелля + + + + Pass + Прохідність + + + + Cut + Вирізати + + + + Copy + Скопіювати + + + + Paste + Вставити + + + + Fill + Заповнити + + + + Fills the selection with obstacles + Заповнити перешкодами + + + + Grid + Сітка + + + + General + Загальний + + + + Map title and description + Назва та опис мапи + + + + Players settings + Налаштування гравців + + + + + Undo + Відмінити + + + + Redo + Повторити + + + + Erase + Стерти + + + + Neutral + Нейтральний + + + + Validate + Перевірити + + + + Update appearance + Оновити вигляд + + + + Recreate obstacles + Оновити перешкоди + + + + Player 1 + Гравець 1 + + + + Player 2 + Гравець 2 + + + + Player 3 + Гравець 3 + + + + Player 4 + Гравець 4 + + + + Player 5 + Гравець 5 + + + + Player 6 + Гравець 6 + + + + Player 7 + Гравець 7 + + + + Player 8 + Гравець 8 + + + + Open map + Відкрити мапу + + + + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) + Всі підтримувані мапи (*.vmap *.h3m);;Мапи VCMI (*.vmap);;Мапи HoMM3 (*.h3m) + + + + + Save map + Зберегти мапу + + + + + VCMI maps (*.vmap) + Мапи VCMI + + + + Type + Тип + + + + View surface + Дивитись поверхню + + + + MapSettings + + + Map settings + Налаштування мапи + + + + General + Загальний + + + + Map name + Назва мапи + + + + Map description + Опис мапи + + + + Abilities + Уміння + + + + Spells + Закляття + + + + Artifacts + Артефакти + + + + Heroes + Герої + + + + Ok + Підтвердити + + + + MessageWidget + + + Message + Повідомлення + + + + PlayerParams + + + No team + Без команди + + + + Human/CPU + Людина/Комп'ютер + + + + CPU only + Тільки комп'ютер + + + + Team + Команда + + + + Main town + Головне місто + + + + Random faction + Випадкова фракція + + + + Generate hero at main + Згенерувати героя + + + + (default) + (за замовчуванням) + + + + Player ID: %1 + Гравець %1 + + + + PlayerSettings + + + Player settings + Налаштування гравця + + + + Players + Гравці + + + + Ok + Підтвердити + + + + QuestWidget + + + Mission goal + Мета місії + + + + RewardsWidget + + + Rewards + Винагороди + + + + Remove selected + Видалити вибране + + + + Delete all + Видалити усі + + + + Add or change + Додати або змінити + + + + TownBulidingsWidget + + + Buildings + Будівлі + + + + Validator + + + Map validation results + Результати валідації карти + + + + Town %1 has undefined owner %2 + Місто %1 має невизначеного володаря %2 + + + + WindowNewMap + + + Create new map + Створення нової мапи + + + + Map size + Розмір мапи + + + + Two level map + Дворівнева мапа + + + + Height + Висота + + + + Width + Ширина + + + + S (36x36) + М (36x36) + + + + M (72x72) + С (72x72) + + + + L (108x108) + В (108x108) + + + + XL (144x144) + ДВ (144x144) + + + + Random map + Випадкова мапа + + + + Players + Гравців + + + + 0 + 0 + + + + Human/Computer + Людина/Комп'ютер + + + + + + + Random + Випадково + + + + Computer only + Тільки комп'ютер + + + + Monster strength + Сила монстрів + + + + Weak + Слабкі + + + + + Normal + Типова + + + + Strong + Сильні + + + + Water content + Наявність води + + + + None + Відсутня + + + + Islands + Острови + + + + Template + Шаблон + + + + Custom seed + Користувацьке зерно + + + + Generate random map + Згенерувати випадкову карту + + + + Ok + Підтвердити + + + + Cancel + Скасувати + + + + main + + + Filepath of the map to open. + Файл мапи, який слід відкрити + + + + Extract original H3 archives into a separate folder. + + + + + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. + + + + + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. + + + + + Delete original files, for the ones splitted / converted. + + + + diff --git a/mapeditor/windownewmap.ui b/mapeditor/windownewmap.ui index ddab13952..242c6f9d7 100644 --- a/mapeditor/windownewmap.ui +++ b/mapeditor/windownewmap.ui @@ -69,7 +69,7 @@ Qt::ImhDigitsOnly - 36 + 36 3 @@ -89,7 +89,7 @@ Qt::ImhDigitsOnly - 36 + 36 3 @@ -228,18 +228,24 @@ 16777215 + + 0 + - 0 + 0 + + 0 + - 0 + 0 @@ -284,42 +290,42 @@ - 1 + 1 - 2 + 2 - 3 + 3 - 4 + 4 - 5 + 5 - 6 + 6 - 7 + 7 - 8 + 8 @@ -353,42 +359,42 @@ - 0 + 0 - 1 + 1 - 2 + 2 - 3 + 3 - 4 + 4 - 5 + 5 - 6 + 6 - 7 + 7 @@ -701,6 +707,9 @@ false + + + -1 diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 81ecb186f..ddbf5fd92 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1012,6 +1012,7 @@ void CGameHandler::makeAttack(const CStack * attacker, const CStack * defender, BattleAttack bat; BattleLogMessage blm; bat.stackAttacking = attacker->unitId(); + bat.tile = targetHex; std::shared_ptr attackerState = attacker->acquireState(); @@ -1117,6 +1118,9 @@ void CGameHandler::makeAttack(const CStack * attacker, const CStack * defender, bat.attackerChanges.changedStacks.push_back(info); } + if (drainedLife > 0) + bat.flags |= BattleAttack::LIFE_DRAIN; + sendAndApply(&bat); { @@ -1145,17 +1149,6 @@ void CGameHandler::makeAttack(const CStack * attacker, const CStack * defender, // drain life effect (as well as log entry) must be applied after the attack if(drainedLife > 0) { - BattleAttack bat; - bat.stackAttacking = attacker->unitId(); - { - CustomEffectInfo customEffect; - customEffect.sound = soundBase::DRAINLIF; - customEffect.effect = 52; - customEffect.stack = attackerState->unitId(); - bat.customEffects.push_back(std::move(customEffect)); - } - sendAndApply(&bat); - MetaString text; attackerState->addText(text, MetaString::GENERAL_TXT, 361); attackerState->addNameReplacement(text, false); @@ -1190,28 +1183,30 @@ void CGameHandler::makeAttack(const CStack * attacker, const CStack * defender, //FIXME: add custom effect on actor } - BattleStackAttacked bsa; - - bsa.stackAttacked = attacker->ID; //invert - bsa.attackerID = uint32_t(-1); - bsa.flags |= BattleStackAttacked::EFFECT; - bsa.effect = 11; - bsa.damageAmount = totalDamage; - attacker->prepareAttacked(bsa, getRandomGenerator()); - - StacksInjured pack; - pack.stacks.push_back(bsa); - sendAndApply(&pack); - - // TODO: this is already implemented in Damage::describeEffect() + if (totalDamage > 0) { - MetaString text; - text.addTxt(MetaString::GENERAL_TXT, 376); - text.addReplacement(MetaString::SPELL_NAME, SpellID::FIRE_SHIELD); - text.addReplacement(totalDamage); - blm.lines.push_back(std::move(text)); + BattleStackAttacked bsa; + + bsa.flags |= BattleStackAttacked::FIRE_SHIELD; + bsa.stackAttacked = attacker->ID; //invert + bsa.attackerID = defender->ID; + bsa.damageAmount = totalDamage; + attacker->prepareAttacked(bsa, getRandomGenerator()); + + StacksInjured pack; + pack.stacks.push_back(bsa); + sendAndApply(&pack); + + // TODO: this is already implemented in Damage::describeEffect() + { + MetaString text; + text.addTxt(MetaString::GENERAL_TXT, 376); + text.addReplacement(MetaString::SPELL_NAME, SpellID::FIRE_SHIELD); + text.addReplacement(totalDamage); + blm.lines.push_back(std::move(text)); + } + addGenericKilledLog(blm, attacker, bsa.killedAmount, false); } - addGenericKilledLog(blm, attacker, bsa.killedAmount, false); } sendAndApply(&blm); @@ -1224,6 +1219,7 @@ int64_t CGameHandler::applyBattleEffects(BattleAttack & bat, std::shared_ptrunitId(); bsa.stackAttacked = def->unitId(); { @@ -1277,8 +1273,12 @@ int64_t CGameHandler::applyBattleEffects(BattleAttack & bat, std::shared_ptrisClone() && - def->hasBonusOfType(Bonus::FIRE_SHIELD) && !attackerState->hasBonusOfType(Bonus::FIRE_IMMUNITY)) + if(!bat.shot() && + !def->isClone() && + def->hasBonusOfType(Bonus::FIRE_SHIELD) && + !attackerState->hasBonusOfType(Bonus::FIRE_IMMUNITY) && + CStack::isMeleeAttackPossible(attackerState.get(), def) // attacked needs to be adjacent to defender for fire shield to trigger (e.g. Dragon Breath attack) + ) { //TODO: use damage with bonus but without penalties auto fireShieldDamage = (std::min(def->getAvailableHealth(), bsa.damageAmount) * def->valOfBonuses(Bonus::FIRE_SHIELD)) / 100; @@ -1303,7 +1303,7 @@ void CGameHandler::addGenericKilledLog(BattleLogMessage & blm, const CStack * de if(killed > 0) { const int32_t txtIndex = (killed > 1) ? 379 : 378; - std::string formatString = VLC->generaltexth->allTexts.at(txtIndex); + std::string formatString = VLC->generaltexth->allTexts[txtIndex]; // these default h3 texts have unnecessary new lines, so get rid of them before displaying (and trim just in case, trimming newlines does not works for some reason) formatString.erase(std::remove(formatString.begin(), formatString.end(), '\n'), formatString.end()); @@ -2990,7 +2990,7 @@ bool CGameHandler::load(const std::string & filename) catch(const CModHandler::Incompatibility & e) { logGlobal->error("Failed to load game: %s", e.what()); - auto errorMsg = VLC->generaltexth->localizedTexts["server"]["errors"]["modsIncompatibility"].String() + '\n'; + auto errorMsg = VLC->generaltexth->translate("vcmi.server.errors.modsIncompatibility") + '\n'; errorMsg += e.what(); lobby->announceMessage(errorMsg); return false; @@ -4478,7 +4478,6 @@ bool CGameHandler::makeBattleAction(BattleAction &ba) case EActionType::SHOOT: //shoot case EActionType::CATAPULT: //catapult case EActionType::STACK_HEAL: //healing with First Aid Tent - case EActionType::DAEMON_SUMMONING: case EActionType::MONSTER_SPELL: if (!stack) @@ -4648,12 +4647,12 @@ bool CGameHandler::makeBattleAction(BattleAction &ba) logGlobal->trace("%s will attack %s", stack->nodeName(), destinationStack->nodeName()); - if(stack->getPosition() != attackPos //we wasn't able to reach destination tile - && !(stack->doubleWide() && (stack->getPosition() == attackPos.cloneInDirection(stack->destShiftDir(), false))) //nor occupy specified hex + if(stack->getPosition() != attackPos + && !(stack->doubleWide() && (stack->getPosition() == attackPos.cloneInDirection(stack->destShiftDir(), false))) ) { - complain("We cannot move this stack to its destination " + stack->getCreature()->namePl); - ok = false; + // we were not able to reach destination tile, nor occupy specified hex + // abort attack attempt, but treat this case as legal - we may have stepped onto a quicksands/mine break; } @@ -4689,8 +4688,8 @@ bool CGameHandler::makeBattleAction(BattleAction &ba) makeAttack(destinationStack, stack, 0, stack->getPosition(), true, false, true); } - //move can cause death, eg. by walking into the moat, first strike can cause death as well - if(stack->alive() && destinationStack->alive()) + //move can cause death, eg. by walking into the moat, first strike can cause death or paralysis/petrification + if(stack->alive() && !stack->hasBonusOfType(Bonus::NOT_ACTIVE) && destinationStack->alive()) { makeAttack(stack, destinationStack, (i ? 0 : distance), destinationTile, i==0, false, false);//no distance travelled on second attack } @@ -4923,13 +4922,13 @@ bool CGameHandler::makeBattleAction(BattleAction &ba) switch(attackedPart) { case EWallPart::KEEP: - posRemove = -2; + posRemove = BattleHex::CASTLE_CENTRAL_TOWER; break; case EWallPart::BOTTOM_TOWER: - posRemove = -3; + posRemove = BattleHex::CASTLE_BOTTOM_TOWER; break; case EWallPart::UPPER_TOWER: - posRemove = -4; + posRemove = BattleHex::CASTLE_UPPER_TOWER; break; } @@ -5012,58 +5011,6 @@ bool CGameHandler::makeBattleAction(BattleAction &ba) } break; } - case EActionType::DAEMON_SUMMONING: - //TODO: From Strategija: - //Summon Demon is a level 2 spell. - { - if(target.size() < 1) - { - complain("Destination required for summon action."); - ok = false; - break; - } - - const CStack * summoner = gs->curB->battleGetStackByID(ba.stackNumber); - const CStack * destStack = gs->curB->battleGetStackByPos(target.at(0).hexValue, false); - - CreatureID summonedType(summoner->getBonusLocalFirst(Selector::type()(Bonus::DAEMON_SUMMONING))->subtype);//in case summoner can summon more than one type of monsters... scream! - - ui64 risedHp = summoner->getCount() * summoner->valOfBonuses(Bonus::DAEMON_SUMMONING, summonedType.toEnum()); - ui64 targetHealth = destStack->getCreature()->MaxHealth() * destStack->baseAmount; - - ui64 canRiseHp = std::min(targetHealth, risedHp); - ui32 canRiseAmount = static_cast(canRiseHp / summonedType.toCreature()->MaxHealth()); - - battle::UnitInfo info; - info.id = gs->curB->battleNextUnitId(); - info.count = std::min(canRiseAmount, destStack->baseAmount); - info.type = summonedType; - info.side = summoner->side; - info.position = gs->curB->getAvaliableHex(summonedType, summoner->side, destStack->getPosition()); - info.summoned = false; - - BattleUnitsChanged addUnits; - addUnits.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD); - info.save(addUnits.changedStacks.back().data); - - if(info.count > 0) //there's rare possibility single creature cannot rise desired type - { - auto wrapper = wrapAction(ba); - - BattleUnitsChanged removeUnits; - removeUnits.changedStacks.emplace_back(destStack->unitId(), UnitChanges::EOperation::REMOVE); - sendAndApply(&removeUnits); - sendAndApply(&addUnits); - - BattleSetStackProperty ssp; - ssp.stackID = ba.stackNumber; - ssp.which = BattleSetStackProperty::CASTS; //reduce number of casts - ssp.val = -1; - ssp.absolute = false; - sendAndApply(&ssp); - } - break; - } case EActionType::MONSTER_SPELL: { auto wrapper = wrapAction(ba); @@ -5095,7 +5042,7 @@ bool CGameHandler::makeBattleAction(BattleAction &ba) break; } } - if(ba.actionType == EActionType::DAEMON_SUMMONING || ba.actionType == EActionType::WAIT || ba.actionType == EActionType::DEFEND + if(ba.actionType == EActionType::WAIT || ba.actionType == EActionType::DEFEND || ba.actionType == EActionType::SHOOT || ba.actionType == EActionType::MONSTER_SPELL) handleDamageFromObstacle(stack); if(ba.stackNumber == gs->curB->activeStack || battleResult.get()) //active stack has moved or battle has finished @@ -5212,7 +5159,7 @@ void CGameHandler::playerMessage(PlayerColor player, const std::string &message, if (cheated) { - SystemMessage temp_message(VLC->generaltexth->allTexts.at(260)); + SystemMessage temp_message(VLC->generaltexth->allTexts[260]); sendAndApply(&temp_message); if(!player.isSpectator()) @@ -5469,33 +5416,29 @@ bool CGameHandler::handleDamageFromObstacle(const CStack * curStack, bool stackI if(!sp) COMPLAIN_RET("Invalid obstacle instance"); + // For the hidden spell created obstacles, e.g. QuickSand, it should be revealed after taking damage + ObstacleChanges changeInfo; + changeInfo.id = spellObstacle->uniqueID; + if (oneTimeObstacle) + changeInfo.operation = ObstacleChanges::EOperation::ACTIVATE_AND_REMOVE; + else + changeInfo.operation = ObstacleChanges::EOperation::ACTIVATE_AND_UPDATE; + + SpellCreatedObstacle changedObstacle; + changedObstacle.uniqueID = spellObstacle->uniqueID; + changedObstacle.revealed = true; + + changeInfo.data.clear(); + JsonSerializer ser(nullptr, changeInfo.data); + ser.serializeStruct("obstacle", changedObstacle); + + BattleObstaclesChanged bocp; + bocp.changes.emplace_back(changeInfo); + sendAndApply(&bocp); + spells::BattleCast battleCast(gs->curB, &caster, spells::Mode::HERO, sp); battleCast.applyEffects(spellEnv, spells::Target(1, spells::Destination(curStack)), true); - - if(oneTimeObstacle) - { - removeObstacle(*obstacle); } - else - { - // For the hidden spell created obstacles, e.g. QuickSand, it should be revealed after taking damage - ObstacleChanges changeInfo; - changeInfo.id = spellObstacle->uniqueID; - changeInfo.operation = ObstacleChanges::EOperation::UPDATE; - - SpellCreatedObstacle changedObstacle; - changedObstacle.uniqueID = spellObstacle->uniqueID; - changedObstacle.revealed = true; - - changeInfo.data.clear(); - JsonSerializer ser(nullptr, changeInfo.data); - ser.serializeStruct("obstacle", changedObstacle); - - BattleObstaclesChanged bocp; - bocp.changes.emplace_back(changeInfo); - sendAndApply(&bocp); - } - } } } else if(obstacle->obstacleType == CObstacleInstance::MOAT) @@ -7237,7 +7180,7 @@ void CGameHandler::handleCheatCode(std::string & cheat, PlayerColor player, cons void CGameHandler::removeObstacle(const CObstacleInstance & obstacle) { BattleObstaclesChanged obsRem; - obsRem.changes.emplace_back(obstacle.uniqueID, BattleChanges::EOperation::REMOVE); + obsRem.changes.emplace_back(obstacle.uniqueID, ObstacleChanges::EOperation::REMOVE); sendAndApply(&obsRem); } diff --git a/test/mock/mock_spells_Mechanics.h b/test/mock/mock_spells_Mechanics.h index e868f82f4..ea70b1cad 100644 --- a/test/mock/mock_spells_Mechanics.h +++ b/test/mock/mock_spells_Mechanics.h @@ -21,7 +21,7 @@ public: MOCK_CONST_METHOD2(adaptProblem, bool(ESpellCastProblem::ESpellCastProblem, Problem &)); MOCK_CONST_METHOD1(adaptGenericProblem, bool(Problem &)); - MOCK_CONST_METHOD2(rangeInHexes, std::vector(BattleHex, bool *)); + MOCK_CONST_METHOD1(rangeInHexes, std::vector(BattleHex)); MOCK_CONST_METHOD1(getAffectedStacks, std::vector(const Target &)); MOCK_CONST_METHOD1(canBeCast, bool(Problem &));