瀏覽代碼

Merge pull request #3166 from IvanSavenko/simturns_ui

UI for Simturns
Ivan Savenko 1 年之前
父節點
當前提交
a9f868b379

+ 8 - 0
Mods/vcmi/Sprites/lobby/checkbox.json

@@ -0,0 +1,8 @@
+{
+	"basepath" : "lobby/",
+	"images" :
+	[
+		{ "frame" : 0, "file" : "checkboxBlueOff.png"},
+		{ "frame" : 1, "file" : "checkboxBlueOn.png"}
+	]
+}

二進制
Mods/vcmi/Sprites/lobby/checkboxBlueOff.png


二進制
Mods/vcmi/Sprites/lobby/checkboxBlueOn.png


二進制
Mods/vcmi/Sprites/lobby/checkboxOff.png


二進制
Mods/vcmi/Sprites/lobby/checkboxOn.png


+ 8 - 8
Mods/vcmi/config/vcmi/chinese.json

@@ -202,14 +202,14 @@
 	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "同盟关系",
 	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "同盟关系",
 	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "道路类型",
 	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "道路类型",
 
 
-	"vcmi.optionsTab.widgets.chessFieldBase.help" : "{额外计时器}\n\n当{转动计时器}达到零时开始倒计时。 它仅在游戏开始时设置一次。 当计时器达到零时,玩家的回合结束。",
-	"vcmi.optionsTab.widgets.chessFieldTurn.help" : "{转动计时器}\n\n当玩家在冒险地图上开始回合时开始倒计时。 它在每回合开始时重置为其初始值。 任何未使用的回合时间将被添加到{额外计时器}(如果正在使用)中。",
-	"vcmi.optionsTab.widgets.chessFieldBattle.help" : "{战斗计时器}\n\n战斗期间当 {堆栈计时器} 达到0时进行倒计时。 每次战斗开始时都会重置为初始值。 如果计时器达到零,当前活动的堆栈将进行防御。",
-	"vcmi.optionsTab.widgets.chessFieldCreature.help" : "{堆栈计时器}\n\n当玩家在战斗中为当前堆栈选择一个动作时开始倒计时。 堆栈操作完成后,它会重置为其初始值。",
-	"vcmi.optionsTab.widgets.labelTimer" : "计时器",
-	"vcmi.optionsTab.widgets.timerModeSwitch.classic" : "经典计时器",
-	"vcmi.optionsTab.widgets.timerModeSwitch.chess" : "国际象棋计时器",
-
+	"vcmi.optionsTab.chessFieldBase.hover" : "额外计时器",
+	"vcmi.optionsTab.chessFieldTurn.hover" : "转动计时器",
+	"vcmi.optionsTab.chessFieldBattle.hover" : "战斗计时器",
+	"vcmi.optionsTab.chessFieldCreature.hover" : "堆栈计时器",
+	"vcmi.optionsTab.chessFieldBase.help" : "当{转动计时器}达到零时开始倒计时。 它仅在游戏开始时设置一次。 当计时器达到零时,玩家的回合结束。",
+	"vcmi.optionsTab.chessFieldTurn.help" : "当玩家在冒险地图上开始回合时开始倒计时。 它在每回合开始时重置为其初始值。 任何未使用的回合时间将被添加到{额外计时器}(如果正在使用)中。",
+	"vcmi.optionsTab.chessFieldBattle.help" : "战斗期间当 {堆栈计时器} 达到0时进行倒计时。 每次战斗开始时都会重置为初始值。 如果计时器达到零,当前活动的堆栈将进行防御。",
+	"vcmi.optionsTab.chessFieldCreature.help" : "当玩家在战斗中为当前堆栈选择一个动作时开始倒计时。 堆栈操作完成后,它会重置为其初始值。",
 
 
 	// Custom victory conditions for H3 campaigns and HotA maps
 	// Custom victory conditions for H3 campaigns and HotA maps
 	"vcmi.map.victoryCondition.daysPassed.toOthers" : "敌人依然存活至今,你失败了!",
 	"vcmi.map.victoryCondition.daysPassed.toOthers" : "敌人依然存活至今,你失败了!",

+ 0 - 5
Mods/vcmi/config/vcmi/czech.json

@@ -192,15 +192,10 @@
 	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "Team Alignments",
 	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "Team Alignments",
 	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "Druhy cest",
 	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "Druhy cest",
 
 
-	"vcmi.optionsTab.widgets.chessFieldBase.help" : "{Extra timer}\n\nStarts counting down when the {turn timer} reaches zero. It is set only once at the beginning of the game. When this timer reaches zero, the player's turn ends.",
-	"vcmi.optionsTab.widgets.chessFieldTurn.help" : "{Turn timer}\n\nStarts counting down when the player starts their turn on the adventure map. It is reset to its initial value at the start of each turn. Any unused turn time will be added to the {Extra timer} if it is in use.",
-	"vcmi.optionsTab.widgets.chessFieldBattle.help" : "{Battle timer}\n\nCounts down during battles when the {stack timer} reaches zero. It is reset to its initial value at the start of each battle. If the timer reaches zero, the currently active stack will defend.",
-	"vcmi.optionsTab.widgets.chessFieldCreature.help" : "{Stack timer}\n\nStarts counting down when the player is selecting an action for the current stack during battle. It resets to its initial value after the stack's action is completed.",
 	"vcmi.optionsTab.widgets.labelTimer" : "Časovač",
 	"vcmi.optionsTab.widgets.labelTimer" : "Časovač",
 	"vcmi.optionsTab.widgets.timerModeSwitch.classic" : "Klasický časovač",
 	"vcmi.optionsTab.widgets.timerModeSwitch.classic" : "Klasický časovač",
 	"vcmi.optionsTab.widgets.timerModeSwitch.chess" : "Šachový časovač",
 	"vcmi.optionsTab.widgets.timerModeSwitch.chess" : "Šachový časovač",
 
 
-
 	// Custom victory conditions for H3 campaigns and HotA maps
 	// Custom victory conditions for H3 campaigns and HotA maps
 	"vcmi.map.victoryCondition.daysPassed.toOthers" : "Nepřítel zvládl přežít do této chvíle. Vítězství je jeho!",
 	"vcmi.map.victoryCondition.daysPassed.toOthers" : "Nepřítel zvládl přežít do této chvíle. Vítězství je jeho!",
 	"vcmi.map.victoryCondition.daysPassed.toSelf" : "Gratulace! Zvládli jste přežít. Vítězství je vaše!",
 	"vcmi.map.victoryCondition.daysPassed.toSelf" : "Gratulace! Zvládli jste přežít. Vítězství je vaše!",

+ 30 - 7
Mods/vcmi/config/vcmi/english.json

@@ -220,14 +220,37 @@
 	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "Team Alignments",
 	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "Team Alignments",
 	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "Road Types",
 	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "Road Types",
 
 
-	"vcmi.optionsTab.widgets.chessFieldBase.help" : "{Extra timer}\n\nStarts counting down when the {turn timer} reaches zero. It is set only once at the beginning of the game. When this timer reaches zero, the player's turn ends.",
-	"vcmi.optionsTab.widgets.chessFieldTurn.help" : "{Turn timer}\n\nStarts counting down when the player starts their turn on the adventure map. It is reset to its initial value at the start of each turn. Any unused turn time will be added to the {Extra timer} if it is in use.",
-	"vcmi.optionsTab.widgets.chessFieldBattle.help" : "{Battle timer}\n\nCounts down during battles when the {stack timer} reaches zero. It is reset to its initial value at the start of each battle. If the timer reaches zero, the currently active stack will defend.",
-	"vcmi.optionsTab.widgets.chessFieldCreature.help" : "{Stack timer}\n\nStarts counting down when the player is selecting an action for the current stack during battle. It resets to its initial value after the stack's action is completed.",
-	"vcmi.optionsTab.widgets.labelTimer" : "Timer",
-	"vcmi.optionsTab.widgets.timerModeSwitch.classic" : "Classic timer",
-	"vcmi.optionsTab.widgets.timerModeSwitch.chess" : "Chess timer",
+	"vcmi.optionsTab.turnOptions.hover" : "Turn Options",
+	"vcmi.optionsTab.turnOptions.help" : "Select turn timer and simultaneous turns options",
 
 
+	"vcmi.optionsTab.chessFieldBase.hover" : "Base timer",
+	"vcmi.optionsTab.chessFieldTurn.hover" : "Turn timer",
+	"vcmi.optionsTab.chessFieldBattle.hover" : "Battle timer",
+	"vcmi.optionsTab.chessFieldCreature.hover" : "Unit timer",
+	"vcmi.optionsTab.chessFieldBase.help" : "Used when {Turn Timer} reaches 0. Set once at game start. On reaching zero, ends current turn. Any ongoing combat with end with a loss.",
+	"vcmi.optionsTab.chessFieldTurn.help" : "Used out of combat or when {Battle Timer} runs out. Reset each turn. Leftover added to {Base Timer} at turn's end.",
+	"vcmi.optionsTab.chessFieldBattle.help" : "Used in battles with AI or in pvp combat when {Unit Timer} runs out. Reset at start of each combat.",
+	"vcmi.optionsTab.chessFieldCreature.help" : "Used when selecting unit action in pvp combat. Reset at start of each unit's turn.",
+
+	"vcmi.optionsTab.simturns" : "Simultaneous turns",
+	"vcmi.optionsTab.simturnsMin.hover" : "At least for",
+	"vcmi.optionsTab.simturnsMax.hover" : "At most for",
+	"vcmi.optionsTab.simturnsAI.hover" : "(Experimental) Simultaneous AI Turns",
+	"vcmi.optionsTab.simturnsMin.help" : "Play simultaneously for specified number of days. Contacts between players during this period are blocked",
+	"vcmi.optionsTab.simturnsMax.help" : "Play simultaneously for specified number of days or until contact with another player",
+	"vcmi.optionsTab.simturnsAI.help" : "{Simultaneous AI Turns}\nExperimental option. Allows AI players to act at the same time as human player when simultaneous turns are enabled.",
+	
+	// Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language
+	// Using this information, VCMI will automatically select correct plural form for every possible amount
+	"vcmi.optionsTab.simturns.days.0" : " %d days",
+	"vcmi.optionsTab.simturns.days.1" : " %d day",
+	"vcmi.optionsTab.simturns.days.2" : " %d days",
+	"vcmi.optionsTab.simturns.weeks.0" : " %d weeks",
+	"vcmi.optionsTab.simturns.weeks.1" : " %d week",
+	"vcmi.optionsTab.simturns.weeks.2" : " %d weeks",
+	"vcmi.optionsTab.simturns.months.0" : " %d months",
+	"vcmi.optionsTab.simturns.months.1" : " %d month",
+	"vcmi.optionsTab.simturns.months.2" : " %d months",
 
 
 	// Custom victory conditions for H3 campaigns and HotA maps
 	// Custom victory conditions for H3 campaigns and HotA maps
 	"vcmi.map.victoryCondition.daysPassed.toOthers" : "The enemy has managed to survive till this day. Victory is theirs!",
 	"vcmi.map.victoryCondition.daysPassed.toOthers" : "The enemy has managed to survive till this day. Victory is theirs!",

+ 8 - 7
Mods/vcmi/config/vcmi/russian.json

