Browse Source

new layout

Laserlicht 1 month ago
parent
commit
c71e671aab

+ 1 - 0
Mods/vcmi/Content/config/english.json

@@ -144,6 +144,7 @@
 	"vcmi.lobby.battleOnlyModeHeroSelect" : "Select Hero",
 	"vcmi.lobby.battleOnlyModeCreatureSelect" : "Select Creature",
 	"vcmi.lobby.battleOnlyModeSelect" : "Select",
+	"vcmi.lobby.battleOnlyModeReset" : "Reset",
 
 	"vcmi.broadcast.failedLoadGame" : "Failed to load game",
 	"vcmi.broadcast.command" : "Use '!help' to list available commands",

+ 1 - 0
Mods/vcmi/Content/config/german.json

@@ -144,6 +144,7 @@
 	"vcmi.lobby.battleOnlyModeHeroSelect" : "Helden auswählen",
 	"vcmi.lobby.battleOnlyModeCreatureSelect" : "Kreatur auswählen",
 	"vcmi.lobby.battleOnlyModeSelect" : "Wählen",
+	"vcmi.lobby.battleOnlyModeReset" : "Zurücksetzen",
 
 	"vcmi.broadcast.failedLoadGame" : "Spiel konnte nicht geladen werden",
 	"vcmi.broadcast.command" : "Benutze '!help' um alle verfügbaren Befehle aufzulisten",

+ 44 - 44
client/lobby/BattleOnlyMode.cpp

