diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 5887898828..9883c90384 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -205,6 +205,17 @@ We have married into the ruling family of [civName], bringing them under our con You have broken your Pledge to Protect [civName]! = City-States grow wary of your aggression. The resting point for Influence has decreased by [amount] for [civName]. = +[cityState] is being attacked by [civName] and asks all major civilizations to help them out by gifting them military units. = +[cityState] is being invaded by Barbarians! Destroy Barbarians near their territory to earn Influence. = +[cityState] is grateful that you killed a Barbarian that was threatening them! = +[cityState] is being attacked by [civName]! Kill [amount] of the attacker's military units and they will be immensely grateful. = +[cityState] is deeply grateful for your assistance in the war against [civName]! = +[cityState] no longer needs your assistance against [civName]. = +War against [civName] = +We need you to help us defend against [civName]. Killing [amount] of their military units would slow their offensive. = +Currently you have killed [amount] of their military units. = +You need to find them first! = + Cultured = Maritime = Mercantile = diff --git a/core/src/com/unciv/logic/battle/Battle.kt b/core/src/com/unciv/logic/battle/Battle.kt index 72d8c75241..3d4150c00b 100644 --- a/core/src/com/unciv/logic/battle/Battle.kt +++ b/core/src/com/unciv/logic/battle/Battle.kt @@ -173,6 +173,11 @@ object Battle { } } } + + // CS war with major pseudo-quest + for (cityState in UncivGame.Current.gameInfo.getAliveCityStates()) { + cityState.questManager.militaryUnitKilledBy(civUnit.getCivInfo(), defeatedUnit.getCivInfo()) + } } private fun tryCaptureUnit(attacker: MapUnitCombatant, defender: MapUnitCombatant): Boolean { diff --git a/core/src/com/unciv/logic/civilization/CityStateFunctions.kt b/core/src/com/unciv/logic/civilization/CityStateFunctions.kt index 9979f6039e..bb962c3307 100644 --- a/core/src/com/unciv/logic/civilization/CityStateFunctions.kt +++ b/core/src/com/unciv/logic/civilization/CityStateFunctions.kt @@ -473,7 +473,7 @@ class CityStateFunctions(val civInfo: CivilizationInfo) { return } - private fun getNumThreateningBarbarians(): Int { + fun getNumThreateningBarbarians(): Int { if (civInfo.gameInfo.gameParameters.noBarbarians) return 0 val barbarianCiv = civInfo.gameInfo.civilizations.firstOrNull { it.isBarbarian() } ?: return 0 @@ -482,11 +482,17 @@ class CityStateFunctions(val civInfo: CivilizationInfo) { fun threateningBarbarianKilledBy(otherCiv: CivilizationInfo) { val diplomacy = civInfo.getDiplomacyManager(otherCiv) + if (diplomacy.diplomaticStatus == DiplomaticStatus.War) return // No reward for enemies + diplomacy.addInfluence(12f) + if (diplomacy.hasFlag(DiplomacyFlags.AngerFreeIntrusion)) diplomacy.setFlag(DiplomacyFlags.AngerFreeIntrusion, diplomacy.getFlag(DiplomacyFlags.AngerFreeIntrusion) + 5) else diplomacy.setFlag(DiplomacyFlags.AngerFreeIntrusion, 5) + + otherCiv.addNotification("[${civInfo.civName}] is grateful that you killed a Barbarian that was threatening them!", + DiplomacyAction(civInfo.civName), civInfo.civName) } /** A city state was bullied. What are its protectors going to do about it??? */ @@ -593,6 +599,10 @@ class CityStateFunctions(val civInfo: CivilizationInfo) { protector.popupAlerts.add(PopupAlert(AlertType.AttackedProtectedMinor, attacker.civName + "@" + civInfo.civName)) // we need to pass both civs as argument, hence the horrible chimera } + + // Set up war with major pseudo-quest + civInfo.questManager.wasAttackedBy(attacker) + civInfo.getDiplomacyManager(attacker).setFlag(DiplomacyFlags.RecentlyAttacked, 2) // Reminder to ask for unit gifts in 2 turns } /** A city state was destroyed. Its protectors are going to be upset! */ @@ -621,4 +631,18 @@ class CityStateFunctions(val civInfo: CivilizationInfo) { .forEach { it.questManager.cityStateConquered(civInfo, attacker) } } + /** Asks all met majors that haven't yet declared wor on [attacker] to at least give some units */ + fun askForUnitGifts(attacker: CivilizationInfo) { + if (attacker.isDefeated() || civInfo.isDefeated()) // nevermind, someone died + return + if (civInfo.cities.isEmpty()) // Can't receive units with no cities + return + + for (thirdCiv in civInfo.getKnownCivs().filter { + it != attacker && it.isAlive() && it.knows(attacker) && !it.isAtWarWith(attacker) }) { + thirdCiv.addNotification("[${civInfo.civName}] is being attacked by [${attacker.civName}] and asks all major civilizations to help them out by gifting them military units.", + civInfo.getCapital().location, civInfo.civName, "OtherIcons/Present") + } + } + } \ No newline at end of file diff --git a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt index 2465332349..d42fca4fb7 100644 --- a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt +++ b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt @@ -508,6 +508,8 @@ class CivilizationInfo { } for ((key, value) in giftAmount) otherCiv.addStat(key, value.toInt()) + + questManager.justMet(otherCiv) // Include them in war with major pseudo-quest } fun discoverNaturalWonder(naturalWonderName: String) { @@ -891,6 +893,7 @@ class CivilizationInfo { fun getTurnsTillNextDiplomaticVote() = flagsCountdown[CivFlags.TurnsTillNextDiplomaticVote.name] fun getRecentBullyingCountdown() = flagsCountdown[CivFlags.RecentlyBullied.name] + fun getTurnsTillCallForBarbHelp() = flagsCountdown[CivFlags.TurnsTillCallForBarbHelp.name] fun mayVoteForDiplomaticVictory() = getTurnsTillNextDiplomaticVote() == 0 @@ -1184,6 +1187,7 @@ class CivilizationInfo { fun getFreeTechForCityState() { cityStateFunctions.getFreeTechForCityState() } + fun getNumThreateningBarbarians() = cityStateFunctions.getNumThreateningBarbarians() fun threateningBarbarianKilledBy(otherCiv: CivilizationInfo) { cityStateFunctions.threateningBarbarianKilledBy(otherCiv) } @@ -1214,5 +1218,6 @@ enum class CivFlags { TurnsTillNextDiplomaticVote, ShowDiplomaticVotingResults, ShouldResetDiplomaticVotes, - RecentlyBullied + RecentlyBullied, + TurnsTillCallForBarbHelp, } diff --git a/core/src/com/unciv/logic/civilization/QuestManager.kt b/core/src/com/unciv/logic/civilization/QuestManager.kt index bc2b9f4631..f5cda6f059 100644 --- a/core/src/com/unciv/logic/civilization/QuestManager.kt +++ b/core/src/com/unciv/logic/civilization/QuestManager.kt @@ -56,6 +56,12 @@ class QuestManager { /** Number of turns left before this city state can start a new individual quest */ private var individualQuestCountdown: HashMap = HashMap() + /** Target number of units to kill for this war, for war with major pseudo-quest */ + private var unitsToKillForCiv: HashMap = HashMap() + + /** For this attacker, number of units killed by each civ */ + private var unitsKilledFromCiv: HashMap> = HashMap() + /** Returns true if [civInfo] have active quests for [challenger] */ fun haveQuestsFor(challenger: CivilizationInfo): Boolean = assignedQuests.any { it.assignee == challenger.civName } @@ -76,6 +82,11 @@ class QuestManager { toReturn.globalQuestCountdown = globalQuestCountdown toReturn.individualQuestCountdown.putAll(individualQuestCountdown) toReturn.assignedQuests.addAll(assignedQuests) + toReturn.unitsToKillForCiv.putAll(unitsToKillForCiv) + for ((attacker, unitsKilled) in unitsKilledFromCiv) { + toReturn.unitsKilledFromCiv[attacker] = HashMap() + toReturn.unitsKilledFromCiv[attacker]!!.putAll(unitsKilled) + } return toReturn } @@ -105,6 +116,9 @@ class QuestManager { tryStartNewGlobalQuest() tryStartNewIndividualQuests() + + tryBarbarianInvasion() + tryEndWarWithMajorQuests() } private fun decrementQuestCountdowns() { @@ -201,6 +215,22 @@ class QuestManager { } } + private fun tryBarbarianInvasion() { + if ((civInfo.getTurnsTillCallForBarbHelp() == null || civInfo.getTurnsTillCallForBarbHelp() == 0) + && civInfo.getNumThreateningBarbarians() >= 2) { + + for (otherCiv in civInfo.getKnownCivs().filter { + it.isMajorCiv() + && it.isAlive() + && !it.isAtWarWith(civInfo) + && it.getProximity(civInfo) <= Proximity.Far }) { + otherCiv.addNotification("[${civInfo.civName}] is being invaded by Barbarians! Destroy Barbarians near their territory to earn Influence.", + LocationAction(listOf(civInfo.getCapital().location)), civInfo.civName, NotificationIcon.War) + } + civInfo.addFlag(CivFlags.TurnsTillCallForBarbHelp.name, 30) + } + } + private fun handleGlobalQuests() { val globalQuestsExpired = assignedQuests.filter { it.isGlobal() && it.isExpired() }.map { it.questName }.distinct() for (globalQuestName in globalQuestsExpired) @@ -484,6 +514,100 @@ class QuestManager { } } + /** Gets notified when we are attacked, for war with major pseudo-quest */ + fun wasAttackedBy(attacker: CivilizationInfo) { + // Set target number units to kill + val totalMilitaryUnits = attacker.getCivUnits().count { !it.isCivilian() } + val unitsToKill = max(3, totalMilitaryUnits / 4) + unitsToKillForCiv[attacker.civName] = unitsToKill + + + val location = if (civInfo.cities.isEmpty()) null + else civInfo.getCapital().location + + // Ask for assistance + for (thirdCiv in civInfo.getKnownCivs().filter { it.isAlive() && !it.isAtWarWith(civInfo) }) { + if (location != null) + thirdCiv.addNotification("[${civInfo.civName}] is being attacked by [${attacker.civName}]! Kill [$unitsToKill] of the attacker's military units and they will be immensely grateful.", + location, civInfo.civName, "OtherIcons/Quest") + else thirdCiv.addNotification("[${civInfo.civName}] is being attacked by [${attacker.civName}]! Kill [$unitsToKill] of the attacker's military units and they will be immensely grateful.", + civInfo.civName, "OtherIcons/Quest") + } + } + + /** Gets notified when [killed]'s military unit was killed by [killer], for war with major pseudo-quest */ + fun militaryUnitKilledBy(killer: CivilizationInfo, killed: CivilizationInfo) { + if (!warWithMajorActive(killed)) return + + // No credit if we're at war or haven't met + if (!civInfo.knows(killer) || civInfo.isAtWarWith(killer)) return + + // Make the map if we haven't already + if (unitsKilledFromCiv[killed.civName] == null) + unitsKilledFromCiv[killed.civName] = HashMap() + + // Update kill count + val updatedKillCount = 1 + (unitsKilledFromCiv[killed.civName]!![killer.civName] ?: 0) + unitsKilledFromCiv[killed.civName]!![killer.civName] = updatedKillCount + + // Quest complete? + if (updatedKillCount >= unitsToKillForCiv[killed.civName]!!) { + killer.addNotification("[${civInfo.civName}] is deeply grateful for your assistance in the war against [${killed.civName}]!", + DiplomacyAction(civInfo.civName), civInfo.civName, "OtherIcons/Quest") + civInfo.getDiplomacyManager(killer).addInfluence(100f) // yikes + endWarWithMajorQuest(killed) + } + } + + /** Called when a major civ meets the city-state for the first time. Mainly for war with major pseudo-quest. */ + fun justMet(otherCiv: CivilizationInfo) { + val location = if (civInfo.cities.isEmpty()) null + else civInfo.getCapital().location + + for ((attackerName, unitsToKill) in unitsToKillForCiv) { + if (location != null) + otherCiv.addNotification("[${civInfo.civName}] is being attacked by [$attackerName]! Kill [$unitsToKill] of the attacker's military units and they will be immensely grateful.", + location, civInfo.civName, "OtherIcons/Quest") + else otherCiv.addNotification("[${civInfo.civName}] is being attacked by [$attackerName]! Kill [$unitsToKill] of the attacker's military units and they will be immensely grateful.", + civInfo.civName, "OtherIcons/Quest") + } + } + + /** Ends War with Major pseudo-quests that aren't relevant any longer */ + private fun tryEndWarWithMajorQuests() { + for (attacker in unitsToKillForCiv.keys.map { civInfo.gameInfo.getCivilization(it) }) { + if (civInfo.isDefeated() + || attacker.isDefeated() + || !civInfo.isAtWarWith(attacker)) { + endWarWithMajorQuest(attacker) + } + } + } + + private fun endWarWithMajorQuest(attacker: CivilizationInfo) { + for (thirdCiv in civInfo.getKnownCivs().filterNot { it.isDefeated() || it == attacker || it.isAtWarWith(civInfo) }) { + if (unitsKilledSoFar(attacker, thirdCiv) >= unitsToKill(attacker)) // Don't show the notification to the one who won the quest + continue + thirdCiv.addNotification("[${civInfo.civName}] no longer needs your assistance against [${attacker.civName}].", + DiplomacyAction(civInfo.civName), civInfo.civName, "OtherIcons/Quest") + } + unitsToKillForCiv.remove(attacker.civName) + unitsKilledFromCiv.remove(attacker.civName) + } + + fun warWithMajorActive(target: CivilizationInfo): Boolean { + return unitsToKillForCiv.containsKey(target.civName) + } + + fun unitsToKill(target: CivilizationInfo): Int { + return unitsToKillForCiv[target.civName] ?: 0 + } + + fun unitsKilledSoFar(target: CivilizationInfo, viewingCiv: CivilizationInfo): Int { + val killMap = unitsKilledFromCiv[target.civName] ?: return 0 + return killMap[viewingCiv.civName] ?: 0 + } + /** * Gets notified when given gold by [donorCiv]. */ diff --git a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt index d8671697f2..a2d6a57ebb 100644 --- a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt +++ b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt @@ -52,6 +52,7 @@ enum class DiplomacyFlags { Denunciation, WaryOf, Bullied, + RecentlyAttacked, } enum class DiplomaticModifiers { @@ -527,6 +528,9 @@ class DiplomacyManager() { DiplomacyFlags.AgreedToNotSettleNearUs.name -> { addModifier(DiplomaticModifiers.FulfilledPromiseToNotSettleCitiesNearUs, 10f) } + DiplomacyFlags.RecentlyAttacked.name -> { + civInfo.cityStateFunctions.askForUnitGifts(otherCiv()) + } // These modifiers don't tick down normally, instead there is a threshold number of turns DiplomacyFlags.RememberDestroyedProtectedMinor.name -> { // 125 removeModifier(DiplomaticModifiers.DestroyedProtectedMinor) diff --git a/core/src/com/unciv/ui/trade/DiplomacyScreen.kt b/core/src/com/unciv/ui/trade/DiplomacyScreen.kt index 047fb22518..4d63b42f1a 100644 --- a/core/src/com/unciv/ui/trade/DiplomacyScreen.kt +++ b/core/src/com/unciv/ui/trade/DiplomacyScreen.kt @@ -369,6 +369,11 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo): CameraStageBaseScreen() diplomacyTable.addSeparator() diplomacyTable.add(getQuestTable(assignedQuest)).row() } + + for (target in otherCiv.getKnownCivs().filter { otherCiv.questManager.warWithMajorActive(it) }) { + diplomacyTable.addSeparator() + diplomacyTable.add(getWarWithMajorTable(target, otherCiv)).row() + } return diplomacyTable } @@ -567,6 +572,24 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo): CameraStageBaseScreen() return questTable } + private fun getWarWithMajorTable(target: CivilizationInfo, otherCiv: CivilizationInfo): Table { + val warTable = Table() + warTable.defaults().pad(10f) + + val title = "War against [${target.civName}]" + val description = "We need you to help us defend against [${target.civName}]. Killing [${otherCiv.questManager.unitsToKill(target)}] of their military units would slow their offensive." + val progress = if (viewingCiv.knows(target)) "Currently you have killed [${otherCiv.questManager.unitsKilledSoFar(target, viewingCiv)}] of their military units." + else "You need to find them first!" + + warTable.add(title.toLabel(fontSize = 24)).row() + warTable.add(description.toLabel().apply { wrap = true; setAlignment(Align.center) }) + .width(stage.width / 2).row() + warTable.add(progress.toLabel().apply { wrap = true; setAlignment(Align.center) }) + .width(stage.width / 2).row() + + return warTable + } + private fun getMajorCivDiplomacyTable(otherCiv: CivilizationInfo): Table { val otherCivDiplomacyManager = otherCiv.getDiplomacyManager(viewingCiv)