diff --git a/android/ImagesToPackSeparately/UnitIcons/Privateer.png b/android/ImagesToPackSeparately/UnitIcons/Privateer.png new file mode 100644 index 0000000000..df0f8397e6 Binary files /dev/null and b/android/ImagesToPackSeparately/UnitIcons/Privateer.png differ diff --git a/android/assets/UnitIcons.atlas b/android/assets/UnitIcons.atlas index b275c178c0..2d28b1cad2 100644 --- a/android/assets/UnitIcons.atlas +++ b/android/assets/UnitIcons.atlas @@ -564,170 +564,177 @@ Pikeman orig: 100, 100 offset: 0, 0 index: -1 -Rifleman +Privateer rotate: false xy: 1300, 868 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Rocket Artillery +Rifleman rotate: false xy: 544, 4 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Samurai +Rocket Artillery rotate: false xy: 652, 112 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Scout +Samurai rotate: false xy: 760, 226 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Settler +Scout rotate: false xy: 868, 328 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Ship of the Line +Settler rotate: false xy: 976, 436 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Sipahi +Ship of the Line rotate: false xy: 1084, 544 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Slinger +Sipahi rotate: false xy: 1192, 653 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Spearman +Slinger rotate: false xy: 1300, 760 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Stealth Bomber +Spearman rotate: false xy: 1408, 868 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Submarine +Stealth Bomber rotate: false xy: 652, 4 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Swordsman +Submarine rotate: false xy: 760, 118 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Tank +Swordsman rotate: false xy: 868, 220 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Tercio +Tank rotate: false xy: 976, 328 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Trebuchet +Tercio rotate: false xy: 1084, 436 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Triplane +Trebuchet rotate: false xy: 1192, 545 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 +Triplane + rotate: false + xy: 1300, 652 + size: 100, 100 + orig: 100, 100 + offset: 0, 0 + index: -1 Trireme rotate: false - xy: 1300, 651 + xy: 1408, 759 size: 100, 101 orig: 100, 101 offset: 0, 0 index: -1 Turtle Ship rotate: false - xy: 1408, 760 + xy: 1516, 868 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 War Chariot rotate: false - xy: 1516, 868 + xy: 760, 10 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 War Elephant rotate: false - xy: 760, 10 + xy: 868, 112 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 Warrior rotate: false - xy: 868, 112 + xy: 976, 220 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 Work Boats rotate: false - xy: 976, 220 + xy: 1084, 328 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 Worker rotate: false - xy: 1084, 328 + xy: 1192, 437 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 Zero rotate: false - xy: 1192, 437 + xy: 1300, 544 size: 100, 100 orig: 100, 100 offset: 0, 0 diff --git a/android/assets/UnitIcons.png b/android/assets/UnitIcons.png index 8035055c0a..f58a9f2d22 100644 Binary files a/android/assets/UnitIcons.png and b/android/assets/UnitIcons.png differ diff --git a/android/assets/jsons/Civ V - Vanilla/UnitPromotions.json b/android/assets/jsons/Civ V - Vanilla/UnitPromotions.json index eb45bc4b80..457fe9bfce 100644 --- a/android/assets/jsons/Civ V - Vanilla/UnitPromotions.json +++ b/android/assets/jsons/Civ V - Vanilla/UnitPromotions.json @@ -213,19 +213,19 @@ { "name": "Coastal Raider I", - "effect": "+[15]% Strength vs [City]", + "uniques": ["+[20]% Strength vs [City]", "Earn [33]% of the damage done to [City] units as [Gold]"], "unitTypes": ["WaterMelee"] }, { "name": "Coastal Raider II", "prerequisites": ["Coastal Raider I"], - "effect": "+[15]% Strength vs [City]", + "uniques": ["+[20]% Strength vs [City]", "Earn [33]% of the damage done to [City] units as [Gold]"], "unitTypes": ["WaterMelee"] }, { "name": "Coastal Raider III", "prerequisites": ["Coastal Raider II"], - "effect": "+[15]% Strength vs [City]", + "uniques": ["+[20]% Strength vs [City]", "Earn [33]% of the damage done to [City] units as [Gold]"], "unitTypes": ["WaterMelee"] }, diff --git a/android/assets/jsons/Civ V - Vanilla/Units.json b/android/assets/jsons/Civ V - Vanilla/Units.json index 93f89b3493..842850bc58 100644 --- a/android/assets/jsons/Civ V - Vanilla/Units.json +++ b/android/assets/jsons/Civ V - Vanilla/Units.json @@ -151,7 +151,7 @@ "strength": 10, "cost": 45, "requiredTech": "Sailing", - "uniques": ["Cannot enter ocean tiles",], + "uniques": ["Cannot enter ocean tiles"], "upgradesTo": "Caravel", "obsoleteTech": "Astronomy", "attackSound": "nonmetalhit" @@ -790,6 +790,17 @@ "uniques": ["+[50]% Strength vs [Mounted]"], "attackSound": "shot" }, + { + "name": "Privateer", + "unitType": "WaterMelee", + "movement": 5, + "strength": 25, + "cost": 150, + "requiredTech": "Navigation", + "upgradesTo": "Destroyer", + "promotions": ["Coastal Raider I"], + "uniques": ["May capture killed [Water] units"] + }, { "name": "Frigate", "unitType": "WaterRanged", @@ -1057,7 +1068,7 @@ "cost": 375, "requiredResource": "Oil", "requiredTech": "Electronics", - "uniques": ["Ranged attacks may be performed over obstacles"], + "uniques": ["Ranged attacks may be performed over obstacles"] // Does not actually upgrade to Missile Cruisers }, { diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index e8c594e3a8..d4d7619723 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -446,6 +446,8 @@ A(n) [nukeType] exploded in our territory! = After being hit by our [nukeType], [civName] has declared war on us! The civilization of [civName] has been destroyed! = The City-State of [name] has been destroyed! = +Your [ourUnit] captured an enemy [theirUnit]! = +Your [ourUnit] plundered [amount] [Stat] from [theirUnit] = We have captured a barbarian encampment and recovered [goldAmount] gold! = A barbarian [unitType] has joined us! = We have found survivors in the ruins - population added to [cityName] = diff --git a/core/src/com/unciv/logic/battle/Battle.kt b/core/src/com/unciv/logic/battle/Battle.kt index 2245834537..cd4ce94d22 100644 --- a/core/src/com/unciv/logic/battle/Battle.kt +++ b/core/src/com/unciv/logic/battle/Battle.kt @@ -12,7 +12,6 @@ import com.unciv.models.AttackableTile import com.unciv.models.ruleset.Unique import com.unciv.models.ruleset.unit.UnitType import com.unciv.models.stats.Stat -import com.unciv.models.translations.tr import java.util.* import kotlin.math.max @@ -72,20 +71,16 @@ object Battle { if (!defender.isDefeated() && defender is MapUnitCombatant && defender.unit.action == Constants.unitActionExplore) defender.unit.action = null - // we're a melee unit and we destroyed\captured an enemy unit - postBattleMoveToAttackedTile(attacker, defender, attackedTile) - - reduceAttackerMovementPointsAndAttacks(attacker, defender) - - if (!isAlreadyDefeatedCity) postBattleAddXp(attacker, defender) - // Add culture when defeating a barbarian when Honor policy is adopted, gold from enemy killed when honor is complete // or any enemy military unit with Sacrificial captives unique (can be either attacker or defender!) + // or check if unit is captured by the attacker (prize ships unique) if (defender.isDefeated() && defender is MapUnitCombatant && !defender.getUnitType().isCivilian()) { tryEarnFromKilling(attacker, defender) + tryCaptureUnit(attacker, defender) tryHealAfterKilling(attacker) } else if (attacker.isDefeated() && attacker is MapUnitCombatant && !attacker.getUnitType().isCivilian()) { tryEarnFromKilling(defender, attacker) + tryCaptureUnit(defender, attacker) tryHealAfterKilling(defender) } @@ -95,6 +90,14 @@ object Battle { else if (attacker.unit.isMoving()) attacker.unit.action = null } + + // we're a melee unit and we destroyed\captured an enemy unit + // Should be called after tryCaptureUnit(), as that might spawn a unit on the tile we go to + postBattleMoveToAttackedTile(attacker, defender, attackedTile) + + reduceAttackerMovementPointsAndAttacks(attacker, defender) + + if (!isAlreadyDefeatedCity) postBattleAddXp(attacker, defender) } private fun tryEarnFromKilling(civUnit: ICombatant, defeatedUnit: MapUnitCombatant) { @@ -125,33 +128,79 @@ object Battle { } catch (ex: Exception) { } // parameter is not a stat } + } + private fun tryCaptureUnit(attacker: ICombatant, defender: ICombatant) { + // https://forums.civfanatics.com/threads/prize-ships-for-land-units.650196/ + // https://civilization.fandom.com/wiki/Module:Data/Civ5/GK/Defines + if (!defender.isDefeated()) return + if (attacker !is MapUnitCombatant) return + if (defender is MapUnitCombatant && !defender.getUnitType().isMilitary()) return + if (attacker.unit.getMatchingUniques("May capture killed [] units").none { defender.matchesCategory(it.params[0]) }) return + + var captureChance = 10 + attacker.getAttackingStrength().toFloat() / defender.getDefendingStrength().toFloat() * 40 + if (captureChance > 80) captureChance = 80f + if (100 * Random().nextFloat() > captureChance) return + + val newUnit = attacker.getCivInfo().placeUnitNearTile(defender.getTile().position, defender.getName()) + if (newUnit == null) return // silently fail + attacker.getCivInfo().addNotification("Your [${attacker.getName()}] captured an enemy [${defender.getName()}]", newUnit.getTile().position, NotificationIcon.War) + + newUnit.currentMovement = 0f + newUnit.health = 50 } private fun takeDamage(attacker: ICombatant, defender: ICombatant) { - var damageToDefender = BattleDamage.calculateDamageToDefender(attacker, attacker.getTile(), defender) - var damageToAttacker = BattleDamage.calculateDamageToAttacker(attacker, attacker.getTile(), defender) + var potentialDamageToDefender = BattleDamage.calculateDamageToDefender(attacker, attacker.getTile(), defender) + var potentialDamageToAttacker = BattleDamage.calculateDamageToAttacker(attacker, attacker.getTile(), defender) + + var damageToAttacker = attacker.getHealth() // These variables names don't make any sense as of yet ... + var damageToDefender = defender.getHealth() if (defender.getUnitType().isCivilian() && attacker.isMelee()) { captureCivilianUnit(attacker, defender as MapUnitCombatant) } else if (attacker.isRanged()) { - defender.takeDamage(damageToDefender) // straight up + defender.takeDamage(potentialDamageToDefender) // straight up } else { //melee attack is complicated, because either side may defeat the other midway //so...for each round, we randomize who gets the attack in. Seems to be a good way to work for now. - while (damageToDefender + damageToAttacker > 0) { - if (Random().nextInt(damageToDefender + damageToAttacker) < damageToDefender) { - damageToDefender-- + while (potentialDamageToDefender + potentialDamageToAttacker > 0) { + if (Random().nextInt(potentialDamageToDefender + potentialDamageToAttacker) < potentialDamageToDefender) { + potentialDamageToDefender-- defender.takeDamage(1) if (defender.isDefeated()) break } else { - damageToAttacker-- + potentialDamageToAttacker-- attacker.takeDamage(1) if (attacker.isDefeated()) break } } } + + damageToAttacker -= attacker.getHealth() // ... but from here on they are accurate + damageToDefender -= defender.getHealth() + + if (attacker is MapUnitCombatant) { + for (unique in attacker.unit.getMatchingUniques("Earn []% of the damage done to [] units as []")) + if (defender.matchesCategory(unique.params[1])) { + val resourcesPlundered = + (unique.params[0].toFloat() / 100f * damageToDefender).toInt() + attacker.getCivInfo().addStat(Stat.valueOf(unique.params[2]), resourcesPlundered) + attacker.getCivInfo() + .addNotification( + "Your [${attacker.getName()}] plundered [${resourcesPlundered}] [${unique.params[2]}] from [${defender.getName()}]", + defender.getTile().position, + NotificationIcon.War + ) + } + } + if (defender is MapUnitCombatant) { + for (unique in defender.unit.getMatchingUniques("Earn []% of the damage done to [] units as []")) + if (attacker.matchesCategory(unique.params[1])) + defender.getCivInfo().addStat(Stat.valueOf(unique.params[2]), (unique.params[0].toFloat() / 100f * damageToAttacker).toInt()) + } + } @@ -380,7 +429,7 @@ object Battle { attacker.popupAlerts.add(PopupAlert(AlertType.Defeated, attackedCiv.civName)) } } - + fun NUKE(attacker: MapUnitCombatant, targetTile: TileInfo) { val attackingCiv = attacker.getCivInfo() fun tryDeclareWar(civSuffered: CivilizationInfo) { @@ -392,8 +441,8 @@ object Battle { attackingCiv.addNotification("After being hit by our [${attacker.getName()}], [${civSuffered}] has declared war on us!", targetTile.position, NotificationIcon.War) } } - - val blastRadius = + + val blastRadius = if (!attacker.unit.hasUnique("Blast radius []")) 2 else attacker.unit.getMatchingUniques("Blast radius []").first().params[0].toInt() @@ -405,13 +454,13 @@ object Battle { // else -> return } - + // Calculate the tiles that are hit val hitTiles = targetTile.getTilesInDistance(blastRadius) - + // Declare war on the owners of all hit tiles for (hitCiv in hitTiles.mapNotNull { it.getOwner() }.distinct()) { - hitCiv.addNotification("A(n) [${attacker.getName()}] exploded in our territory!".tr(), targetTile.position, NotificationIcon.War) + hitCiv.addNotification("A(n) [${attacker.getName()}] exploded in our territory!", targetTile.position, NotificationIcon.War) tryDeclareWar(hitCiv) } @@ -423,14 +472,14 @@ object Battle { } } if (attacker.isDefeated()) return - + // Destroy units on the target tile for (defender in targetTile.getUnits().filter { it != attacker.unit }) { defender.destroy() postBattleNotifications(attacker, MapUnitCombatant(defender), defender.getTile()) destroyIfDefeated(defender.civInfo, attacker.getCivInfo()) } - + for (tile in hitTiles) { // Handle complicated effects when (strength) { @@ -439,19 +488,19 @@ object Battle { else -> nukeStrength1Effect(attacker, tile) } } - + // Instead of postBattleAction() just destroy the unit, all other functions are not relevant if (attacker.unit.hasUnique("Self-destructs when attacking")) { attacker.unit.destroy() } - + // It's unclear whether using nukes results in a penalty with all civs, or only affected civs. // For now I'll make it give a diplomatic penalty to all known civs, but some testing for this would be appreciated for (civ in attackingCiv.getKnownCivs()) { civ.getDiplomacyManager(attackingCiv).setModifier(DiplomaticModifiers.UsedNuclearWeapons, -50f) } } - + private fun nukeStrength1Effect(attacker: MapUnitCombatant, tile: TileInfo) { // https://forums.civfanatics.com/resources/unit-guide-modern-future-units-g-k.25628/ // https://www.carlsguides.com/strategy/civilization5/units/aircraft-nukes.php @@ -462,7 +511,7 @@ object Battle { if (civResources[resource]!! < 0 && !attacker.getCivInfo().isBarbarian()) damageModifierFromMissingResource *= 0.5f // I could not find a source for this number, but this felt about right } - + // Decrease health & population of a hit city val city = tile.getCity() if (city != null && tile.position == city.location) { @@ -484,7 +533,7 @@ object Battle { } postBattleNotifications(attacker, CityCombatant(city), city.getCenterTile()) } - + // Damage and/or destroy units on the tile for (unit in tile.getUnits()) { val defender = MapUnitCombatant(unit) @@ -496,7 +545,7 @@ object Battle { postBattleNotifications(attacker, defender, defender.getTile()) destroyIfDefeated(defender.getCivInfo(), attacker.getCivInfo()) } - + // Remove improvements, add fallout tile.improvement = null tile.improvementInProgress = null @@ -514,7 +563,7 @@ object Battle { } } } - + private fun nukeStrength2Effect(attacker: MapUnitCombatant, tile: TileInfo) { // https://forums.civfanatics.com/threads/unit-guide-modern-future-units-g-k.429987/#2 // https://www.carlsguides.com/strategy/civilization5/units/aircraft-nukes.php @@ -525,7 +574,7 @@ object Battle { if (civResources[resource]!! < 0 && !attacker.getCivInfo().isBarbarian()) damageModifierFromMissingResource *= 0.5f // I could not find a source for this number, but this felt about right } - + // Damage and/or destroy cities val city = tile.getCity() if (city != null && city.location == tile.position) { @@ -549,14 +598,14 @@ object Battle { postBattleNotifications(attacker, CityCombatant(city), city.getCenterTile()) destroyIfDefeated(city.civInfo, attacker.getCivInfo()) } - + // Destroy all hit units for (defender in tile.getUnits()) { defender.destroy() postBattleNotifications(attacker, MapUnitCombatant(defender), defender.currentTile) destroyIfDefeated(defender.civInfo, attacker.getCivInfo()) } - + // Remove improvements tile.improvement = null tile.improvementInProgress = null diff --git a/docs/Credits.md b/docs/Credits.md index ca5fbf5466..4d65ef465f 100644 --- a/docs/Credits.md +++ b/docs/Credits.md @@ -74,6 +74,7 @@ Unless otherwise specified, all the following are from [the Noun Project](https: * [Black powder musket](https://thenounproject.com/term/black-powder-musket/1202078/) By Jarem Fyre for Minuteman * [Rapier](https://thenounproject.com/search/?q=musketeer&i=819822) By Hamish for Musketeer * [Ship](https://thenounproject.com/term/ship/1293899/) By Orin Zuu for Frigate +* [Pirate](https://thenounproject.com/search/?q=pirate&i=2349496) by Berkah Icon for Privateer * [Ship](https://thenounproject.com/search/?q=ship&i=800131) By Mungang Kim for Ship of the Line * [Lance](https://thenounproject.com/search/?q=Lance&i=440122) By parkjisun for Lancer * [Sword](https://thenounproject.com/search/?q=saber&i=1174742) By Daniela Baptista for Sipahi