@@ -66,8 +66,8 @@ BattleOnlyModeWindow::BattleOnlyModeWindow()
 {
 	OBJECT_CONSTRUCTION;
 
-	pos.w = 700;
-	pos.h = 330;
+	pos.w = 519;
+	pos.h = 238;
 
 	updateShadow();
 	center();
@@ -76,11 +76,11 @@ BattleOnlyModeWindow::BattleOnlyModeWindow()
 
 	backgroundTexture = std::make_shared<FilledTexturePlayerColored>(Rect(0, 0, pos.w, pos.h));
 	backgroundTexture->setPlayerColor(PlayerColor(1));
-	buttonOk = std::make_shared<CButton>(Point(281, 288), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ startBattle(); }, EShortcut::GLOBAL_ACCEPT);
-	buttonAbort = std::make_shared<CButton>(Point(355, 288), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ close(); }, EShortcut::GLOBAL_CANCEL);
-	title = std::make_shared<CLabel>(350, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyMode"));
+	buttonOk = std::make_shared<CButton>(Point(191, 203), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ startBattle(); }, EShortcut::GLOBAL_ACCEPT);
+	buttonAbort = std::make_shared<CButton>(Point(265, 203), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ close(); }, EShortcut::GLOBAL_CANCEL);
+	title = std::make_shared<CLabel>(260, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyMode"));
 
-	battlegroundSelector = std::make_shared<CButton>(Point(10, 294), AnimationPath::builtin("GSPButtonClear"), CButton::tooltip(), [this](){
+	battlegroundSelector = std::make_shared<CButton>(Point(29, 174), AnimationPath::builtin("GSPButtonClear"), CButton::tooltip(), [this](){
 		std::vector<std::string> texts;
 		std::vector<std::shared_ptr<IImage>> images;
 
@@ -127,12 +127,33 @@ BattleOnlyModeWindow::BattleOnlyModeWindow()
 			setOkButtonEnabled();
 		}, (selectedTerrain != TerrainId::NONE ? static_cast<int>(selectedTerrain) : static_cast<int>(selectedTown + terrains.size())), images, true, true);
 	});
+	buttonReset = std::make_shared<CButton>(Point(289, 174), AnimationPath::builtin("GSPButtonClear"), CButton::tooltip(), [this](){
+		selectedTerrain = TerrainId::DIRT;
+		selectedTown = FactionID::NONE;
+		setTerrainButtonText();
+		setOkButtonEnabled();
+		heroSelector1->selectedHero.reset();
+		heroSelector2->selectedHero.reset();
+		heroSelector1->selectedArmy->clearSlots();
+		heroSelector2->selectedArmy->clearSlots();
+		for(size_t i=0; i<GameConstants::ARMY_SIZE; i++)
+		{
+			heroSelector1->selectedArmyInput.at(i)->setText("0");
+			heroSelector2->selectedArmyInput.at(i)->setText("0");
+		}
+		heroSelector1->setHeroIcon();
+	    heroSelector1->setCreatureIcons();
+		heroSelector2->setHeroIcon();
+	    heroSelector2->setCreatureIcons();
+		redraw();
+	});
+	buttonReset->setTextOverlay(LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeReset"), EFonts::FONT_SMALL, Colors::WHITE);
 
 	setTerrainButtonText();
 	setOkButtonEnabled();
 
 	heroSelector1 = std::make_shared<BattleOnlyModeHeroSelector>(*this, Point(0, 40));
-	heroSelector2 = std::make_shared<BattleOnlyModeHeroSelector>(*this, Point(0, 160));
+	heroSelector2 = std::make_shared<BattleOnlyModeHeroSelector>(*this, Point(260, 40));
 }
 
 void BattleOnlyModeWindow::init()
@@ -178,7 +199,7 @@ BattleOnlyModeHeroSelector::BattleOnlyModeHeroSelector(BattleOnlyModeWindow& par
 	pos.x += position.x;
 	pos.y += position.y;
 
-	backgroundImage = std::make_shared<CPicture>(ImagePath::builtin("heroSlotsBlue"), Point(0, 0));
+	backgroundImage = std::make_shared<CPicture>(ImagePath::builtin("heroSlotsBlue"), Point(3, 4));
 
 	for(size_t i=0; i<GameConstants::PRIMARY_SKILLS; i++)
 	{
@@ -192,7 +213,12 @@ BattleOnlyModeHeroSelector::BattleOnlyModeHeroSelector(BattleOnlyModeWindow& par
 	}
 
 	creatureImage.resize(GameConstants::ARMY_SIZE);
-	creatureImageLabel.resize(GameConstants::ARMY_SIZE);
+	for(size_t i=0; i<GameConstants::ARMY_SIZE; i++)
+	{
+		selectedArmyInput.push_back(std::make_shared<CTextInput>(Rect(5 + i * 36, 113, 32, 16), EFonts::FONT_SMALL, ETextAlignment::CENTER, false));
+		selectedArmyInput.back()->setFilterNumber(0, 10000000, 3);
+		selectedArmyInput.back()->setText("0");
+	}
 
 	setHeroIcon();
 	setCreatureIcons();
@@ -287,15 +313,11 @@ void BattleOnlyModeHeroSelector::setCreatureIcons()
 	for(int i = 0; i < creatureImage.size(); i++)
 	{
 		if(selectedArmy->slotEmpty(SlotID(i)))
-		{
 			creatureImage[i] = std::make_shared<CPicture>(drawBlackBox(Point(32, 32), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSelect")), Point(6 + i * 36, 78));
-			creatureImageLabel[i].reset();
-		}
 		else
 		{
 			auto creatureID = selectedArmy->Slots().at(SlotID(i))->getCreatureID();
 			creatureImage[i] = std::make_shared<CPicture>(ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("CPRSMALL"), EImageBlitMode::COLORKEY)->getImage(LIBRARY->creh->objects.at(creatureID)->getIconIndex()), Point(6 + i * 36, 78));
-			creatureImageLabel[i] = std::make_shared<CLabel>(36 + i * 36, 112, FONT_SMALL, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, TextOperations::formatMetric(selectedArmy->Slots().at(SlotID(i))->getCount(), 4));
 		}
 
 		creatureImage[i]->addLClickCallback([this, i](){
@@ -338,11 +360,9 @@ void BattleOnlyModeHeroSelector::setCreatureIcons()
 				}
 				index--;
 				auto creature = creatures[index];
-				int amount = selectedArmy->slotEmpty(SlotID(i)) ? 1 : selectedArmy->Slots().at(SlotID(i))->getCount();
-				ENGINE->windows().createAndPushWindow<NumberInputWindow>(amount, [this, creature, i](int quantity){
-					selectedArmy->setCreature(SlotID(i), creature->getId(), quantity);
-					setCreatureIcons();
-				});
+				selectedArmy->setCreature(SlotID(i), creature->getId(), 100);
+				selectedArmyInput[SlotID(i)]->setText("100");
+				setCreatureIcons();
 			}, selectedIndex, images, true, true);
 			window->onPopup = [creatures](int index) {
 				if(index == 0)
@@ -388,7 +408,10 @@ void BattleOnlyModeWindow::startBattle()
 		selector->selectedHero->clearSlots();
 		for(int slot = 0; slot < GameConstants::ARMY_SIZE; slot++)
 			if(!selector->selectedArmy->slotEmpty(SlotID(slot)))
+			{
 				selector->selectedHero->putStack(SlotID(slot), selector->selectedArmy->detachStack(SlotID(slot)));
+				selector->selectedHero->getArmy()->setStackCount(SlotID(slot), TextOperations::parseMetric<int>(selector->selectedArmyInput[slot]->getText()));
+			}
 		map->getEditManager()->insertObject(selector->selectedHero);
 	};
 
@@ -409,7 +432,10 @@ void BattleOnlyModeWindow::startBattle()
 		{
 			for(int slot = 0; slot < GameConstants::ARMY_SIZE; slot++)
 				if(!heroSelector2->selectedArmy->slotEmpty(SlotID(slot)))
+				{
 					townObj->getArmy()->putStack(SlotID(slot), heroSelector2->selectedArmy->detachStack(SlotID(slot)));
+					townObj->getArmy()->setStackCount(SlotID(slot), TextOperations::parseMetric<int>(heroSelector2->selectedArmyInput[slot]->getText()));
+				}
 		}
 		else
 			addHero(heroSelector2, PlayerColor(1), int3(5, 5, 0));
@@ -433,29 +459,3 @@ void BattleOnlyModeWindow::startBattle()
 	GAME->server().setExtraOptionsInfo(extraOptions);
 	GAME->server().sendStartGame();
 }
-
-NumberInputWindow::NumberInputWindow(int initialValue, std::function<void(int)> onValueSelected)
-	: CWindowObject(BORDERED)
-{
-	OBJECT_CONSTRUCTION;
-
-	pos.w = 200;
-	pos.h = 100;
-	center();
-
-	backgroundTexture = std::make_shared<FilledTexturePlayerColored>(Rect(0, 0, pos.w, pos.h));
-	backgroundTexture->setPlayerColor(PlayerColor(1));
-
-	buttonOk = std::make_shared<CButton>(Point(68, 60), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this, onValueSelected](){
-		int value = std::stoi(input->getText());
-		if(onValueSelected)
-			onValueSelected(value);
-		close();
-	}, EShortcut::GLOBAL_ACCEPT);
-
-	backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(50, 20, 100, 20), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1);
-	input = std::make_shared<CTextInput>(Rect(50, 20, 100, 20), EFonts::FONT_MEDIUM, ETextAlignment::CENTER, false);
-	input->setFilterNumber(1, 99999999);
-	input->setText(std::to_string(initialValue));
-	input->giveFocus();
-}

+ 4 - 14
client/lobby/BattleOnlyMode.h

@@ -36,16 +36,6 @@ public:
 	static void openBattleWindow();
 };
 
