Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed the visual gaps in territory borders #5446

Merged
merged 9 commits into from
Oct 11, 2021
Binary file added android/Images/BorderImages/ConcaveInner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added android/Images/BorderImages/ConcaveOuter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added android/Images/BorderImages/ConvexConcaveInner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added android/Images/BorderImages/ConvexConcaveOuter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added android/Images/BorderImages/ConvexInner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added android/Images/BorderImages/ConvexOuter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed android/Images/OtherIcons/Border-inner.png
Binary file not shown.
Binary file removed android/Images/OtherIcons/Border-outer.png
Binary file not shown.
1,312 changes: 670 additions & 642 deletions android/assets/game.atlas

Large diffs are not rendered by default.

Binary file modified android/assets/game.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 19 additions & 3 deletions core/src/com/unciv/logic/HexMath.kt
Original file line number Diff line number Diff line change
Expand Up @@ -184,18 +184,34 @@ object HexMath {
(abs(relativeX) + abs(relativeY)).toInt()
}

private val clockPositionToHexVectorMap: Map<Int, Vector2> = mapOf(
0 to Vector2(1f, 1f), // This alias of 12 makes clock modulo logic easier
12 to Vector2(1f, 1f),
2 to Vector2(0f, 1f),
4 to Vector2(-1f, 0f),
6 to Vector2(-1f, -1f),
8 to Vector2(0f, -1f),
10 to Vector2(1f, 0f)
)

/** Returns the hex-space distance corresponding to [clockPosition], or a zero vector if [clockPosition] is invalid */
fun getClockPositionToHexVector(clockPosition: Int): Vector2 {
return clockPositionToHexVectorMap[clockPosition]?: Vector2.Zero
}