@@ -201,13 +201,14 @@
 	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "Распределение команд",
 	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "Распределение команд",
 	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "Виды дорог",
 	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "Виды дорог",
 
 
-	"vcmi.optionsTab.widgets.chessFieldBase.help" : "{Время игрока}\n\nОбратный отсчет начинается когда {время на ход} истекает. Устанавливается один раз в начале игры. По истечении времени игрок завершает ход.",
-	"vcmi.optionsTab.widgets.chessFieldTurn.help" : "{Время на ход}\n\nОбратный отсчет начивается когда игрок начинает свой ход. В начале каждого хода устанавливается в иходное значение. Все неиспользованное время добавляется ко {времени игрока}, если оно используется.",
-	"vcmi.optionsTab.widgets.chessFieldBattle.help" : "{Время на битву}\n\nОбратный отсчет начинается когда {время на отряд истекает}. В начале каждой битвы устанавливается в исходное значение. По истечении времени текущий отряд получает приказ защищаться.",
-	"vcmi.optionsTab.widgets.chessFieldCreature.help" : "{Время на отряд}\n\nОбратный отсчет начинается когда игрок получает получает контроль над отрядом во время битвы. Устанавливается в исходное значение всякий раз, когда отряд получает возможность действовать.",
-	"vcmi.optionsTab.widgets.labelTimer" : "Таймер",
-	"vcmi.optionsTab.widgets.timerModeSwitch.classic" : "Классические часы",
-	"vcmi.optionsTab.widgets.timerModeSwitch.chess" : "Шахматные часы",
+  "vcmi.optionsTab.chessFieldBase.hover" : "Время игрока",
+  "vcmi.optionsTab.chessFieldTurn.hover" : "Время на ход",
+  "vcmi.optionsTab.chessFieldBattle.hover" : "Время на битву",
+  "vcmi.optionsTab.chessFieldCreature.hover" : "Время на отряд",
+	"vcmi.optionsTab.chessFieldBase.help" : "Обратный отсчет начинается когда {время на ход} истекает. Устанавливается один раз в начале игры. По истечении времени игрок завершает ход.",
+	"vcmi.optionsTab.chessFieldTurn.help" : "Обратный отсчет начивается когда игрок начинает свой ход. В начале каждого хода устанавливается в иходное значение. Все неиспользованное время добавляется ко {времени игрока}, если оно используется.",
+	"vcmi.optionsTab.chessFieldBattle.help" : "Обратный отсчет начинается когда {время на отряд истекает}. В начале каждой битвы устанавливается в исходное значение. По истечении времени текущий отряд получает приказ защищаться.",
+	"vcmi.optionsTab.chessFieldCreature.help" : "Обратный отсчет начинается когда игрок получает получает контроль над отрядом во время битвы. Устанавливается в исходное значение всякий раз, когда отряд получает возможность действовать.",
 
 
 	"mapObject.core.creatureBank.cyclopsStockpile.name" : "Хранилище циклопов",
 	"mapObject.core.creatureBank.cyclopsStockpile.name" : "Хранилище циклопов",
 	"mapObject.core.creatureBank.dragonFlyHive.name" : "Улей летучих змиев",
 	"mapObject.core.creatureBank.dragonFlyHive.name" : "Улей летучих змиев",

+ 58 - 1
Mods/vcmi/config/vcmi/ukrainian.json

@@ -30,6 +30,11 @@
 	"vcmi.capitalColors.6" : "Сизий",
 	"vcmi.capitalColors.6" : "Сизий",
 	"vcmi.capitalColors.7" : "Рожевий",
 	"vcmi.capitalColors.7" : "Рожевий",
 
 
+	"vcmi.heroOverview.startingArmy" : "Початкові загони",
+	"vcmi.heroOverview.warMachine" : "Бойові машини",
+	"vcmi.heroOverview.secondarySkills" : "Навички",
+	"vcmi.heroOverview.spells" : "Закляття",
+
 	"vcmi.radialWheel.mergeSameUnit" : "Об'єднати однакових істот",
 	"vcmi.radialWheel.mergeSameUnit" : "Об'єднати однакових істот",
 	"vcmi.radialWheel.fillSingleUnit" : "Заповнити одиничними істотами",
 	"vcmi.radialWheel.fillSingleUnit" : "Заповнити одиничними істотами",
 	"vcmi.radialWheel.splitSingleUnit" : "Відділити одну істоту",
 	"vcmi.radialWheel.splitSingleUnit" : "Відділити одну істоту",
@@ -37,8 +42,21 @@
 	"vcmi.radialWheel.moveUnit" : "Перемістити істоту до іншої армії",
 	"vcmi.radialWheel.moveUnit" : "Перемістити істоту до іншої армії",
 	"vcmi.radialWheel.splitUnit" : "Розділити істоту в інший слот",
 	"vcmi.radialWheel.splitUnit" : "Розділити істоту в інший слот",
 
 
+	"vcmi.radialWheel.heroGetArmy" : "Отримати армію іншого героя",
+	"vcmi.radialWheel.heroSwapArmy" : "Обміняти армії героїв",
+	"vcmi.radialWheel.heroExchange" : "Відкрити вікно обміну",
+	"vcmi.radialWheel.heroGetArtifacts" : "Отримати артефакти іншого героя",
+	"vcmi.radialWheel.heroSwapArtifacts" : "Обміняти артефакти героїв",
+	"vcmi.radialWheel.heroDismiss" : "Звільнити цього героя",
+
+	"vcmi.radialWheel.moveTop" : "Перемістити на початок",
+	"vcmi.radialWheel.moveUp" : "Перемістити вгору",
+	"vcmi.radialWheel.moveDown" : "Перемістити вниз",
+	"vcmi.radialWheel.moveBottom" : "Перемістити у кінець",
+
 	"vcmi.mainMenu.serverConnecting" : "Підключення...",
 	"vcmi.mainMenu.serverConnecting" : "Підключення...",
 	"vcmi.mainMenu.serverAddressEnter" : "Вкажіть адресу:",
 	"vcmi.mainMenu.serverAddressEnter" : "Вкажіть адресу:",
+	"vcmi.mainMenu.serverConnectionFailed" : "Помилка з'єднання",
 	"vcmi.mainMenu.serverClosing" : "Завершення...",
 	"vcmi.mainMenu.serverClosing" : "Завершення...",
 	"vcmi.mainMenu.hostTCP" : "Створити TCP/IP гру",
 	"vcmi.mainMenu.hostTCP" : "Створити TCP/IP гру",
 	"vcmi.mainMenu.joinTCP" : "Приєднатися до TCP/IP гри",
 	"vcmi.mainMenu.joinTCP" : "Приєднатися до TCP/IP гри",
@@ -46,9 +64,14 @@
 	
 	
 	"vcmi.lobby.filepath" : "Назва файлу",
 	"vcmi.lobby.filepath" : "Назва файлу",
 	"vcmi.lobby.creationDate" : "Дата створення",
 	"vcmi.lobby.creationDate" : "Дата створення",
+	"vcmi.lobby.scenarioName" : "Scenario name",
+	"vcmi.lobby.mapPreview" : "Огляд мапи",
+	"vcmi.lobby.noPreview" : "огляд недоступний",
+	"vcmi.lobby.noUnderground" : "немає підземелля",
 
 
 	"vcmi.server.errors.existingProcess" : "Працює інший процес vcmiserver, будь ласка, спочатку завершіть його",
 	"vcmi.server.errors.existingProcess" : "Працює інший процес vcmiserver, будь ласка, спочатку завершіть його",
 	"vcmi.server.errors.modsToEnable"    : "{Потрібні модифікації для завантаження гри}",
 	"vcmi.server.errors.modsToEnable"    : "{Потрібні модифікації для завантаження гри}",
+	"vcmi.server.errors.modsToDisable"   : "{Модифікації що мають бути вимкнені}",
 	"vcmi.server.confirmReconnect"       : "Підключитися до минулої сесії?",
 	"vcmi.server.confirmReconnect"       : "Підключитися до минулої сесії?",
 
 
 	"vcmi.settingsMainWindow.generalTab.hover" : "Загальні",
 	"vcmi.settingsMainWindow.generalTab.hover" : "Загальні",
@@ -84,6 +107,8 @@
 	"vcmi.systemOptions.framerateButton.help"   : "{Лічильник кадрів}\n\n Перемикає видимість лічильника кадрів на секунду у кутку ігрового вікна",
 	"vcmi.systemOptions.framerateButton.help"   : "{Лічильник кадрів}\n\n Перемикає видимість лічильника кадрів на секунду у кутку ігрового вікна",
 	"vcmi.systemOptions.hapticFeedbackButton.hover"  : "Тактильний відгук",
 	"vcmi.systemOptions.hapticFeedbackButton.hover"  : "Тактильний відгук",
 	"vcmi.systemOptions.hapticFeedbackButton.help"   : "{Тактильний відгук}\n\nВикористовувати вібрацію при використанні сенсорного екрану",
 	"vcmi.systemOptions.hapticFeedbackButton.help"   : "{Тактильний відгук}\n\nВикористовувати вібрацію при використанні сенсорного екрану",
+	"vcmi.systemOptions.enableUiEnhancementsButton.hover"  : "Розширення інтерфейсу",
+	"vcmi.systemOptions.enableUiEnhancementsButton.help"   : "{Розширення інтерфейсу}\n\nУвімкніть різні розширення інтерфейсу для покращення якості життя. Наприклад, більша книга заклинань, рюкзак тощо. Вимкнути, щоб отримати більш класичний досвід.",
 
 
 	"vcmi.adventureOptions.infoBarPick.help" : "{Повідомлення у панелі статусу}\n\nЗа можливості, повідомлення про відвідування об'єктів карти пригод будуть відображені у панелі статусу замість окремого вікна",
 	"vcmi.adventureOptions.infoBarPick.help" : "{Повідомлення у панелі статусу}\n\nЗа можливості, повідомлення про відвідування об'єктів карти пригод будуть відображені у панелі статусу замість окремого вікна",
 	"vcmi.adventureOptions.infoBarPick.hover" : "Повідомлення у панелі статусу",
 	"vcmi.adventureOptions.infoBarPick.hover" : "Повідомлення у панелі статусу",
@@ -167,7 +192,7 @@
 	"vcmi.townHall.greetingCustomBonus"     : "%s дає вам +%d %s%s",
 	"vcmi.townHall.greetingCustomBonus"     : "%s дає вам +%d %s%s",
 	"vcmi.townHall.greetingCustomUntil"     : " до наступної битви.",
 	"vcmi.townHall.greetingCustomUntil"     : " до наступної битви.",
 	"vcmi.townHall.greetingInTownMagicWell" : "%s повністю відновлює ваш запас очків магії.",
 	"vcmi.townHall.greetingInTownMagicWell" : "%s повністю відновлює ваш запас очків магії.",
-	
+
 	"vcmi.logicalExpressions.anyOf"  : "Будь-що з перерахованого:",
 	"vcmi.logicalExpressions.anyOf"  : "Будь-що з перерахованого:",
 	"vcmi.logicalExpressions.allOf"  : "Все з перерахованого:",
 	"vcmi.logicalExpressions.allOf"  : "Все з перерахованого:",
 	"vcmi.logicalExpressions.noneOf" : "Нічого з перерахованого:",
 	"vcmi.logicalExpressions.noneOf" : "Нічого з перерахованого:",
