Can now conquer cities! =D

This commit is contained in:
Yair Morgenstern 2018-04-20 11:33:56 +03:00
parent 7e2015572c
commit 17307f16f9
14 changed files with 220 additions and 127 deletions

View file

@ -58,9 +58,11 @@ class UnCivGame : Game() {
gameInfo.civilizations.add(CivilizationInfo("Greece", Vector2(3f,5f), gameInfo)) // all the rest whatever
barbarianCivilization.civName = "Barbarians"
gameInfo.setTransients() // needs to be before placeBarbarianUnit because it depends on the tilemap having its gameinfo set
(1..5).forEach { gameInfo.placeBarbarianUnit() }
gameInfo.setTransients()
worldScreen = WorldScreen()
setWorldScreen()

View file

@ -137,6 +137,7 @@ class GameInfo {
}
val damageToAttacker = Battle(this).calculateDamageToAttacker(MapUnitCombatant(unit), MapUnitCombatant(unitToAttack))
if(damageToAttacker < unit.health) { // don't attack if we'll die from the attack
unit.headTowards(unitTileToAttack.position)
Battle(this).attack(MapUnitCombatant(unit), MapUnitCombatant(unitToAttack))

View file

@ -1,6 +1,8 @@
package com.unciv.logic.battle
import com.unciv.logic.GameInfo
import com.unciv.logic.city.CityInfo
import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.UnitType
import java.util.*
import kotlin.collections.HashMap
@ -67,13 +69,15 @@ class Battle(val gameInfo:GameInfo) {
var damageToDefender = calculateDamageToDefender(attacker,defender)
var damageToAttacker = calculateDamageToAttacker(attacker,defender)
if (attacker.getCombatantType() == CombatantType.Ranged) {
if(defender.getCombatantType() == CombatantType.Civilian){
defender.takeDamage(100) // kill
}
else if (attacker.getCombatantType() == CombatantType.Ranged) {
defender.takeDamage(damageToDefender) // 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--
@ -87,7 +91,11 @@ class Battle(val gameInfo:GameInfo) {
}
}
// After dust as settled
postBattleAction(attacker,defender,attackedTile)
}
fun postBattleAction(attacker: ICombatant, defender: ICombatant, attackedTile:TileInfo){
if (defender.getCivilization().isPlayerCivilization()) {
val whatHappenedString =
@ -100,10 +108,38 @@ class Battle(val gameInfo:GameInfo) {
gameInfo.getPlayerCivilization().addNotification(notificationString, attackedTile.position)
}
if(defender.isDefeated()
&& defender.getCombatantType() == CombatantType.City
&& attacker.getCombatantType() == CombatantType.Melee){
conquerCity((defender as CityCombatant).city, attacker)
}
if (defender.isDefeated() && attacker.getCombatantType() == CombatantType.Melee)
(attacker as MapUnitCombatant).unit.moveToTile(attackedTile)
if(attacker is MapUnitCombatant) attacker.unit.currentMovement = 0f
}
private fun conquerCity(city: CityInfo, attacker: ICombatant) {
val enemyCiv = city.civInfo
attacker.getCivilization().addNotification("We have conquered the city of ${city.name}!",city.cityLocation)
enemyCiv.cities.remove(city)
attacker.getCivilization().cities.add(city)
city.civInfo = attacker.getCivilization()
city.health = city.getMaxHealth() / 2 // I think that cities recover to half health?
city.getTile().unit = null
city.expansion.cultureStored = 0;
city.expansion.reset()
if(city.cityConstructions.isBuilt("Palace")){
city.cityConstructions.builtBuildings.remove("Palace")
if(enemyCiv.cities.isEmpty()) {
gameInfo.getPlayerCivilization()
.addNotification("The ${enemyCiv.civName} civilization has been destroyed!", null)
}
else{
enemyCiv.cities.first().cityConstructions.builtBuildings.add("Palace") // relocate palace
}
}
}
}

View file

@ -4,7 +4,6 @@ import com.unciv.logic.city.CityInfo
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.map.TileInfo
import com.unciv.models.gamebasics.GameBasics
import kotlin.math.max
class CityCombatant(val city: CityInfo) : ICombatant {
override fun getHealth(): Int = city.health
@ -14,7 +13,8 @@ class CityCombatant(val city: CityInfo) : ICombatant {
override fun isDefeated(): Boolean = city.health==1
override fun takeDamage(damage: Int) {
city.health = max(1, city.health - damage) // min health is 1
city.health -= damage
if(city.health<1) city.health=1 // min health is 1
}
override fun getCombatantType(): CombatantType = CombatantType.City
@ -37,7 +37,9 @@ class CityCombatant(val city: CityInfo) : ICombatant {
// 10% bonus foreach pop
val strengthWithPop = (baseStrength + strengthFromTechs) * (1 + 0.1*city.population.population)
return strengthWithPop.toInt()
return strengthWithPop.toInt() * 100 // *100 because a city is always at 100% strength
}
}

View file

@ -3,5 +3,6 @@ package com.unciv.logic.battle
enum class CombatantType{
Melee,
Ranged,
Civilian,
City
}

View file

@ -25,13 +25,16 @@ class MapUnitCombatant(val unit: MapUnit) : ICombatant {
return attackerStrength*unit.health
}
override fun getDefendingStrength(attacker: ICombatant): Int =
unit.getBaseUnit().strength*unit.health
override fun getDefendingStrength(attacker: ICombatant): Int {
// too: if ranged units get ranged attacked, they use their ranged str to defend!
return unit.getBaseUnit().strength*unit.health
}
override fun getCombatantType(): CombatantType {
when(unit.getBaseUnit().unitType){
UnitType.Melee -> return CombatantType.Melee
UnitType.Ranged -> return CombatantType.Ranged
UnitType.Civilian -> return CombatantType.Civilian
else -> throw Exception("Should never get here!")
}
}

View file

@ -1,11 +1,18 @@
package com.unciv.logic.city
import com.badlogic.gdx.math.Vector2
class CityExpansionManager {
@Transient
lateinit var cityInfo: CityInfo
var cityTiles = ArrayList<Vector2>()
var cultureStored: Int = 0
private var tilesClaimed: Int = 0
fun reset(){
cityTiles = ArrayList(cityInfo.civInfo.gameInfo.tileMap
.getTilesInDistance(cityInfo.cityLocation, 1).map { it.position })
}
// This one has conflicting sources -
// http://civilization.wikia.com/wiki/Mathematics_of_Civilization_V says it's 20+(10(t-1))^1.1
@ -13,31 +20,30 @@ class CityExpansionManager {
// (per game XML files) at 6*(t+0.4813)^1.3
// The second seems to be more based, so I'll go with that
//Speciality of Angkor Wat
val cultureToNextTile: Int
get() {
var cultureToNextTile = 6 * Math.pow(tilesClaimed + 1.4813, 1.3)
if (cityInfo.civInfo.buildingUniques.contains("NewTileCostReduction")) cultureToNextTile *= 0.75
if (cityInfo.civInfo.policies.isAdopted("Tradition")) cultureToNextTile *= 0.75
return Math.round(cultureToNextTile).toInt()
}
fun getCultureToNextTile(): Int {
val numTilesClaimed = cityTiles.size - 7
var cultureToNextTile = 6 * Math.pow(numTilesClaimed + 1.4813, 1.3)
if (cityInfo.civInfo.buildingUniques.contains("NewTileCostReduction")) cultureToNextTile *= 0.75
if (cityInfo.civInfo.policies.isAdopted("Tradition")) cultureToNextTile *= 0.75
return Math.round(cultureToNextTile).toInt()
}
private fun addNewTileWithCulture() {
cultureStored -= cultureToNextTile
cultureStored -= getCultureToNextTile()
for (i in 2..3) {
val tiles = cityInfo.civInfo.gameInfo.tileMap.getTilesInDistance(cityInfo.cityLocation, i).filter { it.owner == null }
if (tiles.isEmpty()) continue
val chosenTile = tiles.maxBy { cityInfo.rankTile(it) }
chosenTile!!.owner = cityInfo.civInfo.civName
tilesClaimed++
cityTiles.add(chosenTile!!.position)
chosenTile.owner = cityInfo.civInfo.civName
return
}
}
fun nextTurn(culture: Float) {
cultureStored += culture.toInt()
if (cultureStored >= cultureToNextTile) {
if (cultureStored >= getCultureToNextTile()) {
addNewTileWithCulture()
cityInfo.civInfo.addNotification(cityInfo.name + " has expanded its borders!", cityInfo.cityLocation)
}

View file

@ -23,6 +23,8 @@ class CityInfo {
var expansion = CityExpansionManager()
var cityStats = CityStats()
internal val tileMap: TileMap
get() = civInfo.gameInfo.tileMap
@ -51,23 +53,22 @@ class CityInfo {
val buildingUniques: List<String?>
get() = cityConstructions.getBuiltBuildings().filter { it.unique!=null }.map { it.unique }
val greatPersonPoints: Stats
get() {
var greatPersonPoints = population.getSpecialists().times(3f)
fun getGreatPersonPoints(): Stats {
var greatPersonPoints = population.getSpecialists().times(3f)
for (building in cityConstructions.getBuiltBuildings())
if (building.greatPersonPoints != null)
greatPersonPoints.add(building.greatPersonPoints!!)
for (building in cityConstructions.getBuiltBuildings())
if (building.greatPersonPoints != null)
greatPersonPoints.add(building.greatPersonPoints!!)
if (civInfo.buildingUniques.contains("GreatPersonGenerationIncrease"))
greatPersonPoints = greatPersonPoints.times(1.33f)
if (civInfo.policies.isAdopted("Entrepreneurship"))
greatPersonPoints.gold *= 1.25f
if (civInfo.policies.isAdopted("Freedom"))
greatPersonPoints = greatPersonPoints.times(1.25f)
if (civInfo.buildingUniques.contains("GreatPersonGenerationIncrease"))
greatPersonPoints = greatPersonPoints.times(1.33f)
if (civInfo.policies.isAdopted("Entrepreneurship"))
greatPersonPoints.gold *= 1.25f
if (civInfo.policies.isAdopted("Freedom"))
greatPersonPoints = greatPersonPoints.times(1.25f)
return greatPersonPoints
}
return greatPersonPoints
}
constructor() // for json parsing, we need to have a default constructor
@ -88,9 +89,8 @@ class CityInfo {
cityConstructions.currentConstruction = "Worker" // Default for first city only!
}
for (tileInfo in civInfo.gameInfo.tileMap.getTilesInDistance(cityLocation, 1)) {
tileInfo.owner = civInfo.civName
}
expansion.reset()
val tile = getTile()
tile.workingCity = this.name

View file

@ -141,7 +141,7 @@ class CivilizationInfo {
for (city in cities) {
city.nextTurn()
greatPeople.addGreatPersonPoints(city.greatPersonPoints)
greatPeople.addGreatPersonPoints(city.getGreatPersonPoints())
}
val greatPerson = greatPeople.getNewGreatPerson()

View file

@ -21,6 +21,11 @@ class TileInfo {
var improvement: String? = null
var improvementInProgress: String? = null
var owner: String? = null // owning civ name
get() {
val containingCity = tileMap.gameInfo.civilizations.flatMap { it.cities }.firstOrNull{it.expansion.cityTiles.contains(position)}
if(containingCity==null) return null
return containingCity.civInfo.civName
}
var workingCity: String? = null // Working City name
var roadStatus = RoadStatus.None
var explored = false

View file

@ -17,7 +17,8 @@ class UnitMovementAlgorithms(val tileMap: TileMap){
for (tileToCheck in tilesToCheck)
for (maybeUpdatedTile in tileToCheck.neighbors) {
if(maybeUpdatedTile.owner != null && maybeUpdatedTile.owner != civInfo.civName && maybeUpdatedTile.isCityCenter)
continue
continue // Enemy city, can't move through it!
// if(maybeUpdatedTile.unit!=null && maybeUpdatedTile.unit!!.civInfo != civInfo) continue // Enemy unit!
var distanceBetweenTiles = maybeUpdatedTile.lastTerrain.movementCost.toFloat() // no road
if (tileToCheck.roadStatus !== RoadStatus.None && maybeUpdatedTile.roadStatus !== RoadStatus.None) //Road
@ -39,6 +40,11 @@ class UnitMovementAlgorithms(val tileMap: TileMap){
tilesToCheck = updatedTiles
}
// now that we've used the tiles that have friendly units to "pass through" them,
// we have to remove them from the final result since we can't actually get there.
// distanceToTiles.filterKeys { it.unit!=null }.forEach { distanceToTiles.remove(it.key) }
return distanceToTiles
}

View file

@ -34,7 +34,7 @@ class CityStatsTable(val cityScreen: CityScreen) : Table(){
cityStatsValues["Gold"] = Math.round(stats.gold).toString() + ""
cityStatsValues["Science"] = Math.round(stats.science).toString() + ""
cityStatsValues["Culture"] = (Math.round(stats.culture).toString()
+ " (" + city.expansion.cultureStored + "/" + city.expansion.cultureToNextTile + ")")
+ " (" + city.expansion.cultureStored + "/" + city.expansion.getCultureToNextTile() + ")")
cityStatsValues["Population"] = city.population.getFreePopulation().toString() + "/" + city.population.population
for (key in cityStatsValues.keys) {

View file

@ -57,50 +57,10 @@ open class TileGroup(var tileInfo: TileInfo) : Group() {
return
}
if(terrainFeatureImage==null && tileInfo.terrainFeature!=null){
terrainFeatureImage = ImageGetter.getImage("TerrainIcons/${tileInfo.terrainFeature}.png")
addActor(terrainFeatureImage)
terrainFeatureImage!!.run {
setSize(30f,30f)
setColor(1f,1f,1f,0.5f)
setPosition(this@TileGroup.width /2-width/2,
this@TileGroup.height/2-height/2)
}
}
if(terrainFeatureImage!=null && tileInfo.terrainFeature==null){
terrainFeatureImage!!.remove()
terrainFeatureImage=null
}
val RGB= tileInfo.getBaseTerrain().RGB!!
hexagon.color = Color(RGB[0]/255f,RGB[1]/255f,RGB[2]/255f,1f)
if(!isViewable) hexagon.color = hexagon.color.lerp(Color.BLACK,0.6f)
if (tileInfo.hasViewableResource(tileInfo.tileMap.gameInfo.getPlayerCivilization()) && resourceImage == null) { // Need to add the resource image!
val fileName = "ResourceIcons/" + tileInfo.resource + "_(Civ5).png"
resourceImage = ImageGetter.getImage(fileName)
resourceImage!!.setSize(20f, 20f)
resourceImage!!.setPosition(width/2 - resourceImage!!.width/2-20f,
height/2 - resourceImage!!.height/2) // left
addActor(resourceImage!!)
}
if (tileInfo.improvement != null && tileInfo.improvement != improvementType) {
improvementImage = ImageGetter.getImage("ImprovementIcons/" + tileInfo.improvement!!.replace(' ', '_') + "_(Civ5).png")
addActor(improvementImage)
improvementImage!!.run {
setSize(20f, 20f)
setPosition(this@TileGroup.width/2 - width/2+20f,
this@TileGroup.height/2 - height/2) // right
}
improvementType = tileInfo.improvement
}
updateTerrainFeatureImage()
updateTileColor(isViewable)
updateResourceImage()
updateImprovementImage()
if (populationImage != null) {
if (tileInfo.workingCity != null)
@ -109,6 +69,43 @@ open class TileGroup(var tileInfo: TileInfo) : Group() {
populationImage!!.color = Color.GRAY
}
updateRoadImages()
updateBorderImages()
}
private fun updateBorderImages() {
for (border in borderImages) border.remove() //clear
borderImages = arrayListOf()
if (tileInfo.owner != null) {
for (neighbor in tileInfo.neighbors.filter { it.owner != tileInfo.owner }) {
val image = ImageGetter.getImage(ImageGetter.WhiteDot)
val relativeHexPosition = tileInfo.position.cpy().sub(neighbor.position)
val relativeWorldPosition = HexMath.Hex2WorldCoords(relativeHexPosition)
// This is some crazy voodoo magic so I'll explain.
image.setSize(35f, 2f)
image.moveBy(width / 2 - image.width / 2, // center
height / 2 - image.height / 2)
// in addTiles, we set the position of groups by relative world position *0.8*groupSize, filter groupSize = 50
// Here, we want to have the borders start HALFWAY THERE and extend towards the tiles, so we give them a position of 0.8*25.
// BUT, we don't actually want it all the way out there, because we want to display the borders of 2 different civs!
// So we set it to 0.75
image.moveBy(-relativeWorldPosition.x * 0.75f * 25f, -relativeWorldPosition.y * 0.75f * 25f)
image.color = tileInfo.getOwner()!!.getCivilization().getColor()
image.setOrigin(image.width / 2, image.height / 2) // This is so that the rotation is calculated from the middle of the road and not the edge
image.rotation = (90 + 180 / Math.PI * Math.atan2(relativeWorldPosition.y.toDouble(), relativeWorldPosition.x.toDouble())).toFloat()
addActor(image)
borderImages.add(image)
}
}
}
private fun updateRoadImages() {
if (tileInfo.roadStatus !== RoadStatus.None) {
for (neighbor in tileInfo.neighbors) {
if (neighbor.roadStatus === RoadStatus.None) continue
@ -137,37 +134,54 @@ open class TileGroup(var tileInfo: TileInfo) : Group() {
roadImages[neighbor.position.toString()]!!.color = Color.BROWN // road
}
}
// Borders
if(tileInfo.owner!=null){
for (border in borderImages) border.remove()
for (neighbor in tileInfo.neighbors.filter { it.owner!=tileInfo.owner }){
val image = ImageGetter.getImage(ImageGetter.WhiteDot)
val relativeHexPosition = tileInfo.position.cpy().sub(neighbor.position)
val relativeWorldPosition = HexMath.Hex2WorldCoords(relativeHexPosition)
// This is some crazy voodoo magic so I'll explain.
image.setSize(35f, 2f)
image.moveBy(width/2-image.width/2, // center
height/2-image.height/2)
// in addTiles, we set the position of groups by relative world position *0.8*groupSize, filter groupSize = 50
// Here, we want to have the borders start HALFWAY THERE and extend towards the tiles, so we give them a position of 0.8*25.
// BUT, we don't actually want it all the way out there, because we want to display the borders of 2 different civs!
// So we set it to 0.75
image.moveBy(-relativeWorldPosition.x * 0.75f * 25f, -relativeWorldPosition.y * 0.75f * 25f)
image.color = tileInfo.getOwner()!!.getCivilization().getColor()
image.setOrigin(image.width/2, image.height/2) // This is so that the rotation is calculated from the middle of the road and not the edge
image.rotation = (90 + 180 / Math.PI * Math.atan2(relativeWorldPosition.y.toDouble(), relativeWorldPosition.x.toDouble())).toFloat()
addActor(image)
borderImages.add(image)
}
}
}
}
private fun updateTileColor(isViewable: Boolean) {
val RGB = tileInfo.getBaseTerrain().RGB!!
hexagon.color = Color(RGB[0] / 255f, RGB[1] / 255f, RGB[2] / 255f, 1f)
if (!isViewable) hexagon.color = hexagon.color.lerp(Color.BLACK, 0.6f)
}
private fun updateTerrainFeatureImage() {
if (terrainFeatureImage == null && tileInfo.terrainFeature != null) {
terrainFeatureImage = ImageGetter.getImage("TerrainIcons/${tileInfo.terrainFeature}.png")
addActor(terrainFeatureImage)
terrainFeatureImage!!.run {
setSize(30f, 30f)
setColor(1f, 1f, 1f, 0.5f)
setPosition(this@TileGroup.width / 2 - width / 2,
this@TileGroup.height / 2 - height / 2)
}
}
if (terrainFeatureImage != null && tileInfo.terrainFeature == null) {
terrainFeatureImage!!.remove()
terrainFeatureImage = null
}
}
private fun updateImprovementImage() {
if (tileInfo.improvement != null && tileInfo.improvement != improvementType) {
improvementImage = ImageGetter.getImage("ImprovementIcons/" + tileInfo.improvement!!.replace(' ', '_') + "_(Civ5).png")
addActor(improvementImage)
improvementImage!!.run {
setSize(20f, 20f)
setPosition(this@TileGroup.width / 2 - width / 2 + 20f,
this@TileGroup.height / 2 - height / 2) // right
}
improvementType = tileInfo.improvement
}
}
private fun updateResourceImage() {
if (tileInfo.hasViewableResource(tileInfo.tileMap.gameInfo.getPlayerCivilization()) && resourceImage == null) { // Need to add the resource image!
val fileName = "ResourceIcons/" + tileInfo.resource + "_(Civ5).png"
resourceImage = ImageGetter.getImage(fileName)
resourceImage!!.setSize(20f, 20f)
resourceImage!!.setPosition(width / 2 - resourceImage!!.width / 2 - 20f,
height / 2 - resourceImage!!.height / 2) // left
addActor(resourceImage!!)
}
}
}

View file

@ -108,21 +108,38 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
row().pad(5f)
val attackButton = TextButton("Attack", skin)
attackButton.addClickListener {
if(attacker.getCombatantType() == CombatantType.Melee)
attacker.unit.headTowards(defender.getTile().position)
battle.attack(attacker,defender)
worldScreen.update()
val attackerDistanceToTiles = attacker.unit.getDistanceToTiles()
if(attacker.getCombatantType() == CombatantType.Melee){
val tilesCanAttackFrom = attackerDistanceToTiles.filter {
attacker.unit.currentMovement - it.value > 0 // once we reach it we'll still have energy to attack
&& it.key.unit==null
&& it.key.neighbors.contains(defender.getTile()) }
if(tilesCanAttackFrom.isEmpty()) attackButton.disable()
else {
val tileToMoveTo = tilesCanAttackFrom.minBy { it.value }!!.key // travel least distance
attackButton.addClickListener {
attacker.unit.moveToTile(tileToMoveTo)
battle.attack(attacker,defender)
worldScreen.update()
}
}
}
val attackerCanReachDefender:Boolean
if (attacker.getCombatantType() == CombatantType.Ranged) {
else { // ranged
val tilesInRange = UnCivGame.Current.gameInfo.tileMap.getTilesInDistance(attacker.getTile().position, 2)
attackerCanReachDefender = tilesInRange.contains(defender.getTile())
val attackerCanReachDefender = tilesInRange.contains(defender.getTile())
if(!attackerCanReachDefender) attackButton.disable()
else {
attackButton.addClickListener {
battle.attack(attacker, defender)
worldScreen.update()
}
}
}
else attackerCanReachDefender = attacker.unit.getDistanceToTiles().containsKey(defender.getTile())
if(attacker.unit.currentMovement==0f || !attackerCanReachDefender) attackButton.disable()
if(attacker.unit.currentMovement==0f) attackButton.disable()
add(attackButton).colspan(2)
pack()