-class NumberInputWindow : public CWindowObject
-{
-	std::shared_ptr<FilledTexturePlayerColored> backgroundTexture;
-	std::shared_ptr<TransparentFilledRectangle> backgroundOverlay;
-	std::shared_ptr<CButton> buttonOk;
-	std::shared_ptr<CTextInput> input;
-public:
-	NumberInputWindow(int initialValue, std::function<void(int)> onValueSelected);
-};
-
 class BattleOnlyModeHeroSelector : public CIntObject
 {
 private:
@@ -55,10 +45,6 @@ private:
 	std::shared_ptr<CPicture> heroImage;
 	std::shared_ptr<CLabel> heroLabel;
 	std::vector<std::shared_ptr<CPicture>> creatureImage;
-	std::vector<std::shared_ptr<CLabel>> creatureImageLabel;
-
-	void setHeroIcon();
-	void setCreatureIcons();
 public:
 	std::vector<std::shared_ptr<CAnimImage>> primSkills;
 	std::vector<std::shared_ptr<GraphicalPrimitiveCanvas>> primSkillsBorder;
@@ -66,7 +52,10 @@ public:
 
 	std::shared_ptr<CGHeroInstance> selectedHero;
 	std::shared_ptr<CCreatureSet> selectedArmy;
+	std::vector<std::shared_ptr<CTextInput>> selectedArmyInput;
 
+	void setHeroIcon();
+	void setCreatureIcons();
 	BattleOnlyModeHeroSelector(BattleOnlyModeWindow& parent, Point position);
 };
 
