浏览代码

Merge pull request #3258 from SoundSSGood/trade-panels

Market rework
Ivan Savenko 1 年之前
父节点
当前提交
019a4151b9

+ 8 - 4
client/CMakeLists.txt

@@ -96,14 +96,12 @@ set(client_SRCS
 	renderSDL/SDL_Extensions.cpp
 
 	widgets/Buttons.cpp
-	widgets/CAltar.cpp
 	widgets/CArtifactHolder.cpp
 	widgets/CComponent.cpp
 	widgets/CExchangeController.cpp
 	widgets/CGarrisonInt.cpp
 	widgets/CreatureCostBox.cpp
 	widgets/ComboBox.cpp
-	widgets/CTradeBase.cpp
 	widgets/Images.cpp
 	widgets/MiscWidgets.cpp
 	widgets/ObjectLists.cpp
@@ -118,6 +116,10 @@ set(client_SRCS
 	widgets/CArtifactsOfHeroBackpack.cpp
 	widgets/CWindowWithArtifacts.cpp
 	widgets/RadialMenu.cpp
+	widgets/markets/CAltarArtifacts.cpp
+	widgets/markets/CAltarCreatures.cpp
+	widgets/markets/CTradeBase.cpp
+	widgets/markets/TradePanels.cpp
 
 	windows/CAltarWindow.cpp
 	windows/CCastleInterface.cpp
@@ -269,14 +271,12 @@ set(client_HEADERS
 	renderSDL/SDL_PixelAccess.h
 
 	widgets/Buttons.h
-	widgets/CAltar.h
 	widgets/CArtifactHolder.h
 	widgets/CComponent.h
 	widgets/CExchangeController.h
 	widgets/CGarrisonInt.h
 	widgets/CreatureCostBox.h
 	widgets/ComboBox.h
-	widgets/CTradeBase.h
 	widgets/Images.h
 	widgets/MiscWidgets.h
 	widgets/ObjectLists.h
@@ -291,6 +291,10 @@ set(client_HEADERS
 	widgets/CArtifactsOfHeroBackpack.h
 	widgets/CWindowWithArtifacts.h
 	widgets/RadialMenu.h
+	widgets/markets/CAltarArtifacts.h
+	widgets/markets/CAltarCreatures.h
+	widgets/markets/CTradeBase.h
+	widgets/markets/TradePanels.h
 
 	windows/CAltarWindow.h
 	windows/CCastleInterface.h

+ 0 - 466
client/widgets/CAltar.cpp

@@ -1,466 +0,0 @@
-/*
- * CAltarWindow.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 "CAltar.h"
-
-#include "../widgets/CAltar.h"
-#include "../gui/CGuiHandler.h"
-#include "../gui/CursorHandler.h"
-#include "../widgets/Buttons.h"
-#include "../widgets/Slider.h"
-#include "../widgets/TextControls.h"
-
-#include "../CGameInfo.h"
-#include "../CPlayerInterface.h"
-
-#include "../../CCallback.h"
-
-#include "../../lib/networkPacks/ArtifactLocation.h"
-#include "../../lib/CGeneralTextHandler.h"
-#include "../../lib/mapObjects/CGHeroInstance.h"
-#include "../../lib/mapObjects/CGMarket.h"
-
-CAltar::CAltar(const IMarket * market, const CGHeroInstance * hero)
-	: CTradeBase(market, hero)
-{
-	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
-
-	// Experience needed to reach next level
-	texts.emplace_back(std::make_shared<CTextBox>(CGI->generaltexth->allTexts[475], Rect(15, 415, 125, 50), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW));
-	// Total experience on the Altar
-	texts.emplace_back(std::make_shared<CTextBox>(CGI->generaltexth->allTexts[476], Rect(15, 495, 125, 40), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW));
-	deal = std::make_shared<CButton>(Point(269, 520), AnimationPath::builtin("ALTSACR.DEF"), CGI->generaltexth->zelp[585], std::bind(&CAltar::makeDeal, this));
-	expToLevel = std::make_shared<CLabel>(75, 477, FONT_SMALL, ETextAlignment::CENTER);
-	expForHero = std::make_shared<CLabel>(75, 545, FONT_SMALL, ETextAlignment::CENTER);
-}
-
-void CAltar::deselect()
-{
-	hLeft = hRight = nullptr;
-	deal->block(true);
-}
-
-CAltarArtifacts::CAltarArtifacts(const IMarket * market, const CGHeroInstance * hero)
-	: CAltar(market, hero)
-{
-	OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE);
-
-	labels.emplace_back(std::make_shared<CLabel>(450, 34, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[477]));
-	labels.emplace_back(std::make_shared<CLabel>(302, 423, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[478]));
-	selectedCost = std::make_shared<CLabel>(302, 500, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
-	selectedArt = std::make_shared<CArtPlace>(Point(280, 442));
-
-	sacrificeAllButton = std::make_shared<CButton>(Point(393, 520), AnimationPath::builtin("ALTFILL.DEF"),
-		CGI->generaltexth->zelp[571], std::bind(&CAltar::sacrificeAll, this));
-	sacrificeAllButton->block(hero->artifactsInBackpack.empty() && hero->artifactsWorn.empty());
-
-	sacrificeBackpackButton = std::make_shared<CButton>(Point(147, 520), AnimationPath::builtin("ALTEMBK.DEF"),
-		CGI->generaltexth->zelp[570], std::bind(&CAltarArtifacts::sacrificeBackpack, this));
-	sacrificeBackpackButton->block(hero->artifactsInBackpack.empty());
-
-	arts = std::make_shared<CArtifactsOfHeroAltar>(Point(-365, -11));
-	arts->setHero(hero);
-
-	int slotNum = 0;
-	for(auto & altarSlotPos : posSlotsAltar)
-	{
-		auto altarSlot = std::make_shared<CTradeableItem>(altarSlotPos, EType::ARTIFACT_PLACEHOLDER, -1, false, slotNum++);
-		altarSlot->clickPressedCallback = std::bind(&CAltarArtifacts::onSlotClickPressed, this, _1);
-		altarSlot->subtitle = "";
-		items.front().emplace_back(altarSlot);
-	}
-
-	calcExpAltarForHero();
-	deselect();
-};
-
-TExpType CAltarArtifacts::calcExpAltarForHero()
-{
-	auto artifactsOfHero = std::dynamic_pointer_cast<CArtifactsOfHeroAltar>(arts);
-	TExpType expOnAltar(0);
-	for(const auto art : artifactsOfHero->artifactsOnAltar)
-	{
-		int dmp, expOfArt;
-		market->getOffer(art->artType->getId(), 0, dmp, expOfArt, EMarketMode::ARTIFACT_EXP);
-		expOnAltar += expOfArt;
-	}
-	auto resultExp = hero->calculateXp(expOnAltar);
-	expForHero->setText(std::to_string(resultExp));
-	return resultExp;
-}
-
-void CAltarArtifacts::makeDeal()
-{
-	std::vector<TradeItemSell> positions;
-	for(const auto art : arts->artifactsOnAltar)
-	{
-		positions.push_back(hero->getSlotByInstance(art));
-	}
-	std::sort(positions.begin(), positions.end());
-	std::reverse(positions.begin(), positions.end());
-
-	LOCPLINT->cb->trade(market, EMarketMode::ARTIFACT_EXP, positions, std::vector<TradeItemBuy>(), std::vector<ui32>(), hero);
-	arts->artifactsOnAltar.clear();
-
-	for(auto item : items[0])
-	{
-		item->setID(-1);
-		item->subtitle = "";
-	}
-	deal->block(true);
-	calcExpAltarForHero();
-}
-
-void CAltarArtifacts::sacrificeAll()
-{
-	std::vector<ConstTransitivePtr<CArtifactInstance>> artsForMove;
-	for(const auto & slotInfo : arts->getHero()->artifactsWorn)
-	{
-		if(!slotInfo.second.locked && slotInfo.second.artifact->artType->isTradable())
-			artsForMove.push_back(slotInfo.second.artifact);
-	}
-	for(auto artInst : artsForMove)
-		moveArtToAltar(nullptr, artInst);
-	arts->updateWornSlots();
-	sacrificeBackpack();
-}
-
-void CAltarArtifacts::sacrificeBackpack()
-{
-	while(!arts->visibleArtSet.artifactsInBackpack.empty())
-	{
-		if(!putArtOnAltar(nullptr, arts->visibleArtSet.artifactsInBackpack[0].artifact))
-			break;
-	};
-	calcExpAltarForHero();
-}
-
-void CAltarArtifacts::setSelectedArtifact(const CArtifactInstance * art)
-{
-	if(art)
-	{
-		selectedArt->setArtifact(art);
-		int dmp, exp;
-		market->getOffer(art->getTypeId(), 0, dmp, exp, EMarketMode::ARTIFACT_EXP);
-		selectedCost->setText(std::to_string(hero->calculateXp(exp)));
-	}
-	else
-	{
-		selectedArt->setArtifact(nullptr);
-		selectedCost->setText("");
-	}
-}
-
-void CAltarArtifacts::moveArtToAltar(std::shared_ptr<CTradeableItem> altarSlot, const CArtifactInstance * art)
-{
-	if(putArtOnAltar(altarSlot, art))
-	{
-		CCS->curh->dragAndDropCursor(nullptr);
-		arts->unmarkSlots();
-	}
-}
-
-std::shared_ptr<CArtifactsOfHeroAltar> CAltarArtifacts::getAOHset() const
-{
-	return arts;
-}
-
-bool CAltarArtifacts::putArtOnAltar(std::shared_ptr<CTradeableItem> altarSlot, const CArtifactInstance * art)
-{
-	if(!art->artType->isTradable())
-	{
-		logGlobal->warn("Cannot put special artifact on altar!");
-		return false;
-	}
-
-	if(!altarSlot || altarSlot->id != -1)
-	{
-		int slotIndex = -1;
-		while(items[0][++slotIndex]->id >= 0 && slotIndex + 1 < items[0].size());
-		slotIndex = items[0][slotIndex]->id == -1 ? slotIndex : -1;
-		if(slotIndex < 0)
-		{
-			logGlobal->warn("No free slots on altar!");
-			return false;
-		}
-		altarSlot = items[0][slotIndex];
-	}
-
-	int dmp, exp;
-	market->getOffer(art->artType->getId(), 0, dmp, exp, EMarketMode::ARTIFACT_EXP);
-	exp = static_cast<int>(hero->calculateXp(exp));
-
-	arts->artifactsOnAltar.insert(art);
-	altarSlot->setArtInstance(art);
-	altarSlot->subtitle = std::to_string(exp);
-
-	deal->block(false);
-	return true;
-};
-
-void CAltarArtifacts::onSlotClickPressed(std::shared_ptr<CTradeableItem> altarSlot)
-{
-	const auto pickedArtInst = arts->getPickedArtifact();
-	if(pickedArtInst)
-	{
-		arts->pickedArtMoveToAltar(ArtifactPosition::TRANSITION_POS);
-		moveArtToAltar(altarSlot, pickedArtInst);
-	}
-	else if(const CArtifactInstance * art = altarSlot->getArtInstance())
-	{
-		const auto hero = arts->getHero();
-		const auto slot = hero->getSlotByInstance(art);
-		assert(slot != ArtifactPosition::PRE_FIRST);
-		LOCPLINT->cb->swapArtifacts(ArtifactLocation(hero->id, slot),
-			ArtifactLocation(hero->id, ArtifactPosition::TRANSITION_POS));
-		arts->pickedArtFromSlot = slot;
-		arts->artifactsOnAltar.erase(art);
-		altarSlot->setID(-1);
-		altarSlot->subtitle.clear();
-		deal->block(!arts->artifactsOnAltar.size());
-	}
-	calcExpAltarForHero();
-}
-
-CAltarCreatures::CAltarCreatures(const IMarket * market, const CGHeroInstance * hero)
-	: CAltar(market, hero)
-{
-	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
-
-	labels.emplace_back(std::make_shared<CLabel>(155, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW,
-		boost::str(boost::format(CGI->generaltexth->allTexts[272]) % hero->getNameTranslated())));
-	labels.emplace_back(std::make_shared<CLabel>(450, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[479]));
-	texts.emplace_back(std::make_unique<CTextBox>(CGI->generaltexth->allTexts[480], Rect(320, 56, 256, 40), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW));
-	lSubtitle = std::make_shared<CLabel>(180, 503, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
-	rSubtitle = std::make_shared<CLabel>(426, 503, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
-
-	unitsSlider = std::make_shared<CSlider>(Point(231, 481), 137, std::bind(&CAltarCreatures::onUnitsSliderMoved, this, _1), 0, 0, 0, Orientation::HORIZONTAL);
-	maxUnits = std::make_shared<CButton>(Point(147, 520), AnimationPath::builtin("IRCBTNS.DEF"), CGI->generaltexth->zelp[578], std::bind(&CSlider::scrollToMax, unitsSlider));
-
-	unitsOnAltar.resize(GameConstants::ARMY_SIZE, 0);
-	expPerUnit.resize(GameConstants::ARMY_SIZE, 0);
-	sacrificeAllButton = std::make_shared<CButton>(
-		Point(393, 520), AnimationPath::builtin("ALTARMY.DEF"), CGI->generaltexth->zelp[579], std::bind(&CAltar::sacrificeAll, this));
-
-	// Creating slots for hero creatures
-	for(int slotIdx = 0; slotIdx < GameConstants::ARMY_SIZE; slotIdx++)
-	{
-		CreatureID creatureId = CreatureID::NONE;
-		if(const auto & creature = hero->getCreature(SlotID(slotIdx)))
-			creatureId = creature->getId();
-		else
-			continue;
-
-		auto heroSlot = std::make_shared<CTradeableItem>(posSlotsHero[slotIdx], EType::CREATURE, creatureId.num, true, slotIdx);
-		heroSlot->clickPressedCallback = [this](std::shared_ptr<CTradeableItem> altarSlot) -> void
-			{
-				onSlotClickPressed(altarSlot, items[0], hLeft, hRight);
-			};
-		heroSlot->subtitle = std::to_string(hero->getStackCount(SlotID(slotIdx)));
-		items[1].emplace_back(heroSlot);
-	}
-
-	// Creating slots for creatures on altar
-	assert(items[1].size() <= posSlotsAltar.size());
-	for(const auto & heroSlot : items[1])
-	{
-		auto altarSlot = std::make_shared<CTradeableItem>(posSlotsAltar[heroSlot->serial], EType::CREATURE_PLACEHOLDER, heroSlot->id, false, heroSlot->serial);
-		altarSlot->pos.w = heroSlot->pos.w; altarSlot->pos.h = heroSlot->pos.h;
-		altarSlot->clickPressedCallback = [this](std::shared_ptr<CTradeableItem> altarSlot) -> void
-			{
-				onSlotClickPressed(altarSlot, items[1], hRight, hLeft);
-			};
-		items[0].emplace_back(altarSlot);
-	}
-
-	readExpValues();
-	calcExpAltarForHero();
-	deselect();
-};
-
-void CAltarCreatures::readExpValues()
-{
-	int dump;
-	for(auto heroSlot : items[1])
-	{
-		if(heroSlot->id >= 0)
-			market->getOffer(heroSlot->id, 0, dump, expPerUnit[heroSlot->serial], EMarketMode::CREATURE_EXP);
-	}
-}
-
-void CAltarCreatures::updateControls()
-{
-	int sliderAmount = 0;
-	if(hLeft)
-	{
-		std::optional<SlotID> lastSlot;
-		for(auto slot = SlotID(0); slot.num < GameConstants::ARMY_SIZE; slot++)
-		{
-			if(hero->getStackCount(slot) > unitsOnAltar[slot.num])
-			{
-				if(lastSlot.has_value())
-				{
-					lastSlot = std::nullopt;
-					break;
-				}
-				else
-				{
-					lastSlot = slot;
-				}
-			}
-		}
-		sliderAmount = hero->getStackCount(SlotID(hLeft->serial));
-		if(lastSlot.has_value() && lastSlot.value() == SlotID(hLeft->serial))
-			sliderAmount--;
-	}
-	unitsSlider->setAmount(sliderAmount);
-	unitsSlider->block(!unitsSlider->getAmount());
-	if(hLeft)
-		unitsSlider->scrollTo(unitsOnAltar[hLeft->serial]);
-	maxUnits->block(unitsSlider->getAmount() == 0);
-}
-
-void CAltarCreatures::updateSubtitlesForSelected()
-{
-	if(hLeft)
-		lSubtitle->setText(std::to_string(unitsSlider->getValue()));
-	else
-		lSubtitle->setText("");
-	if(hRight)
-		rSubtitle->setText(hRight->subtitle);
-	else
-		rSubtitle->setText("");
-}
-
-void CAltarCreatures::updateGarrison()
-{
-	std::set<std::shared_ptr<CTradeableItem>> empty;
-	getEmptySlots(empty);
-	removeItems(empty);
-	readExpValues();
-	for(auto & heroSlot : items[1])
-		heroSlot->subtitle = std::to_string(hero->getStackCount(SlotID(heroSlot->serial)));
-}
-
-void CAltarCreatures::deselect()
-{
-	CAltar::deselect();
-	unitsSlider->block(true);
-	maxUnits->block(true);
-	updateSubtitlesForSelected();
-}
-
-TExpType CAltarCreatures::calcExpAltarForHero()
-{
-	TExpType expOnAltar(0);
-	auto oneUnitExp = expPerUnit.begin();
-	for(const auto units : unitsOnAltar)
-		expOnAltar += *oneUnitExp++ * units;
-	auto resultExp = hero->calculateXp(expOnAltar);
-	expForHero->setText(std::to_string(resultExp));
-	return resultExp;
-}
-
-void CAltarCreatures::makeDeal()
-{
-	deselect();
-	unitsSlider->scrollTo(0);
-	expForHero->setText(std::to_string(0));
-
-	std::vector<TradeItemSell> ids;
-	std::vector<ui32> toSacrifice;
-
-	for(int i = 0; i < unitsOnAltar.size(); i++)
-	{
-		if(unitsOnAltar[i])
-		{
-			ids.push_back(SlotID(i));
-			toSacrifice.push_back(unitsOnAltar[i]);
-		}
-	}
-
-	LOCPLINT->cb->trade(market, EMarketMode::CREATURE_EXP, ids, {}, toSacrifice, hero);
-
-	for(int & units : unitsOnAltar)
-		units = 0;
-
-	for(auto heroSlot : items[0])
-	{
-		heroSlot->setType(CREATURE_PLACEHOLDER);
-		heroSlot->subtitle = "";
-	}
-}
-
-void CAltarCreatures::sacrificeAll()
-{
-	std::optional<SlotID> lastSlot;
-	for(auto heroSlot : items[1])
-	{
-		auto stackCount = hero->getStackCount(SlotID(heroSlot->serial));
-		if(stackCount > unitsOnAltar[heroSlot->serial])
-		{
-			if(!lastSlot.has_value())
-				lastSlot = SlotID(heroSlot->serial);
-			unitsOnAltar[heroSlot->serial] = stackCount;
-		}
-	}
-	assert(lastSlot.has_value());
-	unitsOnAltar[lastSlot.value().num]--;
-
-	if(hRight)
-		unitsSlider->scrollTo(unitsOnAltar[hRight->serial]);
-	for(auto altarSlot : items[0])
-		updateAltarSlot(altarSlot);
-	updateSubtitlesForSelected();
-
-	deal->block(calcExpAltarForHero() == 0);
-}
-
-void CAltarCreatures::updateAltarSlot(std::shared_ptr<CTradeableItem> slot)
-{
-	auto units = unitsOnAltar[slot->serial];
-	slot->setType(units > 0 ? CREATURE : CREATURE_PLACEHOLDER);
-	slot->subtitle = units > 0 ?
-		boost::str(boost::format(CGI->generaltexth->allTexts[122]) % std::to_string(hero->calculateXp(units * expPerUnit[slot->serial]))) : "";
-}
-
-void CAltarCreatures::onUnitsSliderMoved(int newVal)
-{
-	if(hLeft)
-		unitsOnAltar[hLeft->serial] = newVal;
-	if(hRight)
-		updateAltarSlot(hRight);
-	deal->block(calcExpAltarForHero() == 0);
-	updateControls();
-	updateSubtitlesForSelected();
-}
-
-void CAltarCreatures::onSlotClickPressed(std::shared_ptr<CTradeableItem> altarSlot,
-	std::vector<std::shared_ptr<CTradeableItem>> & oppositeSlots,
-	std::shared_ptr<CTradeableItem> & hCurSide, std::shared_ptr<CTradeableItem> & hOppSide)
-{
-	std::shared_ptr<CTradeableItem> oppositeSlot;
-	for(const auto & slot : oppositeSlots)
-		if(slot->serial == altarSlot->serial)
-		{
-			oppositeSlot = slot;
-			break;
-		}
-
-	if(hCurSide != altarSlot && oppositeSlot)
-	{
-		hCurSide = altarSlot;
-		hOppSide = oppositeSlot;
-		updateControls();
-		updateSubtitlesForSelected();
-		redraw();
-	}
-}

+ 0 - 103
client/widgets/CAltar.h

@@ -1,103 +0,0 @@
-/*
- * CAltar.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 "../widgets/CArtifactsOfHeroAltar.h"
-#include "../widgets/CTradeBase.h"
-
-class CSlider;
-
-class CAltar : public CTradeBase, public CIntObject
-{
-public:
-	std::shared_ptr<CLabel> expToLevel;
-	std::shared_ptr<CLabel> expForHero;
-	std::shared_ptr<CButton> sacrificeAllButton;
-
-	CAltar(const IMarket * market, const CGHeroInstance * hero);
-	virtual ~CAltar() = default;
-	virtual void sacrificeAll() = 0;
-	virtual void deselect();
-	virtual TExpType calcExpAltarForHero() = 0;
-};
-
-class CAltarArtifacts : public CAltar
-{
-public:
-	CAltarArtifacts(const IMarket * market, const CGHeroInstance * hero);
-	TExpType calcExpAltarForHero() override;
-	void makeDeal() override;
-	void sacrificeAll() override;
-	void sacrificeBackpack();
-	void setSelectedArtifact(const CArtifactInstance * art);
-	void moveArtToAltar(std::shared_ptr<CTradeableItem>, const CArtifactInstance * art);
-	std::shared_ptr<CArtifactsOfHeroAltar> getAOHset() const;
-
-private:
-	std::shared_ptr<CArtPlace> selectedArt;
-	std::shared_ptr<CLabel> selectedCost;
-	std::shared_ptr<CButton> sacrificeBackpackButton;
-	std::shared_ptr<CArtifactsOfHeroAltar> arts;
-
-	const std::vector<Point> posSlotsAltar =
-	{
-		Point(317, 53), Point(371, 53), Point(425, 53),
-		Point(479, 53), Point(533, 53), Point(317, 123),
-		Point(371, 123), Point(425, 123), Point(479, 123),
-		Point(533, 123), Point(317, 193), Point(371, 193),
-		Point(425, 193), Point(479, 193), Point(533, 193),
-		Point(317, 263), Point(371, 263), Point(425, 263),
-		Point(479, 263), Point(533, 263), Point(398, 333),
-		Point(452, 333)
-	};
-
-	bool putArtOnAltar(std::shared_ptr<CTradeableItem> altarSlot, const CArtifactInstance * art);
-	void onSlotClickPressed(std::shared_ptr<CTradeableItem> altarSlot);
-};
-
-class CAltarCreatures : public CAltar
-{
-public:
-	CAltarCreatures(const IMarket * market, const CGHeroInstance * hero);
-	void updateGarrison();
-	void deselect() override;
-	TExpType calcExpAltarForHero() override;
-	void makeDeal() override;
-	void sacrificeAll() override;
-	void updateAltarSlot(std::shared_ptr<CTradeableItem> slot);
-
-private:
-	std::shared_ptr<CButton> maxUnits;
-	std::shared_ptr<CSlider> unitsSlider;
-	std::vector<int> unitsOnAltar;
-	std::vector<int> expPerUnit;
-	std::shared_ptr<CLabel> lSubtitle, rSubtitle;
-
-	const std::vector<Point> posSlotsAltar =
-	{
-		Point(334, 110), Point(417, 110), Point(500, 110),
-		Point(334, 208), Point(417, 208), Point(500, 208),
-		Point(417, 306)
-	};
-	const std::vector<Point> posSlotsHero =
-	{
-		Point(45, 110), Point(128, 110), Point(211, 110),
-		Point(45, 208), Point(128, 208), Point(211, 208),
-		Point(128, 306)
-	};
-
-	void readExpValues();
-	void updateControls();
-	void updateSubtitlesForSelected();
-	void onUnitsSliderMoved(int newVal);
-	void onSlotClickPressed(std::shared_ptr<CTradeableItem> altarSlot,
-		std::vector<std::shared_ptr<CTradeableItem>> & oppositeSlots,
-		std::shared_ptr<CTradeableItem> & hCurSide, std::shared_ptr<CTradeableItem> & hOppSide);
-};

+ 3 - 24
client/widgets/CArtifactHolder.cpp

@@ -75,13 +75,11 @@ void CArtPlace::setInternals(const CArtifactInstance * artInst)
 	text = artInst->getDescription();
 }
 
-CArtPlace::CArtPlace(Point position, const CArtifactInstance * art) 
-	: ourArt(art)
+CArtPlace::CArtPlace(Point position, const CArtifactInstance * art)
+	: SelectableSlot(Rect(position, Point(44, 44)), Point(1, 1))
+	, ourArt(art)
 	, locked(false)
 {
-	pos += position;
-	pos.w = pos.h = 44;
-
 	OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE);
 
 	imageIndex = 0;
@@ -92,9 +90,6 @@ CArtPlace::CArtPlace(Point position, const CArtifactInstance * art)
 
 	image = std::make_shared<CAnimImage>(AnimationPath::builtin("artifact"), imageIndex);
 	image->disable();
-
-	selection = std::make_shared<CAnimImage>(AnimationPath::builtin("artifact"), ArtifactID::ART_SELECTION, 0, -1, -1);
-	selection->visible = false;
 }
 
 const CArtifactInstance * CArtPlace::getArt()
@@ -170,16 +165,6 @@ bool CArtPlace::isLocked() const
 	return locked;
 }
 
-void CArtPlace::selectSlot(bool on)
-{
-	selection->visible = on;
-}
-
-bool CArtPlace::isSelected() const
-{
-	return selection->visible;
-}
-
 void CArtPlace::clickPressed(const Point & cursorPosition)
 {
 	if(clickPressedCallback)
@@ -201,12 +186,6 @@ void CArtPlace::gesture(bool on, const Point & initialPosition, const Point & fi
 		gestureCallback(*this, initialPosition);
 }
 
-void CArtPlace::showAll(Canvas & to)
-{
-	CIntObject::showAll(to);
-	selection->showAll(to);
-}
-
 void CArtPlace::setArtifact(const CArtifactInstance * art)
 {
 	setInternals(art);

+ 1 - 5
client/widgets/CArtifactHolder.h

@@ -29,7 +29,7 @@ public:
 	virtual void artifactAssembled(const ArtifactLocation & artLoc)=0;
 };
 
-class CArtPlace : public LRClickableAreaWTextComp
+class CArtPlace : public SelectableSlot
 {
 public:
 	using ClickFunctor = std::function<void(CArtPlace&, const Point&)>;
@@ -40,9 +40,6 @@ public:
 	const CArtifactInstance * getArt();
 	void lockSlot(bool on);
 	bool isLocked() const;
-	void selectSlot(bool on);
-	bool isSelected() const;
-	void showAll(Canvas & to) override;
 	void setArtifact(const CArtifactInstance * art);
 	void setClickPressedCallback(ClickFunctor callback);
 	void setShowPopupCallback(ClickFunctor callback);
@@ -55,7 +52,6 @@ protected:
 	std::shared_ptr<CAnimImage> image;
 	const CArtifactInstance * ourArt;
 	int imageIndex;
-	std::shared_ptr<CAnimImage> selection;
 	bool locked;
 	ClickFunctor clickPressedCallback;
 	ClickFunctor showPopupCallback;

+ 5 - 0
client/widgets/CArtifactsOfHeroMarket.cpp

@@ -19,6 +19,11 @@ CArtifactsOfHeroMarket::CArtifactsOfHeroMarket(const Point & position)
 		std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2),
 		position,
 		std::bind(&CArtifactsOfHeroMarket::scrollBackpack, this, _1));
+
+	for(const auto & [slot, artPlace] : artWorn)
+		artPlace->setSelectionWidth(2);
+	for(auto artPlace : backpack)
+		artPlace->setSelectionWidth(2);
 };
 
 void CArtifactsOfHeroMarket::scrollBackpack(int offset)

+ 0 - 285
client/widgets/CTradeBase.cpp

@@ -1,285 +0,0 @@
-/*
- * CTradeBase.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 "CTradeBase.h"
-
-#include "../gui/CGuiHandler.h"
-#include "../render/Canvas.h"
-#include "../widgets/TextControls.h"
-#include "../windows/InfoWindows.h"
-
-#include "../CGameInfo.h"
-
-#include "../../lib/CGeneralTextHandler.h"
-#include "../../lib/mapObjects/CGHeroInstance.h"
-
-CTradeBase::CTradeableItem::CTradeableItem(Point pos, EType Type, int ID, bool Left, int Serial)
-	: CIntObject(LCLICK | HOVER | SHOW_POPUP, pos)
-	, type(EType(-1)) // set to invalid, will be corrected in setType
-	, id(ID)
-	, serial(Serial)
-	, left(Left)
-{
-	OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE);
-	downSelection = false;
-	hlp = nullptr;
-	setType(Type);
-	if(image)
-	{
-		this->pos.w = image->pos.w;
-		this->pos.h = image->pos.h;
-	}
-}
-
-void CTradeBase::CTradeableItem::setType(EType newType)
-{
-	if(type != newType)
-	{
-		OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
-		type = newType;
-
-		if(getIndex() < 0)
-		{
-			image = std::make_shared<CAnimImage>(getFilename(), 0);
-			image->disable();
-		}
-		else
-		{
-			image = std::make_shared<CAnimImage>(getFilename(), getIndex());
-		}
-	}
-}
-
-void CTradeBase::CTradeableItem::setID(int newID)
-{
-	if(id != newID)
-	{
-		id = newID;
-		if(image)
-		{
-			int index = getIndex();
-			if(index < 0)
-				image->disable();
-			else
-			{
-				image->enable();
-				image->setFrame(index);
-			}
-		}
-	}
-}
-
-AnimationPath CTradeBase::CTradeableItem::getFilename()
-{
-	switch(type)
-	{
-	case RESOURCE:
-		return AnimationPath::builtin("RESOURCE");
-	case PLAYER:
-		return AnimationPath::builtin("CREST58");
-	case ARTIFACT_TYPE:
-	case ARTIFACT_PLACEHOLDER:
-	case ARTIFACT_INSTANCE:
-		return AnimationPath::builtin("artifact");
-	case CREATURE:
-		return AnimationPath::builtin("TWCRPORT");
-	default:
-		return {};
-	}
-}
-
-int CTradeBase::CTradeableItem::getIndex()
-{
-	if(id < 0)
-		return -1;
-
-	switch(type)
-	{
-	case RESOURCE:
-	case PLAYER:
-		return id;
-	case ARTIFACT_TYPE:
-	case ARTIFACT_INSTANCE:
-	case ARTIFACT_PLACEHOLDER:
-		return CGI->artifacts()->getByIndex(id)->getIconIndex();
-	case CREATURE:
-		return CGI->creatures()->getByIndex(id)->getIconIndex();
-	default:
-		return -1;
-	}
-}
-
-void CTradeBase::CTradeableItem::showAll(Canvas & to)
-{
-	Point posToBitmap;
-	Point posToSubCenter;
-
-	switch (type)
-	{
-	case RESOURCE:
-		posToBitmap = Point(19, 9);
-		posToSubCenter = Point(36, 59);
-		break;
-	case CREATURE_PLACEHOLDER:
-	case CREATURE:
-		posToSubCenter = Point(29, 77);
-		break;
-	case PLAYER:
-		posToSubCenter = Point(31, 76);
-		break;
-	case ARTIFACT_PLACEHOLDER:
-	case ARTIFACT_INSTANCE:
-		posToSubCenter = Point(19, 54);
-		if (downSelection)
-			posToSubCenter.y += 8;
-		break;
-	case ARTIFACT_TYPE:
-		posToSubCenter = Point(19, 58);
-		break;
-	}
-
-	if(image)
-	{
-		image->moveTo(pos.topLeft() + posToBitmap);
-		CIntObject::showAll(to);
-	}
-
-	to.drawText(pos.topLeft() + posToSubCenter, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, subtitle);
-}
-
-void CTradeBase::CTradeableItem::clickPressed(const Point& cursorPosition)
-{
-	if(clickPressedCallback)
-		clickPressedCallback(shared_from_this());
-}
-
-void CTradeBase::CTradeableItem::showAllAt(const Point& dstPos, const std::string& customSub, Canvas& to)
-{
-	Rect oldPos = pos;
-	std::string oldSub = subtitle;
-	downSelection = true;
-
-	moveTo(dstPos);
-	subtitle = customSub;
-	showAll(to);
-
-	downSelection = false;
-	moveTo(oldPos.topLeft());
-	subtitle = oldSub;
-}
-
-void CTradeBase::CTradeableItem::hover(bool on)
-{
-	if(!on)
-	{
-		GH.statusbar()->clear();
-		return;
-	}
-
-	switch(type)
-	{
-	case CREATURE:
-	case CREATURE_PLACEHOLDER:
-		GH.statusbar()->write(boost::str(boost::format(CGI->generaltexth->allTexts[481]) % CGI->creh->objects[id]->getNamePluralTranslated()));
-		break;
-	case ARTIFACT_PLACEHOLDER:
-		if(id < 0)
-			GH.statusbar()->write(CGI->generaltexth->zelp[582].first);
-		else
-			GH.statusbar()->write(CGI->artifacts()->getByIndex(id)->getNameTranslated());
-		break;
-	}
-}
-
-void CTradeBase::CTradeableItem::showPopupWindow(const Point& cursorPosition)
-{
-	switch(type)
-	{
-	case CREATURE:
-	case CREATURE_PLACEHOLDER:
-		break;
-	case ARTIFACT_TYPE:
-	case ARTIFACT_PLACEHOLDER:
-		//TODO: it's would be better for market to contain actual CArtifactInstance and not just ids of certain artifact type so we can use getEffectiveDescription.
-		if (id >= 0)
-			CRClickPopup::createAndPush(CGI->artifacts()->getByIndex(id)->getDescriptionTranslated());
-		break;
-	}
-}
-
-std::string CTradeBase::CTradeableItem::getName(int number) const
-{
-	switch(type)
-	{
-	case PLAYER:
-		return CGI->generaltexth->capColors[id];
-	case RESOURCE:
-		return CGI->generaltexth->restypes[id];
-	case CREATURE:
-		if (number == 1)
-			return CGI->creh->objects[id]->getNameSingularTranslated();
-		else
-			return CGI->creh->objects[id]->getNamePluralTranslated();
-	case ARTIFACT_TYPE:
-	case ARTIFACT_INSTANCE:
-		return CGI->artifacts()->getByIndex(id)->getNameTranslated();
-	}
-	logGlobal->error("Invalid trade item type: %d", (int)type);
-	return "";
-}
-
-const CArtifactInstance* CTradeBase::CTradeableItem::getArtInstance() const
-{
-	switch(type)
-	{
-	case ARTIFACT_PLACEHOLDER:
-	case ARTIFACT_INSTANCE:
-		return hlp;
-	default:
-		return nullptr;
-	}
-}
-
-void CTradeBase::CTradeableItem::setArtInstance(const CArtifactInstance * art)
-{
-	assert(type == ARTIFACT_PLACEHOLDER || type == ARTIFACT_INSTANCE);
-	hlp = art;
-	if(art)
-		setID(art->artType->getId());
-	else
-		setID(-1);
-}
-
-CTradeBase::CTradeBase(const IMarket * market, const CGHeroInstance * hero)
-	: market(market)
-	, hero(hero)
-{
-}
-
-void CTradeBase::removeItems(const std::set<std::shared_ptr<CTradeableItem>> & toRemove)
-{
-	for(auto item : toRemove)
-		removeItem(item);
-}
-
-void CTradeBase::removeItem(std::shared_ptr<CTradeableItem> item)
-{
-	items[item->left] -= item;
-
-	if(hRight == item)
-		hRight.reset();
-}
-
-void CTradeBase::getEmptySlots(std::set<std::shared_ptr<CTradeableItem>> & toRemove)
-{
-	for(auto item : items[1])
-		if(!hero->getStackCount(SlotID(item->serial)))
-			toRemove.insert(item);
-}

+ 0 - 88
client/widgets/CTradeBase.h

@@ -1,88 +0,0 @@
-/*
- * CTradeBase.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 "Images.h"
-
-#include "../../lib/FunctionList.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-class IMarket;
-class CGHeroInstance;
-
-VCMI_LIB_NAMESPACE_END
-
-class CButton;
-class CTextBox;
-
-class CTradeBase
-{
-public:
-	enum EType
-	{
-		RESOURCE, PLAYER, ARTIFACT_TYPE, CREATURE, CREATURE_PLACEHOLDER, ARTIFACT_PLACEHOLDER, ARTIFACT_INSTANCE
-	};
-
-	class CTradeableItem : public CIntObject, public std::enable_shared_from_this<CTradeableItem>
-	{
-		std::shared_ptr<CAnimImage> image;
-		AnimationPath getFilename();
-		int getIndex();
-	public:
-		const CArtifactInstance * hlp; //holds ptr to artifact instance id type artifact
-		EType type;
-		int id;
-		const int serial;
-		const bool left;
-		std::string subtitle; //empty if default
-		std::function<void(std::shared_ptr<CTradeableItem> altarSlot)> clickPressedCallback;
-
-		void setType(EType newType);
-		void setID(int newID);
-
-		const CArtifactInstance* getArtInstance() const;
-		void setArtInstance(const CArtifactInstance * art);
-
-		CFunctionList<void()> callback;
-		bool downSelection;
-
-		void showAllAt(const Point & dstPos, const std::string & customSub, Canvas & to);
-
-		void showPopupWindow(const Point & cursorPosition) override;
-		void hover(bool on) override;
-		void showAll(Canvas & to) override;
-		void clickPressed(const Point & cursorPosition) override;
-		std::string getName(int number = -1) const;
-		CTradeableItem(Point pos, EType Type, int ID, bool Left, int Serial);
-	};
-
-	const IMarket * market;
-	const CGHeroInstance * hero;
-
-	//all indexes: 1 = left, 0 = right
-	std::array<std::vector<std::shared_ptr<CTradeableItem>>, 2> items;
-
-	//highlighted items (nullptr if no highlight)
-	std::shared_ptr<CTradeableItem> hLeft;
-	std::shared_ptr<CTradeableItem> hRight;
-	std::shared_ptr<CButton> deal;
-
-	CTradeBase(const IMarket * market, const CGHeroInstance * hero);
-	void removeItems(const std::set<std::shared_ptr<CTradeableItem>> & toRemove);
-	void removeItem(std::shared_ptr<CTradeableItem> item);
-	void getEmptySlots(std::set<std::shared_ptr<CTradeableItem>> & toRemove);
-	virtual void makeDeal() = 0;
-
-protected:
-	std::vector<std::shared_ptr<CLabel>> labels;
-	std::vector<std::shared_ptr<CButton>> buttons;
-	std::vector<std::shared_ptr<CTextBox>> texts;
-};

+ 52 - 4
client/widgets/MiscWidgets.cpp

@@ -662,22 +662,37 @@ void CCreaturePic::setAmount(int newAmount)
 }
 
 TransparentFilledRectangle::TransparentFilledRectangle(Rect position, ColorRGBA color) :
-	color(color), colorLine(ColorRGBA()), drawLine(false)
+	color(color), colorLine(ColorRGBA()), drawLine(false), lineWidth(0)
 {
 	pos = position + pos.topLeft();
 }
 
-TransparentFilledRectangle::TransparentFilledRectangle(Rect position, ColorRGBA color, ColorRGBA colorLine) :
-	color(color), colorLine(colorLine), drawLine(true)
+TransparentFilledRectangle::TransparentFilledRectangle(Rect position, ColorRGBA color, ColorRGBA colorLine, int width) :
+	color(color), colorLine(colorLine), drawLine(true), lineWidth(width)
 {
 	pos = position + pos.topLeft();
 }
 
+void TransparentFilledRectangle::setDrawBorder(bool on)
+{
+	drawLine = on;
+}
+
+bool TransparentFilledRectangle::getDrawBorder()
+{
+	return drawLine;
+}
+
+void TransparentFilledRectangle::setBorderWidth(int width)
+{
+	lineWidth = width;
+}
+
 void TransparentFilledRectangle::showAll(Canvas & to) 
 {
 	to.drawColorBlended(pos, color);
 	if(drawLine)
-		to.drawBorder(pos, colorLine);
+		to.drawBorder(pos, colorLine, lineWidth);
 }
 
 SimpleLine::SimpleLine(Point pos1, Point pos2, ColorRGBA color) :
@@ -688,3 +703,36 @@ void SimpleLine::showAll(Canvas & to)
 {
 	to.drawLine(pos1 + pos.topLeft(), pos2 + pos.topLeft(), color, color);
 }
+
+SelectableSlot::SelectableSlot(Rect area, Point oversize, const int width)
+	: LRClickableAreaWTextComp(area)
+{
+	selection = std::make_unique<TransparentFilledRectangle>(
+		Rect(area.topLeft() - oversize, area.dimensions() + oversize * 2), Colors::TRANSPARENCY, Colors::YELLOW, width);
+	selectSlot(false);
+}
+
+SelectableSlot::SelectableSlot(Rect area, Point oversize)
+	: SelectableSlot(area, oversize, 1)
+{
+}
+
+SelectableSlot::SelectableSlot(Rect area, const int width)
+	: SelectableSlot(area, Point(), width)
+{
+}
+
+void SelectableSlot::selectSlot(bool on)
+{
+	selection->setDrawBorder(on);
+}
+
+bool SelectableSlot::isSelected() const
+{
+	return selection->getDrawBorder();
+}
+
+void SelectableSlot::setSelectionWidth(int width)
+{
+	selection->setBorderWidth(width);
+}

+ 19 - 1
client/widgets/MiscWidgets.h

@@ -252,9 +252,14 @@ class TransparentFilledRectangle : public CIntObject
 	ColorRGBA color;
 	ColorRGBA colorLine;
 	bool drawLine;
+	int lineWidth;
+
 public:
     TransparentFilledRectangle(Rect position, ColorRGBA color);
-    TransparentFilledRectangle(Rect position, ColorRGBA color, ColorRGBA colorLine);
+    TransparentFilledRectangle(Rect position, ColorRGBA color, ColorRGBA colorLine, int width = 1);
+	void setDrawBorder(bool on);
+	bool getDrawBorder();
+	void setBorderWidth(int width);
     void showAll(Canvas & to) override;
 };
 
@@ -267,3 +272,16 @@ public:
     SimpleLine(Point pos1, Point pos2, ColorRGBA color);
     void showAll(Canvas & to) override;
 };
+
+class SelectableSlot : public LRClickableAreaWTextComp
+{
+	std::unique_ptr<TransparentFilledRectangle> selection;
+
+public:
+	SelectableSlot(Rect area, Point oversize, const int width);
+	SelectableSlot(Rect area, Point oversize);
+	SelectableSlot(Rect area, const int width = 1);
+	void selectSlot(bool on);
+	bool isSelected() const;
+	void setSelectionWidth(int width);
+};

+ 215 - 0
client/widgets/markets/CAltarArtifacts.cpp

@@ -0,0 +1,215 @@
+/*
+ * CAltarArtifacts.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 "CAltarArtifacts.h"
+
+#include "../../gui/CGuiHandler.h"
+#include "../../gui/CursorHandler.h"
+#include "../../widgets/Buttons.h"
+#include "../../widgets/TextControls.h"
+
+#include "../../CGameInfo.h"
+#include "../../CPlayerInterface.h"
+
+#include "../../../CCallback.h"
+
+#include "../../../lib/networkPacks/ArtifactLocation.h"
+#include "../../../lib/CGeneralTextHandler.h"
+#include "../../../lib/mapObjects/CGHeroInstance.h"
+#include "../../../lib/mapObjects/CGMarket.h"
+
+CAltarArtifacts::CAltarArtifacts(const IMarket * market, const CGHeroInstance * hero)
+	: CTradeBase(market, hero)
+{
+	OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE);
+
+	deal = std::make_shared<CButton>(dealButtonPos, AnimationPath::builtin("ALTSACR.DEF"),
+		CGI->generaltexth->zelp[585], [this]() {CAltarArtifacts::makeDeal(); });
+	labels.emplace_back(std::make_shared<CLabel>(450, 34, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[477]));
+	labels.emplace_back(std::make_shared<CLabel>(302, 423, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[478]));
+	selectedCost = std::make_shared<CLabel>(302, 500, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
+	selectedArt = std::make_shared<CArtPlace>(Point(280, 442));
+
+	sacrificeAllButton = std::make_shared<CButton>(Point(393, 520), AnimationPath::builtin("ALTFILL.DEF"),
+		CGI->generaltexth->zelp[571], std::bind(&CExperienceAltar::sacrificeAll, this));
+	sacrificeAllButton->block(hero->artifactsInBackpack.empty() && hero->artifactsWorn.empty());
+
+	sacrificeBackpackButton = std::make_shared<CButton>(Point(147, 520), AnimationPath::builtin("ALTEMBK.DEF"),
+		CGI->generaltexth->zelp[570], std::bind(&CAltarArtifacts::sacrificeBackpack, this));
+	sacrificeBackpackButton->block(hero->artifactsInBackpack.empty());
+
+	arts = std::make_shared<CArtifactsOfHeroAltar>(Point(-365, -11));
+	arts->setHero(hero);
+
+	int slotNum = 0;
+	for(auto & altarSlotPos : posSlotsAltar)
+	{
+		auto altarSlot = std::make_shared<CTradeableItem>(Rect(altarSlotPos, Point(44, 44)), EType::ARTIFACT_PLACEHOLDER, -1, false, slotNum);
+		altarSlot->clickPressedCallback = std::bind(&CAltarArtifacts::onSlotClickPressed, this, _1, hRight);
+		altarSlot->subtitle.clear();
+		items.front().emplace_back(altarSlot);
+		slotNum++;
+	}
+
+	expForHero->setText(std::to_string(0));
+	CTradeBase::deselect();
+};
+
+TExpType CAltarArtifacts::calcExpAltarForHero()
+{
+	auto artifactsOfHero = std::dynamic_pointer_cast<CArtifactsOfHeroAltar>(arts);
+	TExpType expOnAltar(0);
+	for(const auto art : artifactsOfHero->artifactsOnAltar)
+	{
+		int dmp = 0;
+		int expOfArt = 0;
+		market->getOffer(art->getTypeId(), 0, dmp, expOfArt, EMarketMode::ARTIFACT_EXP);
+		expOnAltar += expOfArt;
+	}
+	auto resultExp = hero->calculateXp(expOnAltar);
+	expForHero->setText(std::to_string(resultExp));
+	return resultExp;
+}
+
+void CAltarArtifacts::makeDeal()
+{
+	std::vector<TradeItemSell> positions;
+	for(const auto art : arts->artifactsOnAltar)
+	{
+		positions.push_back(hero->getSlotByInstance(art));
+	}
+	std::sort(positions.begin(), positions.end());
+	std::reverse(positions.begin(), positions.end());
+
+	LOCPLINT->cb->trade(market, EMarketMode::ARTIFACT_EXP, positions, std::vector<TradeItemBuy>(), std::vector<ui32>(), hero);
+	arts->artifactsOnAltar.clear();
+
+	for(auto item : items[0])
+	{
+		item->setID(-1);
+		item->subtitle.clear();
+	}
+	deal->block(true);
+	calcExpAltarForHero();
+}
+
+void CAltarArtifacts::sacrificeAll()
+{
+	std::vector<ConstTransitivePtr<CArtifactInstance>> artsForMove;
+	for(const auto & [slot, slotInfo] : arts->getHero()->artifactsWorn)
+	{
+		if(!slotInfo.locked && slotInfo.artifact->artType->isTradable())
+			artsForMove.emplace_back(slotInfo.artifact);
+	}
+	for(auto artInst : artsForMove)
+		moveArtToAltar(nullptr, artInst);
+	arts->updateWornSlots();
+	sacrificeBackpack();
+}
+
+void CAltarArtifacts::sacrificeBackpack()
+{
+	while(!arts->visibleArtSet.artifactsInBackpack.empty())
+	{
+		if(!putArtOnAltar(nullptr, arts->visibleArtSet.artifactsInBackpack[0].artifact))
+			break;
+	};
+	calcExpAltarForHero();
+}
+
+void CAltarArtifacts::setSelectedArtifact(const CArtifactInstance * art)
+{
+	if(art)
+	{
+		selectedArt->setArtifact(art);
+		int dmp = 0;
+		int exp = 0;
+		market->getOffer(art->getTypeId(), 0, dmp, exp, EMarketMode::ARTIFACT_EXP);
+		selectedCost->setText(std::to_string(hero->calculateXp(exp)));
+	}
+	else
+	{
+		selectedArt->setArtifact(nullptr);
+		selectedCost->setText("");
+	}
+}
+
+void CAltarArtifacts::moveArtToAltar(std::shared_ptr<CTradeableItem> altarSlot, const CArtifactInstance * art)
+{
+	if(putArtOnAltar(altarSlot, art))
+	{
+		CCS->curh->dragAndDropCursor(nullptr);
+		arts->unmarkSlots();
+	}
+}
+
+std::shared_ptr<CArtifactsOfHeroAltar> CAltarArtifacts::getAOHset() const
+{
+	return arts;
+}
+
+bool CAltarArtifacts::putArtOnAltar(std::shared_ptr<CTradeableItem> altarSlot, const CArtifactInstance * art)
+{
+	if(!art->artType->isTradable())
+	{
+		logGlobal->warn("Cannot put special artifact on altar!");
+		return false;
+	}
+
+	if(!altarSlot || altarSlot->id != -1)
+	{
+		int slotIndex = -1;
+		while(items[0][++slotIndex]->id >= 0 && slotIndex + 1 < items[0].size());
+		slotIndex = items[0][slotIndex]->id == -1 ? slotIndex : -1;
+		if(slotIndex < 0)
+		{
+			logGlobal->warn("No free slots on altar!");
+			return false;
+		}
+		altarSlot = items[0][slotIndex];
+	}
+
+	int dmp = 0;
+	int exp = 0;
+	market->getOffer(art->artType->getId(), 0, dmp, exp, EMarketMode::ARTIFACT_EXP);
+	exp = static_cast<int>(hero->calculateXp(exp));
+
+	arts->artifactsOnAltar.insert(art);
+	altarSlot->setArtInstance(art);
+	altarSlot->subtitle = std::to_string(exp);
+
+	deal->block(false);
+	return true;
+};
+
+void CAltarArtifacts::onSlotClickPressed(const std::shared_ptr<CTradeableItem> & newSlot, std::shared_ptr<CTradeableItem> & hCurSlot)
+{
+	const auto pickedArtInst = arts->getPickedArtifact();
+	if(pickedArtInst)
+	{
+		arts->pickedArtMoveToAltar(ArtifactPosition::TRANSITION_POS);
+		moveArtToAltar(newSlot, pickedArtInst);
+	}
+	else if(const CArtifactInstance * art = newSlot->getArtInstance())
+	{
+		const auto hero = arts->getHero();
+		const auto slot = hero->getSlotByInstance(art);
+		assert(slot != ArtifactPosition::PRE_FIRST);
+		LOCPLINT->cb->swapArtifacts(ArtifactLocation(hero->id, slot),
+			ArtifactLocation(hero->id, ArtifactPosition::TRANSITION_POS));
+		arts->pickedArtFromSlot = slot;
+		arts->artifactsOnAltar.erase(art);
+		newSlot->setID(-1);
+		newSlot->subtitle.clear();
+		deal->block(!arts->artifactsOnAltar.size());
+	}
+	calcExpAltarForHero();
+}

+ 47 - 0
client/widgets/markets/CAltarArtifacts.h

@@ -0,0 +1,47 @@
+/*
+ * CAltarArtifacts.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 "../CArtifactsOfHeroAltar.h"
+#include "CTradeBase.h"
+
+class CAltarArtifacts : public CExperienceAltar
+{
+public:
+	CAltarArtifacts(const IMarket * market, const CGHeroInstance * hero);
+	TExpType calcExpAltarForHero() override;
+	void makeDeal() override;
+	void sacrificeAll() override;
+	void sacrificeBackpack();
+	void setSelectedArtifact(const CArtifactInstance * art);
+	void moveArtToAltar(std::shared_ptr<CTradeableItem>, const CArtifactInstance * art);
+	std::shared_ptr<CArtifactsOfHeroAltar> getAOHset() const;
+
+private:
+	std::shared_ptr<CArtPlace> selectedArt;
+	std::shared_ptr<CLabel> selectedCost;
+	std::shared_ptr<CButton> sacrificeBackpackButton;
+	std::shared_ptr<CArtifactsOfHeroAltar> arts;
+
+	const std::vector<Point> posSlotsAltar =
+	{
+		Point(317, 53), Point(371, 53), Point(425, 53),
+		Point(479, 53), Point(533, 53), Point(317, 123),
+		Point(371, 123), Point(425, 123), Point(479, 123),
+		Point(533, 123), Point(317, 193), Point(371, 193),
+		Point(425, 193), Point(479, 193), Point(533, 193),
+		Point(317, 263), Point(371, 263), Point(425, 263),
+		Point(479, 263), Point(533, 263), Point(398, 333),
+		Point(452, 333)
+	};
+
+	bool putArtOnAltar(std::shared_ptr<CTradeableItem> altarSlot, const CArtifactInstance * art);
+	void onSlotClickPressed(const std::shared_ptr<CTradeableItem> & newSlot, std::shared_ptr<CTradeableItem> & hCurSlot) override;
+};

+ 251 - 0
client/widgets/markets/CAltarCreatures.cpp

@@ -0,0 +1,251 @@
+/*
+ * CAltarCreatures.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 "CAltarCreatures.h"
+
+#include "../../gui/CGuiHandler.h"
+#include "../../widgets/Buttons.h"
+#include "../../widgets/Slider.h"
+#include "../../widgets/TextControls.h"
+
+#include "../../CGameInfo.h"
+#include "../../CPlayerInterface.h"
+
+#include "../../../CCallback.h"
+
+#include "../../../lib/CGeneralTextHandler.h"
+#include "../../../lib/mapObjects/CGHeroInstance.h"
+#include "../../../lib/mapObjects/CGMarket.h"
+
+CAltarCreatures::CAltarCreatures(const IMarket * market, const CGHeroInstance * hero)
+	: CTradeBase(market, hero)
+{
+	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
+
+	deal = std::make_shared<CButton>(dealButtonPos, AnimationPath::builtin("ALTSACR.DEF"),
+		CGI->generaltexth->zelp[584], [this]() {CAltarCreatures::makeDeal();});
+	labels.emplace_back(std::make_shared<CLabel>(155, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW,
+		boost::str(boost::format(CGI->generaltexth->allTexts[272]) % hero->getNameTranslated())));
+	labels.emplace_back(std::make_shared<CLabel>(450, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[479]));
+	texts.emplace_back(std::make_unique<CTextBox>(CGI->generaltexth->allTexts[480], Rect(320, 56, 256, 40), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW));
+	lSubtitle = std::make_shared<CLabel>(180, 503, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
+	rSubtitle = std::make_shared<CLabel>(426, 503, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
+
+	offerSlider = std::make_shared<CSlider>(Point(231, 481), 137, std::bind(&CAltarCreatures::onOfferSliderMoved, this, _1), 0, 0, 0, Orientation::HORIZONTAL);
+	maxUnits = std::make_shared<CButton>(Point(147, 520), AnimationPath::builtin("IRCBTNS.DEF"), CGI->generaltexth->zelp[578], std::bind(&CSlider::scrollToMax, offerSlider));
+
+	unitsOnAltar.resize(GameConstants::ARMY_SIZE, 0);
+	expPerUnit.resize(GameConstants::ARMY_SIZE, 0);
+	sacrificeAllButton = std::make_shared<CButton>(
+		Point(393, 520), AnimationPath::builtin("ALTARMY.DEF"), CGI->generaltexth->zelp[579], std::bind(&CExperienceAltar::sacrificeAll, this));
+
+	// Hero creatures panel
+	assert(leftTradePanel);
+	leftTradePanel->moveBy(Point(45, 110));
+	leftTradePanel->updateSlotsCallback = std::bind(&CCreaturesSelling::updateSubtitle, this);
+	for(const auto & slot : leftTradePanel->slots)
+		slot->clickPressedCallback = [this](const std::shared_ptr<CTradeableItem> & heroSlot) {CAltarCreatures::onSlotClickPressed(heroSlot, hLeft);};
+
+	// Altar creatures panel
+	rightTradePanel = std::make_shared<CreaturesPanel>([this](const std::shared_ptr<CTradeableItem> & altarSlot)
+		{
+			CAltarCreatures::onSlotClickPressed(altarSlot, hRight);
+		}, leftTradePanel->slots);
+	rightTradePanel->moveBy(Point(334, 110));
+
+	leftTradePanel->deleteSlotsCheck = rightTradePanel->deleteSlotsCheck = std::bind(&CCreaturesSelling::slotDeletingCheck, this, _1);
+	readExpValues();
+	expForHero->setText(std::to_string(0));
+	CAltarCreatures::deselect();
+};
+
+void CAltarCreatures::readExpValues()
+{
+	int dump;
+	for(auto heroSlot : leftTradePanel->slots)
+	{
+		if(heroSlot->id >= 0)
+			market->getOffer(heroSlot->id, 0, dump, expPerUnit[heroSlot->serial], EMarketMode::CREATURE_EXP);
+	}
+}
+
+void CAltarCreatures::updateControls()
+{
+	int sliderAmount = 0;
+	if(hLeft)
+	{
+		std::optional<SlotID> lastSlot;
+		for(auto slot = SlotID(0); slot.num < GameConstants::ARMY_SIZE; slot++)
+		{
+			if(hero->getStackCount(slot) > unitsOnAltar[slot.num])
+			{
+				if(lastSlot.has_value())
+				{
+					lastSlot = std::nullopt;
+					break;
+				}
+				else
+				{
+					lastSlot = slot;
+				}
+			}
+		}
+		sliderAmount = hero->getStackCount(SlotID(hLeft->serial));
+		if(lastSlot.has_value() && lastSlot.value() == SlotID(hLeft->serial))
+			sliderAmount--;
+	}
+	offerSlider->setAmount(sliderAmount);
+	offerSlider->block(!offerSlider->getAmount());
+	if(hLeft)
+		offerSlider->scrollTo(unitsOnAltar[hLeft->serial]);
+	maxUnits->block(offerSlider->getAmount() == 0);
+}
+
+void CAltarCreatures::updateSubtitlesForSelected()
+{
+	if(hLeft)
+		lSubtitle->setText(std::to_string(offerSlider->getValue()));
+	else
+		lSubtitle->setText("");
+	if(hRight)
+		rSubtitle->setText(hRight->subtitle);
+	else
+		rSubtitle->setText("");
+}
+
+void CAltarCreatures::updateSlots()
+{
+	rightTradePanel->deleteSlots();
+	leftTradePanel->deleteSlots();
+	assert(leftTradePanel->slots.size() == rightTradePanel->slots.size());
+	readExpValues();
+	leftTradePanel->updateSlots();
+}
+
+void CAltarCreatures::deselect()
+{
+	CTradeBase::deselect();
+	offerSlider->block(true);
+	maxUnits->block(true);
+	updateSubtitlesForSelected();
+}
+
+TExpType CAltarCreatures::calcExpAltarForHero()
+{
+	TExpType expOnAltar(0);
+	auto oneUnitExp = expPerUnit.begin();
+	for(const auto units : unitsOnAltar)
+		expOnAltar += *oneUnitExp++ * units;
+	auto resultExp = hero->calculateXp(expOnAltar);
+	expForHero->setText(std::to_string(resultExp));
+	return resultExp;
+}
+
+void CAltarCreatures::makeDeal()
+{
+	deselect();
+	offerSlider->scrollTo(0);
+	expForHero->setText(std::to_string(0));
+
+	std::vector<TradeItemSell> ids;
+	std::vector<ui32> toSacrifice;
+
+	for(int i = 0; i < unitsOnAltar.size(); i++)
+	{
+		if(unitsOnAltar[i])
+		{
+			ids.push_back(SlotID(i));
+			toSacrifice.push_back(unitsOnAltar[i]);
+		}
+	}
+
+	LOCPLINT->cb->trade(market, EMarketMode::CREATURE_EXP, ids, {}, toSacrifice, hero);
+
+	for(int & units : unitsOnAltar)
+		units = 0;
+
+	for(auto heroSlot : rightTradePanel->slots)
+	{
+		heroSlot->setType(EType::CREATURE_PLACEHOLDER);
+		heroSlot->subtitle.clear();
+	}
+}
+
+void CAltarCreatures::sacrificeAll()
+{
+	std::optional<SlotID> lastSlot;
+	for(auto heroSlot : leftTradePanel->slots)
+	{
+		auto stackCount = hero->getStackCount(SlotID(heroSlot->serial));
+		if(stackCount > unitsOnAltar[heroSlot->serial])
+		{
+			if(!lastSlot.has_value())
+				lastSlot = SlotID(heroSlot->serial);
+			unitsOnAltar[heroSlot->serial] = stackCount;
+		}
+	}
+	assert(lastSlot.has_value());
+	unitsOnAltar[lastSlot.value().num]--;
+
+	if(hRight)
+		offerSlider->scrollTo(unitsOnAltar[hRight->serial]);
+	for(auto altarSlot : rightTradePanel->slots)
+		updateAltarSlot(altarSlot);
+	updateSubtitlesForSelected();
+
+	deal->block(calcExpAltarForHero() == 0);
+}
+
+void CAltarCreatures::updateAltarSlot(std::shared_ptr<CTradeableItem> slot)
+{
+	auto units = unitsOnAltar[slot->serial];
+	slot->setType(units > 0 ? EType::CREATURE : EType::CREATURE_PLACEHOLDER);
+	slot->subtitle = units > 0 ?
+		boost::str(boost::format(CGI->generaltexth->allTexts[122]) % std::to_string(hero->calculateXp(units * expPerUnit[slot->serial]))) : "";
+}
+
+void CAltarCreatures::onOfferSliderMoved(int newVal)
+{
+	if(hLeft)
+		unitsOnAltar[hLeft->serial] = newVal;
+	if(hRight)
+		updateAltarSlot(hRight);
+	deal->block(calcExpAltarForHero() == 0);
+	updateControls();
+	updateSubtitlesForSelected();
+}
+
+void CAltarCreatures::onSlotClickPressed(const std::shared_ptr<CTradeableItem> & newSlot, std::shared_ptr<CTradeableItem> & hCurSide)
+{
+	if(hCurSide == newSlot)
+		return;
+
+	auto * oppositeSlot = &hLeft;
+	auto oppositePanel = leftTradePanel;
+	CTradeBase::onSlotClickPressed(newSlot, hCurSide);
+	if(hCurSide == hLeft)
+	{
+		oppositeSlot = &hRight;
+		oppositePanel = rightTradePanel;
+	}
+	std::shared_ptr<CTradeableItem> oppositeNewSlot;
+	for(const auto & slot : oppositePanel->slots)
+		if(slot->serial == newSlot->serial)
+		{
+			oppositeNewSlot = slot;
+			break;
+		}
+	assert(oppositeNewSlot);
+	CTradeBase::onSlotClickPressed(oppositeNewSlot, *oppositeSlot);
+	updateControls();
+	updateSubtitlesForSelected();
+	redraw();
+}

+ 37 - 0
client/widgets/markets/CAltarCreatures.h

@@ -0,0 +1,37 @@
+/*
+ * CAltarCreatures.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 "CTradeBase.h"
+
+class CAltarCreatures : public CExperienceAltar, public CCreaturesSelling
+{
+public:
+	CAltarCreatures(const IMarket * market, const CGHeroInstance * hero);
+	void updateSlots();
+	void deselect() override;
+	TExpType calcExpAltarForHero() override;
+	void makeDeal() override;
+	void sacrificeAll() override;
+	void updateAltarSlot(std::shared_ptr<CTradeableItem> slot);
+
+private:
+	std::shared_ptr<CButton> maxUnits;
+	std::vector<int> unitsOnAltar;
+	std::vector<int> expPerUnit;
+	std::shared_ptr<CLabel> lSubtitle;
+	std::shared_ptr<CLabel> rSubtitle;
+
+	void readExpValues();
+	void updateControls();
+	void updateSubtitlesForSelected();
+	void onOfferSliderMoved(int newVal);
+	void onSlotClickPressed(const std::shared_ptr<CTradeableItem> & newSlot, std::shared_ptr<CTradeableItem> & hCurSide) override;
+};

+ 105 - 0
client/widgets/markets/CTradeBase.cpp

@@ -0,0 +1,105 @@
+/*
+ * CTradeBase.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 "CTradeBase.h"
+
+#include "../MiscWidgets.h"
+
+#include "../../gui/CGuiHandler.h"
+#include "../../widgets/Buttons.h"
+#include "../../widgets/TextControls.h"
+
+#include "../../CGameInfo.h"
+
+#include "../../../lib/CGeneralTextHandler.h"
+#include "../../../lib/mapObjects/CGHeroInstance.h"
+
+CTradeBase::CTradeBase(const IMarket * market, const CGHeroInstance * hero)
+	: market(market)
+	, hero(hero)
+{
+}
+
+void CTradeBase::removeItems(const std::set<std::shared_ptr<CTradeableItem>> & toRemove)
+{
+	for(auto item : toRemove)
+		removeItem(item);
+}
+
+void CTradeBase::removeItem(std::shared_ptr<CTradeableItem> item)
+{
+	rightTradePanel->slots.erase(std::remove(rightTradePanel->slots.begin(), rightTradePanel->slots.end(), item));
+
+	if(hRight == item)
+		hRight.reset();
+}
+
+void CTradeBase::getEmptySlots(std::set<std::shared_ptr<CTradeableItem>> & toRemove)
+{
+	for(auto item : leftTradePanel->slots)
+		if(!hero->getStackCount(SlotID(item->serial)))
+			toRemove.insert(item);
+}
+
+void CTradeBase::deselect()
+{
+	if(hLeft)
+		hLeft->selectSlot(false);
+	if(hRight)
+		hRight->selectSlot(false);
+	hLeft = hRight = nullptr;
+	deal->block(true);
+}
+
+void CTradeBase::onSlotClickPressed(const std::shared_ptr<CTradeableItem> & newSlot, std::shared_ptr<CTradeableItem> & hCurSlot)
+{
+	if(newSlot == hCurSlot)
+		return;
+
+	if(hCurSlot)
+		hCurSlot->selectSlot(false);
+	hCurSlot = newSlot;
+	newSlot->selectSlot(true);
+}
+
+CExperienceAltar::CExperienceAltar()
+{
+	OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE);
+
+	// Experience needed to reach next level
+	texts.emplace_back(std::make_shared<CTextBox>(CGI->generaltexth->allTexts[475], Rect(15, 415, 125, 50), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW));
+	// Total experience on the Altar
+	texts.emplace_back(std::make_shared<CTextBox>(CGI->generaltexth->allTexts[476], Rect(15, 495, 125, 40), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW));
+	expToLevel = std::make_shared<CLabel>(75, 477, FONT_SMALL, ETextAlignment::CENTER);
+	expForHero = std::make_shared<CLabel>(75, 545, FONT_SMALL, ETextAlignment::CENTER);
+}
+
+CCreaturesSelling::CCreaturesSelling()
+{
+	assert(hero);
+	CreaturesPanel::slotsData slots;
+	for(auto slotId = SlotID(0); slotId.num < GameConstants::ARMY_SIZE; slotId++)
+	{
+		if(const auto & creature = hero->getCreature(slotId))
+			slots.emplace_back(std::make_tuple(creature->getId(), slotId, hero->getStackCount(slotId)));
+	}
+	leftTradePanel = std::make_shared<CreaturesPanel>(nullptr, slots);
+}
+
+bool CCreaturesSelling::slotDeletingCheck(const std::shared_ptr<CTradeableItem> & slot)
+{
+	return hero->getStackCount(SlotID(slot->serial)) == 0 ? true : false;
+}
+
+void CCreaturesSelling::updateSubtitle()
+{
+	for(auto & heroSlot : leftTradePanel->slots)
+		heroSlot->subtitle = std::to_string(this->hero->getStackCount(SlotID(heroSlot->serial)));
+}

+ 74 - 0
client/widgets/markets/CTradeBase.h

@@ -0,0 +1,74 @@
+/*
+ * CTradeBase.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 "TradePanels.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class IMarket;
+class CGHeroInstance;
+
+VCMI_LIB_NAMESPACE_END
+
+class CButton;
+class CSlider;
+
+class CTradeBase
+{
+public:
+	const IMarket * market;
+	const CGHeroInstance * hero;
+
+	//all indexes: 1 = left, 0 = right
+	std::array<std::vector<std::shared_ptr<CTradeableItem>>, 2> items;
+	std::shared_ptr<TradePanelBase> leftTradePanel;
+	std::shared_ptr<TradePanelBase> rightTradePanel;
+
+	//highlighted items (nullptr if no highlight)
+	std::shared_ptr<CTradeableItem> hLeft;
+	std::shared_ptr<CTradeableItem> hRight;
+	std::shared_ptr<CButton> deal;
+	std::shared_ptr<CSlider> offerSlider;
+
+	std::vector<std::shared_ptr<CLabel>> labels;
+	std::vector<std::shared_ptr<CButton>> buttons;
+	std::vector<std::shared_ptr<CTextBox>> texts;
+
+	CTradeBase(const IMarket * market, const CGHeroInstance * hero);
+	void removeItems(const std::set<std::shared_ptr<CTradeableItem>> & toRemove);
+	void removeItem(std::shared_ptr<CTradeableItem> item);
+	void getEmptySlots(std::set<std::shared_ptr<CTradeableItem>> & toRemove);
+	virtual void makeDeal() = 0;
+	virtual void deselect();
+	virtual void onSlotClickPressed(const std::shared_ptr<CTradeableItem> & newSlot, std::shared_ptr<CTradeableItem> & hCurSlot);
+};
+
+// Market subclasses
+class CExperienceAltar : virtual public CTradeBase, virtual public CIntObject
+{
+public:
+	std::shared_ptr<CLabel> expToLevel;
+	std::shared_ptr<CLabel> expForHero;
+	std::shared_ptr<CButton> sacrificeAllButton;
+	const Point dealButtonPos = Point(269, 520);
+
+	CExperienceAltar();
+	virtual void sacrificeAll() = 0;
+	virtual TExpType calcExpAltarForHero() = 0;
+};
+
+class CCreaturesSelling : virtual public CTradeBase, virtual public CIntObject
+{
+public:
+	CCreaturesSelling();
+	bool slotDeletingCheck(const std::shared_ptr<CTradeableItem> & slot);
+	void updateSubtitle();
+};

+ 392 - 0
client/widgets/markets/TradePanels.cpp

@@ -0,0 +1,392 @@
+/*
+ * TradePanels.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 "TradePanels.h"
+
+#include "../../gui/CGuiHandler.h"
+#include "../../render/Canvas.h"
+#include "../../widgets/TextControls.h"
+#include "../../windows/InfoWindows.h"
+
+#include "../../CGameInfo.h"
+#include "../../CPlayerInterface.h"
+
+#include "../../../CCallback.h"
+
+#include "../../../lib/CGeneralTextHandler.h"
+#include "../../../lib/mapObjects/CGHeroInstance.h"
+
+CTradeableItem::CTradeableItem(const Rect & area, EType Type, int ID, bool Left, int Serial)
+	: SelectableSlot(area, Point(1, 1))
+	, artInstance(nullptr)
+	, type(EType(-1)) // set to invalid, will be corrected in setType
+	, id(ID)
+	, serial(Serial)
+	, left(Left)
+	, downSelection(false)
+{
+	OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE);
+
+	addUsedEvents(LCLICK);
+	addUsedEvents(HOVER);
+	addUsedEvents(SHOW_POPUP);
+	
+	setType(Type);
+
+	this->pos.w = area.w;
+	this->pos.h = area.h;
+}
+
+void CTradeableItem::setType(EType newType)
+{
+	if(type != newType)
+	{
+		OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
+		type = newType;
+
+		if(getIndex() < 0)
+		{
+			image = std::make_shared<CAnimImage>(getFilename(), 0);
+			image->disable();
+		}
+		else
+		{
+			image = std::make_shared<CAnimImage>(getFilename(), getIndex());
+		}
+	}
+}
+
+void CTradeableItem::setID(int newID)
+{
+	if(id != newID)
+	{
+		id = newID;
+		if(image)
+		{
+			int index = getIndex();
+			if(index < 0)
+				image->disable();
+			else
+			{
+				image->enable();
+				image->setFrame(index);
+			}
+		}
+	}
+}
+
+AnimationPath CTradeableItem::getFilename()
+{
+	switch(type)
+	{
+	case EType::RESOURCE:
+		return AnimationPath::builtin("RESOURCE");
+	case EType::PLAYER:
+		return AnimationPath::builtin("CREST58");
+	case EType::ARTIFACT_TYPE:
+	case EType::ARTIFACT_PLACEHOLDER:
+	case EType::ARTIFACT_INSTANCE:
+		return AnimationPath::builtin("artifact");
+	case EType::CREATURE:
+		return AnimationPath::builtin("TWCRPORT");
+	default:
+		return {};
+	}
+}
+
+int CTradeableItem::getIndex()
+{
+	if(id < 0)
+		return -1;
+
+	switch(type)
+	{
+	case EType::RESOURCE:
+	case EType::PLAYER:
+		return id;
+	case EType::ARTIFACT_TYPE:
+	case EType::ARTIFACT_INSTANCE:
+	case EType::ARTIFACT_PLACEHOLDER:
+		return CGI->artifacts()->getByIndex(id)->getIconIndex();
+	case EType::CREATURE:
+		return CGI->creatures()->getByIndex(id)->getIconIndex();
+	default:
+		return -1;
+	}
+}
+
+void CTradeableItem::showAll(Canvas & to)
+{
+	Point posToBitmap;
+	Point posToSubCenter;
+
+	switch(type)
+	{
+	case EType::RESOURCE:
+		posToBitmap = Point(19, 9);
+		posToSubCenter = Point(35, 57);
+		break;
+	case EType::CREATURE_PLACEHOLDER:
+	case EType::CREATURE:
+		posToSubCenter = Point(29, 77);
+		break;
+	case EType::PLAYER:
+		posToSubCenter = Point(31, 77);
+		break;
+	case EType::ARTIFACT_PLACEHOLDER:
+	case EType::ARTIFACT_INSTANCE:
+		posToSubCenter = Point(22, 51);
+		if (downSelection)
+			posToSubCenter.y += 8;
+		break;
+	case EType::ARTIFACT_TYPE:
+		posToSubCenter = Point(35, 57);
+		posToBitmap = Point(13, 0);
+		break;
+	}
+
+	if(image)
+	{
+		image->moveTo(pos.topLeft() + posToBitmap);
+		CIntObject::showAll(to);
+	}
+
+	to.drawText(pos.topLeft() + posToSubCenter, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, subtitle);
+}
+
+void CTradeableItem::clickPressed(const Point & cursorPosition)
+{
+	if(clickPressedCallback)
+		clickPressedCallback(shared_from_this());
+}
+
+void CTradeableItem::showAllAt(const Point & dstPos, const std::string & customSub, Canvas & to)
+{
+	Rect oldPos = pos;
+	std::string oldSub = subtitle;
+	downSelection = true;
+
+	moveTo(dstPos);
+	subtitle = customSub;
+	showAll(to);
+
+	downSelection = false;
+	moveTo(oldPos.topLeft());
+	subtitle = oldSub;
+}
+
+void CTradeableItem::hover(bool on)
+{
+	if(!on)
+	{
+		GH.statusbar()->clear();
+		return;
+	}
+
+	switch(type)
+	{
+	case EType::CREATURE:
+	case EType::CREATURE_PLACEHOLDER:
+		GH.statusbar()->write(boost::str(boost::format(CGI->generaltexth->allTexts[481]) % CGI->creh->objects[id]->getNamePluralTranslated()));
+		break;
+	case EType::ARTIFACT_PLACEHOLDER:
+		if(id < 0)
+			GH.statusbar()->write(CGI->generaltexth->zelp[582].first);
+		else
+			GH.statusbar()->write(CGI->artifacts()->getByIndex(id)->getNameTranslated());
+		break;
+	}
+}
+
+void CTradeableItem::showPopupWindow(const Point & cursorPosition)
+{
+	switch(type)
+	{
+	case EType::CREATURE:
+	case EType::CREATURE_PLACEHOLDER:
+		break;
+	case EType::ARTIFACT_TYPE:
+	case EType::ARTIFACT_PLACEHOLDER:
+		//TODO: it's would be better for market to contain actual CArtifactInstance and not just ids of certain artifact type so we can use getEffectiveDescription.
+		if (id >= 0)
+			CRClickPopup::createAndPush(CGI->artifacts()->getByIndex(id)->getDescriptionTranslated());
+		break;
+	}
+}
+
+std::string CTradeableItem::getName(int number) const
+{
+	switch(type)
+	{
+	case EType::PLAYER:
+		return CGI->generaltexth->capColors[id];
+	case EType::RESOURCE:
+		return CGI->generaltexth->restypes[id];
+	case EType::CREATURE:
+		if (number == 1)
+			return CGI->creh->objects[id]->getNameSingularTranslated();
+		else
+			return CGI->creh->objects[id]->getNamePluralTranslated();
+	case EType::ARTIFACT_TYPE:
+	case EType::ARTIFACT_INSTANCE:
+		return CGI->artifacts()->getByIndex(id)->getNameTranslated();
+	}
+	logGlobal->error("Invalid trade item type: %d", (int)type);
+	return "";
+}
+
+const CArtifactInstance * CTradeableItem::getArtInstance() const
+{
+	switch(type)
+	{
+	case EType::ARTIFACT_PLACEHOLDER:
+	case EType::ARTIFACT_INSTANCE:
+		return artInstance;
+	default:
+		return nullptr;
+	}
+}
+
+void CTradeableItem::setArtInstance(const CArtifactInstance * art)
+{
+	assert(type == EType::ARTIFACT_PLACEHOLDER || type == EType::ARTIFACT_INSTANCE);
+	artInstance = art;
+	if(art)
+		setID(art->getTypeId());
+	else
+		setID(-1);
+}
+
+void TradePanelBase::updateSlots()
+{
+	if(updateSlotsCallback)
+		updateSlotsCallback();
+}
+
+void TradePanelBase::deselect()
+{
+	for(const auto & slot : slots)
+		slot->selectSlot(false);
+}
+
+void TradePanelBase::clearSubtitles()
+{
+	for(const auto & slot : slots)
+		slot->subtitle.clear();
+}
+
+void TradePanelBase::updateOffer(CTradeableItem & slot, int cost, int qty)
+{
+	slot.subtitle = std::to_string(qty);
+	if(cost != 1)
+	{
+		slot.subtitle.append("/");
+		slot.subtitle.append(std::to_string(cost));
+	}
+}
+
+void TradePanelBase::deleteSlots()
+{
+	if(deleteSlotsCheck)
+		slots.erase(std::remove_if(slots.begin(), slots.end(), deleteSlotsCheck), slots.end());
+}
+
+ResourcesPanel::ResourcesPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback, UpdateSlotsFunctor updateSubtitles)
+{
+	assert(resourcesForTrade.size() == slotsPos.size());
+	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
+
+	for(const auto & res : resourcesForTrade)
+	{
+		auto slot = slots.emplace_back(std::make_shared<CTradeableItem>(Rect(slotsPos[res.num], slotDimension),
+			EType::RESOURCE, res.num, true, res.num));
+		slot->clickPressedCallback = clickPressedCallback;
+		slot->setSelectionWidth(selectionWidth);
+	}
+	updateSlotsCallback = updateSubtitles;
+}
+
+ArtifactsPanel::ArtifactsPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback, UpdateSlotsFunctor updateSubtitles,
+	const std::vector<TradeItemBuy> & arts)
+{
+	assert(slotsForTrade == slotsPos.size());
+	assert(slotsForTrade == arts.size());
+	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
+
+	for(auto slotIdx = 0; slotIdx < slotsForTrade; slotIdx++)
+	{
+		auto artType = arts[slotIdx].getNum();
+		if(artType != ArtifactID::NONE)
+		{
+			auto slot = slots.emplace_back(std::make_shared<CTradeableItem>(Rect(slotsPos[slotIdx], slotDimension),
+				EType::ARTIFACT_TYPE, artType, false, slotIdx));
+			slot->clickPressedCallback = clickPressedCallback;
+			slot->setSelectionWidth(selectionWidth);
+		}
+	}
+	updateSlotsCallback = updateSubtitles;
+}
+
+PlayersPanel::PlayersPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback)
+{
+	assert(PlayerColor::PLAYER_LIMIT_I <= slotsPos.size() + 1);
+	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
+
+	std::vector<PlayerColor> players;
+	for(auto player = PlayerColor(0); player < PlayerColor::PLAYER_LIMIT_I; player++)
+	{
+		if(player != LOCPLINT->playerID && LOCPLINT->cb->getPlayerStatus(player) == EPlayerStatus::INGAME)
+			players.emplace_back(player);
+	}
+
+	slots.resize(players.size());
+	int slotNum = 0;
+	for(auto & slot : slots)
+	{
+		slot = std::make_shared<CTradeableItem>(Rect(slotsPos[slotNum], slotDimension), EType::PLAYER, players[slotNum].num, false, slotNum);
+		slot->clickPressedCallback = clickPressedCallback;
+		slot->setSelectionWidth(selectionWidth);
+		slot->subtitle = CGI->generaltexth->capColors[players[slotNum].num];
+		slotNum++;
+	}
+}
+
+CreaturesPanel::CreaturesPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback, const slotsData & initialSlots)
+{
+	assert(initialSlots.size() <= GameConstants::ARMY_SIZE);
+	assert(slotsPos.size() <= GameConstants::ARMY_SIZE);
+	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
+
+	for(const auto & [creatureId, slotId, creaturesNum] : initialSlots)
+	{
+		auto slot = slots.emplace_back(std::make_shared<CTradeableItem>(Rect(slotsPos[slotId.num], slotDimension),
+			creaturesNum == 0 ? EType::CREATURE_PLACEHOLDER : EType::CREATURE, creatureId.num, true, slotId));
+		slot->clickPressedCallback = clickPressedCallback;
+		if(creaturesNum != 0)
+			slot->subtitle = std::to_string(creaturesNum);
+		slot->setSelectionWidth(selectionWidth);
+	}
+}
+
+CreaturesPanel::CreaturesPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback,
+	const std::vector<std::shared_ptr<CTradeableItem>> & srcSlots, bool emptySlots)
+{
+	assert(slots.size() <= GameConstants::ARMY_SIZE);
+	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
+
+	for(const auto & srcSlot : srcSlots)
+	{
+		auto slot = slots.emplace_back(std::make_shared<CTradeableItem>(Rect(slotsPos[srcSlot->serial], srcSlot->pos.dimensions()),
+			emptySlots ? EType::CREATURE_PLACEHOLDER : EType::CREATURE, srcSlot->id, true, srcSlot->serial));
+		slot->clickPressedCallback = clickPressedCallback;
+		slot->subtitle = emptySlots ? "" : srcSlot->subtitle;
+		slot->setSelectionWidth(selectionWidth);
+	}
+}

+ 141 - 0
client/widgets/markets/TradePanels.h

@@ -0,0 +1,141 @@
+/*
+ * TradePanels.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 "../MiscWidgets.h"
+#include "../Images.h"
+
+#include "../../../lib/networkPacks/TradeItem.h"
+
+enum class EType
+{
+	RESOURCE, PLAYER, ARTIFACT_TYPE, CREATURE, CREATURE_PLACEHOLDER, ARTIFACT_PLACEHOLDER, ARTIFACT_INSTANCE
+};
+
+class CTradeableItem : public SelectableSlot, public std::enable_shared_from_this<CTradeableItem>
+{
+public:
+	std::shared_ptr<CAnimImage> image;
+	AnimationPath getFilename();
+	int getIndex();
+	using ClickPressedFunctor = std::function<void(const std::shared_ptr<CTradeableItem>&)>;
+
+	const CArtifactInstance * artInstance; //holds ptr to artifact instance id type artifact
+	EType type;
+	int id;
+	const int serial;
+	const bool left;
+	std::string subtitle;
+	ClickPressedFunctor clickPressedCallback;
+
+	void setType(EType newType);
+	void setID(int newID);
+
+	const CArtifactInstance * getArtInstance() const;
+	void setArtInstance(const CArtifactInstance * art);
+
+	bool downSelection;
+
+	void showAllAt(const Point & dstPos, const std::string & customSub, Canvas & to);
+
+	void showPopupWindow(const Point & cursorPosition) override;
+	void hover(bool on) override;
+	void showAll(Canvas & to) override;
+	void clickPressed(const Point & cursorPosition) override;
+	std::string getName(int number = -1) const;
+	CTradeableItem(const Rect & area, EType Type, int ID, bool Left, int Serial);
+};
+
+class TradePanelBase : public CIntObject
+{
+public:
+	using UpdateSlotsFunctor = std::function<void()>;
+	using DeleteSlotsCheck = std::function<bool(const std::shared_ptr<CTradeableItem>&)>;
+
+	std::vector<std::shared_ptr<CTradeableItem>> slots;
+	UpdateSlotsFunctor updateSlotsCallback;
+	DeleteSlotsCheck deleteSlotsCheck;
+	std::shared_ptr<CTradeableItem> selected;
+	const int selectionWidth = 2;
+
+	virtual void updateSlots();
+	virtual void deselect();
+	virtual void clearSubtitles();
+	void updateOffer(CTradeableItem & slot, int, int);
+	void deleteSlots();
+};
+
+class ResourcesPanel : public TradePanelBase
+{
+	const std::vector<GameResID> resourcesForTrade =
+	{
+		GameResID::WOOD, GameResID::MERCURY, GameResID::ORE,
+		GameResID::SULFUR, GameResID::CRYSTAL, GameResID::GEMS,
+		GameResID::GOLD
+	};
+	const std::vector<Point> slotsPos =
+	{
+		Point(0, 0), Point(83, 0), Point(166, 0),
+		Point(0, 79), Point(83, 79), Point(166, 79),
+		Point(83, 158)
+	};
+	const Point slotDimension = Point(69, 66);
+
+public:
+	ResourcesPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback, UpdateSlotsFunctor updateSubtitles);
+};
+
+class ArtifactsPanel : public TradePanelBase
+{
+	const std::vector<Point> slotsPos =
+	{
+		Point(0, 0), Point(83, 0), Point(166, 0),
+		Point(0, 79), Point(83, 79), Point(166, 79),
+		Point(83, 158)
+	};
+	const size_t slotsForTrade = 7;
+	const Point slotDimension = Point(69, 66);
+
+public:
+	ArtifactsPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback, UpdateSlotsFunctor updateSubtitles,
+		const std::vector<TradeItemBuy> & arts);
+};
+
+class PlayersPanel : public TradePanelBase
+{
+	const std::vector<Point> slotsPos =
+	{
+		Point(0, 0), Point(83, 0), Point(166, 0),
+		Point(0, 118), Point(83, 118), Point(166, 118),
+		Point(83, 236)
+	};
+	const Point slotDimension = Point(58, 64);
+
+public:
+	explicit PlayersPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback);
+};
+
+class CreaturesPanel : public TradePanelBase
+{
+	const std::vector<Point> slotsPos =
+	{
+		Point(0, 0), Point(83, 0), Point(166, 0),
+		Point(0, 98), Point(83, 98), Point(166, 98),
+		Point(83, 196)
+	};
+	const Point slotDimension = Point(58, 64);
+
+public:
+	using slotsData = std::vector<std::tuple<CreatureID, SlotID, int>>;
+
+	CreaturesPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback, const slotsData & initialSlots);
+	CreaturesPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback,
+		const std::vector<std::shared_ptr<CTradeableItem>> & srcSlots, bool emptySlots = true);
+};

+ 1 - 3
client/windows/CAltarWindow.cpp

@@ -48,7 +48,7 @@ void CAltarWindow::updateExpToLevel()
 void CAltarWindow::updateGarrisons()
 {
 	if(auto altarCreatures = std::static_pointer_cast<CAltarCreatures>(altar))
-		altarCreatures->updateGarrison();
+		altarCreatures->updateSlots();
 }
 
 bool CAltarWindow::holdsGarrison(const CArmedInstance * army)
@@ -130,12 +130,10 @@ void CAltarWindow::showAll(Canvas & to)
 
 	if(altar->hRight)
 	{
-		to.drawBorder(Rect::createAround(altar->hRight->pos, 1), Colors::BRIGHT_YELLOW, 2);
 		altar->hRight->showAllAt(altar->pos.topLeft() + Point(396, 423), "", to);
 	}
 	if(altar->hLeft)
 	{
-		to.drawBorder(Rect::createAround(altar->hLeft->pos, 1), Colors::BRIGHT_YELLOW, 2);
 		altar->hLeft->showAllAt(altar->pos.topLeft() + Point(150, 423), "", to);
 	}
 }

+ 3 - 2
client/windows/CAltarWindow.h

@@ -9,7 +9,8 @@
  */
 #pragma once
 
-#include "../widgets/CAltar.h"
+#include "../widgets/markets/CAltarArtifacts.h"
+#include "../widgets/markets/CAltarCreatures.h"
 #include "../widgets/CWindowWithArtifacts.h"
 #include "CWindowObject.h"
 
@@ -28,7 +29,7 @@ public:
 
 private:
 	const CGHeroInstance * hero;
-	std::shared_ptr<CAltar> altar;
+	std::shared_ptr<CExperienceAltar> altar;
 	std::shared_ptr<CButton> changeModeButton;
 	std::shared_ptr<CButton> quitButton;
 	std::function<void()> windowClosedCallback;

+ 122 - 234
client/windows/CTradeWindow.cpp

@@ -47,24 +47,24 @@ void CTradeWindow::initTypes()
 	switch(mode)
 	{
 	case EMarketMode::RESOURCE_RESOURCE:
-		itemsType[1] = RESOURCE;
-		itemsType[0] = RESOURCE;
+		itemsType[1] = EType::RESOURCE;
+		itemsType[0] = EType::RESOURCE;
 		break;
 	case EMarketMode::RESOURCE_PLAYER:
-		itemsType[1] = RESOURCE;
-		itemsType[0] = PLAYER;
+		itemsType[1] = EType::RESOURCE;
+		itemsType[0] = EType::PLAYER;
 		break;
 	case EMarketMode::CREATURE_RESOURCE:
-		itemsType[1] = CREATURE;
-		itemsType[0] = RESOURCE;
+		itemsType[1] = EType::CREATURE;
+		itemsType[0] = EType::RESOURCE;
 		break;
 	case EMarketMode::RESOURCE_ARTIFACT:
-		itemsType[1] = RESOURCE;
-		itemsType[0] = ARTIFACT_TYPE;
+		itemsType[1] = EType::RESOURCE;
+		itemsType[0] = EType::ARTIFACT_TYPE;
 		break;
 	case EMarketMode::ARTIFACT_RESOURCE:
-		itemsType[1] = ARTIFACT_INSTANCE;
-		itemsType[0] = RESOURCE;
+		itemsType[1] = EType::ARTIFACT_INSTANCE;
+		itemsType[0] = EType::RESOURCE;
 		break;
 	}
 }
@@ -73,215 +73,115 @@ void CTradeWindow::initItems(bool Left)
 {
 	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE);
 
-	if(Left && (itemsType[1] == ARTIFACT_TYPE || itemsType[1] == ARTIFACT_INSTANCE))
+	if(Left && (itemsType[1] == EType::ARTIFACT_TYPE || itemsType[1] == EType::ARTIFACT_INSTANCE))
 	{
 		if(mode == EMarketMode::ARTIFACT_RESOURCE)
 		{
-			auto item = std::make_shared<CTradeableItem>(Point(137, 469), itemsType[Left], -1, 1, 0);
+			auto item = std::make_shared<CTradeableItem>(Rect(Point(137, 469), Point()), itemsType[Left], -1, 1, 0);
 			item->recActions &= ~(UPDATE | SHOWALL);
 			items[Left].push_back(item);
 		}
 	}
 	else
 	{
-		std::vector<int> *ids = getItemsIds(Left);
-		std::vector<Rect> pos;
-		int amount = -1;
-
-		getPositionsFor(pos, Left, itemsType[Left]);
-
-		if(Left || !ids)
-			amount = 7;
-		else
-			amount = static_cast<int>(ids->size());
-
-		if(ids)
-			vstd::amin(amount, ids->size());
-
-		for(int j=0; j<amount; j++)
+		auto updRightSub = [this](EMarketMode marketMode)
 		{
-			int id = (ids && ids->size()>j) ? (*ids)[j] : j;
-			if(id < 0 && mode != EMarketMode::ARTIFACT_EXP)  //when sacrificing artifacts we need to prepare empty slots
-				continue;
-
-			auto item = std::make_shared<CTradeableItem>(pos[j].topLeft(), itemsType[Left], id, Left, j);
-			item->pos = pos[j] + this->pos.topLeft();
-			if(mode != EMarketMode::ARTIFACT_EXP)
-				item->clickPressedCallback = [this](std::shared_ptr<CTradeableItem> altarSlot) -> void
+			if(hLeft)
+				for(const auto & slot : rightTradePanel->slots)
 				{
-					if(altarSlot->left)
-					{
-						if(hLeft != altarSlot)
-							hLeft = altarSlot;
-						else
-							return;
-					}
-					else
-					{
-						if(hRight != altarSlot)
-							hRight = altarSlot;
-						else
-							return;
-					}
-					selectionChanged(altarSlot->left);
-				};
+					int h1, h2; //hlp variables for getting offer
+					market->getOffer(hLeft->id, slot->id, h1, h2, marketMode);
 
-			items[Left].push_back(item);
-		}
-		vstd::clear_pointer(ids);
-		initSubs(Left);
-	}
-}
+					rightTradePanel->updateOffer(*slot, h1, h2);
+				}
+			else
+				rightTradePanel->clearSubtitles();
+		};
 