@@ -195,6 +220,38 @@
 	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "Розподіл команд",
 	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "Розподіл команд",
 	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "Види доріг",
 	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "Види доріг",
 
 
+	"vcmi.optionsTab.turnOptions.hover" : "Параметри ходів",
+	"vcmi.optionsTab.turnOptions.help" : "Виберіть опції таймера ходів та одночасних ходів",
+
+	"vcmi.optionsTab.chessFieldBase.hover" : "Основний таймер",
+	"vcmi.optionsTab.chessFieldTurn.hover" : "Таймер ходу",
+	"vcmi.optionsTab.chessFieldBattle.hover" : "Таймер битви",
+	"vcmi.optionsTab.chessFieldCreature.hover" : "Таймер загону",
+	"vcmi.optionsTab.chessFieldBase.help" : "Встановлюється один раз на початку гри. Коли вичерпується, поточний хід буде перервано, поточна битва буде програна.",
+	"vcmi.optionsTab.chessFieldTurn.help" : "Використовується під час ходу. Встановлюється кожен хід. Залишок додається до {основного таймеру} у кінці ходу",
+	"vcmi.optionsTab.chessFieldBattle.help" : "Використовується у боях з ШІ чи у боях з гравцями якщо {таймер загону} вичерпується. Встановлюється на початку кожного бою.",
+	"vcmi.optionsTab.chessFieldCreature.help" : "Використовується при обираннія дії загону у боях з гравцями. Встановлюється на початку кожної дії.",
+
+	"vcmi.optionsTab.simturns" : "Одночасні ходи",
+	"vcmi.optionsTab.simturnsMin.hover" : "Щонайменше",
+	"vcmi.optionsTab.simturnsMax.hover" : "Щонайбільше",
+	"vcmi.optionsTab.simturnsAI.hover" : "(Експериментально) Одночасні ходи ШІ",
+	"vcmi.optionsTab.simturnsMin.help" : "Грати одночасно обрану кількість днів. Контакти між гравцями у цей період заблоковані",
+	"vcmi.optionsTab.simturnsMax.help" : "Грати одночасно обрану кількість днів чи до першого контакту з іншим гравцем",
+	"vcmi.optionsTab.simturnsAI.help" : "{Одночасні ходи ШІ}\nЕкспериментальна опція. Дозволяє гравцям-ШІ діяти одночасно с гравцями-людьми якщо одночасні ходи увімкнені.",
+	
+	// Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language
+	// Using this information, VCMI will automatically select correct plural form for every possible amount
+	"vcmi.optionsTab.simturns.days.0" : " %d днів",
+	"vcmi.optionsTab.simturns.days.1" : " %d день",
+	"vcmi.optionsTab.simturns.days.2" : " %d дні",
+	"vcmi.optionsTab.simturns.weeks.0" : " %d тижнів",
+	"vcmi.optionsTab.simturns.weeks.1" : " %d тиждень",
+	"vcmi.optionsTab.simturns.weeks.2" : " %d тижні",
+	"vcmi.optionsTab.simturns.months.0" : " %d місяців",
+	"vcmi.optionsTab.simturns.months.1" : " %d місяць",
+	"vcmi.optionsTab.simturns.months.2" : " %d місяці",
+
 	// Custom victory conditions for H3 campaigns and HotA maps
 	// Custom victory conditions for H3 campaigns and HotA maps
 	"vcmi.map.victoryCondition.daysPassed.toOthers" : "Ворогу вдалося вижити до сьогоднішнього дня. Він переміг!",
 	"vcmi.map.victoryCondition.daysPassed.toOthers" : "Ворогу вдалося вижити до сьогоднішнього дня. Він переміг!",
 	"vcmi.map.victoryCondition.daysPassed.toSelf" : "Вітаємо! Вам вдалося залишитися в живих. Перемога за вами!",
 	"vcmi.map.victoryCondition.daysPassed.toSelf" : "Вітаємо! Вам вдалося залишитися в живих. Перемога за вами!",

+ 8 - 7
Mods/vcmi/config/vcmi/vietnamese.json

@@ -198,13 +198,14 @@
   "vcmi.randomMapTab.widgets.teamAlignmentsLabel": "Sắp đội",
   "vcmi.randomMapTab.widgets.teamAlignmentsLabel": "Sắp đội",
   "vcmi.randomMapTab.widgets.roadTypesLabel": "Kiểu đường xá",
   "vcmi.randomMapTab.widgets.roadTypesLabel": "Kiểu đường xá",
 
 
-  "vcmi.optionsTab.widgets.chessFieldBase.help": "{Thời gian thêm}\n\nBắt đầu đếm ngược khi {Thời gian lượt} giảm đến 0. Được đặt 1 lần khi bắt đầu trò chơi. Khi thời gian này giảm đến 0, lượt của người chơi kết thúc.",
-  "vcmi.optionsTab.widgets.chessFieldTurn.help": "{Thời gian lượt}\n\nBắt đầu đếm ngược khi đến lượt người chơi trên bản đồ phiêu lưu. Nó được đặt lại giá trị ban đầu khi bắt đầu mỗi lượt. Thời gian lượt chưa sử dụng sẽ được thêm vào {Thời gian thêm} nếu có.",
-  "vcmi.optionsTab.widgets.chessFieldBattle.help": "{Thời gian trận đánh}\n\nĐếm ngược trong suốt trận đánh khi {Thời gian lính} giảm đến 0. Nó được đặt lại giá trị ban đầu khi bắt đầu mỗi trận đánh. Nếu thời gian giảm đến 0, đội lính hiện tại sẽ phòng thủ.",
-  "vcmi.optionsTab.widgets.chessFieldCreature.help": "{Thời gian lính}\n\nBắt đầu đếm ngược khi người chơi đang chọn hành động cho đội linh hiện tại trong suốt trận đánh. Nó được đặt lại giá trị ban đầu sau khi hành động của đội lính hoàn tất.",
-  "vcmi.optionsTab.widgets.labelTimer": "Đồng hồ",
-  "vcmi.optionsTab.widgets.timerModeSwitch.classic": "Đồng hồ cơ bản",
-  "vcmi.optionsTab.widgets.timerModeSwitch.chess": "Đồng hồ đánh cờ",
+  "vcmi.optionsTab.chessFieldBase.hover" : "Thời gian thêm",
+  "vcmi.optionsTab.chessFieldTurn.hover" : "Thời gian lượt",
+  "vcmi.optionsTab.chessFieldBattle.hover" : "Thời gian trận đánh",
+  "vcmi.optionsTab.chessFieldCreature.hover" : "Thời gian lính",
+  "vcmi.optionsTab.chessFieldBase.help": "Bắt đầu đếm ngược khi {Thời gian lượt} giảm đến 0. Được đặt 1 lần khi bắt đầu trò chơi. Khi thời gian này giảm đến 0, lượt của người chơi kết thúc.",
+  "vcmi.optionsTab.chessFieldTurn.help": "Bắt đầu đếm ngược khi đến lượt người chơi trên bản đồ phiêu lưu. Nó được đặt lại giá trị ban đầu khi bắt đầu mỗi lượt. Thời gian lượt chưa sử dụng sẽ được thêm vào {Thời gian thêm} nếu có.",
+  "vcmi.optionsTab.chessFieldBattle.help": "Đếm ngược trong suốt trận đánh khi {Thời gian lính} giảm đến 0. Nó được đặt lại giá trị ban đầu khi bắt đầu mỗi trận đánh. Nếu thời gian giảm đến 0, đội lính hiện tại sẽ phòng thủ.",
+  "vcmi.optionsTab.chessFieldCreature.help": "Bắt đầu đếm ngược khi người chơi đang chọn hành động cho đội linh hiện tại trong suốt trận đánh. Nó được đặt lại giá trị ban đầu sau khi hành động của đội lính hoàn tất.",
 
 
   "vcmi.map.victoryCondition.daysPassed.toOthers": "Đối thủ đã xoay xở để sinh tồn đến ngày này. Họ giành chiến thắng!",
   "vcmi.map.victoryCondition.daysPassed.toOthers": "Đối thủ đã xoay xở để sinh tồn đến ngày này. Họ giành chiến thắng!",
   "vcmi.map.victoryCondition.daysPassed.toSelf": "Chúc mừng! Bạn đã vượt khó để sinh tồn. Chiến thắng thuộc về bạn!",
   "vcmi.map.victoryCondition.daysPassed.toSelf": "Chúc mừng! Bạn đã vượt khó để sinh tồn. Chiến thắng thuộc về bạn!",

+ 4 - 0
client/CMakeLists.txt