@@ -83,6 +72,7 @@ private:
 	std::shared_ptr<CLabel> title;
 
 	std::shared_ptr<CButton> battlegroundSelector;
+	std::shared_ptr<CButton> buttonReset;
 	std::shared_ptr<BattleOnlyModeHeroSelector> heroSelector1;
 	std::shared_ptr<BattleOnlyModeHeroSelector> heroSelector2;
 

+ 4 - 2
client/render/AssetGenerator.cpp

@@ -953,9 +953,11 @@ AssetGenerator::CanvasPtr AssetGenerator::createHeroSlotsColored(PlayerColor bac
 	static const std::array<ColorFilter, PlayerColor::PLAYER_LIMIT_I> filters = getColorFilters();
 	img->adjustPalette(filters[backColor.getNum()], 0);
 
-	auto image = ENGINE->renderHandler().createImage(img->dimensions(), CanvasScalingPolicy::IGNORE);
+	auto image = ENGINE->renderHandler().createImage(Point(260, 150), CanvasScalingPolicy::IGNORE);
 	Canvas canvas = image->getCanvas();
-	canvas.draw(img, Point(0, 0));
+	canvas.draw(img, Point(0, 0), Rect(3, 4, 253, 107));
+	for(int i = 0; i<7; i++)
+		canvas.draw(img, Point(1 + i * 36, 108), Rect(76, 57, 35, 17));
 
 	return image;
 }

+ 59 - 32
client/widgets/CTextInput.cpp

@@ -121,9 +121,9 @@ void CTextInput::setFilterFilename()
 	onTextFiltering = std::bind(&CTextInput::filenameFilter, _1, _2);
 }
 
-void CTextInput::setFilterNumber(int minValue, int maxValue)
+void CTextInput::setFilterNumber(int minValue, int maxValue, int metricDigits)
 {
-	onTextFiltering = std::bind(&CTextInput::numberFilter, _1, _2, minValue, maxValue);
+	onTextFiltering = std::bind(&CTextInput::numberFilter, _1, _2, minValue, maxValue, metricDigits);
 }
 
 std::string CTextInput::getVisibleText() const
@@ -177,6 +177,10 @@ void CTextInput::keyPressed(EShortcut key)
 
 	if(redrawNeeded)
 	{
+		std::string oldText = currentText;
+		if(onTextFiltering)
+			onTextFiltering(currentText, oldText);
+
 		updateLabel();
 		if(onTextEdited)
 			onTextEdited(currentText);
@@ -216,9 +220,9 @@ void CTextInput::textInputted(const std::string & enteredText)
 	if(onTextFiltering)
 		onTextFiltering(currentText, oldText);
 
+	updateLabel();
 	if(currentText != oldText)
 	{
-		updateLabel();
 		if(onTextEdited)
 			onTextEdited(currentText);
 	}
@@ -242,40 +246,63 @@ void CTextInput::filenameFilter(std::string & text, const std::string &oldText)
 		text.erase(pos, 1);
 }
 