-std::vector<int> *CTradeWindow::getItemsIds(bool Left)
-{
-	std::vector<int> *ids = nullptr;
+		auto clickPressedTradePanel = [this](const std::shared_ptr<CTradeableItem> & newSlot, bool left)
+		{
+			CTradeBase::onSlotClickPressed(newSlot, left ? hLeft : hRight);
+			selectionChanged(left);
+		};
 
-	if(Left)
-	{
-		switch(itemsType[1])
+		if(Left && mode == EMarketMode::CREATURE_RESOURCE)
 		{
-		case CREATURE:
-			ids = new std::vector<int>;
-			for(int i = 0; i < 7; i++)
+			CreaturesPanel::slotsData slots;
+			for(auto slotId = SlotID(0); slotId.num < GameConstants::ARMY_SIZE; slotId++)
 			{
-				if(const CCreature *c = hero->getCreature(SlotID(i)))
-					ids->push_back(c->getId());
-				else
-					ids->push_back(-1);
+				if(const auto & creature = hero->getCreature(slotId))
+					slots.emplace_back(std::make_tuple(creature->getId(), slotId, hero->getStackCount(slotId)));
 			}
-			break;
+			leftTradePanel = std::make_shared<CreaturesPanel>(std::bind(clickPressedTradePanel, _1, true), slots);
+			leftTradePanel->moveBy(Point(45, 123));
+			leftTradePanel->deleteSlotsCheck = [this](const std::shared_ptr<CTradeableItem> & slot)
+			{
+				return this->hero->getStackCount(SlotID(slot->serial)) == 0 ? true : false;
+			};
 		}
-	}
-	else
-	{
-		switch(itemsType[0])
+		else if(Left && (mode == EMarketMode::RESOURCE_RESOURCE || mode == EMarketMode::RESOURCE_ARTIFACT || mode == EMarketMode::RESOURCE_PLAYER))
 		{
-		case PLAYER:
-			ids = new std::vector<int>;
-			for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; i++)
-				if(PlayerColor(i) != LOCPLINT->playerID && LOCPLINT->cb->getPlayerStatus(PlayerColor(i)) == EPlayerStatus::INGAME)
-					ids->push_back(i);
-			break;
-
-		case ARTIFACT_TYPE:
-			ids = new std::vector<int>;
-			for (auto const & item : market->availableItemsIds(mode))
-				ids->push_back(item.getNum());
-			break;
+			leftTradePanel = std::make_shared<ResourcesPanel>(
+				[clickPressedTradePanel](const std::shared_ptr<CTradeableItem> & newSlot)
+				{
+					clickPressedTradePanel(newSlot, true);
+				},
+				[this]()
+				{
+					for(const auto & slot : leftTradePanel->slots)
+						slot->subtitle = std::to_string(LOCPLINT->cb->getResourceAmount(static_cast<EGameResID>(slot->serial)));
+				});
+			leftTradePanel->moveBy(Point(39, 182));
+			leftTradePanel->updateSlots();
+		}
+		else if(!Left && mode == EMarketMode::RESOURCE_RESOURCE)
+		{
+			rightTradePanel = std::make_shared<ResourcesPanel>(
+				[clickPressedTradePanel](const std::shared_ptr<CTradeableItem> & newSlot)
+				{
+					clickPressedTradePanel(newSlot, false);
+				},
+				[this, updRightSub]()
+				{
+					updRightSub(EMarketMode::RESOURCE_RESOURCE);
+					if(hLeft)
+						rightTradePanel->slots[hLeft->serial]->subtitle = CGI->generaltexth->allTexts[164]; // n/a
+				});
+			rightTradePanel->moveBy(Point(327, 181));
+		}
+		else if(!Left && (mode == EMarketMode::ARTIFACT_RESOURCE || mode == EMarketMode::CREATURE_RESOURCE))
+		{
+			rightTradePanel = std::make_shared<ResourcesPanel>(std::bind(clickPressedTradePanel, _1, false),
+				std::bind(updRightSub, EMarketMode::ARTIFACT_RESOURCE));
+			rightTradePanel->moveBy(Point(327, 181));
+		}
+		else if(!Left && mode == EMarketMode::RESOURCE_ARTIFACT)
+		{
+			rightTradePanel = std::make_shared<ArtifactsPanel>(std::bind(clickPressedTradePanel, _1, false),
+				std::bind(updRightSub, EMarketMode::RESOURCE_ARTIFACT), market->availableItemsIds(mode));
+			rightTradePanel->moveBy(Point(327, 181));
+			rightTradePanel->deleteSlotsCheck = [this](const std::shared_ptr<CTradeableItem> & slot)
+			{
+				return vstd::contains(market->availableItemsIds(EMarketMode::RESOURCE_ARTIFACT), ArtifactID(slot->id)) ? false : true;
+			};
+		}
+		else if(!Left && mode == EMarketMode::RESOURCE_PLAYER)
+		{
+			rightTradePanel = std::make_shared<PlayersPanel>(std::bind(clickPressedTradePanel, _1, false));
+			rightTradePanel->moveBy(Point(333, 83));
 		}
-	}
-
-	return ids;
-}
-
-void CTradeWindow::getPositionsFor(std::vector<Rect> &poss, bool Left, EType type) const
-{
-	//seven boxes:
-	//  X  X  X
-	//  X  X  X
-	//     X
-	int h = 0, w = 0, x = 0, y = 0, dx = 0, dy = 0;
-
-	switch(type)
-	{
-	case RESOURCE:
-		dx = 82;
-		dy = 79;
-		x = 39;
-		y = 180;
-		h = 68;
-		w = 70;
-		break;
-	case PLAYER:
-		dx = 83;
-		dy = 118;
-		h = 64;
-		w = 58;
-		x = 44;
-		y = 83;
-		assert(!Left);
-		break;
-	case CREATURE://45,123
-		x = 45;
-		y = 123;
-		w = 58;
-		h = 64;
-		dx = 83;
-		dy = 98;
-		assert(Left);
-		break;
-	case ARTIFACT_TYPE://45,123
-		x = 340 - 289;
-		y = 180;
-		w = 44;
-		h = 44;
-		dx = 83;
-		dy = 79;
-		break;
-	}
-	int leftToRightOffset = 289;
-
-	const std::vector<Rect> tmp =
-	{
-		Rect(Point(x + 0 * dx, y + 0 * dx), Point(w, h) ),
-		Rect(Point(x + 1 * dx, y + 0 * dx), Point(w, h) ),
-		Rect(Point(x + 2 * dx, y + 0 * dx), Point(w, h) ),
-		Rect(Point(x + 0 * dx, y + 1 * dy), Point(w, h) ),
-		Rect(Point(x + 1 * dx, y + 1 * dy), Point(w, h) ),
-		Rect(Point(x + 2 * dx, y + 1 * dy), Point(w, h) ),
-		Rect(Point(x + 1 * dx, y + 2 * dy), Point(w, h) )
-	};
-
-	vstd::concatenate(poss, tmp);
-
-	if(!Left)
-	{
-		for(Rect &r : poss)
-			r.x += leftToRightOffset;
 	}
 }
 
 void CTradeWindow::initSubs(bool Left)
 {
-	for(auto item : items[Left])
-	{
+	if(itemsType[Left] == EType::RESOURCE || itemsType[Left] == EType::ARTIFACT_TYPE)
+	{ 
 		if(Left)
-		{
-			switch(itemsType[1])
-			{
-			case CREATURE:
-				item->subtitle = std::to_string(hero->getStackCount(SlotID(item->serial)));
-				break;
-			case RESOURCE:
-				item->subtitle = std::to_string(LOCPLINT->cb->getResourceAmount(static_cast<EGameResID>(item->serial)));
-				break;
-			}
-		}
-		else //right side
-		{
-			if(itemsType[0] == PLAYER)
-			{
-				item->subtitle = CGI->generaltexth->capColors[item->id];
-			}
-			else if(hLeft)//artifact, creature
-			{
-				int h1, h2; //hlp variables for getting offer
-				market->getOffer(hLeft->id, item->id, h1, h2, mode);
-				if(item->id != hLeft->id || mode != EMarketMode::RESOURCE_RESOURCE) //don't allow exchanging same resources
-				{
-					std::ostringstream oss;
-					oss << h2;
-					if(h1!=1)
-						oss << "/" << h1;
-					item->subtitle = oss.str();
-				}
-				else
-					item->subtitle = CGI->generaltexth->allTexts[164]; // n/a
-			}
-			else
-				item->subtitle = "";
-		}
+			leftTradePanel->updateSlots();
+		else
+			rightTradePanel->updateSlots();
+		return;
 	}
 }
 
@@ -289,11 +189,6 @@ void CTradeWindow::showAll(Canvas & to)
 {
 	CWindowObject::showAll(to);
 
-	if(hRight)
-		to.drawBorder(Rect::createAround(hRight->pos, 1), Colors::BRIGHT_YELLOW, 2);
-	if(hLeft && hLeft->type != ARTIFACT_INSTANCE)
-		to.drawBorder(Rect::createAround(hLeft->pos, 1), Colors::BRIGHT_YELLOW, 2);
-
 	if(readyToTrade)
 	{
 		if(hLeft)
@@ -548,6 +443,10 @@ void CMarketplaceWindow::makeDeal()
 	madeTransaction = true;
 	hLeft = nullptr;
 	hRight = nullptr;
+	if(leftTradePanel)
+		leftTradePanel->deselect();
+	assert(rightTradePanel);
+	rightTradePanel->deselect();
 	selectionChanged(true);
 }
 
@@ -573,9 +472,9 @@ void CMarketplaceWindow::selectionChanged(bool side)
 		if(slider)
 		{
 			int newAmount = -1;
-			if(itemsType[1] == RESOURCE)
+			if(itemsType[1] == EType::RESOURCE)
 				newAmount = LOCPLINT->cb->getResourceAmount(static_cast<EGameResID>(soldItemId));
-			else if(itemsType[1] ==  CREATURE)
+			else if(itemsType[1] == EType::CREATURE)
 				newAmount = hero->getStackCount(SlotID(hLeft->serial)) - (hero->stacksCount() == 1  &&  hero->needsLastStack());
 			else
 				assert(0);
@@ -585,7 +484,7 @@ void CMarketplaceWindow::selectionChanged(bool side)
 			max->block(false);
 			deal->block(false);
 		}
-		else if(itemsType[1] == RESOURCE) //buying -> check if we can afford transaction
+		else if(itemsType[1] == EType::RESOURCE) //buying -> check if we can afford transaction
 		{
 			deal->block(LOCPLINT->cb->getResourceAmount(static_cast<EGameResID>(soldItemId)) < r1);
 		}
@@ -603,7 +502,7 @@ void CMarketplaceWindow::selectionChanged(bool side)
 		deal->block(true);
 	}
 
-	if(side && itemsType[0] != PLAYER) //items[1] selection changed, recalculate offers
+	if(side && itemsType[0] != EType::PLAYER) //items[1] selection changed, recalculate offers
 		initSubs(false);
 
 	updateTraderText();
@@ -638,10 +537,8 @@ void CMarketplaceWindow::updateGarrison()
 	if(mode != EMarketMode::CREATURE_RESOURCE)
 		return;
 
-	std::set<std::shared_ptr<CTradeableItem>> toRemove;
-	getEmptySlots(toRemove);
-	removeItems(toRemove);
-	initSubs(true);
+	leftTradePanel->deleteSlots();
+	leftTradePanel->updateSlots();
 }
 
 void CMarketplaceWindow::artifactsChanged(bool Left)