// Statically allocate the Vectors (in World coordinates)
// of the 6 clock directions for border and road drawing in TileGroup
private val clockToWorldVectors: Map<Int,Vector2> = mapOf(
private val clockPositionToWorldVectorMap: Map<Int,Vector2> = mapOf(
2 to hex2WorldCoords(Vector2(0f, -1f)),
4 to hex2WorldCoords(Vector2(1f, 0f)),
6 to hex2WorldCoords(Vector2(1f, 1f)),
8 to hex2WorldCoords(Vector2(0f, 1f)),
10 to hex2WorldCoords(Vector2(-1f, 0f)),
12 to hex2WorldCoords(Vector2(-1f, -1f)) )

fun getClockDirectionToWorldVector(clockDirection: Int): Vector2 =
clockToWorldVectors[clockDirection] ?: Vector2.Zero
/** Returns the world/screen-space distance corresponding to [clockPosition], or a zero vector if [clockPosition] is invalid */
fun getClockPositionToWorldVector(clockPosition: Int): Vector2 =
clockPositionToWorldVectorMap[clockPosition] ?: Vector2.Zero

fun getDistanceFromEdge(vector: Vector2, mapParameters: MapParameters): Int {
val x = vector.x.toInt()
Expand Down
10 changes: 10 additions & 0 deletions core/src/com/unciv/logic/map/TileInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,16 @@ open class TileInfo {
// We have to .toList() so that the values are stored together once for caching,
// and the toSequence so that aggregations (like neighbors.flatMap{it.units} don't take up their own space

/** Returns the left shared neighbor of [this] and [neighbor] (relative to the view direction [this]->[neighbor]), or null if there is no such tile. */
fun getLeftSharedNeighbor(neighbor: TileInfo): TileInfo? {
return tileMap.getClockPositionNeighborTile(this,(tileMap.getNeighborTileClockPosition(this, neighbor) - 2) % 12)
}

/** Returns the right shared neighbor [this] and [neighbor] (relative to the view direction [this]->[neighbor]), or null if there is no such tile. */
fun getRightSharedNeighbor(neighbor: TileInfo): TileInfo? {
return tileMap.getClockPositionNeighborTile(this,(tileMap.getNeighborTileClockPosition(this, neighbor) + 2) % 12)
}

@delegate:Transient
val height : Int by lazy {
getAllTerrains().flatMap { it.uniqueObjects }
Expand Down
16 changes: 14 additions & 2 deletions core/src/com/unciv/logic/map/TileMap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ class TileMap {
}

/**
* Returns the clockPosition of [otherTile] seen from [tile]'s position
* Returns the clock position of [otherTile] seen from [tile]'s position
* Returns -1 if not neighbors
*/
fun getNeighborTileClockPosition(tile: TileInfo, otherTile: TileInfo): Int {
Expand All @@ -249,12 +249,24 @@ class TileMap {
}
}

/**
* Returns the neighbor tile of [tile] at [clockPosition], if it exists.
* Takes world wrap into account
* Returns null if there is no such neighbor tile or if [clockPosition] is not a valid clock position
*/
fun getClockPositionNeighborTile(tile: TileInfo, clockPosition: Int): TileInfo? {
val difference = HexMath.getClockPositionToHexVector(clockPosition)
if (difference == Vector2.Zero) return null
val possibleNeighborPosition = tile.position.cpy().add(difference)
return getIfTileExistsOrNull(possibleNeighborPosition.x.toInt(), possibleNeighborPosition.y.toInt())
}

/** Convert relative direction of [otherTile] seen from [tile]'s position into a vector
* in world coordinates of length sqrt(3), so that it can be used to go from tile center to
* the edge of the hex in that direction (meaning the center of the border between the hexes)
*/
fun getNeighborTilePositionAsWorldCoords(tile: TileInfo, otherTile: TileInfo): Vector2 =
HexMath.getClockDirectionToWorldVector(getNeighborTileClockPosition(tile, otherTile))
HexMath.getClockPositionToWorldVector(getNeighborTileClockPosition(tile, otherTile))

/**
* Returns the closest position to (0, 0) outside the map which can be wrapped
Expand Down
92 changes: 70 additions & 22 deletions core/src/com/unciv/ui/tilegroups/TileGroup.kt
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,19 @@ open class TileGroup(var tileInfo: TileInfo, var tileSetStrings:TileSetStrings,

var resourceImage: Actor? = null
var resource: String? = null

class RoadImage {
var roadStatus: RoadStatus = RoadStatus.None
var image: Image? = null
}
data class BorderSegment(
var images: List<Image>,
var isLeftConcave: Boolean = false,
var isRightConcave: Boolean = false,
)

private val roadImages = HashMap<TileInfo, RoadImage>()
private val borderImages = HashMap<TileInfo, List<Image>>() // map of neighboring tile to border images
private val borderSegments = HashMap<TileInfo, BorderSegment>() // map of neighboring tile to border segments

@Suppress("LeakingThis") // we trust TileGroupIcons not to use our `this` in its constructor except storing it for later
val icons = TileGroupIcons(this)
Expand Down Expand Up @@ -110,11 +121,6 @@ open class TileGroup(var tileInfo: TileInfo, var tileSetStrings:TileSetStrings,
var showEntireMap = UncivGame.Current.viewEntireMapForDebug
var forMapEditorIcon = false

class RoadImage {
var roadStatus: RoadStatus = RoadStatus.None
var image: Image? = null
}

init {
this.setSize(groupSize, groupSize)
this.addActor(baseLayerGroup)
Expand Down Expand Up @@ -290,7 +296,7 @@ open class TileGroup(var tileInfo: TileInfo, var tileSetStrings:TileSetStrings,
updatePixelMilitaryUnit(false)
updatePixelCivilianUnit(false)

if (borderImages.isNotEmpty()) clearBorders()
if (borderSegments.isNotEmpty()) clearBorders()

icons.update(false,false ,false, false, null)

Expand Down Expand Up @@ -402,11 +408,11 @@ open class TileGroup(var tileInfo: TileInfo, var tileSetStrings:TileSetStrings,
}

private fun clearBorders() {
for (images in borderImages.values)
for (image in images)
for (borderSegment in borderSegments.values)
for (image in borderSegment.images)
image.remove()

borderImages.clear()
borderSegments.clear()
}

private var previousTileOwner: CivilizationInfo? = null
Expand All @@ -416,7 +422,6 @@ open class TileGroup(var tileInfo: TileInfo, var tileSetStrings:TileSetStrings,
// removing all the border images and putting them back again!
val tileOwner = tileInfo.getOwner()


if (previousTileOwner != tileOwner) clearBorders()

previousTileOwner = tileOwner
Expand All @@ -425,25 +430,65 @@ open class TileGroup(var tileInfo: TileInfo, var tileSetStrings:TileSetStrings,
val civOuterColor = tileInfo.getOwner()!!.nation.getOuterColor()
val civInnerColor = tileInfo.getOwner()!!.nation.getInnerColor()
for (neighbor in tileInfo.neighbors) {
var shouldRemoveBorderSegment = false
var shouldAddBorderSegment = false

var borderSegmentShouldBeLeftConcave = false
var borderSegmentShouldBeRightConcave = false

val neighborOwner = neighbor.getOwner()
if (neighborOwner == tileOwner && borderImages.containsKey(neighbor)) // the neighbor used to not belong to us, but now it's ours
{
for (image in borderImages[neighbor]!!)
if (neighborOwner == tileOwner && borderSegments.containsKey(neighbor)) { // the neighbor used to not belong to us, but now it's ours
shouldRemoveBorderSegment = true
}
else if (neighborOwner != tileOwner) {
val leftSharedNeighbor = tileInfo.getLeftSharedNeighbor(neighbor)
val rightSharedNeighbor = tileInfo.getRightSharedNeighbor(neighbor)

// If a shared neighbor doesn't exist (because it's past a map edge), we act as if it's our tile for border concave/convex-ity purposes.
// This is because we do not draw borders against non-existing tiles either.
borderSegmentShouldBeLeftConcave = leftSharedNeighbor == null || leftSharedNeighbor.getOwner() == tileOwner
borderSegmentShouldBeRightConcave = rightSharedNeighbor == null || rightSharedNeighbor.getOwner() == tileOwner

if (!borderSegments.containsKey(neighbor)) { // there should be a border here but there isn't
shouldAddBorderSegment = true
}
else if (
borderSegmentShouldBeLeftConcave != borderSegments[neighbor]!!.isLeftConcave ||
borderSegmentShouldBeRightConcave != borderSegments[neighbor]!!.isRightConcave
) { // the concave/convex-ity of the border here is wrong
shouldRemoveBorderSegment = true
shouldAddBorderSegment = true
}
}

if (shouldRemoveBorderSegment) {
for (image in borderSegments[neighbor]!!.images)
image.remove()
borderImages.remove(neighbor)
borderSegments.remove(neighbor)
}
if (neighborOwner != tileOwner && !borderImages.containsKey(neighbor)) { // there should be a border here but there isn't
if (shouldAddBorderSegment) {
val images = mutableListOf<Image>()
val borderSegment = BorderSegment(images, borderSegmentShouldBeLeftConcave, borderSegmentShouldBeRightConcave)
borderSegments[neighbor] = borderSegment

val borderShapeString = when {
borderSegment.isLeftConcave && borderSegment.isRightConcave -> "Concave"
!borderSegment.isLeftConcave && !borderSegment.isRightConcave -> "Convex"
else -> "ConvexConcave"
}
val isConcaveConvex = borderSegment.isLeftConcave && !borderSegment.isRightConcave

val relativeWorldPosition = tileInfo.tileMap.getNeighborTilePositionAsWorldCoords(tileInfo, neighbor)

// This is some crazy voodoo magic so I'll explain.
val images = mutableListOf<Image>()
borderImages[neighbor] = images
val sign = if (relativeWorldPosition.x < 0) -1 else 1
val angle = sign * (atan(sign * relativeWorldPosition.y / relativeWorldPosition.x) * 180 / PI - 90.0).toFloat()

val innerBorderImage = ImageGetter.getImage("OtherIcons/Border-inner")
innerBorderImage.width = 38f
val innerBorderImage = ImageGetter.getImage("BorderImages/${borderShapeString}Inner")
if (isConcaveConvex) {
innerBorderImage.scaleX = -innerBorderImage.scaleX
}
innerBorderImage.width = hexagonImageWidth
innerBorderImage.setOrigin(Align.center) // THEN the origin is correct,
innerBorderImage.rotateBy(angle) // and the rotation works.
innerBorderImage.center(this) // move to center of tile
Expand All @@ -452,8 +497,11 @@ open class TileGroup(var tileInfo: TileInfo, var tileSetStrings:TileSetStrings,
miscLayerGroup.addActor(innerBorderImage)
images.add(innerBorderImage)

val outerBorderImage = ImageGetter.getImage("OtherIcons/Border-outer")
outerBorderImage.width = 38f
val outerBorderImage = ImageGetter.getImage("BorderImages/${borderShapeString}Outer")
if (isConcaveConvex) {
outerBorderImage.scaleX = -outerBorderImage.scaleX
}
outerBorderImage.width = hexagonImageWidth
outerBorderImage.setOrigin(Align.center) // THEN the origin is correct,
outerBorderImage.rotateBy(angle) // and the rotation works.
outerBorderImage.center(this) // move to center of tile
Expand Down