-void CTextInput::numberFilter(std::string & text, const std::string & oldText, int minValue, int maxValue)
+std::optional<char> getMetricSuffix(const std::string& text)
+{
+	const std::string suffixes = "kKmMgGtTpPeE";
+	std::vector<char> found;
+
+	// Collect all suffixes in the string
+	for (char c : text) {
+		if (suffixes.find(c) != std::string::npos) {
+			// Normalize: 'k' lowercase, others uppercase
+			found.push_back((c == 'k' || c == 'K') ? 'k' : static_cast<char>(std::toupper(c)));
+		}
+	}
+
+	if (found.empty()) return std::nullopt;            // No suffix
+	if (found.size() == 1) return found[0];           // Single suffix
+	// More than one suffix
+	bool allSame = std::all_of(found.begin(), found.end(), [&](char c){ return c == found[0]; });
+	if (allSame) return std::nullopt;                 // Multiple but identical → nullopt
+	return found.back();                               // Multiple different → last suffix
+}
+
+
+void CTextInput::numberFilter(std::string & text, const std::string & oldText, int minValue, int maxValue, int metricDigits)
 {
 	assert(minValue < maxValue);
 
-	if(text.empty())
-		text = "0";
+	bool isNegative = std::count_if(text.begin(), text.end(), [](char c){ return c == '-'; }) == 1 && minValue < 0;
+	auto suffix = getMetricSuffix(text);
+	if(metricDigits == 0)
+		suffix = std::nullopt;
 
-	size_t pos = 0;
-	if(text[0] == '-') //allow '-' sign as first symbol only
-		pos++;
+	// Remove all non-digit characters
+	text.erase(std::remove_if(text.begin(), text.end(), [](char c){ return !isdigit(c); }), text.end());
 
-	while(pos < text.size())
-	{
-		if(text[pos] < '0' || text[pos] > '9')
-		{
-			text = oldText;
-			return; //new text is not number.
-		}
-		pos++;
-	}
-	try
-	{
-		int value = boost::lexical_cast<int>(text);
-		if(value < minValue)
-			text = std::to_string(minValue);
-		else if(value > maxValue)
-			text = std::to_string(maxValue);
-	}
-	catch(boost::bad_lexical_cast &)
-	{
-		//Should never happen. Unless I missed some cases
-		logGlobal->warn("Warning: failed to convert %s to number!", text);
-		text = oldText;
-	}
+	// Remove leading zeros
+	size_t firstNonZero = text.find_first_not_of('0');
+	if (firstNonZero > 0)
+		text.erase(0, firstNonZero);
+
+	if (text.empty())
+		text = "0";
+
+	// Add negative sign
+	text = (isNegative ? "-" : "") + text;
+
+	// Restore suffix if it exists
+	if (suffix)
+		text += *suffix;
+
+	// Clamp value
+	int value = TextOperations::parseMetric<int>(text);
+	if (metricDigits)
+		text = (isNegative && value == 0 ? "-" : "") + TextOperations::formatMetric(value, metricDigits);
+	if (value < minValue)
+		text = metricDigits ? TextOperations::formatMetric(minValue, metricDigits) : std::to_string(minValue);
+	else if (value > maxValue)
+		text = metricDigits ? TextOperations::formatMetric(maxValue, metricDigits) : std::to_string(maxValue);
 }
 
 void CTextInput::activate()

+ 2 - 2
client/widgets/CTextInput.h

@@ -65,7 +65,7 @@ class CTextInput final : public CFocusable
 	static void filenameFilter(std::string & text, const std::string & oldText);
 	//Filter that will allow only input of numbers in range min-max (min-max are allowed)
 	//min-max should be set via something like std::bind