@@ -649,17 +546,8 @@ void CMarketplaceWindow::artifactsChanged(bool Left)
 	assert(!Left);
 	if(mode != EMarketMode::RESOURCE_ARTIFACT)
 		return;
-
-	std::vector<TradeItemBuy> available = market->availableItemsIds(mode);
-	std::set<std::shared_ptr<CTradeableItem>> toRemove;
-	for(auto item : items[0])
-		if(!vstd::contains(available, ArtifactID(item->id)))
-			toRemove.insert(item);
-
-	removeItems(toRemove);
-
-	// clear set to erase final instance of shared_ptr - we want to redraw screen only after it has been deleted
-	toRemove.clear();
+	
+	rightTradePanel->deleteSlots();
 	redraw();
 }
 
@@ -669,8 +557,8 @@ std::string CMarketplaceWindow::updateSlotSubtitle(bool Left) const
 	{
 		switch(itemsType[1])
 		{
-		case RESOURCE:
-		case CREATURE:
+		case EType::RESOURCE:
+		case EType::CREATURE:
 			{
 				int val = slider
 					? slider->getValue() * r1
@@ -678,7 +566,7 @@ std::string CMarketplaceWindow::updateSlotSubtitle(bool Left) const
 
 				return std::to_string(val);
 			}
-		case ARTIFACT_INSTANCE:
+		case EType::ARTIFACT_INSTANCE:
 			return ((deal->isBlocked()) ? "0" : "1");
 		}
 	}
