Added Horseman, Catapult and Spearman, with all that entails

This commit is contained in:
Yair Morgenstern
2018-05-08 10:58:26 +03:00
parent 16ae62a29b
commit 2d302ea2da
24 changed files with 167 additions and 96 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -18,7 +18,7 @@
{
name:"Scout",
baseDescription: "Has no abilites, can only explore",
unbuildable:true,
cost:25,
unitType:"Melee",
strength:5,
uniques:["Ignores terrain cost"]
@ -31,32 +31,68 @@
movement:2,
strength:8,
cost: 40,
hurryCostModifier:20
hurryCostModifier:20,
upgradesTo:"Swordsman"
},
{
name:"Archer",
baseDescription: "A basic fighting unit",
unitType:"Ranged",
unitType:"Archery",
movement:2,
strength:5,
rangedStrength:7,
cost: 40,
uniques:["Penalty vs City 33%"]
hurryCostModifier:20
},
{
name:"Chariot Archer",
baseDescription: "",
unitType:"Ranged",
unitType:"Archery",
movement:4,
strength:6,
rangedStrength:10,
cost: 56,
requiredTech:"The Wheel",
requiredResource:"Horses",
uniques:["No Defensive Terrain Bonus","Rough Terrain Penalty"],
uniques:["No defensive terrain bonus","Rough terrain penalty"],
hurryCostModifier:20
},
{
name:"Spearman",
baseDescription: "",
unitType:"Melee",
movement:2,
strength:11,
cost: 56,
requiredTech:"Bronze Working",
uniques:["Bonus vs Mounted 50%"],
hurryCostModifier:20
},
{
name:"Catapult",
baseDescription: "",
unitType:"Siege",
movement:2,
strength:7,
rangedStrength:8,
cost: 75,
requiredTech:"Mathematics",
uniques:["Bonus vs City 200%","No defensive terrain bonus","Must set up to ranged attack"],
hurryCostModifier:20
},
{
name:"Horseman",
baseDescription:"",
unitType:"Mounted",
movement:4,
strength:12,
cost: 75,
requiredTech:"Horseback Riding",
requiredResource:"Horses",
uniques:["Can move after attacking","No defensive terrain bonus","Penalty vs City 33%" ],
hurryCostModifier:20
},
{

View File

@ -5,8 +5,8 @@ import com.unciv.logic.city.CityInfo
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.UnitType
import com.unciv.models.gamebasics.Building
import com.unciv.models.gamebasics.GameBasics
import com.unciv.models.gamebasics.Unit
import com.unciv.ui.utils.getRandom
class Automation {
@ -62,14 +62,20 @@ class Automation {
}
private fun trainCombatUnit(city: CityInfo) {
city.cityConstructions.currentConstruction = "Archer" // when we have more units then we'll see.
val buildableUnits = city.cityConstructions.getConstructableUnits()
val chosenUnit:Unit
if(city.getCenterTile().unit==null && buildableUnits.any { it.unitType==UnitType.Archery }) // this is for defence so get an archery if we can
chosenUnit = buildableUnits.filter { it.unitType==UnitType.Archery }.maxBy { it.cost }!!
else chosenUnit = buildableUnits.maxBy { it.cost }!!
city.cityConstructions.currentConstruction = chosenUnit.name
}
fun chooseNextConstruction(cityConstructions: CityConstructions) {
cityConstructions.run {
val buildableNotWonders = getBuildableBuildings().filterNot { (getConstruction(it) as Building).isWonder }
val buildableWonders = getBuildableBuildings().filter { (getConstruction(it) as Building).isWonder }
val buildableNotWonders = getBuildableBuildings().filterNot { it.isWonder }
val buildableWonders = getBuildableBuildings().filter { it.isWonder }
val civUnits = cityInfo.civInfo.getCivUnits()
val militaryUnits = civUnits.filter { it.getBaseUnit().unitType != UnitType.Civilian }.size
@ -77,12 +83,12 @@ class Automation {
val cities = cityInfo.civInfo.cities.size
when {
!buildableNotWonders.isEmpty() -> currentConstruction = buildableNotWonders.first()
!buildableNotWonders.isEmpty() -> currentConstruction = buildableNotWonders.first().name
militaryUnits==0 -> trainCombatUnit(cityInfo)
workers==0 -> currentConstruction = CityConstructions.Worker
militaryUnits<cities -> trainCombatUnit(cityInfo)
workers<cities -> currentConstruction = CityConstructions.Worker
buildableWonders.isNotEmpty() -> currentConstruction = buildableWonders.getRandom()
buildableWonders.isNotEmpty() -> currentConstruction = buildableWonders.getRandom().name
else -> trainCombatUnit(cityInfo)
}

View File

@ -71,7 +71,7 @@ class UnitAutomation{
val damageToAttacker = Battle(unit.civInfo.gameInfo).calculateDamageToAttacker(MapUnitCombatant(unit), MapUnitCombatant(unitToAttack))
if (damageToAttacker < unit.health) { // don't attack if we'll die from the attack
if(unit.getBaseUnit().unitType == UnitType.Melee)
if(MapUnitCombatant(unit).isMelee())
unit.headTowards(unitTileToAttack.position)
Battle(unit.civInfo.gameInfo).attack(MapUnitCombatant(unit), MapUnitCombatant(unitToAttack))
return

View File

@ -9,13 +9,36 @@ import kotlin.collections.HashMap
class Battle(val gameInfo:GameInfo) {
fun getAttackModifiers(attacker: ICombatant, defender: ICombatant): HashMap<String, Float> {
fun getGeneralModifiers(combatant:ICombatant,enemy:ICombatant): HashMap<String, Float> {
val modifiers = HashMap<String,Float>()
if(attacker.getCombatantType()==CombatantType.Melee) {
if(combatant is MapUnitCombatant){
val uniques = combatant.unit.getBaseUnit().uniques
if(uniques!=null) {
// This beut allows us to have generic unit uniques: "Bonus vs City 75%", "Penatly vs Mounted 25%" etc.
for (unique in uniques){
val regexResult = Regex("""(Bonus|Penalty) vs (\S*) (\d*)%""").matchEntire(unique)
if(regexResult==null) continue
val vsType = UnitType.valueOf(regexResult.groups[2]!!.value)
val modificationAmount = regexResult.groups[3]!!.value.toFloat()/100 // if it says 15%, that's 0.15f in modification
if(enemy.getUnitType() == vsType){
if(regexResult.groups[1]!!.value=="Bonus")
modifiers.put("Bonus vs $vsType",modificationAmount)
else modifiers.put("Penalty vs $vsType",-modificationAmount)
}
}
}
}
return modifiers
}
fun getAttackModifiers(attacker: ICombatant, defender: ICombatant): HashMap<String, Float> {
val modifiers = getGeneralModifiers(attacker,defender)
if(attacker.isMelee()) {
val numberOfAttackersSurroundingDefender = defender.getTile().neighbors.count {
it.unit != null
&& it.unit!!.owner == attacker.getCivilization().civName
&& it.unit!!.getBaseUnit().unitType == UnitType.Melee
&& MapUnitCombatant(it.unit!!).isMelee()
}
if(numberOfAttackersSurroundingDefender >1) modifiers["Flanking"] = 0.15f
}
@ -24,8 +47,8 @@ class Battle(val gameInfo:GameInfo) {
}
fun getDefenceModifiers(attacker: ICombatant, defender: ICombatant): HashMap<String, Float> {
val modifiers = HashMap<String,Float>()
if(!(defender is MapUnitCombatant && defender.unit.getBaseUnit().hasUnique("No Defensive Terrain Bonus"))){
val modifiers = getGeneralModifiers(defender,attacker)
if(!(defender is MapUnitCombatant && defender.unit.getBaseUnit().hasUnique("No defensive terrain bonus"))){
val tileDefenceBonus = defender.getTile().getDefensiveBonus()
if (tileDefenceBonus > 0) modifiers["Terrain"] = tileDefenceBonus
}
@ -57,7 +80,7 @@ class Battle(val gameInfo:GameInfo) {
}
fun calculateDamageToAttacker(attacker: ICombatant, defender: ICombatant): Int {
if(attacker.getCombatantType() == CombatantType.Ranged) return 0
if(attacker.isRanged()) return 0
return (getDefendingStrength(attacker,defender) * 50 / getAttackingStrength(attacker,defender)).toInt()
}
@ -71,10 +94,10 @@ class Battle(val gameInfo:GameInfo) {
var damageToDefender = calculateDamageToDefender(attacker,defender)
var damageToAttacker = calculateDamageToAttacker(attacker,defender)
if(defender.getCombatantType() == CombatantType.Civilian){
if(defender.getUnitType() == CombatantType.Civilian){
defender.takeDamage(100) // kill
}
else if (attacker.getCombatantType() == CombatantType.Ranged) {
else if (attacker.isRanged()) {
defender.takeDamage(damageToDefender) // straight up
} else {
//melee attack is complicated, because either side may defeat the other midway
@ -104,7 +127,7 @@ class Battle(val gameInfo:GameInfo) {
if (attacker.isDefeated()) " was destroyed while attacking"
else " has " + (if (defender.isDefeated()) "destroyed" else "attacked")
val defenderString =
if (defender.getCombatantType() == CombatantType.City) defender.getName()
if (defender.getUnitType() == CombatantType.City) defender.getName()
else " our " + defender.getName()
val notificationString = "An enemy " + attacker.getName() + whatHappenedString + defenderString
gameInfo.getPlayerCivilization().addNotification(notificationString, attackedTile.position)
@ -112,12 +135,12 @@ class Battle(val gameInfo:GameInfo) {
if(defender.isDefeated()
&& defender.getCombatantType() == CombatantType.City
&& attacker.getCombatantType() == CombatantType.Melee){
&& defender.getUnitType() == CombatantType.City
&& attacker.isMelee()){
conquerCity((defender as CityCombatant).city, attacker)
}
if (defender.isDefeated() && attacker.getCombatantType() == CombatantType.Melee)
if (defender.isDefeated() && attacker.isMelee())
(attacker as MapUnitCombatant).unit.moveToTile(attackedTile)
if(attacker is MapUnitCombatant) attacker.unit.currentMovement = 0f
@ -151,5 +174,4 @@ class Battle(val gameInfo:GameInfo) {
(attacker as MapUnitCombatant).unit.moveToTile(city.getCenterTile())
city.civInfo.gameInfo.updateTilesToCities()
}
}

View File

@ -3,6 +3,7 @@ package com.unciv.logic.battle
import com.unciv.logic.city.CityInfo
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.UnitType
import com.unciv.models.gamebasics.GameBasics
class CityCombatant(val city: CityInfo) : ICombatant {
@ -17,7 +18,7 @@ class CityCombatant(val city: CityInfo) : ICombatant {
if(city.health<1) city.health=1 // min health is 1
}
override fun getCombatantType(): CombatantType = CombatantType.City
override fun getUnitType():UnitType=UnitType.City
override fun getAttackingStrength(defender: ICombatant): Int = getCityStrength()
override fun getDefendingStrength(attacker: ICombatant): Int = getCityStrength()

View File

@ -1,8 +0,0 @@
package com.unciv.logic.battle
enum class CombatantType{
Melee,
Ranged,
Civilian,
City
}

View File

@ -2,15 +2,23 @@ package com.unciv.logic.battle
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.UnitType
interface ICombatant{
fun getName(): String
fun getHealth():Int
fun getCombatantType(): CombatantType
fun getUnitType(): UnitType
fun getAttackingStrength(defender: ICombatant): Int
fun getDefendingStrength(attacker: ICombatant): Int
fun takeDamage(damage:Int)
fun isDefeated():Boolean
fun getCivilization(): CivilizationInfo
fun getTile(): TileInfo
fun isMelee(): Boolean {
return this.getUnitType() in listOf(UnitType.Melee,UnitType.Mounted)
}
fun isRanged(): Boolean {
return this.getUnitType() in listOf(UnitType.Archery,UnitType.Siege)
}
}

View File

@ -18,10 +18,9 @@ class MapUnitCombatant(val unit: MapUnit) : ICombatant {
}
override fun getAttackingStrength(defender: ICombatant): Int {
val attackerStrength =
if (unit.getBaseUnit().unitType == UnitType.Ranged)
unit.getBaseUnit().rangedStrength
else unit.getBaseUnit().strength
val attackerStrength: Int
if (isRanged()) attackerStrength = unit.getBaseUnit().rangedStrength
else attackerStrength = unit.getBaseUnit().strength
return attackerStrength*unit.health
}
@ -30,13 +29,8 @@ class MapUnitCombatant(val unit: MapUnit) : ICombatant {
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!")
}
override fun getUnitType(): UnitType {
return unit.getBaseUnit().unitType
}
override fun toString(): String {

View File

@ -16,11 +16,11 @@ class CityConstructions {
var currentConstruction: String = "Monument" // default starting building!
internal fun getBuildableBuildings(): List<String> = GameBasics.Buildings.values
.filter { it.isBuildable(this) }.map { it.name }
internal fun getBuildableBuildings(): List<Building> = GameBasics.Buildings.values
.filter { it.isBuildable(this) }
fun getConstructableUnits() = GameBasics.Units.values
.filter { it.isBuildable(this) }.map { it.name }
.filter { it.isBuildable(this) }
// Library and public school unique (not actualy unique, though...hmm)
fun getStats(): Stats {

View File

@ -7,13 +7,6 @@ import com.unciv.models.gamebasics.GameBasics
import com.unciv.models.gamebasics.Unit
import java.text.DecimalFormat
enum class UnitType{
Civilian,
Melee,
Ranged
}
class MapUnit {
@Transient
lateinit var civInfo: CivilizationInfo
@ -69,45 +62,11 @@ class MapUnit {
tile.improvementInProgress = null
}
/**
* @param origin
* @param destination
* @return The tile that we reached this turn
*/
fun headTowards(destination: Vector2): TileInfo {
val currentTile = getTile()
val tileMap = currentTile.tileMap
val finalDestinationTile = tileMap[destination]
val distanceToTiles = getDistanceToTiles()
val destinationTileThisTurn:TileInfo
if (distanceToTiles.containsKey(finalDestinationTile)) { // we can get there this turn
if (finalDestinationTile.unit == null)
destinationTileThisTurn = finalDestinationTile
else // Someone is blocking to the path to the final tile...
{
val destinationNeighbors = tileMap[destination].neighbors
if(destinationNeighbors.contains(currentTile)) // We're right nearby anyway, no need to move
return currentTile
val reachableDestinationNeighbors = destinationNeighbors.filter { distanceToTiles.containsKey(it) && it.unit==null }
if(reachableDestinationNeighbors.isEmpty()) // We can't get closer...
return currentTile
destinationTileThisTurn = reachableDestinationNeighbors.minBy { distanceToTiles[it]!! }!!
}
}
else { // If the tile is far away, we need to build a path how to get there, and then take the first step
val path = UnitMovementAlgorithms(tileMap)
.getShortestPath(currentTile.position, destination, this)
destinationTileThisTurn = path.first()
}
moveToTile(destinationTileThisTurn)
return destinationTileThisTurn
return UnitMovementAlgorithms(getTile().tileMap).headTowards(this,destination)
}
private fun heal(){

View File

@ -14,7 +14,7 @@ class UnitMovementAlgorithms(val tileMap: TileMap){
}
if(unit.getBaseUnit().hasUnique("Ignores terrain cost")) return 1f;
if(unit.getBaseUnit().hasUnique("Rough Terrain Penalty")
if(unit.getBaseUnit().hasUnique("Rough terrain penalty")
&& (to.baseTerrain=="Hill" || to.terrainFeature=="Forest" || to.terrainFeature=="Jungle"))
return 4f
@ -100,4 +100,46 @@ class UnitMovementAlgorithms(val tileMap: TileMap){
}
}
/**
* @param origin
* @param destination
* @return The tile that we reached this turn
*/
fun headTowards(unit:MapUnit,destination: Vector2): TileInfo {
val currentTile = unit.getTile()
val tileMap = currentTile.tileMap
val finalDestinationTile = tileMap[destination]
val distanceToTiles = unit.getDistanceToTiles()
val destinationTileThisTurn:TileInfo
if (distanceToTiles.containsKey(finalDestinationTile)) { // we can get there this turn
if (finalDestinationTile.unit == null)
destinationTileThisTurn = finalDestinationTile
else // Someone is blocking to the path to the final tile...
{
val destinationNeighbors = tileMap[destination].neighbors
if(destinationNeighbors.contains(currentTile)) // We're right nearby anyway, no need to move
return currentTile
val reachableDestinationNeighbors = destinationNeighbors.filter { distanceToTiles.containsKey(it) && it.unit==null }
if(reachableDestinationNeighbors.isEmpty()) // We can't get closer...
return currentTile
destinationTileThisTurn = reachableDestinationNeighbors.minBy { distanceToTiles[it]!! }!!
}
}
else { // If the tile is far away, we need to build a path how to get there, and then take the first step
val path = getShortestPath(currentTile.position, destination, unit)
destinationTileThisTurn = path.first()
}
unit.moveToTile(destinationTileThisTurn)
return destinationTileThisTurn
}
}

View File

@ -0,0 +1,10 @@
package com.unciv.logic.map
enum class UnitType{
City,
Civilian,
Melee,
Archery,
Mounted,
Siege
}

View File

@ -66,12 +66,12 @@ open class TileGroup(var tileInfo: TileInfo) : Group() {
open fun update(isViewable: Boolean) {
hideCircle()
if (!tileInfo.tileMap.gameInfo.getPlayerCivilization().exploredTiles.contains(tileInfo.position)) {
hexagon.color = Color.BLACK
return
}
hideCircle()
updateTerrainFeatureImage()
updateTileColor(isViewable)
updateResourceImage()

View File

@ -107,8 +107,9 @@ class TileMapHolder(internal val worldScreen: WorldScreen, internal val tileMap:
val attackableTiles:List<TileInfo>
when(unit.getBaseUnit().unitType){
UnitType.Civilian -> return
UnitType.Melee -> attackableTiles = unit.getDistanceToTiles().keys.toList()
UnitType.Ranged -> attackableTiles = unit.getTile().getTilesInDistance(2)
UnitType.Melee, UnitType.Mounted -> attackableTiles = unit.getDistanceToTiles().keys.toList()
UnitType.Ranged, UnitType.Siege -> attackableTiles = unit.getTile().getTilesInDistance(2)
UnitType.City -> throw Exception("How are you attacking with a city?")
}
for (tile in attackableTiles.filter { it.unit!=null && it.unit!!.owner != unit.owner && civViewableTiles.contains(it)})