@@ -51,7 +51,9 @@ set(client_SRCS
 	lobby/CSavingScreen.cpp
 	lobby/CSavingScreen.cpp
 	lobby/CScenarioInfoScreen.cpp
 	lobby/CScenarioInfoScreen.cpp
 	lobby/CSelectionBase.cpp
 	lobby/CSelectionBase.cpp
+	lobby/TurnOptionsTab.cpp
 	lobby/OptionsTab.cpp
 	lobby/OptionsTab.cpp
+	lobby/OptionsTabBase.cpp
 	lobby/RandomMapTab.cpp
 	lobby/RandomMapTab.cpp
 	lobby/SelectionTab.cpp
 	lobby/SelectionTab.cpp
 
 
@@ -211,7 +213,9 @@ set(client_HEADERS
 	lobby/CSavingScreen.h
 	lobby/CSavingScreen.h
 	lobby/CScenarioInfoScreen.h
 	lobby/CScenarioInfoScreen.h
 	lobby/CSelectionBase.h
 	lobby/CSelectionBase.h
+	lobby/TurnOptionsTab.h
 	lobby/OptionsTab.h
 	lobby/OptionsTab.h
+	lobby/OptionsTabBase.h
 	lobby/RandomMapTab.h
 	lobby/RandomMapTab.h
 	lobby/SelectionTab.h
 	lobby/SelectionTab.h
 
 

+ 17 - 4
client/gui/InterfaceObjectConfigurable.cpp

@@ -501,12 +501,25 @@ std::shared_ptr<CSlider> InterfaceObjectConfigurable::buildSlider(const JsonNode
 	auto position = readPosition(config["position"]);
 	auto position = readPosition(config["position"]);
 	int length = config["size"].Integer();
 	int length = config["size"].Integer();
 	auto style = config["style"].String() == "brown" ? CSlider::BROWN : CSlider::BLUE;
 	auto style = config["style"].String() == "brown" ? CSlider::BROWN : CSlider::BLUE;
-	auto itemsVisible = config["itemsVisible"].Integer();
-	auto itemsTotal = config["itemsTotal"].Integer();
 	auto value = config["selected"].Integer();
 	auto value = config["selected"].Integer();
 	bool horizontal = config["orientation"].String() == "horizontal";
 	bool horizontal = config["orientation"].String() == "horizontal";
-	const auto & result =
-		std::make_shared<CSlider>(position, length, callbacks_int.at(config["callback"].String()), itemsVisible, itemsTotal, value, horizontal ? Orientation::HORIZONTAL : Orientation::VERTICAL, style);
+	auto orientation = horizontal ? Orientation::HORIZONTAL : Orientation::VERTICAL;
+
+	std::shared_ptr<CSlider> result;
+
+	if (config["items"].isNull())
+	{
+		auto itemsVisible = config["itemsVisible"].Integer();
+		auto itemsTotal = config["itemsTotal"].Integer();
+
+		result = std::make_shared<CSlider>(position, length, callbacks_int.at(config["callback"].String()), itemsVisible, itemsTotal, value, orientation, style);
+	}
+	else
+	{
+		auto items = config["items"].convertTo<std::vector<int>>();
+		result = std::make_shared<SliderNonlinear>(position, length, callbacks_int.at(config["callback"].String()), items, value, orientation, style);
+	}
+
 
 
 	if(!config["scrollBounds"].isNull())
 	if(!config["scrollBounds"].isNull())
 	{
 	{

+ 29 - 8
client/lobby/CLobbyScreen.cpp

@@ -8,14 +8,15 @@
  *
  *
  */
  */
 #include "StdInc.h"
 #include "StdInc.h"
-
 #include "CLobbyScreen.h"
 #include "CLobbyScreen.h"
+
 #include "CBonusSelection.h"
 #include "CBonusSelection.h"
-#include "SelectionTab.h"
-#include "RandomMapTab.h"
+#include "TurnOptionsTab.h"
 #include "OptionsTab.h"
 #include "OptionsTab.h"
-#include "../CServerHandler.h"
+#include "RandomMapTab.h"
+#include "SelectionTab.h"
 
 
+#include "../CServerHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/Shortcut.h"
 #include "../gui/Shortcut.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/Buttons.h"
@@ -24,12 +25,13 @@
 
 
 #include "../../CCallback.h"
 #include "../../CCallback.h"
 
 
-#include "../CGameInfo.h"
-#include "../../lib/networkPacks/PacksForLobby.h"
+#include "../../lib/CConfigHandler.h"
 #include "../../lib/CGeneralTextHandler.h"
 #include "../../lib/CGeneralTextHandler.h"
 #include "../../lib/campaign/CampaignHandler.h"
 #include "../../lib/campaign/CampaignHandler.h"
 #include "../../lib/mapping/CMapInfo.h"
 #include "../../lib/mapping/CMapInfo.h"
+#include "../../lib/networkPacks/PacksForLobby.h"
 #include "../../lib/rmg/CMapGenOptions.h"
 #include "../../lib/rmg/CMapGenOptions.h"
+#include "../CGameInfo.h"
 
 
 CLobbyScreen::CLobbyScreen(ESelectionScreen screenType)
 CLobbyScreen::CLobbyScreen(ESelectionScreen screenType)
 	: CSelectionBase(screenType), bonusSel(nullptr)
 	: CSelectionBase(screenType), bonusSel(nullptr)
@@ -50,6 +52,8 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType)
 		});
 		});
 
 
 		buttonOptions = std::make_shared<CButton>(Point(411, 510), AnimationPath::builtin("GSPBUTT.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabOpt), EShortcut::LOBBY_ADDITIONAL_OPTIONS);
 		buttonOptions = std::make_shared<CButton>(Point(411, 510), AnimationPath::builtin("GSPBUTT.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabOpt), EShortcut::LOBBY_ADDITIONAL_OPTIONS);
+		if(settings["general"]["enableUiEnhancements"].Bool())
+			buttonTurnOptions = std::make_shared<CButton>(Point(619, 510), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabTurnOptions), EShortcut::NONE);
 	};
 	};
 
 
 	buttonChat = std::make_shared<CButton>(Point(619, 80), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[48], std::bind(&CLobbyScreen::toggleChat, this), EShortcut::LOBBY_HIDE_CHAT);
 	buttonChat = std::make_shared<CButton>(Point(619, 80), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[48], std::bind(&CLobbyScreen::toggleChat, this), EShortcut::LOBBY_HIDE_CHAT);
@@ -60,6 +64,7 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType)
 	case ESelectionScreen::newGame:
 	case ESelectionScreen::newGame:
 	{
 	{
 		tabOpt = std::make_shared<OptionsTab>();
 		tabOpt = std::make_shared<OptionsTab>();
+		tabTurnOptions = std::make_shared<TurnOptionsTab>();
 		tabRand = std::make_shared<RandomMapTab>();
 		tabRand = std::make_shared<RandomMapTab>();
 		tabRand->mapInfoChanged += std::bind(&IServerAPI::setMapInfo, CSH, _1, _2);
 		tabRand->mapInfoChanged += std::bind(&IServerAPI::setMapInfo, CSH, _1, _2);
 		buttonRMG = std::make_shared<CButton>(Point(411, 105), AnimationPath::builtin("GSPBUTT.DEF"), CGI->generaltexth->zelp[47], 0, EShortcut::LOBBY_RANDOM_MAP);
 		buttonRMG = std::make_shared<CButton>(Point(411, 105), AnimationPath::builtin("GSPBUTT.DEF"), CGI->generaltexth->zelp[47], 0, EShortcut::LOBBY_RANDOM_MAP);
@@ -78,6 +83,7 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType)
 	case ESelectionScreen::loadGame:
 	case ESelectionScreen::loadGame:
 	{
 	{
 		tabOpt = std::make_shared<OptionsTab>();
 		tabOpt = std::make_shared<OptionsTab>();
+		tabTurnOptions = std::make_shared<TurnOptionsTab>();
 		buttonStart = std::make_shared<CButton>(Point(411, 535), AnimationPath::builtin("SCNRLOD.DEF"), CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, true), EShortcut::LOBBY_LOAD_GAME);
 		buttonStart = std::make_shared<CButton>(Point(411, 535), AnimationPath::builtin("SCNRLOD.DEF"), CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, true), EShortcut::LOBBY_LOAD_GAME);
 		initLobby();
 		initLobby();
 		break;
 		break;
@@ -145,6 +151,10 @@ void CLobbyScreen::toggleMode(bool host)
 	auto buttonColor = host ? Colors::WHITE : Colors::ORANGE;
 	auto buttonColor = host ? Colors::WHITE : Colors::ORANGE;
 	buttonSelect->addTextOverlay(CGI->generaltexth->allTexts[500], FONT_SMALL, buttonColor);
 	buttonSelect->addTextOverlay(CGI->generaltexth->allTexts[500], FONT_SMALL, buttonColor);
 	buttonOptions->addTextOverlay(CGI->generaltexth->allTexts[501], FONT_SMALL, buttonColor);
 	buttonOptions->addTextOverlay(CGI->generaltexth->allTexts[501], FONT_SMALL, buttonColor);
+
+	if (buttonTurnOptions)
+		buttonTurnOptions->addTextOverlay(CGI->generaltexth->translate("vcmi.optionsTab.turnOptions.hover"), FONT_SMALL, buttonColor);
+
 	if(buttonRMG)
 	if(buttonRMG)
 	{
 	{
 		buttonRMG->addTextOverlay(CGI->generaltexth->allTexts[740], FONT_SMALL, buttonColor);
 		buttonRMG->addTextOverlay(CGI->generaltexth->allTexts[740], FONT_SMALL, buttonColor);
@@ -153,8 +163,14 @@ void CLobbyScreen::toggleMode(bool host)
 	buttonSelect->block(!host);
 	buttonSelect->block(!host);
 	buttonOptions->block(!host);
 	buttonOptions->block(!host);
 
 
+	if (buttonTurnOptions)
+		buttonTurnOptions->block(!host);
+
 	if(CSH->mi)
 	if(CSH->mi)
+	{
 		tabOpt->recreate();
 		tabOpt->recreate();
+		tabTurnOptions->recreate();
+	}
 }
 }
 
 
 void CLobbyScreen::toggleChat()
 void CLobbyScreen::toggleChat()
@@ -168,8 +184,13 @@ void CLobbyScreen::toggleChat()
 
 
 void CLobbyScreen::updateAfterStateChange()
 void CLobbyScreen::updateAfterStateChange()
 {
 {
-	if(CSH->mi && tabOpt)
-		tabOpt->recreate();
+	if(CSH->mi)
+	{
+		if (tabOpt)
+			tabOpt->recreate();
+		if (tabTurnOptions)
+		tabTurnOptions->recreate();
+	}
 
 
 	buttonStart->block(CSH->mi == nullptr || CSH->isGuest());
 	buttonStart->block(CSH->mi == nullptr || CSH->isGuest());
 
 

+ 3 - 0
client/lobby/CSelectionBase.h

@@ -26,6 +26,7 @@ class CAnimImage;
 class CToggleGroup;
 class CToggleGroup;
 class RandomMapTab;
 class RandomMapTab;
 class OptionsTab;
 class OptionsTab;
+class TurnOptionsTab;
 class SelectionTab;
 class SelectionTab;
 class InfoCard;
 class InfoCard;
 class CChatBox;
 class CChatBox;
@@ -57,12 +58,14 @@ public:
 	std::shared_ptr<CButton> buttonSelect;
 	std::shared_ptr<CButton> buttonSelect;
 	std::shared_ptr<CButton> buttonRMG;
 	std::shared_ptr<CButton> buttonRMG;
 	std::shared_ptr<CButton> buttonOptions;
 	std::shared_ptr<CButton> buttonOptions;
+	std::shared_ptr<CButton> buttonTurnOptions;
 	std::shared_ptr<CButton> buttonStart;
 	std::shared_ptr<CButton> buttonStart;
 	std::shared_ptr<CButton> buttonBack;
 	std::shared_ptr<CButton> buttonBack;
 	std::shared_ptr<CButton> buttonSimturns;
 	std::shared_ptr<CButton> buttonSimturns;
 
 
 	std::shared_ptr<SelectionTab> tabSel;
 	std::shared_ptr<SelectionTab> tabSel;
 	std::shared_ptr<OptionsTab> tabOpt;
 	std::shared_ptr<OptionsTab> tabOpt;
+	std::shared_ptr<TurnOptionsTab> tabTurnOptions;
 	std::shared_ptr<RandomMapTab> tabRand;
 	std::shared_ptr<RandomMapTab> tabRand;
 	std::shared_ptr<CIntObject> curTab;
 	std::shared_ptr<CIntObject> curTab;
 
 

+ 4 - 215
client/lobby/OptionsTab.cpp

@@ -42,164 +42,10 @@
 #include "../../lib/mapping/CMapInfo.h"
 #include "../../lib/mapping/CMapInfo.h"
 #include "../../lib/mapping/CMapHeader.h"
 #include "../../lib/mapping/CMapHeader.h"
 
 
-OptionsTab::OptionsTab() : humanPlayers(0)
+OptionsTab::OptionsTab()
+	: OptionsTabBase(JsonPath::builtin("config/widgets/playerOptionsTab.json"))
+	, humanPlayers(0)
 {
 {
-	recActions = 0;
-		
-	addCallback("setTimerPreset", [&](int index){
-		if(!variables["timerPresets"].isNull())
-		{
-			auto tpreset = variables["timerPresets"].Vector().at(index).Vector();
-			TurnTimerInfo tinfo;
-			tinfo.baseTimer = tpreset.at(0).Integer() * 1000;
-			tinfo.turnTimer = tpreset.at(1).Integer() * 1000;
-			tinfo.battleTimer = tpreset.at(2).Integer() * 1000;
-			tinfo.creatureTimer = tpreset.at(3).Integer() * 1000;
-			CSH->setTurnTimerInfo(tinfo);
-		}
-	});
-
-	addCallback("setSimturnDuration", [&](int index){
-		SimturnsInfo info;
-		info.optionalTurns = index;
-		CSH->setSimturnsInfo(info);
-	});
-	
-	//helper function to parse string containing time to integer reflecting time in seconds
-	//assumed that input string can be modified by user, function shall support user's intention
-	// normal: 2:00, 12:30
-	// adding symbol: 2:005 -> 2:05, 2:305 -> 23:05,
-	// adding symbol (>60 seconds): 12:095 -> 129:05
-	// removing symbol: 129:0 -> 12:09, 2:0 -> 0:20, 0:2 -> 0:02
-	auto parseTimerString = [](const std::string & str) -> int
-	{
-		auto sc = str.find(":");
-		if(sc == std::string::npos)
-			return str.empty() ? 0 : std::stoi(str);
-		
-		auto l = str.substr(0, sc);
-		auto r = str.substr(sc + 1, std::string::npos);
-		if(r.length() == 3) //symbol added
-		{
-			l.push_back(r.front());
-			r.erase(r.begin());
-		}
-		else if(r.length() == 1) //symbol removed
-		{
-			r.insert(r.begin(), l.back());
-			l.pop_back();
-		}
-		else if(r.empty())
-			r = "0";
-		
-		int sec = std::stoi(r);
-		if(sec >= 60)
-		{
-			if(l.empty()) //9:00 -> 0:09
-				return sec / 10;
-			
-			l.push_back(r.front()); //0:090 -> 9:00
-			r.erase(r.begin());
-		}
-		else if(l.empty())
-			return sec;
-		
-		return std::stoi(l) * 60 + std::stoi(r);
-	};
-	
-	addCallback("parseAndSetTimer_base", [parseTimerString](const std::string & str){
-		int time = parseTimerString(str) * 1000;
-		if(time >= 0)
-		{
-			TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo;
-			tinfo.baseTimer = time;
-			CSH->setTurnTimerInfo(tinfo);
-		}
-	});
-	addCallback("parseAndSetTimer_turn", [parseTimerString](const std::string & str){
-		int time = parseTimerString(str) * 1000;
-		if(time >= 0)
-		{
-			TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo;
-			tinfo.turnTimer = time;
-			CSH->setTurnTimerInfo(tinfo);
-		}
-	});
-	addCallback("parseAndSetTimer_battle", [parseTimerString](const std::string & str){
-		int time = parseTimerString(str) * 1000;
-		if(time >= 0)
-		{
-			TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo;
-			tinfo.battleTimer = time;
-			CSH->setTurnTimerInfo(tinfo);
-		}
-	});
-	addCallback("parseAndSetTimer_creature", [parseTimerString](const std::string & str){
-		int time = parseTimerString(str) * 1000;
-		if(time >= 0)
-		{
-			TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo;
-			tinfo.creatureTimer = time;
-			CSH->setTurnTimerInfo(tinfo);
-		}
-	});
-	
-	const JsonNode config(JsonPath::builtin("config/widgets/optionsTab.json"));
-	build(config);
-	
-	//set timers combo box callbacks
-	if(auto w = widget<ComboBox>("timerModeSwitch"))
-	{
-		w->onConstructItems = [&](std::vector<const void *> & curItems){
-			if(variables["timers"].isNull())
-				return;
-			
-			for(auto & p : variables["timers"].Vector())
-			{
-				curItems.push_back(&p);
-			}
-		};
-		
-		w->onSetItem = [&](const void * item){
-			if(item)
-			{
-				if(auto * tObj = reinterpret_cast<const JsonNode *>(item))
-				{
-					for(auto wname : (*tObj)["hideWidgets"].Vector())
-					{
-						if(auto w = widget<CIntObject>(wname.String()))
-							w->setEnabled(false);
-					}
-					for(auto wname : (*tObj)["showWidgets"].Vector())
-					{
-						if(auto w = widget<CIntObject>(wname.String()))
-							w->setEnabled(true);
-					}
-					if((*tObj)["default"].isVector())
-					{
-						TurnTimerInfo tinfo;
-						tinfo.baseTimer = (*tObj)["default"].Vector().at(0).Integer() * 1000;
-						tinfo.turnTimer = (*tObj)["default"].Vector().at(1).Integer() * 1000;
-						tinfo.battleTimer = (*tObj)["default"].Vector().at(2).Integer() * 1000;
-						tinfo.creatureTimer = (*tObj)["default"].Vector().at(3).Integer() * 1000;
-						CSH->setTurnTimerInfo(tinfo);
-					}
-				}
-				redraw();
-			}
-		};
-		
-		w->getItemText = [this](int idx, const void * item){
-			if(item)
-			{
-				if(auto * tObj = reinterpret_cast<const JsonNode *>(item))
-					return readText((*tObj)["text"]);
-			}
-			return std::string("");
-		};
-		
-		w->setItem(0);
-	}
 }
 }
 
 
 void OptionsTab::recreate()
 void OptionsTab::recreate()
@@ -221,64 +67,7 @@ void OptionsTab::recreate()
 		entries.insert(std::make_pair(pInfo.first, std::make_shared<PlayerOptionsEntry>(pInfo.second, * this)));
 		entries.insert(std::make_pair(pInfo.first, std::make_shared<PlayerOptionsEntry>(pInfo.second, * this)));
 	}
 	}
 
 
-	//Simultaneous turns
-	if(auto turnSlider = widget<CSlider>("labelSimturnsDurationValue"))
-		turnSlider->scrollTo(SEL->getStartInfo()->simturnsInfo.optionalTurns);
-
-	if(auto w = widget<CLabel>("labelSimturnsDurationValue"))
-	{
-		MetaString message;
-		message.appendRawString("Simturns: up to %d days");
-		message.replaceNumber(SEL->getStartInfo()->simturnsInfo.optionalTurns);
-		w->setText(message.toString());
-	}
-	
-	const auto & turnTimerRemote = SEL->getStartInfo()->turnTimerInfo;
-	
-	//classic timer
-	if(auto turnSlider = widget<CSlider>("sliderTurnDuration"))
-	{
-		if(!variables["timerPresets"].isNull() && !turnTimerRemote.battleTimer && !turnTimerRemote.creatureTimer && !turnTimerRemote.baseTimer)
-		{
-			for(int idx = 0; idx < variables["timerPresets"].Vector().size(); ++idx)
-			{
-				auto & tpreset = variables["timerPresets"].Vector()[idx];
-				if(tpreset.Vector().at(1).Integer() == turnTimerRemote.turnTimer / 1000)
-				{
-					turnSlider->scrollTo(idx);
-					if(auto w = widget<CLabel>("labelTurnDurationValue"))
-						w->setText(CGI->generaltexth->turnDurations[idx]);
-				}
-			}
-		}
-	}
-	
-	//chess timer
-	auto timeToString = [](int time) -> std::string
-	{
-		std::stringstream ss;
-		ss << time / 1000 / 60 << ":" << std::setw(2) << std::setfill('0') << time / 1000 % 60;
-		return ss.str();
-	};
-	
-	if(auto ww = widget<CTextInput>("chessFieldBase"))
-		ww->setText(timeToString(turnTimerRemote.baseTimer), false);
-	if(auto ww = widget<CTextInput>("chessFieldTurn"))
-		ww->setText(timeToString(turnTimerRemote.turnTimer), false);
-	if(auto ww = widget<CTextInput>("chessFieldBattle"))
-		ww->setText(timeToString(turnTimerRemote.battleTimer), false);
-	if(auto ww = widget<CTextInput>("chessFieldCreature"))
-		ww->setText(timeToString(turnTimerRemote.creatureTimer), false);
-	
-	if(auto w = widget<ComboBox>("timerModeSwitch"))
-	{
-		if(turnTimerRemote.battleTimer || turnTimerRemote.creatureTimer || turnTimerRemote.baseTimer)
-		{
-			if(auto turnSlider = widget<CSlider>("sliderTurnDuration"))
-				if(turnSlider->isActive())
-					w->setItem(1);
-		}
-	}
+	OptionsTabBase::recreate();
 }
 }
 
 
 size_t OptionsTab::CPlayerSettingsHelper::getImageIndex(bool big)
 size_t OptionsTab::CPlayerSettingsHelper::getImageIndex(bool big)

