From 8f7acf37d270c304d092f5389e29a616ad5f6186 Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Sun, 1 Oct 2023 08:45:02 +0200 Subject: [PATCH] Pedia pixel units (#10187) * Display Pixel Unit Art in Civilopedia * Pixel Unit Art in Civilopedia - Setting UI * Change FormattedLine.extraImage sizing to apply to longer coordinate * Pixel Unit Art in Civilopedia - better centering using 'crop to content' --- .../jsons/translations/template.properties | 2 + .../com/unciv/models/metadata/GameSettings.kt | 3 + .../BaseUnitDescriptions.kt | 25 +++++- .../com/unciv/ui/popups/options/DisplayTab.kt | 20 ++++- .../civilopediascreen/FormattedLine.kt | 76 ++++++++++++++++++- .../5-Miscellaneous-JSON-files.md | 30 ++++---- 6 files changed, 133 insertions(+), 23 deletions(-) diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 94646493a9..6ff8b02ae1 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -748,6 +748,8 @@ Reset = Show zoom buttons in world screen = Experimental Demographics scoreboard = +Size of Unitset art in Civilopedia = + ### Visual Hints subgroup Visual Hints = diff --git a/core/src/com/unciv/models/metadata/GameSettings.kt b/core/src/com/unciv/models/metadata/GameSettings.kt index 0baf070438..e3b9ab5a99 100644 --- a/core/src/com/unciv/models/metadata/GameSettings.kt +++ b/core/src/com/unciv/models/metadata/GameSettings.kt @@ -134,6 +134,9 @@ class GameSettings { enum class NationPickerListMode { Icons, List } var nationPickerListMode = NationPickerListMode.List + /** Size of automatic display of UnitSet art in Civilopedia - 0 to disable */ + var pediaUnitArtSize = 0f + /** used to migrate from older versions of the settings */ var version: Int? = null diff --git a/core/src/com/unciv/ui/objectdescriptions/BaseUnitDescriptions.kt b/core/src/com/unciv/ui/objectdescriptions/BaseUnitDescriptions.kt index 12aaedb267..b82f9cb9a4 100644 --- a/core/src/com/unciv/ui/objectdescriptions/BaseUnitDescriptions.kt +++ b/core/src/com/unciv/ui/objectdescriptions/BaseUnitDescriptions.kt @@ -2,7 +2,10 @@ package com.unciv.ui.objectdescriptions import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.unciv.GUI import com.unciv.logic.city.City +import com.unciv.models.metadata.GameSettings +import com.unciv.models.ruleset.IRulesetObject import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.UniqueFlag @@ -14,11 +17,10 @@ import com.unciv.models.stats.Stat import com.unciv.models.translations.tr import com.unciv.ui.components.Fonts import com.unciv.ui.components.extensions.getConsumesAmountString -import com.unciv.ui.components.extensions.toPercent +import com.unciv.ui.images.ImageGetter import com.unciv.ui.screens.basescreen.BaseScreen import com.unciv.ui.screens.civilopediascreen.FormattedLine import com.unciv.ui.screens.civilopediascreen.MarkupRenderer -import kotlin.math.pow object BaseUnitDescriptions { @@ -74,6 +76,9 @@ object BaseUnitDescriptions { fun getCivilopediaTextLines(baseUnit: BaseUnit, ruleset: Ruleset): List { val textList = ArrayList() + // Potentially show pixel unit on top (other civilopediaText is handled by the caller) + textList.addPixelUnitImage(baseUnit) + // Don't call baseUnit.getType() here - coming from the main menu baseUnit isn't fully initialized val unitTypeLink = ruleset.unitTypes[baseUnit.unitType]?.makeLink() ?: "" textList += FormattedLine("{Unit type}: ${baseUnit.unitType.tr()}", unitTypeLink) @@ -199,6 +204,22 @@ object BaseUnitDescriptions { return textList } + /** Show Pixel Unit Art for the unit. + * * _Unless_ the mod already uses [extraImage][FormattedLine.extraImage] in the unit's [civilopediaText][IRulesetObject.civilopediaText] + * * _Unless_ user has selected no [unitSet][GameSettings.unitSet] + * * For units with era or style variants, only the default is shown (todo: extend FormattedLine with slideshow capability) + */ + // Note: By popular request (this is a simple variant of one of the ideas in #10175) + private fun ArrayList.addPixelUnitImage(baseUnit: BaseUnit) { + if (baseUnit.civilopediaText.any { it.extraImage.isNotEmpty() }) return + val settings = GUI.getSettings() + if (settings.unitSet.isNullOrEmpty() || settings.pediaUnitArtSize < 1f) return + val imageName = "TileSets/${settings.unitSet}/Units/${baseUnit.name}" + if (!ImageGetter.imageExists(imageName)) return // Some units don't have Unit art (e.g. nukes) + add(FormattedLine(extraImage = imageName, imageSize = settings.pediaUnitArtSize, centered = true)) + add(FormattedLine(separator = true, color = "#7f7f7f")) + } + @Suppress("RemoveExplicitTypeArguments") // for faster IDE - inferring sequence types can be slow fun UnitType.getUnitTypeCivilopediaTextLines(ruleset: Ruleset): List { fun getDomainLines() = sequence { diff --git a/core/src/com/unciv/ui/popups/options/DisplayTab.kt b/core/src/com/unciv/ui/popups/options/DisplayTab.kt index 7622379df0..9747d64af2 100644 --- a/core/src/com/unciv/ui/popups/options/DisplayTab.kt +++ b/core/src/com/unciv/ui/popups/options/DisplayTab.kt @@ -12,18 +12,18 @@ import com.unciv.models.metadata.ScreenSize import com.unciv.models.skins.SkinCache import com.unciv.models.tilesets.TileSetCache import com.unciv.models.translations.tr +import com.unciv.ui.components.TranslatedSelectBox import com.unciv.ui.components.UncivSlider import com.unciv.ui.components.WrappableLabel import com.unciv.ui.components.extensions.addSeparator import com.unciv.ui.components.extensions.brighten -import com.unciv.ui.components.input.onChange -import com.unciv.ui.components.input.onClick import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.extensions.toTextButton +import com.unciv.ui.components.input.onChange +import com.unciv.ui.components.input.onClick import com.unciv.ui.images.ImageGetter import com.unciv.ui.popups.ConfirmPopup import com.unciv.ui.screens.basescreen.BaseScreen -import com.unciv.ui.components.TranslatedSelectBox import com.unciv.ui.screens.worldscreen.NotificationsScroll import com.unciv.utils.Display import com.unciv.utils.ScreenMode @@ -71,6 +71,7 @@ fun displayTab( addResetTutorials(this, settings) optionsPopup.addCheckbox(this, "Show zoom buttons in world screen", settings.showZoomButtons, true) { settings.showZoomButtons = it } optionsPopup.addCheckbox(this, "Experimental Demographics scoreboard", settings.useDemographics, true) { settings.useDemographics = it } + addPediaUnitArtSizeSlider(this, settings, optionsPopup.selectBoxMinWidth) addSeparator() add("Visual Hints".toLabel(fontSize = 24)).colspan(2).row() @@ -159,6 +160,19 @@ private fun addUnitIconAlphaSlider(table: Table, settings: GameSettings, selectB table.add(unitIconAlphaSlider).minWidth(selectBoxMinWidth).pad(10f).row() } +private fun addPediaUnitArtSizeSlider(table: Table, settings: GameSettings, selectBoxMinWidth: Float) { + table.add("Size of Unitset art in Civilopedia".toLabel()).left().fillX() + + val unitArtSizeSlider = UncivSlider( + 0f, 360f, 1f, initial = settings.pediaUnitArtSize + ) { + settings.pediaUnitArtSize = it + GUI.setUpdateWorldOnNextRender() + } + unitArtSizeSlider.setSnapToValues(floatArrayOf(0f, 32f, 48f, 64f, 96f, 120f, 180f, 240f, 360f), 60f) + table.add(unitArtSizeSlider).minWidth(selectBoxMinWidth).pad(10f).row() +} + private fun addScreenModeSelectBox(table: Table, settings: GameSettings, selectBoxMinWidth: Float) { table.add("Screen Mode".toLabel()).left().fillX() diff --git a/core/src/com/unciv/ui/screens/civilopediascreen/FormattedLine.kt b/core/src/com/unciv/ui/screens/civilopediascreen/FormattedLine.kt index 4be65ff1c1..32a52cb2af 100644 --- a/core/src/com/unciv/ui/screens/civilopediascreen/FormattedLine.kt +++ b/core/src/com/unciv/ui/screens/civilopediascreen/FormattedLine.kt @@ -2,8 +2,15 @@ package com.unciv.ui.screens.civilopediascreen import com.badlogic.gdx.Gdx import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.graphics.Pixmap +import com.badlogic.gdx.graphics.TextureData +import com.badlogic.gdx.graphics.g2d.TextureRegion +import com.badlogic.gdx.graphics.glutils.FileTextureData +import com.badlogic.gdx.graphics.glutils.PixmapTextureData import com.badlogic.gdx.scenes.scene2d.Actor +import com.badlogic.gdx.scenes.scene2d.ui.Image import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable import com.badlogic.gdx.utils.Align import com.unciv.Constants import com.unciv.UncivGame @@ -238,15 +245,19 @@ class FormattedLine ( try { val image = when { ImageGetter.imageExists(extraImage) -> - ImageGetter.getImage(extraImage) + if (centered) ImageGetter.getDrawable(extraImage).cropToContent() + else ImageGetter.getImage(extraImage) Gdx.files.internal("ExtraImages/$extraImage.png").exists() -> ImageGetter.getExternalImage("$extraImage.png") Gdx.files.internal("ExtraImages/$extraImage.jpg").exists() -> ImageGetter.getExternalImage("$extraImage.jpg") else -> return table } - val width = if (imageSize.isNaN()) labelWidth else imageSize - val height = width * image.height / image.width + // limit larger cordinate to a given max size + val maxSize = if (imageSize.isNaN()) labelWidth else imageSize + val (width, height) = if (image.width > image.height) + maxSize to maxSize * image.height / image.width + else maxSize * image.width / image.height to maxSize table.add(image).size(width, height) } catch (exception: Exception) { Log.error("Exception while rendering civilopedia text", exception) @@ -337,4 +348,63 @@ class FormattedLine ( else -> "'$text'->$link" } } + + // region Helpers to crop an image to content + private fun TextureRegionDrawable.cropToContent(): Image { + val rect = getContentSize() + val newRegion = TextureRegion(region.texture, rect.x, rect.y, rect.width, rect.height) + return Image(TextureRegionDrawable(newRegion)) + } + + private fun TextureRegionDrawable.getContentSize(): java.awt.Rectangle { + val pixMap = region.texture.textureData.getReadonlyPixmap() + val result = java.awt.Rectangle(region.regionX, region.regionY, region.regionWidth, region.regionHeight) // Not Gdx: integers! + val original = java.awt.Rectangle(result) + + while (result.height > 0 && pixMap.isRowEmpty(result, result.height - 1)) { + result.height -= 1 + } + while (result.height > 0 && pixMap.isRowEmpty(result, 0)) { + result.y += 1 + result.height -= 1 + } + while (result.width > 0 && pixMap.isColumnEmpty(result, result.width - 1)) { + result.width -= 1 + } + while (result.width > 0 && pixMap.isColumnEmpty(result, 0)) { + result.x += 1 + result.width -= 1 + } + + result.grow((original.width / 40).coerceAtLeast(1), (original.height / 40).coerceAtLeast(1)) + return result.intersection(original) + } + + private fun Pixmap.isRowEmpty(bounds: java.awt.Rectangle, relativeY: Int): Boolean { + val y = bounds.y + relativeY + return (bounds.x until bounds.x + bounds.width).all { + getPixel(it, y) and 255 == 0 + } + } + + private fun Pixmap.isColumnEmpty(bounds: java.awt.Rectangle, relativeX: Int): Boolean { + val x = bounds.x + relativeX + return (bounds.y until bounds.y + bounds.height).all { + getPixel(x, it) and 255 == 0 + } + } + + /** Retrieve a texture Pixmap without reload or ownership transfer, useable for read operations only. + * + * (FileTextureData.consumePixmap forces a reload of the entire file - inefficient if we only want to look at pixel values) */ + private fun TextureData.getReadonlyPixmap(): Pixmap { + if (!isPrepared) prepare() + if (this is PixmapTextureData) return consumePixmap() + if (this !is FileTextureData) throw TypeCastException("getReadonlyPixmap only works on file or pixmap based textures") + val field = FileTextureData::class.java.getDeclaredField("pixmap") + field.isAccessible = true + return field.get(this) as Pixmap + } + // endregion + } diff --git a/docs/Modders/Mod-file-structure/5-Miscellaneous-JSON-files.md b/docs/Modders/Mod-file-structure/5-Miscellaneous-JSON-files.md index 081d123f40..416332e743 100644 --- a/docs/Modders/Mod-file-structure/5-Miscellaneous-JSON-files.md +++ b/docs/Modders/Mod-file-structure/5-Miscellaneous-JSON-files.md @@ -296,21 +296,21 @@ An example of the format is: List of attributes - note not all combinations are valid: -| Attribute | Type | Description | -| --------- | ---- | ----------- | -|`text`|String|Text to display.| -|`link`|String|Create link and icon, format: Category/Name or _external_ link ('http://','https://','mailto:').| -|`icon`|String|Show icon without linking, format: Category/Name.| -|`extraImage`|String|Display an Image instead of text. Can be a path found in a texture atlas or or the name of a png or jpg in the ExtraImages folder.| -|`imageSize`|Float|Width in world units of the [extraImage], height is calculated preserving aspect ratio. Defaults to available width.| -|`header`|Integer|Header level. 1 means double text size and decreases from there.| -|`size`|Integer|Text size, default is 18. Use `size` or `header` but not both.| -|`indent`|Integer|Indent level. 0 means text will follow icons, 1 aligns to the right of all icons, each further step is 30 units.| -|`padding`|Float|Vertical padding between rows, defaults to 5 units.| -|`color`|String|Sets text color, accepts names or 6/3-digit web colors (e.g. #FFA040).| -|`separator`|Boolean|Renders a separator line instead of text. Can be combined only with `color` and `size` (line width, default 2).| -|`starred`|Boolean|Decorates text with a star icon - if set, it receives the `color` instead of the text.| -|`centered`|Boolean|Centers the line (and turns off automatic wrap).| +| Attribute | Type | Description | +|--------------|---------|-------------------------------------------------------------------------------------------------------------------------------------| +| `text` | String | Text to display. | +| `link` | String | Create link and icon, format: Category/Name or _external_ link ('http://','https://','mailto:'). | +| `icon` | String | Show icon without linking, format: Category/Name. | +| `extraImage` | String | Display an Image instead of text. Can be a path found in a texture atlas or or the name of a png or jpg in the ExtraImages folder. | +| `imageSize` | Float | Size in world units of the [extraImage], the smaller coordinate is calculated preserving aspect ratio. Defaults to available width. | +| `header` | Integer | Header level. 1 means double text size and decreases from there. | +| `size` | Integer | Text size, default is 18. Use `size` or `header` but not both. | +| `indent` | Integer | Indent level. 0 means text will follow icons, 1 aligns to the right of all icons, each further step is 30 units. | +| `padding` | Float | Vertical padding between rows, defaults to 5 units. | +| `color` | String | Sets text color, accepts names or 6/3-digit web colors (e.g. #FFA040). | +| `separator` | Boolean | Renders a separator line instead of text. Can be combined only with `color` and `size` (line width, default 2). | +| `starred` | Boolean | Decorates text with a star icon - if set, it receives the `color` instead of the text. | +| `centered` | Boolean | Centers the line (and turns off automatic wrap). For an `extraImage`, turns on crop-to-content to equalize transparent borders. | The lines from json will 'surround' the automatically generated lines such that the latter are inserted just above the first json line carrying a link, if any. If no json lines have links, they will be inserted between the automatic title and the automatic info. This method may, however, change in the future.