@@ -686,14 +574,14 @@ std::string CMarketplaceWindow::updateSlotSubtitle(bool Left) const
 	{
 		switch(itemsType[0])
 		{
-		case RESOURCE:
+		case EType::RESOURCE:
 			if(slider)
 				return std::to_string( slider->getValue() * r2 );
 			else
 				return std::to_string(r2);
-		case ARTIFACT_TYPE:
+		case EType::ARTIFACT_TYPE:
 			return ((deal->isBlocked()) ? "0" : "1");
-		case PLAYER:
+		case EType::PLAYER:
 			return (hRight ? CGI->generaltexth->capColors[hRight->id] : "");
 		}
 	}
@@ -707,26 +595,26 @@ Point CMarketplaceWindow::selectionOffset(bool Left) const
 	{
 		switch(itemsType[1])
 		{
-		case RESOURCE:
-			return Point(122, 446);
-		case CREATURE:
+		case EType::RESOURCE:
+			return Point(122, 448);
+		case EType::CREATURE:
 			return Point(128, 450);
-		case ARTIFACT_INSTANCE:
-			return Point(134, 466);
+		case EType::ARTIFACT_INSTANCE:
+			return Point(134, 469);
 		}
 	}
 	else
 	{
 		switch(itemsType[0])
 		{
-		case RESOURCE:
+		case EType::RESOURCE:
 			if(mode == EMarketMode::ARTIFACT_RESOURCE)
-				return Point(410, 469);
+				return Point(410, 471);
 			else
-				return Point(410, 446);
-		case ARTIFACT_TYPE:
-			return Point(425, 447);
-		case PLAYER:
+				return Point(410, 448);
+		case EType::ARTIFACT_TYPE:
+			return Point(411, 449);
+		case EType::PLAYER:
 			return Point(417, 451);
 		}
 	}

+ 1 - 3
client/windows/CTradeWindow.h

@@ -9,7 +9,7 @@
  */
 #pragma once
 
-#include "../widgets/CTradeBase.h"
+#include "../widgets/markets/CTradeBase.h"
 #include "../widgets/CWindowWithArtifacts.h"
 #include "CWindowObject.h"
 
@@ -36,8 +36,6 @@ public:
 	void initSubs(bool Left);
 	void initTypes();
 	void initItems(bool Left);
-	std::vector<int> *getItemsIds(bool Left); //nullptr if default
-	void getPositionsFor(std::vector<Rect> &poss, bool Left, EType type) const;
 	void setMode(EMarketMode Mode); //mode setter
 
 	void artifactSelected(CArtPlace * slot); //used when selling artifacts -> called when user clicked on artifact slot