-	static void numberFilter(std::string & text, const std::string & oldText, int minValue, int maxValue);
+	static void numberFilter(std::string & text, const std::string & oldText, int minValue, int maxValue, int metricDigits);
 
 	std::string getVisibleText() const;
 	void createLabel(bool giveFocusToInput);
@@ -98,7 +98,7 @@ public:
 	/// Enables filtering entered text that ensures that text is valid filename (existing or not)
 	void setFilterFilename();
 	/// Enable filtering entered text that ensures that text is valid number in provided range [min, max]
-	void setFilterNumber(int minValue, int maxValue);
+	void setFilterNumber(int minValue, int maxValue, int metricDigits=0);
 
 	void setFont(EFonts Font);
 	void setColor(const ColorRGBA & Color);

+ 68 - 0
lib/texts/TextOperations.h

@@ -9,6 +9,8 @@
  */
 #pragma once
 
+#include <boost/lexical_cast.hpp>
+
 VCMI_LIB_NAMESPACE_BEGIN
 
 /// Namespace that provides utilities for unicode support (UTF-8)
@@ -54,6 +56,9 @@ namespace TextOperations
 	template<typename Arithmetic>
 	inline std::string formatMetric(Arithmetic number, int maxDigits);
 
+	template<typename Arithmetic>
+	inline Arithmetic parseMetric(const std::string &text);
+
 	/// replaces all symbols that normally need escaping with appropriate escape sequences
 	std::string escapeString(std::string input);
 
@@ -116,4 +121,67 @@ inline std::string TextOperations::formatMetric(Arithmetic number, int maxDigits
 	return std::to_string(number) + *iter;
 }
 
+template<typename Arithmetic>
+inline Arithmetic TextOperations::parseMetric(const std::string &text)
+{
+	static const std::string symbols = " kMGTPE";
+	if (text.empty())
+		return 0;
+
+	// Trim whitespace
+	std::string trimmed = text;
+	trimmed.erase(trimmed.begin(), std::find_if(trimmed.begin(), trimmed.end(), [](unsigned char ch){ return !std::isspace(ch); }));
+	trimmed.erase(std::find_if(trimmed.rbegin(), trimmed.rend(), [](unsigned char ch){ return !std::isspace(ch); }).base(), trimmed.end());
+
+	// Check if last character is a metric suffix
+	char last = trimmed.back();
+	int power = 0; // number of *1000 multiplications
+
+	switch (std::toupper(last))
+	{
+		case 'K': power = 1; break;
+		case 'M': power = 2; break;
+		case 'G': power = 3; break;
+		case 'T': power = 4; break;
+		case 'P': power = 5; break;
+		case 'E': power = 6; break;
+		default: power = 0; break; // no suffix
+	}
+
+	std::string numberPart = trimmed;
+	if (power > 0)
+		numberPart.pop_back();
+
+	// Remove any non-digit or minus sign (same spirit as your numberFilter)
+	numberPart.erase(std::remove_if(numberPart.begin(), numberPart.end(), [](char c)
+	{
+		return !(std::isdigit(static_cast<unsigned char>(c)) || c == '-');
+	}), numberPart.end());
+
+	if (numberPart.empty() || (numberPart == "-"))
+		return 0;
+
+	try
+	{
+		Arithmetic value = boost::lexical_cast<Arithmetic>(numberPart);
+
+		for (int i = 0; i < power; ++i)
+		{
+			// Multiply by 1000, check for overflow if desired
+			if (value > std::numeric_limits<Arithmetic>::max() / 1000)
+				return std::numeric_limits<Arithmetic>::max();
+			if (value < std::numeric_limits<Arithmetic>::min() / 1000)
+				return std::numeric_limits<Arithmetic>::min();
+
+			value *= static_cast<Arithmetic>(1000);
+		}
+
+		return value;
+	}
+	catch (boost::bad_lexical_cast &)
+	{
+		return 0;
+	}
+}
+
 VCMI_LIB_NAMESPACE_END