diff --git a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt index 40617a7024..be791c179d 100644 --- a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt +++ b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt @@ -367,15 +367,24 @@ class CivilizationInfo { fun isAtWar() = diplomacy.values.any { it.diplomaticStatus == DiplomaticStatus.War && !it.otherCiv().isDefeated() } + /** + * Returns a civilization caption suitable for greetings including player type info: + * Like "Milan" if the nation is a city state, "Caesar of Rome" otherwise, with an added + * " (AI)", " (Human - Hotseat)", or " (Human - Multiplayer)" if the game is multiplayer. + */ fun getLeaderDisplayName(): String { + val severalHumans = gameInfo.civilizations.count { it.playerType == PlayerType.Human } > 1 + val online = gameInfo.gameParameters.isOnlineMultiplayer return nation.getLeaderDisplayName().tr() + when { + !online && !severalHumans -> + "" // offline single player will know everybody else is AI playerType == PlayerType.AI -> " (" + "AI".tr() + ")" - gameInfo.civilizations.count { it.playerType == PlayerType.Human } > 1 -> - " (" + "Human".tr() + " - " + "Hotseat".tr() + ")" - else -> + online -> " (" + "Human".tr() + " - " + "Multiplayer".tr() + ")" + else -> + " (" + "Human".tr() + " - " + "Hotseat".tr() + ")" } } diff --git a/core/src/com/unciv/ui/trade/DiplomacyScreen.kt b/core/src/com/unciv/ui/trade/DiplomacyScreen.kt index f8a5acaabd..cc27ab0e7d 100644 --- a/core/src/com/unciv/ui/trade/DiplomacyScreen.kt +++ b/core/src/com/unciv/ui/trade/DiplomacyScreen.kt @@ -49,7 +49,7 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() { closeButton.labelCell.pad(10f) closeButton.pack() closeButton.y = stage.height - closeButton.height - 10 - closeButton.x = 10f + closeButton.x = (stage.width * 0.2f - closeButton.width) / 2 // center, leftSideTable.width not known yet stage.addActor(closeButton) // This must come after the split pane so it will be above, that the button will be clickable } @@ -99,11 +99,7 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() { val diplomacyTable = Table() diplomacyTable.defaults().pad(10f) - val displayNameTable = Table() - displayNameTable.add(ImageGetter.getNationIndicator(otherCiv.nation, 24f)) - .pad(0f, 0f, 5f, 10f) - displayNameTable.add(otherCiv.getLeaderDisplayName().toLabel(fontSize = 24)) - diplomacyTable.add(displayNameTable).row() + diplomacyTable.add(LeaderIntroTable(otherCiv)).row() diplomacyTable.add("{Type}: {${otherCiv.cityStateType}}".toLabel()).row() diplomacyTable.add("{Personality}: {${otherCiv.cityStatePersonality}}".toLabel()).row() @@ -248,19 +244,10 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() { val diplomacyTable = Table() diplomacyTable.defaults().pad(10f) - val displayNameTable = Table() - displayNameTable.add(ImageGetter.getNationIndicator(otherCiv.nation, 24f)).pad(0f, 0f, 5f, 5f) - displayNameTable.add(otherCiv.getLeaderDisplayName().toLabel(fontSize = 24)).row() - val helloText = if (otherCivDiplomacyManager.relationshipLevel() <= RelationshipLevel.Enemy) otherCiv.nation.hateHello - else otherCiv.nation.neutralHello - displayNameTable.add(helloText.toLabel()).colspan(2) - - val leaderIntroTable = Table() - val leaderPortraitImage = "LeaderIcons/" + otherCiv.nation.leaderName - if (ImageGetter.imageExists(leaderPortraitImage)) - leaderIntroTable.add(ImageGetter.getImage(leaderPortraitImage)).size(100f).padRight(10f) - leaderIntroTable.add(displayNameTable) - + val helloText = if (otherCivDiplomacyManager.relationshipLevel() <= RelationshipLevel.Enemy) + otherCiv.nation.hateHello + else otherCiv.nation.neutralHello + val leaderIntroTable = LeaderIntroTable(otherCiv, helloText) diplomacyTable.add(leaderIntroTable).row() diplomacyTable.addSeparator() @@ -471,12 +458,17 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() { private fun setRightSideFlavorText(otherCiv: CivilizationInfo, flavorText: String, response: String) { val diplomacyTable = Table() diplomacyTable.defaults().pad(10f) - diplomacyTable.add(otherCiv.getLeaderDisplayName().toLabel()) + diplomacyTable.add(LeaderIntroTable(otherCiv)) diplomacyTable.addSeparator() diplomacyTable.add(flavorText.toLabel()).row() val responseButton = response.toTextButton() - responseButton.onClick { updateRightSide(otherCiv) } + val action = { + keyPressDispatcher.remove(KeyCharAndCode.SPACE) + updateRightSide(otherCiv) + } + responseButton.onClick(action) + keyPressDispatcher[KeyCharAndCode.SPACE] = action diplomacyTable.add(responseButton) rightSideTable.clear() diff --git a/core/src/com/unciv/ui/trade/LeaderIntroTable.kt b/core/src/com/unciv/ui/trade/LeaderIntroTable.kt new file mode 100644 index 0000000000..f32ad7bf74 --- /dev/null +++ b/core/src/com/unciv/ui/trade/LeaderIntroTable.kt @@ -0,0 +1,49 @@ +package com.unciv.ui.trade + +import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.badlogic.gdx.utils.Align +import com.unciv.logic.civilization.CivilizationInfo +import com.unciv.ui.utils.CameraStageBaseScreen +import com.unciv.ui.utils.ImageGetter +import com.unciv.ui.utils.toLabel + +/** + * This is meant to be used for any kind of civ introduction - [DiplomacyScreen], + * [AlertPopup][com.unciv.ui.worldscreen.AlertPopup] [types][com.unciv.logic.civilization.AlertType] WarDeclaration, FirstContact etc. + * + * @param civInfo The civilization to display + * @param hello Optional additional message + */ +class LeaderIntroTable ( + civInfo: CivilizationInfo, + hello: String = "" +): Table(CameraStageBaseScreen.skin) { + /** + * Build either a Table(icon, leaderName
hello) or + * a Table(Portrait, Table(leaderName, icon
hello)) + * + * City states in vanilla have leaderName=="" - but don't test CS, test leaderName to allow modding CS to have portraits + */ + init { + defaults().align(Align.center) + val nation = civInfo.nation + val leaderPortraitFile = "LeaderIcons/" + nation.leaderName + val leaderLabel = civInfo.getLeaderDisplayName().toLabel(fontSize = 24) + val nationIndicator = ImageGetter.getNationIndicator(nation, 24f) + if (nation.leaderName.isNotEmpty() && ImageGetter.imageExists(leaderPortraitFile)) { + val nameTable = Table() + nameTable.add(leaderLabel) + nameTable.add(nationIndicator).pad(0f, 10f, 5f, 0f).row() + if (hello.isNotEmpty()) + nameTable.add(hello.toLabel()).colspan(2) + add(ImageGetter.getImage(leaderPortraitFile)).size(100f) + .padRight(10f) + add(nameTable) + } else { + add(nationIndicator).pad(0f, 0f, 5f, 10f) + add(leaderLabel).row() + if (hello.isNotEmpty()) + add(hello.toLabel()).colspan(2) + } + } +} \ No newline at end of file diff --git a/core/src/com/unciv/ui/worldscreen/AlertPopup.kt b/core/src/com/unciv/ui/worldscreen/AlertPopup.kt index a9afe5c5c6..4720f2dd97 100644 --- a/core/src/com/unciv/ui/worldscreen/AlertPopup.kt +++ b/core/src/com/unciv/ui/worldscreen/AlertPopup.kt @@ -8,22 +8,41 @@ import com.unciv.logic.civilization.AlertType import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.PopupAlert import com.unciv.models.translations.fillPlaceholders +import com.unciv.ui.trade.LeaderIntroTable import com.unciv.ui.utils.* +/** + * [Popup] communicating events other than trade offers to the player. + * (e.g. First Contact, Wonder built, Tech researched,...) + * + * Called in [WorldScreen].update, which pulls them from viewingCiv.popupAlerts. + * + * @param worldScreen The parent screen + * @param popupAlert The [PopupAlert] entry to present + * + * @see AlertType + */ class AlertPopup(val worldScreen: WorldScreen, val popupAlert: PopupAlert): Popup(worldScreen) { - fun getCloseButton(text: String, action: (() -> Unit)? = null): TextButton { + fun getCloseButton(text: String, key: Char = Char.MIN_VALUE, action: (() -> Unit)? = null): TextButton { + // Popup.addCloseButton is close but AlertPopup needs the flexibility to add these inside a wrapper val button = text.toTextButton() - button.onClick { + val buttonAction = { if (action != null) action() worldScreen.shouldUpdate = true close() } + button.onClick(buttonAction) + if (key == Char.MIN_VALUE) { + keyPressDispatcher[KeyCharAndCode.BACK] = buttonAction + keyPressDispatcher[KeyCharAndCode.SPACE] = buttonAction + } else { + keyPressDispatcher[key] = buttonAction + } return button } fun addLeaderName(civInfo: CivilizationInfo) { - val otherCivLeaderName = civInfo.getLeaderDisplayName() - add(otherCivLeaderName.toLabel()) + add(LeaderIntroTable(civInfo)) addSeparator() } @@ -35,6 +54,7 @@ class AlertPopup(val worldScreen: WorldScreen, val popupAlert: PopupAlert): Popu addLeaderName(civInfo) addGoodSizedLabel(civInfo.nation.declaringWar).row() val responseTable = Table() + responseTable.defaults().pad(0f, 5f) responseTable.add(getCloseButton("You'll pay for this!")) responseTable.add(getCloseButton("Very well.")) add(responseTable) @@ -48,12 +68,11 @@ class AlertPopup(val worldScreen: WorldScreen, val popupAlert: PopupAlert): Popu AlertType.FirstContact -> { val civInfo = worldScreen.gameInfo.getCivilization(popupAlert.value) val nation = civInfo.nation + addLeaderName(civInfo) if (civInfo.isCityState()) { - addLeaderName(civInfo) addGoodSizedLabel("We have encountered the City-State of [${nation.name}]!").row() add(getCloseButton("Excellent!")) } else { - addLeaderName(civInfo) addGoodSizedLabel(nation.introduction).row() add(getCloseButton("A pleasure to meet you.")) } @@ -67,55 +86,67 @@ class AlertPopup(val worldScreen: WorldScreen, val popupAlert: PopupAlert): Popu if (city.foundingCiv != "" && city.civInfo.civName != city.foundingCiv // can't liberate if the city actually belongs to those guys && conqueringCiv.civName != city.foundingCiv) { // or belongs originally to us - add("Liberate (city returns to [originalOwner])".fillPlaceholders(city.foundingCiv).toTextButton().onClick { + val liberateAction = { city.liberateCity(conqueringCiv) worldScreen.shouldUpdate = true close() - }).row() + } + val liberateText = "Liberate (city returns to [originalOwner])".fillPlaceholders(city.foundingCiv) + add(liberateText.toTextButton().onClick(function = liberateAction)).row() + keyPressDispatcher['l'] = liberateAction addGoodSizedLabel("Liberating a city returns it to its original owner, giving you a massive relationship boost with them!") addSeparator() } if (conqueringCiv.isOneCityChallenger()) { - add("Destroy".toTextButton().onClick { + val destroyAction = { city.puppetCity(conqueringCiv) city.destroyCity() worldScreen.shouldUpdate = true close() - }).row() + } + add("Destroy".toTextButton().onClick(function = destroyAction)).row() + keyPressDispatcher['d'] = destroyAction addGoodSizedLabel("Destroying the city instantly razes the city to the ground.").row() } else { - add("Annex".toTextButton().onClick { + val annexAction = { city.puppetCity(conqueringCiv) city.annexCity() worldScreen.shouldUpdate = true close() - }).row() + } + add("Annex".toTextButton().onClick(function = annexAction)).row() + keyPressDispatcher['a'] = annexAction addGoodSizedLabel("Annexed cities become part of your regular empire.").row() addGoodSizedLabel("Their citizens generate 2x the unhappiness, unless you build a courthouse.").row() addSeparator() - add("Puppet".toTextButton().onClick { + val puppetAction = { city.puppetCity(conqueringCiv) worldScreen.shouldUpdate = true close() - }).row() + } + add("Puppet".toTextButton().onClick(function = puppetAction) ).row() + keyPressDispatcher['p'] = puppetAction addGoodSizedLabel("Puppeted cities do not increase your tech or policy cost, but their citizens generate 1.5x the regular unhappiness.").row() addGoodSizedLabel("You have no control over the the production of puppeted cities.").row() addGoodSizedLabel("Puppeted cities also generate 25% less Gold and Science.").row() addGoodSizedLabel("A puppeted city can be annexed at any time.").row() addSeparator() - + val razeAction = { + city.puppetCity(conqueringCiv) + city.annexCity() + city.isBeingRazed = true + worldScreen.shouldUpdate = true + close() + } add("Raze".toTextButton().apply { if (city.isOriginalCapital) disable() - else onClick { - city.puppetCity(conqueringCiv) - city.annexCity() - city.isBeingRazed = true - worldScreen.shouldUpdate = true - close() - } + else { + onClick(function = razeAction) + keyPressDispatcher['r'] = razeAction + } }).row() addGoodSizedLabel("Razing the city annexes it, and starts razing the city to the ground.").row() addGoodSizedLabel("The population will gradually dwindle until the city is destroyed.").row() @@ -126,6 +157,7 @@ class AlertPopup(val worldScreen: WorldScreen, val popupAlert: PopupAlert): Popu addLeaderName(civInfo) addGoodSizedLabel("Remove your troops in our border immediately!").row() val responseTable = Table() + responseTable.defaults().pad(0f, 5f) responseTable.add(getCloseButton("Sorry.")) responseTable.add(getCloseButton("Never!")) add(responseTable) @@ -135,10 +167,10 @@ class AlertPopup(val worldScreen: WorldScreen, val popupAlert: PopupAlert): Popu val playerDiploManager = worldScreen.viewingCiv.getDiplomacyManager(otherciv) addLeaderName(otherciv) addGoodSizedLabel("Please don't settle new cities near us.").row() - add(getCloseButton("Very well, we shall look for new lands to settle.") { + add(getCloseButton("Very well, we shall look for new lands to settle.", 'y') { playerDiploManager.agreeNotToSettleNear() }).row() - add(getCloseButton("We shall do as we please.") { + add(getCloseButton("We shall do as we please.", 'n') { playerDiploManager.refuseDemandNotToSettleNear() }).row() } @@ -184,8 +216,8 @@ class AlertPopup(val worldScreen: WorldScreen, val popupAlert: PopupAlert): Popu val playerDiploManager = worldScreen.viewingCiv.getDiplomacyManager(otherciv) addLeaderName(otherciv) addGoodSizedLabel("My friend, shall we declare our friendship to the world?").row() - add(getCloseButton("We are not interested.")).row() - add(getCloseButton("Declare Friendship ([30] turns)") { + add(getCloseButton("We are not interested.", 'n')).row() + add(getCloseButton("Declare Friendship ([30] turns)", 'y') { playerDiploManager.signDeclarationOfFriendship() }).row() } @@ -196,4 +228,4 @@ class AlertPopup(val worldScreen: WorldScreen, val popupAlert: PopupAlert): Popu worldScreen.viewingCiv.popupAlerts.remove(popupAlert) super.close() } -} \ No newline at end of file +} diff --git a/core/src/com/unciv/ui/worldscreen/TradePopup.kt b/core/src/com/unciv/ui/worldscreen/TradePopup.kt index a451707d7e..40132737ec 100644 --- a/core/src/com/unciv/ui/worldscreen/TradePopup.kt +++ b/core/src/com/unciv/ui/worldscreen/TradePopup.kt @@ -1,5 +1,6 @@ package com.unciv.ui.worldscreen +import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.scenes.scene2d.ui.Table import com.unciv.Constants import com.unciv.logic.civilization.NotificationIcon @@ -10,13 +11,26 @@ import com.unciv.logic.trade.TradeOffer import com.unciv.logic.trade.TradeType import com.unciv.models.translations.tr import com.unciv.ui.trade.DiplomacyScreen -import com.unciv.ui.utils.Popup -import com.unciv.ui.utils.disable -import com.unciv.ui.utils.toLabel +import com.unciv.ui.trade.LeaderIntroTable +import com.unciv.ui.utils.* import kotlin.math.max import kotlin.math.min import com.unciv.ui.utils.AutoScrollPane as ScrollPane +/* TODO: + different Notification wording for peace treaties? + Let Notification jump to Diplomacy.trade with empty offers (accepted) + or a clone of the rejected offer (denied) ...? + Unify separator implementations (here, Table.addSeparator, ModManagementScreen) +*/ + +/** + * [Popup] communicating trade offers of others to the player. + * + * Called in [WorldScreen].update, which checks if there are any in viewingCiv.tradeRequests. + * + * @param worldScreen The parent screen + */ class TradePopup(worldScreen: WorldScreen): Popup(worldScreen){ val viewingCiv = worldScreen.viewingCiv val tradeRequest = viewingCiv.tradeRequests.first() @@ -24,14 +38,16 @@ class TradePopup(worldScreen: WorldScreen): Popup(worldScreen){ init{ val requestingCiv = worldScreen.gameInfo.getCivilization(tradeRequest.requestingCiv) val nation = requestingCiv.nation - val otherCivLeaderName = "[${nation.leaderName}] of [${nation.name}]".tr() - add(otherCivLeaderName.toLabel()) + val leaderIntroTable = LeaderIntroTable(requestingCiv) + add(leaderIntroTable) addSeparator() val trade = tradeRequest.trade val tradeOffersTable = Table().apply { defaults().pad(10f) } tradeOffersTable.add("[${nation.name}]'s trade offer".toLabel()) + // empty column to separate offers columns better + tradeOffersTable.add().pad(0f, 15f) tradeOffersTable.add("Our trade offer".toLabel()) tradeOffersTable.row() val ourResources = viewingCiv.getCivResourcesByName() @@ -43,11 +59,12 @@ class TradePopup(worldScreen: WorldScreen): Popup(worldScreen){ return tradeText } - for(i in 0..max(trade.theirOffers.lastIndex, trade.ourOffers.lastIndex)){ - if(trade.theirOffers.lastIndex>=i) tradeOffersTable.add(getOfferText(trade.theirOffers[i]).toLabel()) - else tradeOffersTable.add() - if(trade.ourOffers.lastIndex>=i) tradeOffersTable.add(getOfferText(trade.ourOffers[i]).toLabel()) - else tradeOffersTable.add() + for (i in 0..max(trade.theirOffers.lastIndex, trade.ourOffers.lastIndex)) { + if (trade.theirOffers.lastIndex < i) tradeOffersTable.add() + else tradeOffersTable.add(getOfferText(trade.theirOffers[i]).toLabel()) + tradeOffersTable.add() + if (trade.ourOffers.lastIndex < i) tradeOffersTable.add() + else tradeOffersTable.add(getOfferText(trade.ourOffers[i]).toLabel()) tradeOffersTable.row() } tradeOffersTable.pack() @@ -55,25 +72,18 @@ class TradePopup(worldScreen: WorldScreen): Popup(worldScreen){ val scrollHeight = min(tradeOffersTable.height, worldScreen.stage.height/2) add(ScrollPane(tradeOffersTable)).height(scrollHeight).row() - addGoodSizedLabel(nation.tradeRequest).colspan(2).row() + val separator = Table() + separator.background = ImageGetter.getBackground(Color.DARK_GRAY) + add(separator).minHeight(1f).fillX().row() - val soundsGoodButton = addButton("Sounds good!"){ + addGoodSizedLabel(nation.tradeRequest).pad(15f).row() + + val soundsGoodButton = addButton("Sounds good!", 'y') { val tradeLogic = TradeLogic(viewingCiv, requestingCiv) tradeLogic.currentTrade.set(trade) tradeLogic.acceptTrade() close() - Popup(worldScreen).apply { - add(otherCivLeaderName.toLabel()).colspan(2) - addSeparator() - addGoodSizedLabel("Excellent!").row() - addButton("Farewell."){ - close() - worldScreen.shouldUpdate=true - // in all cases, worldScreen.shouldUpdate should be set to true when we remove the last of the popups - // in order for the next trade to appear immediately - } - open() - } + TradeThanksPopup(leaderIntroTable, worldScreen) requestingCiv.addNotification("[${viewingCiv.civName}] has accepted your trade request", viewingCiv.civName, NotificationIcon.Trade) } @@ -81,30 +91,30 @@ class TradePopup(worldScreen: WorldScreen): Popup(worldScreen){ if(!TradeEvaluation().isTradeValid(trade,viewingCiv,requestingCiv)) soundsGoodButton.actor.disable() - addButton("Not this time.".tr()){ + addButton("Not this time.", 'n') { val diplomacyManager = requestingCiv.getDiplomacyManager(viewingCiv) - if(trade.ourOffers.all { it.type==TradeType.Luxury_Resource } && trade.theirOffers.all { it.type==TradeType.Luxury_Resource }) + if(trade.ourOffers.all { it.type == TradeType.Luxury_Resource } && trade.theirOffers.all { it.type==TradeType.Luxury_Resource }) diplomacyManager.setFlag(DiplomacyFlags.DeclinedLuxExchange,20) // offer again in 20 turns - if(trade.ourOffers.any { it.name==Constants.researchAgreement }) + if(trade.ourOffers.any { it.name == Constants.researchAgreement }) diplomacyManager.setFlag(DiplomacyFlags.DeclinedResearchAgreement,20) // offer again in 20 turns - - if(trade.ourOffers.any{ it.type==TradeType.Treaty && it.name== Constants.peaceTreaty }) + if(trade.ourOffers.any { it.type == TradeType.Treaty && it.name == Constants.peaceTreaty }) diplomacyManager.setFlag(DiplomacyFlags.DeclinedPeace,5) close() requestingCiv.addNotification("[${viewingCiv.civName}] has denied your trade request", viewingCiv.civName, NotificationIcon.Trade) - worldScreen.shouldUpdate=true + worldScreen.shouldUpdate = true } - addButton("How about something else...".tr()){ + + addButton("How about something else...", 'e') { close() - val diplomacyScreen= DiplomacyScreen(viewingCiv) - val tradeTable = diplomacyScreen.setTrade(requestingCiv) + val diplomacyScreen = DiplomacyScreen(viewingCiv) + val tradeTable = diplomacyScreen.setTrade(requestingCiv) tradeTable.tradeLogic.currentTrade.set(trade) tradeTable.offerColumnsTable.update() worldScreen.game.setScreen(diplomacyScreen) - worldScreen.shouldUpdate=true + worldScreen.shouldUpdate = true } } @@ -112,4 +122,18 @@ class TradePopup(worldScreen: WorldScreen): Popup(worldScreen){ viewingCiv.tradeRequests.remove(tradeRequest) super.close() } + + class TradeThanksPopup(leaderIntroTable: LeaderIntroTable, worldScreen: WorldScreen): Popup(worldScreen) { + init { + add(leaderIntroTable) + addSeparator().padBottom(15f) + addGoodSizedLabel("Excellent!").row() + addCloseButton("Farewell.", KeyCharAndCode.SPACE) { + worldScreen.shouldUpdate=true + // in all cases, worldScreen.shouldUpdate should be set to true when we remove the last of the popups + // in order for the next trade to appear immediately + } + open() + } + } }