+ 2 - 2
client/lobby/OptionsTab.h

@@ -9,9 +9,9 @@
  */
  */
 #pragma once
 #pragma once
 
 
+#include "OptionsTabBase.h"
 #include "../windows/CWindowObject.h"
 #include "../windows/CWindowObject.h"
 #include "../widgets/Scrollable.h"
 #include "../widgets/Scrollable.h"
-#include "../gui/InterfaceObjectConfigurable.h"
 
 
 VCMI_LIB_NAMESPACE_BEGIN
 VCMI_LIB_NAMESPACE_BEGIN
 struct PlayerSettings;
 struct PlayerSettings;
@@ -30,7 +30,7 @@ class CButton;
 class FilledTexturePlayerColored;
 class FilledTexturePlayerColored;
 
 
 /// The options tab which is shown at the map selection phase.
 /// The options tab which is shown at the map selection phase.
-class OptionsTab : public InterfaceObjectConfigurable
+class OptionsTab : public OptionsTabBase
 {
 {
 	struct PlayerOptionsEntry;
 	struct PlayerOptionsEntry;
 	
 	

+ 295 - 0
client/lobby/OptionsTabBase.cpp

@@ -0,0 +1,295 @@
+/*
+ * OptionsTabBase.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 "OptionsTabBase.h"
+#include "CSelectionBase.h"
+
+#include "../widgets/ComboBox.h"
+#include "../widgets/Slider.h"
+#include "../widgets/TextControls.h"
+#include "../CServerHandler.h"
+#include "../CGameInfo.h"
+
+#include "../../lib/StartInfo.h"
+#include "../../lib/Languages.h"
+#include "../../lib/MetaString.h"
+#include "../../lib/CGeneralTextHandler.h"
+
+OptionsTabBase::OptionsTabBase(const JsonPath & configPath)
+{
+	recActions = 0;
+
+	addCallback("setTimerPreset", [&](int index){
+		if(!variables["timerPresets"].isNull())
+		{
+			auto tpreset = variables["timerPresets"].Vector().at(index).Vector();
+			TurnTimerInfo tinfo;
+			tinfo.baseTimer = tpreset.at(0).Integer() * 1000;
+			tinfo.turnTimer = tpreset.at(1).Integer() * 1000;
+			tinfo.battleTimer = tpreset.at(2).Integer() * 1000;
+			tinfo.creatureTimer = tpreset.at(3).Integer() * 1000;
+			CSH->setTurnTimerInfo(tinfo);
+		}
+	});
+
+	addCallback("setSimturnDurationMin", [&](int index){
+		SimturnsInfo info = SEL->getStartInfo()->simturnsInfo;
+		info.requiredTurns = index;
+		info.optionalTurns = std::max(info.optionalTurns, index);
+		CSH->setSimturnsInfo(info);
+	});
+
+	addCallback("setSimturnDurationMax", [&](int index){
+		SimturnsInfo info = SEL->getStartInfo()->simturnsInfo;
+		info.optionalTurns = index;
+		info.requiredTurns = std::min(info.requiredTurns, index);
+		CSH->setSimturnsInfo(info);
+	});
+
+	addCallback("setSimturnAI", [&](int index){
+		SimturnsInfo info = SEL->getStartInfo()->simturnsInfo;
+		info.allowHumanWithAI = index;
+		CSH->setSimturnsInfo(info);
+	});
+
+	//helper function to parse string containing time to integer reflecting time in seconds
+	//assumed that input string can be modified by user, function shall support user's intention
+	// normal: 2:00, 12:30
+	// adding symbol: 2:005 -> 2:05, 2:305 -> 23:05,
+	// adding symbol (>60 seconds): 12:095 -> 129:05
+	// removing symbol: 129:0 -> 12:09, 2:0 -> 0:20, 0:2 -> 0:02
+	auto parseTimerString = [](const std::string & str) -> int
+	{
+		auto sc = str.find(":");
+		if(sc == std::string::npos)
+			return str.empty() ? 0 : std::stoi(str);
+
+		auto l = str.substr(0, sc);
+		auto r = str.substr(sc + 1, std::string::npos);
+		if(r.length() == 3) //symbol added
+		{
+			l.push_back(r.front());
+			r.erase(r.begin());
+		}
+		else if(r.length() == 1) //symbol removed
+		{
+			r.insert(r.begin(), l.back());
+			l.pop_back();
+		}
+		else if(r.empty())
+			r = "0";
+
+		int sec = std::stoi(r);
+		if(sec >= 60)
+		{
+			if(l.empty()) //9:00 -> 0:09
+				return sec / 10;
+
+			l.push_back(r.front()); //0:090 -> 9:00
+			r.erase(r.begin());
+		}
+		else if(l.empty())
+			return sec;
+
+		return std::stoi(l) * 60 + std::stoi(r);
+	};
+
+	addCallback("parseAndSetTimer_base", [parseTimerString](const std::string & str){
+		int time = parseTimerString(str) * 1000;
+		if(time >= 0)
+		{
+			TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo;
+			tinfo.baseTimer = time;
+			CSH->setTurnTimerInfo(tinfo);
+		}
+	});
+	addCallback("parseAndSetTimer_turn", [parseTimerString](const std::string & str){
+		int time = parseTimerString(str) * 1000;
+		if(time >= 0)
+		{
+			TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo;
+			tinfo.turnTimer = time;
+			CSH->setTurnTimerInfo(tinfo);
+		}
+	});
+	addCallback("parseAndSetTimer_battle", [parseTimerString](const std::string & str){
+		int time = parseTimerString(str) * 1000;
+		if(time >= 0)
+		{
+			TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo;
+			tinfo.battleTimer = time;
+			CSH->setTurnTimerInfo(tinfo);
+		}
+	});
+	addCallback("parseAndSetTimer_creature", [parseTimerString](const std::string & str){
+		int time = parseTimerString(str) * 1000;
+		if(time >= 0)
+		{
+			TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo;
+			tinfo.creatureTimer = time;
+			CSH->setTurnTimerInfo(tinfo);
+		}
+	});
+
+	const JsonNode config(configPath);
+	build(config);
+
+	//set timers combo box callbacks
+	if(auto w = widget<ComboBox>("timerModeSwitch"))
+	{
+		w->onConstructItems = [&](std::vector<const void *> & curItems){
+			if(variables["timers"].isNull())
+				return;
+
+			for(auto & p : variables["timers"].Vector())
+			{
+				curItems.push_back(&p);
+			}
+		};
+
+		w->onSetItem = [&](const void * item){
+			if(item)
+			{
+				if(auto * tObj = reinterpret_cast<const JsonNode *>(item))
+				{
+					for(auto wname : (*tObj)["hideWidgets"].Vector())
+					{
+						if(auto w = widget<CIntObject>(wname.String()))
+							w->setEnabled(false);
+					}
+					for(auto wname : (*tObj)["showWidgets"].Vector())
+					{
+						if(auto w = widget<CIntObject>(wname.String()))
+							w->setEnabled(true);
+					}
+					if((*tObj)["default"].isVector())
+					{
+						TurnTimerInfo tinfo;
+						tinfo.baseTimer = (*tObj)["default"].Vector().at(0).Integer() * 1000;
+						tinfo.turnTimer = (*tObj)["default"].Vector().at(1).Integer() * 1000;
+						tinfo.battleTimer = (*tObj)["default"].Vector().at(2).Integer() * 1000;
+						tinfo.creatureTimer = (*tObj)["default"].Vector().at(3).Integer() * 1000;
+						CSH->setTurnTimerInfo(tinfo);
+					}
+				}
+				redraw();
+			}
+		};
+
+		w->getItemText = [this](int idx, const void * item){
+			if(item)
+			{
+				if(auto * tObj = reinterpret_cast<const JsonNode *>(item))
+					return readText((*tObj)["text"]);
+			}
+			return std::string("");
+		};
+
+		w->setItem(0);
+	}
+}
+
+void OptionsTabBase::recreate()
+{
+	auto const & generateSimturnsDurationText = [](int days) -> std::string
+	{
+		if (days == 0)
+			return CGI->generaltexth->translate("core.genrltxt.523");
+
+		if (days >= 1000000) // Not "unlimited" but close enough
+			return CGI->generaltexth->translate("core.turndur.10");
+
+		bool canUseMonth = days % 28 == 0 && days >= 28*2;
+		bool canUseWeek = days % 7 == 0 && days >= 7*2;
+
+		int value = days;
+		std::string text = "vcmi.optionsTab.simturns.days";
+
+		if (canUseWeek && !canUseMonth)
+		{
+			value = days / 7;
+			text = "vcmi.optionsTab.simturns.weeks";
+		}
+
+		if (canUseMonth)
+		{
+			value = days / 28;
+			text = "vcmi.optionsTab.simturns.months";
+		}
+
+		MetaString message;
+		message.appendTextID(Languages::getPluralFormTextID( CGI->generaltexth->getPreferredLanguage(), value, text));
+		message.replaceNumber(value);
+		return message.toString();
+	};
+
+	//Simultaneous turns
+	if(auto turnSlider = widget<CSlider>("simturnsDurationMin"))
+		turnSlider->setValue(SEL->getStartInfo()->simturnsInfo.requiredTurns);
+
+	if(auto turnSlider = widget<CSlider>("simturnsDurationMax"))
+		turnSlider->setValue(SEL->getStartInfo()->simturnsInfo.optionalTurns);
+
+	if(auto w = widget<CLabel>("labelSimturnsDurationValueMin"))
+		w->setText(generateSimturnsDurationText(SEL->getStartInfo()->simturnsInfo.requiredTurns));
+
+	if(auto w = widget<CLabel>("labelSimturnsDurationValueMax"))
+		w->setText(generateSimturnsDurationText(SEL->getStartInfo()->simturnsInfo.optionalTurns));
+
+	if(auto buttonSimturnsAI = widget<CToggleButton>("buttonSimturnsAI"))
+		buttonSimturnsAI->setSelectedSilent(SEL->getStartInfo()->simturnsInfo.allowHumanWithAI);
+
+	const auto & turnTimerRemote = SEL->getStartInfo()->turnTimerInfo;
+
+	//classic timer
+	if(auto turnSlider = widget<CSlider>("sliderTurnDuration"))
+	{
+		if(!variables["timerPresets"].isNull() && !turnTimerRemote.battleTimer && !turnTimerRemote.creatureTimer && !turnTimerRemote.baseTimer)
+		{
+			for(int idx = 0; idx < variables["timerPresets"].Vector().size(); ++idx)
+			{
+				auto & tpreset = variables["timerPresets"].Vector()[idx];
+				if(tpreset.Vector().at(1).Integer() == turnTimerRemote.turnTimer / 1000)
+				{
+					turnSlider->scrollTo(idx);
+					if(auto w = widget<CLabel>("labelTurnDurationValue"))
+						w->setText(CGI->generaltexth->turnDurations[idx]);
+				}
+			}
+		}
+	}
+
+	//chess timer
+	auto timeToString = [](int time) -> std::string
+	{
+		std::stringstream ss;
+		ss << time / 1000 / 60 << ":" << std::setw(2) << std::setfill('0') << time / 1000 % 60;
+		return ss.str();
+	};
+
+	if(auto ww = widget<CTextInput>("chessFieldBase"))
+		ww->setText(timeToString(turnTimerRemote.baseTimer), false);
+	if(auto ww = widget<CTextInput>("chessFieldTurn"))
+		ww->setText(timeToString(turnTimerRemote.turnTimer), false);
+	if(auto ww = widget<CTextInput>("chessFieldBattle"))
+		ww->setText(timeToString(turnTimerRemote.battleTimer), false);
+	if(auto ww = widget<CTextInput>("chessFieldCreature"))
+		ww->setText(timeToString(turnTimerRemote.creatureTimer), false);
+
+	if(auto w = widget<ComboBox>("timerModeSwitch"))
+	{
+		if(turnTimerRemote.battleTimer || turnTimerRemote.creatureTimer || turnTimerRemote.baseTimer)
+		{
+			if(auto turnSlider = widget<CSlider>("sliderTurnDuration"))
+				if(turnSlider->isActive())
+					w->setItem(1);
+		}
+	}
+}

+ 22 - 0
client/lobby/OptionsTabBase.h

@@ -0,0 +1,22 @@
+/*
+ * OptionsTabBase.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 "../gui/InterfaceObjectConfigurable.h"
+#include "../../lib/filesystem/ResourcePath.h"
+
+/// The options tab which is shown at the map selection phase.
+class OptionsTabBase : public InterfaceObjectConfigurable
+{
+public:
+	OptionsTabBase(const JsonPath & configPath);
+
+	void recreate();
+};

+ 18 - 0
client/lobby/TurnOptionsTab.cpp

@@ -0,0 +1,18 @@
+/*
+ * TurnOptionsTab.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 "TurnOptionsTab.h"
+
+TurnOptionsTab::TurnOptionsTab()
+	: OptionsTabBase(JsonPath::builtin("config/widgets/turnOptionsTab.json"))
+{
+
+}

+ 18 - 0
client/lobby/TurnOptionsTab.h

@@ -0,0 +1,18 @@
+/*
+ * TurnOptionsTab.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 "OptionsTabBase.h"
+
+class TurnOptionsTab : public OptionsTabBase
+{
+public:
+	TurnOptionsTab();
+};

+ 35 - 2
client/widgets/Slider.cpp

@@ -69,6 +69,11 @@ int CSlider::getValue() const
 	return value;
 	return value;
 }
 }
 
 
+void CSlider::setValue(int to)
+{
+	scrollTo(value);
+}
+
 int CSlider::getCapacity() const
 int CSlider::getCapacity() const
 {
 {
 	return capacity;
 	return capacity;
@@ -119,7 +124,7 @@ void CSlider::scrollTo(int to)
 
 
 	updateSliderPos();
 	updateSliderPos();
 
 
-	moved(to);
+	moved(getValue());
 }
 }
 
 
 void CSlider::clickPressed(const Point & cursorPosition)
 void CSlider::clickPressed(const Point & cursorPosition)
@@ -164,7 +169,7 @@ bool CSlider::receiveEvent(const Point &position, int eventType) const
 	return testTarget.isInside(position);
 	return testTarget.isInside(position);
 }
 }
 
 
-CSlider::CSlider(Point position, int totalw, std::function<void(int)> Moved, int Capacity, int Amount, int Value, Orientation orientation, CSlider::EStyle style)
+CSlider::CSlider(Point position, int totalw, const std::function<void(int)> & Moved, int Capacity, int Amount, int Value, Orientation orientation, CSlider::EStyle style)
 	: Scrollable(LCLICK | DRAG, position, orientation ),
 	: Scrollable(LCLICK | DRAG, position, orientation ),
 	capacity(Capacity),
 	capacity(Capacity),
 	amount(Amount),
 	amount(Amount),
@@ -297,3 +302,31 @@ void CSlider::scrollToMax()
 {
 {
 	scrollTo(amount);
 	scrollTo(amount);
 }
 }
+
+SliderNonlinear::SliderNonlinear(Point position, int length, const std::function<void(int)> & Moved, const std::vector<int> & values, int Value, Orientation orientation, EStyle style)
+	: CSlider(position, length, Moved, 1, values.size(), Value, orientation, style)
+	, scaledValues(values)
+{
+
+}
+
+int SliderNonlinear::getValue() const
+{
+	return scaledValues.at(CSlider::getValue());
+}
+
+void SliderNonlinear::setValue(int to)
+{
+	size_t nearest = 0;
+
+	for(size_t i = 0; i < scaledValues.size(); ++i)
+	{
+		int nearestDistance = std::abs(to - scaledValues[nearest]);
+		int currentDistance = std::abs(to - scaledValues[i]);
+
+		if(currentDistance < nearestDistance)
+			nearest = i;
+	}
+
+	scrollTo(nearest);
+}

+ 16 - 2
client/widgets/Slider.h

@@ -59,10 +59,11 @@ public:
 
 
 	/// Amount modifier
 	/// Amount modifier
 	void setAmount(int to);
 	void setAmount(int to);
+	virtual void setValue(int to);
 
 
 	/// Accessors
 	/// Accessors
 	int getAmount() const;
 	int getAmount() const;
-	int getValue() const;
+	virtual int getValue() const;
 	int getCapacity() const;
 	int getCapacity() const;
 
 
 	void addCallback(std::function<void(int)> callback);
 	void addCallback(std::function<void(int)> callback);
@@ -80,7 +81,20 @@ public:
 	 /// @param Capacity maximal number of visible at once elements
 	 /// @param Capacity maximal number of visible at once elements
 	 /// @param Amount total amount of elements, including not visible
 	 /// @param Amount total amount of elements, including not visible
 	 /// @param Value starting position
 	 /// @param Value starting position
-	CSlider(Point position, int length, std::function<void(int)> Moved, int Capacity, int Amount,
+	CSlider(Point position, int length, const std::function<void(int)> & Moved, int Capacity, int Amount,
 		int Value, Orientation orientation, EStyle style = BROWN);
 		int Value, Orientation orientation, EStyle style = BROWN);
 	~CSlider();
 	~CSlider();
 };
 };
+
+class SliderNonlinear : public CSlider
+{
+	/// If non-empty then slider has non-linear values, e.g. if slider is at position 5 out of 10 then actual "value" is not 5, but 5th value in this vector
+	std::vector<int> scaledValues;
+
+	using CSlider::setAmount; // make private
+public:
+	void setValue(int to) override;
+	int getValue() const override;
+
+	SliderNonlinear(Point position, int length, const std::function<void(int)> & Moved, const std::vector<int> & values, int Value, Orientation orientation, EStyle style);
+};

+ 5 - 31
config/widgets/optionsTab.json → config/widgets/playerOptionsTab.json

@@ -73,41 +73,16 @@
 			"adoptHeight": true
 			"adoptHeight": true
 		},
 		},
 		
 		
+		// timer
 		{
 		{
-			"name": "simturnsDuration",
-			"type": "slider",
-			"orientation": "horizontal",
-			"position": {"x": 55, "y": 537},
-			"size": 194,
-			"callback": "setSimturnDuration",
-			"itemsVisible": 1,
-			"itemsTotal": 28,
-			"selected": 0,
-			"style": "blue",
-			"scrollBounds": {"x": 0, "y": 0, "w": 194, "h": 32},
-			"panningStep": 20
-		},
-		
-		{
-			"name": "labelSimturnsDurationValue",
 			"type": "label",
 			"type": "label",
 			"font": "small",
 			"font": "small",
 			"alignment": "center",
 			"alignment": "center",
-			"color": "white",
-			"text": "",
-			"position": {"x": 319, "y": 545}
+			"color": "yellow",
+			"text": "core.genrltxt.521",
+			"position": {"x": 222, "y": 544}
 		},
 		},
 		
 		
-		// timer
-		//{
-		//	"type": "label",
-		//	"font": "small",
-		//	"alignment": "center",
-		//	"color": "yellow",
-		//	"text": "core.genrltxt.521",
-		//	"position": {"x": 222, "y": 544}
-		//},
-		
 		{
 		{
 			"name": "labelTurnDurationValue",
 			"name": "labelTurnDurationValue",
 			"type": "label",
 			"type": "label",
@@ -129,8 +104,7 @@
 			"itemsTotal": 11,
 			"itemsTotal": 11,
 			"selected": 11,
 			"selected": 11,
 			"style": "blue",
 			"style": "blue",
-			"scrollBounds": {"x": 0, "y": 0, "w": 194, "h": 32},
-			//"scrollBounds": {"x": -3, "y": -25, "w": 337, "h": 43},
+			"scrollBounds": {"x": -3, "y": -25, "w": 337, "h": 43},
 			"panningStep": 20
 			"panningStep": 20
 		},
 		},
 	],
 	],

+ 285 - 0
config/widgets/turnOptionsTab.json

@@ -0,0 +1,285 @@
+{
+	"customTypes" : {
+		"verticalLayout66" : {
+			"type" : "layout",
+			"vertical" : true,
+			"dynamic" : false,
+			"distance" : 66
+		},
+		"labelTitle" : {
+			"type": "label",
+			"font": "small",
+			"alignment": "left",
+			"color": "yellow"
+		},
+		"labelDescription" : {
+			"type": "multiLineLabel",
+			"font": "tiny",
+			"alignment": "center",
+			"color": "white",
+			"rect": {"x": 0, "y": 0, "w": 300, "h": 35},
+			"adoptHeight": true
+		},
+		"timeInput" : {
+			"type": "textInput",
+			"alignment": "center",
+			"text": "00:00",
+			"rect": {"x": 0, "y": 0, "w": 86, "h": 23},
+			"offset": {"x": 0, "y": 0}
+		},
+		"timeInputBackground" : {
+			"type": "transparentFilledRectangle",
+			"rect": {"x": 0, "y": 0, "w": 86, "h": 23},
+			"color": [0, 0, 0, 128],
+			"colorLine": [64, 80, 128, 128]
+		}
+	},
+	
+	"items":
+	[
+		{
+			"name": "background",
+			"type": "picture",
+			"image": "RANMAPBK",
+			"position": {"x": 0, "y": 6}
+		},
+		
+		{
+			"name": "labelTitle",
+			"type": "label",
+			"font": "big",
+			"alignment": "center",
+			"color": "yellow",
+			"text": "vcmi.optionsTab.turnOptions.hover",
+			"position": {"x": 222, "y": 36}
+		},
+		
+		{
+			"name": "labelSubTitle",
+			"type": "multiLineLabel",
+			"font": "small",
+			"alignment": "center",
+			"color": "white",
+			"text": "vcmi.optionsTab.turnOptions.help",
+			"rect": {"x": 60, "y": 48, "w": 320, "h": 0},
+			"adoptHeight": true
+		},
+		{
+			"type": "texture",
+			"image": "DiBoxBck",
+			"color" : "blue", 
+			"rect": {"x" : 64, "y" : 394, "w": 316, "h": 124}
+		},
+		{
+			"type": "transparentFilledRectangle",
+			"rect": {"x" : 64, "y" : 394, "w": 316, "h": 124},
+			"color": [0, 0, 0, 0],
+			"colorLine": [64, 80, 128, 128]
+		},
+		{
+			"type": "transparentFilledRectangle",
+			"rect": {"x" : 65, "y" : 416, "w": 314, "h": 1},
+			"color": [0, 0, 0, 0],
+			"colorLine": [80, 96, 160, 128]
+		},
+		{
+			"type": "transparentFilledRectangle",
+			"rect": {"x" : 65, "y" : 417, "w": 314, "h": 1},
+			"color": [0, 0, 0, 0],
+			"colorLine": [32, 40, 128, 128]
+		},
+		{
+			"type": "transparentFilledRectangle",
+			"rect": {"x" : 65, "y" : 466, "w": 314, "h": 1},
+			"color": [0, 0, 0, 0],
+			"colorLine": [80, 96, 160, 128]
+		},
+		{
+			"type": "transparentFilledRectangle",
+			"rect": {"x" : 65, "y" : 467, "w": 314, "h": 1},
+			"color": [0, 0, 0, 0],
+			"colorLine": [32, 40, 128, 128]
+		},
+		{
+			"type" : "verticalLayout66",
+			"customType" : "labelTitle",
+			"position": {"x": 70, "y": 134},
+			"items":
+			[
+				{
+					"text": "vcmi.optionsTab.chessFieldBase.hover"
+				},
+				{
+					"text": "vcmi.optionsTab.chessFieldTurn.hover"
+				},
+				{
+					"text": "vcmi.optionsTab.chessFieldBattle.hover"
+				},
+				{
+					"text": "vcmi.optionsTab.chessFieldCreature.hover"
+				},
+				{
+					"text": "vcmi.optionsTab.simturns"
+				}
+			]
+		},
+		
+		{
+			"type" : "verticalLayout66",
+			"customType" : "labelDescription",
+			"position": {"x": 70, "y": 155},
+			"items":
+			[
+				{
+					"text": "vcmi.optionsTab.chessFieldBase.help"
+				},
+				{
+					"text": "vcmi.optionsTab.chessFieldTurn.help"
+				},
+				{
+					"text": "vcmi.optionsTab.chessFieldBattle.help"
+				},
+				{
+					"text": "vcmi.optionsTab.chessFieldCreature.help"
+				}
+			]
+		},
+
+		{
+			"type" : "verticalLayout66",
+			"customType" : "timeInputBackground",
+			"position": {"x": 294, "y": 129},
+			"items":
+			[
+				{},
+				{},
+				{},
+				{}
+			]
+		},
+
+		{
+			"type" : "verticalLayout66",
+			"customType" : "timeInput",
+			"position": {"x": 294, "y": 129},
+			"items":
+			[
+				{
+					"name": "chessFieldBase",
+					"callback": "parseAndSetTimer_base",
+					"help": "vcmi.optionsTab.chessFieldBase.help"
+				},
+				{
+					"name": "chessFieldTurn",
+					"callback": "parseAndSetTimer_turn",
+					"help": "vcmi.optionsTab.chessFieldTurn.help"
+				},
+				{
+					"name": "chessFieldBattle",
+					"callback": "parseAndSetTimer_battle",
+					"help": "vcmi.optionsTab.chessFieldBattle.help"
+				},
+				{
+					"name": "chessFieldCreature",
+					"callback": "parseAndSetTimer_creature",
+					"help": "vcmi.optionsTab.chessFieldCreature.help"
+				}
+			]
+		},
+
+		{
+			"type": "label",
+			"font": "small",
+			"alignment": "left",
+			"color": "white",
+			"text": "vcmi.optionsTab.simturnsMin.hover",
+			"position": {"x": 70, "y": 420}
+		},
+		{
+			"type": "label",
+			"font": "small",
+			"alignment": "left",
+			"color": "white",
+			"text": "vcmi.optionsTab.simturnsMax.hover",
+			"position": {"x": 70, "y": 470}
+		},
+
+		{
+			"name": "simturnsDurationMin",
+			"type": "slider",
+			"orientation": "horizontal",
+			"position": {"x": 178, "y": 420},
+			"size": 200,
+			"callback": "setSimturnDurationMin",
+			"items": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 21, 28, 35, 42, 49, 56, 84, 112, 140, 168 ],
+			"selected": 0,
+			"style": "blue",
+			"scrollBounds": {"x": 0, "y": 0, "w": 194, "h": 32},
+			"panningStep": 20
+		},
+		{
+			"name": "simturnsDurationMax",
+			"type": "slider",
+			"orientation": "horizontal",
+			"position": {"x": 178, "y": 470},
+			"size": 200,
+			"callback": "setSimturnDurationMax",
+			"items": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 21, 28, 35, 42, 49, 56, 84, 112, 140, 168, 1000000 ],
+			"selected": 0,
+			"style": "blue",
+			"scrollBounds": {"x": 0, "y": 0, "w": 194, "h": 32},
+			"panningStep": 20
+		},
+		{
+			"name": "labelSimturnsDurationValueMin",
+			"type": "label",
+			"font": "small",
+			"alignment": "center",
+			"color": "white",
+			"text": "",
+			"position": {"x": 278, "y": 428}
+		},
+		{
+			"name": "labelSimturnsDurationValueMax",
+			"type": "label",
+			"font": "small",
+			"alignment": "center",
+			"color": "white",
+			"text": "",
+			"position": {"x": 278, "y": 478}
+		},
+		{
+			"type" : "label",
+			"text": "vcmi.optionsTab.simturnsMin.help",
+			"type": "multiLineLabel",
+			"font": "tiny",
+			"alignment": "center",
+			"color": "white",
+			"rect": {"x": 70, "y": 430, "w": 300, "h": 40}
+		},
+		{
+			"type" : "label",
+			"text": "vcmi.optionsTab.simturnsMax.help",
+			"type": "multiLineLabel",
+			"font": "tiny",
+			"alignment": "center",
+			"color": "white",
+			"rect": {"x": 70, "y": 480, "w": 300, "h": 40}
+		},
+		{
+			"name": "buttonSimturnsAI",
+			"position": {"x": 70, "y": 535},
+			"type": "toggleButton",
+			"image": "lobby/checkbox"
+		},
+		{
+			"name": "labelSimturnsAI",
+			"type": "label",
+			"font": "small",
+			"alignment": "left",
+			"color": "yellow",
+			"text": "vcmi.optionsTab.simturnsAI.hover",
+			"position": {"x": 110, "y": 540}
+		}
+	]
+}

+ 81 - 21
lib/Languages.h

@@ -12,6 +12,17 @@
 namespace Languages
 namespace Languages
 {
 {
 
 
+enum class EPluralForms
+{
+	NONE,
+	VI_1, // Single plural form, (Vietnamese)
+	EN_2, // Two forms, singular used for one only (English)
+	FR_2, // Two forms, singular used for zero and one (French)
+	UK_3, // Three forms, special cases for numbers ending in 1 and 2, 3, 4, except those ending in 1[1-4] (Ukrainian)
+	CZ_3, // Three forms, special cases for 1 and 2, 3, 4 (Czech)
+	PL_3, // Three forms, special case for one and some numbers ending in 2, 3, or 4 (Polish)
+};
+
 enum class ELanguages
 enum class ELanguages
 {
 {
 	CZECH,
 	CZECH,
@@ -57,6 +68,9 @@ struct Options
 	/// primary IETF language tag
 	/// primary IETF language tag
 	std::string tagIETF;
 	std::string tagIETF;
 
 
+	/// Ruleset for plural forms in this language
+	EPluralForms pluralForms = EPluralForms::NONE;
+
 	/// VCMI supports translations into this language
 	/// VCMI supports translations into this language
 	bool hasTranslation = false;
 	bool hasTranslation = false;
 };
 };
@@ -65,27 +79,27 @@ inline const auto & getLanguageList()
 {
 {
 	static const std::array<Options, 20> languages
 	static const std::array<Options, 20> languages
 	{ {
 	{ {
-		{ "czech",      "Czech",      "Čeština",    "CP1250", "cs", true },
-		{ "chinese",    "Chinese",    "简体中文",       "GBK",    "zh", true }, // Note: actually Simplified Chinese
-		{ "english",    "English",    "English",    "CP1252", "en", true },
-		{ "finnish",    "Finnish",    "Suomi",      "CP1252", "fi", true },
-		{ "french",     "French",     "Français",   "CP1252", "fr", true },
-		{ "german",     "German",     "Deutsch",    "CP1252", "de", true },
-		{ "hungarian",  "Hungarian",  "Magyar",     "CP1250", "hu", true },
-		{ "italian",    "Italian",    "Italiano",   "CP1250", "it", true },
-		{ "korean",     "Korean",     "한국어",        "CP949",  "ko", true },
-		{ "polish",     "Polish",     "Polski",     "CP1250", "pl", true },
-		{ "portuguese", "Portuguese", "Português",  "CP1252", "pt", true }, // Note: actually Brazilian Portuguese
-		{ "russian",    "Russian",    "Русский",    "CP1251", "ru", true },
-		{ "spanish",    "Spanish",    "Español",    "CP1252", "es", true },
-		{ "swedish",    "Swedish",    "Svenska",    "CP1252", "sv", true },
-		{ "turkish",    "Turkish",    "Türkçe",     "CP1254", "tr", true },
-		{ "ukrainian",  "Ukrainian",  "Українська", "CP1251", "uk", true },
-		{ "vietnamese",  "Vietnamese",  "Tiếng Việt", "UTF-8", "vi", true }, // Fan translation uses special encoding
-
-		{ "other_cp1250", "Other (East European)",   "", "CP1250", "", false },
-		{ "other_cp1251", "Other (Cyrillic Script)", "", "CP1251", "", false },
-		{ "other_cp1252", "Other (West European)",   "", "CP1252", "", false }
+		{ "czech",       "Czech",       "Čeština",    "CP1250", "cs", EPluralForms::CZ_3, true },
+		{ "chinese",     "Chinese",     "简体中文",       "GBK",    "zh", EPluralForms::VI_1, true }, // Note: actually Simplified Chinese
+		{ "english",     "English",     "English",    "CP1252", "en", EPluralForms::EN_2, true },
+		{ "finnish",     "Finnish",     "Suomi",      "CP1252", "fi", EPluralForms::EN_2, true },
+		{ "french",      "French",      "Français",   "CP1252", "fr", EPluralForms::FR_2, true },
+		{ "german",      "German",      "Deutsch",    "CP1252", "de", EPluralForms::EN_2, true },
+		{ "hungarian",   "Hungarian",   "Magyar",     "CP1250", "hu", EPluralForms::EN_2, true },
+		{ "italian",     "Italian",     "Italiano",   "CP1250", "it", EPluralForms::EN_2, true },
+		{ "korean",      "Korean",      "한국어",        "CP949",  "ko", EPluralForms::VI_1, true },
+		{ "polish",      "Polish",      "Polski",     "CP1250", "pl", EPluralForms::PL_3, true },
+		{ "portuguese",  "Portuguese",  "Português",  "CP1252", "pt", EPluralForms::EN_2, true }, // Note: actually Brazilian Portuguese
+		{ "russian",     "Russian",     "Русский",    "CP1251", "ru", EPluralForms::UK_3, true },
+		{ "spanish",     "Spanish",     "Español",    "CP1252", "es", EPluralForms::EN_2, true },
+		{ "swedish",     "Swedish",     "Svenska",    "CP1252", "sv", EPluralForms::EN_2, true },
+		{ "turkish",     "Turkish",     "Türkçe",     "CP1254", "tr", EPluralForms::EN_2, true },
+		{ "ukrainian",   "Ukrainian",   "Українська", "CP1251", "uk", EPluralForms::UK_3, true },
+		{ "vietnamese",  "Vietnamese",  "Tiếng Việt", "UTF-8",  "vi", EPluralForms::VI_1, true }, // Fan translation uses special encoding
+
+		{ "other_cp1250", "Other (East European)",   "", "CP1250", "", EPluralForms::NONE, false },
+		{ "other_cp1251", "Other (Cyrillic Script)", "", "CP1251", "", EPluralForms::NONE, false },
+		{ "other_cp1252", "Other (West European)",   "", "CP1252", "", EPluralForms::NONE, false }
 	} };
 	} };
 	static_assert(languages.size() == static_cast<size_t>(ELanguages::COUNT), "Languages array is missing a value!");
 	static_assert(languages.size() == static_cast<size_t>(ELanguages::COUNT), "Languages array is missing a value!");
 
 
@@ -109,4 +123,50 @@ inline const Options & getLanguageOptions(const std::string & language)
 	return emptyValue;
 	return emptyValue;
 }
 }
 
 
+template<typename Numeric>
+inline constexpr int getPluralFormIndex(EPluralForms form, Numeric value)
+{
+	// Based on https://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html
+	switch(form)
+	{
+		case EPluralForms::NONE:
+		case EPluralForms::VI_1:
+			return 0;
+		case EPluralForms::EN_2:
+			if (value == 1)
+				return 1;
+			return 2;
+		case EPluralForms::FR_2:
+			if (value == 1 || value == 0)
+				return 1;
+			return 2;
+		case EPluralForms::UK_3:
+			if (value % 10 == 1 && value % 100 != 11)
+				return 1;
+			if (value%10>=2 && value%10<=4 && (value%100<10 || value%100>=20))
+				return 2;
+			return 0;
+		case EPluralForms::CZ_3:
+			if (value == 1)
+				return 1;
+			if (value>=2 && value<=4)
+				return 2;
+			return 0;
+		case EPluralForms::PL_3:
+			if (value == 1)
+				return 1;
+			if (value%10>=2 && value%10<=4 && (value%100<10 || value%100>=20))
+				return 2;
+			return 0;
+	}
+	throw std::runtime_error("Invalid plural form enumeration received!");
+}
+
+template<typename Numeric>
+inline std::string getPluralFormTextID(std::string languageName, Numeric value, std::string textID)
+{
+	int formIndex = getPluralFormIndex(getLanguageOptions(languageName).pluralForms, value);
+	return textID + '.' + std::to_string(formIndex);
+}
+
 }
 }

+ 1 - 1
lib/StartInfo.h

@@ -30,7 +30,7 @@ struct DLL_LINKAGE SimturnsInfo
 	/// Maximum number of turns that might be played simultaneously unless contact is detected
 	/// Maximum number of turns that might be played simultaneously unless contact is detected
 	int optionalTurns = 0;
 	int optionalTurns = 0;
 	/// If set to true, human and 1 AI can act at the same time
 	/// If set to true, human and 1 AI can act at the same time
-	bool allowHumanWithAI = true;
+	bool allowHumanWithAI = false;
 
 
 	template <typename Handler>
 	template <typename Handler>
 	void serialize(Handler &h, const int version)
 	void serialize(Handler &h, const int version)