mirror of
https://github.com/yairm210/Unciv.git
synced 2025-01-13 00:04:38 +07:00
UI candy: WLTK fireworks (#11616)
* Particle effect fireworks for WLTK * Refactor fireworks, try another location calculation * Save todo list * Fireworks assets * ParticleEffectAnimation framework refactor and FasterUIDevelopment * ParticleEffectAnimation rework - works correctly now * ParticleEffectAnimation - credits and fine-tune * ParticleEffectAnimation - atlas * ParticleEffectAnimation - clean up testing code * ParticleEffectAnimation - fix bungled texture move
This commit is contained in:
parent
7e3bbb6053
commit
a046e43dbf
BIN
android/Images.ConstructionIcons/pp_firework.png
Normal file
BIN
android/Images.ConstructionIcons/pp_firework.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Before Width: | Height: | Size: 923 KiB After Width: | Height: | Size: 928 KiB |
Binary file not shown.
Before Width: | Height: | Size: 14 KiB |
2205
android/assets/effects/fireworks.p
Normal file
2205
android/assets/effects/fireworks.p
Normal file
File diff suppressed because it is too large
Load Diff
200
core/src/com/unciv/ui/components/ParticleEffectAnimation.kt
Normal file
200
core/src/com/unciv/ui/components/ParticleEffectAnimation.kt
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
package com.unciv.ui.components
|
||||||
|
|
||||||
|
import com.badlogic.gdx.Gdx
|
||||||
|
import com.badlogic.gdx.graphics.GL20
|
||||||
|
import com.badlogic.gdx.graphics.g2d.ParticleEffect
|
||||||
|
import com.badlogic.gdx.graphics.g2d.ParticleEmitter
|
||||||
|
import com.badlogic.gdx.graphics.g2d.ParticleEmitter.RangedNumericValue
|
||||||
|
import com.badlogic.gdx.math.Interpolation
|
||||||
|
import com.badlogic.gdx.math.Rectangle
|
||||||
|
import com.badlogic.gdx.math.Vector2
|
||||||
|
import com.badlogic.gdx.scenes.scene2d.Stage
|
||||||
|
import com.badlogic.gdx.utils.Array
|
||||||
|
import com.badlogic.gdx.utils.Disposable
|
||||||
|
import com.unciv.UncivGame
|
||||||
|
import com.unciv.ui.images.ImageGetter
|
||||||
|
|
||||||
|
/** Hosts one template ParticleEffect and any number of clones, and each can travel linearly over part of its lifetime.
|
||||||
|
* @property load Must be called from a subclass at least once - see detailed Kdoc
|
||||||
|
* @property configure Optionally modifies all clones - see detailed Kdoc
|
||||||
|
* @property getTargetBounds Where to draw on the stage - see detailed Kdoc
|
||||||
|
* @property getScale Optionally scales all effects - see detailed Kdoc
|
||||||
|
* @property render Needs to be called from the hosting Screen in its render override
|
||||||
|
* @property dispose Required - this is just the Gdx Disposable interface, no automatic call
|
||||||
|
*/
|
||||||
|
abstract class ParticleEffectAnimation : Disposable {
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate", "ConstPropertyName")
|
||||||
|
companion object {
|
||||||
|
const val defaultAtlasName = "ConstructionIcons"
|
||||||
|
|
||||||
|
fun isEnabled(game: UncivGame, atlasName: String = defaultAtlasName) =
|
||||||
|
game.settings.continuousRendering && ImageGetter.getSpecificAtlas(atlasName) != null
|
||||||
|
|
||||||
|
private val halfPoint = Vector2(0.5f, 0.5f)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val templateEffect = ParticleEffect()
|
||||||
|
/** Unit: ms */
|
||||||
|
private var maxDuration: Float = 0f
|
||||||
|
// val pool: ParticleEffectPool - can't, since we're filtering emitters by removing them, we need a fresh clone for every repetition. A pool won't give fresh clones...
|
||||||
|
private var nextIndex = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents one currently running effect
|
||||||
|
* - Points are relative to the bounds provided by [getTargetBounds], and would normally stay in the (0..1) range. They default to (0.5,0.5).
|
||||||
|
* - Time units are seconds (while Gdx [ParticleEffect] uses milliseconds), matching [render] delta.
|
||||||
|
* @property index A sequential counter, just in case a [configure] or [onComplete] needs some handle to differentiate parallel or repeated effects. Starts at 0.
|
||||||
|
* @property effect One clone of the template effect [ParticleEffectAnimation] loads (use e.g. [removeEmitters] to modify)
|
||||||
|
* @property startPoint If the effect should travel over time, this is the start point.
|
||||||
|
* @property endPoint If the effect should travel over time, this is the end point.
|
||||||
|
* @property delay This is the initial delay before travel starts. Defaults to 0, when >0 all emitters in the effect get this added to their own delay value.
|
||||||
|
* @property travelTime This is the travel duration - after this is up, the effect stays at [endPoint]. Defaults to the maximum (duration+delay) of all emitters.
|
||||||
|
* @property interpolation Applied to travel time percentage
|
||||||
|
* @property accumulatedTime For [render] to accumulate time in, as effect has none, and individual emitters do not accumulate in a readable way over the entire effect duration.
|
||||||
|
*/
|
||||||
|
protected data class ParticleEffectData(
|
||||||
|
val index: Int,
|
||||||
|
val effect: ParticleEffect,
|
||||||
|
var startPoint: Vector2 = halfPoint,
|
||||||
|
var endPoint: Vector2 = halfPoint,
|
||||||
|
var delay: Float = 0f,
|
||||||
|
var travelTime: Float,
|
||||||
|
var interpolation: Interpolation = Interpolation.linear
|
||||||
|
) {
|
||||||
|
private var accumulatedTime = 0f
|
||||||
|
private var percent = 0f
|
||||||
|
fun update(delta: Float) {
|
||||||
|
accumulatedTime += delta
|
||||||
|
val rawPercent = (accumulatedTime - delay) / travelTime
|
||||||
|
percent = interpolation.apply(rawPercent.coerceIn(0f, 1f))
|
||||||
|
}
|
||||||
|
fun currentX() = startPoint.x + percent * (endPoint.x - startPoint.x)
|
||||||
|
fun currentY() = startPoint.y + percent * (endPoint.y - startPoint.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val activeEffectData = arrayListOf<ParticleEffectData>()
|
||||||
|
//private val effectsBatch: Batch = SpriteBatch()
|
||||||
|
private val targetBounds = Rectangle()
|
||||||
|
private var lastScale = 1f
|
||||||
|
|
||||||
|
/** Fetch where to draw in stage coordinates
|
||||||
|
* - Implementation should ***set*** the fields of the provided rectangle instance.
|
||||||
|
* - Note the bounds do not limit the effects themselves, but determine effect travel over time.
|
||||||
|
* - Actual size of the effects are determined by the effect definition and [getScale].
|
||||||
|
* - For stationary effects, width=0 and height=0 are perfectly acceptable.
|
||||||
|
*/
|
||||||
|
abstract fun getTargetBounds(bounds: Rectangle)
|
||||||
|
|
||||||
|
/** Return how the effects should be scaled relative to their definition (which already uses world coordinates).
|
||||||
|
* - Changing this while the effects are rendering is relatively expensive
|
||||||
|
*/
|
||||||
|
protected open fun getScale() = 1f
|
||||||
|
|
||||||
|
/** Allows the subclass to change an effect about to be started.
|
||||||
|
* - You *can* modify the effect directly, e.g. to [remove emitters][removeEmitters]
|
||||||
|
* - You can alter startPoint and endPoint by assigning new Vector2 instances - do not mutate the default
|
||||||
|
* - You can alter delay and travelTime
|
||||||
|
* @see ParticleEffectData
|
||||||
|
*/
|
||||||
|
protected open fun ParticleEffectData.configure() {}
|
||||||
|
|
||||||
|
/** Called whenever an effect says it's complete.
|
||||||
|
* @param effectData The info on the just-completed effect - most clients won't need this
|
||||||
|
* @return whether and how many effects should ***restart*** once completed (actually creates new clones of the template effect and also calls [configure])
|
||||||
|
*/
|
||||||
|
protected open fun onComplete(effectData: ParticleEffectData) = 0
|
||||||
|
|
||||||
|
/** @return number of currently running effect clones */
|
||||||
|
protected fun activeCount() = activeEffectData.size
|
||||||
|
|
||||||
|
/** Loads the effect definition, creates a pool from it and starts [count] instances potentially modified by [configure]. */
|
||||||
|
protected fun load(effectsFile: String, atlasName: String, count: Int) {
|
||||||
|
activeEffectData.clear()
|
||||||
|
val atlas = ImageGetter.getSpecificAtlas(atlasName)!!
|
||||||
|
templateEffect.load(Gdx.files.internal(effectsFile), atlas)
|
||||||
|
templateEffect.setEmittersCleanUpBlendFunction(false) // Treat it as Unknown whether the effect file changes blend state -> do it ourselves
|
||||||
|
maxDuration = templateEffect.emitters.maxOf { it.getDuration().lowMax + (if (it.delay.isActive) it.delay.lowMax else 0f) }
|
||||||
|
|
||||||
|
repeat(count, ::newEffect)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNUSED_PARAMETER") // Signature to match `repeat` argument
|
||||||
|
private fun newEffect(dummy: Int) {
|
||||||
|
val effect = ParticleEffect(templateEffect)
|
||||||
|
val data = ParticleEffectData(nextIndex, effect, travelTime = maxDuration / 1000)
|
||||||
|
nextIndex++
|
||||||
|
data.configure()
|
||||||
|
if (data.delay > 0f) {
|
||||||
|
for (emitter in effect.emitters)
|
||||||
|
emitter.delay.add(data.delay * 1000)
|
||||||
|
}
|
||||||
|
if (lastScale != 1f)
|
||||||
|
effect.scaleEffect(lastScale)
|
||||||
|
effect.start()
|
||||||
|
activeEffectData += data
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun RangedNumericValue.add(delta: Float) {
|
||||||
|
if (isActive) {
|
||||||
|
lowMin += delta
|
||||||
|
lowMax += delta
|
||||||
|
} else {
|
||||||
|
isActive = true
|
||||||
|
lowMin = delta
|
||||||
|
lowMax = delta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
val effects = activeEffectData.toList()
|
||||||
|
activeEffectData.clear()
|
||||||
|
for (effect in effects) effect.effect.dispose()
|
||||||
|
templateEffect.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun render(stage: Stage?, delta: Float) {
|
||||||
|
if (maxDuration == 0f) return
|
||||||
|
val effectsBatch = stage?.batch ?: return
|
||||||
|
effectsBatch.projectionMatrix = stage.viewport.camera.combined
|
||||||
|
|
||||||
|
getTargetBounds(targetBounds)
|
||||||
|
val newScale = getScale()
|
||||||
|
if (newScale != lastScale) {
|
||||||
|
val scaleChange = newScale / lastScale
|
||||||
|
lastScale = newScale
|
||||||
|
for ((_, effect) in activeEffectData) {
|
||||||
|
effect.scaleEffect(scaleChange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var repeatCount = 0
|
||||||
|
|
||||||
|
effectsBatch.begin()
|
||||||
|
val iterator = activeEffectData.iterator()
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
val effectData = iterator.next()
|
||||||
|
val effect = effectData.effect
|
||||||
|
effectData.update(delta)
|
||||||
|
val x = targetBounds.x + targetBounds.width * effectData.currentX()
|
||||||
|
val y = targetBounds.y + targetBounds.height * effectData.currentY()
|
||||||
|
effect.setPosition(x, y)
|
||||||
|
effect.draw(effectsBatch, delta)
|
||||||
|
if (effect.isComplete) {
|
||||||
|
repeatCount += onComplete(effectData)
|
||||||
|
effect.dispose()
|
||||||
|
iterator.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
effectsBatch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA)
|
||||||
|
effectsBatch.end()
|
||||||
|
|
||||||
|
repeat(repeatCount, ::newEffect)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun ParticleEffect.removeEmitters(predicate: (String)->Boolean) {
|
||||||
|
val matches = Array<ParticleEmitter>()
|
||||||
|
for (emitter in emitters)
|
||||||
|
if (predicate(emitter.name)) matches.add(emitter)
|
||||||
|
emitters.removeAll(matches, true) // This is a Gdx method, not kotlin MutableCollection.removeAll
|
||||||
|
}
|
||||||
|
}
|
73
core/src/com/unciv/ui/components/ParticleEffectFireworks.kt
Normal file
73
core/src/com/unciv/ui/components/ParticleEffectFireworks.kt
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package com.unciv.ui.components
|
||||||
|
|
||||||
|
import com.badlogic.gdx.math.Interpolation
|
||||||
|
import com.badlogic.gdx.math.Vector2
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
abstract class ParticleEffectFireworks : ParticleEffectAnimation() {
|
||||||
|
@Suppress("ConstPropertyName")
|
||||||
|
companion object {
|
||||||
|
const val effectsFile = "effects/fireworks.p"
|
||||||
|
|
||||||
|
private val initialEndPoints = arrayOf(
|
||||||
|
Vector2(0.5f, 1.5f),
|
||||||
|
Vector2(0.2f, 1f),
|
||||||
|
Vector2(0.8f, 1f),
|
||||||
|
)
|
||||||
|
private val startPoint = Vector2(0.5f, 0f)
|
||||||
|
|
||||||
|
// These define the range of random endPoints and thus how far out of the offered Actor bounds the rockets can fly
|
||||||
|
private const val amplitudeX = 1f
|
||||||
|
private const val offsetX = 0f
|
||||||
|
private const val amplitudeY = 1f
|
||||||
|
private const val offsetY = 0.5f
|
||||||
|
// Duration of tracer effect taken from the definition file
|
||||||
|
private const val travelTime = 0.6f
|
||||||
|
// Delay between initial effects
|
||||||
|
private const val initialDelay = travelTime
|
||||||
|
// Max delay for next effect
|
||||||
|
private const val amplitudeDelay = 0.25f
|
||||||
|
// These two determine how often onComplete will ask for an extra restart or omit one
|
||||||
|
private const val minEffects = 3
|
||||||
|
private const val maxEffects = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
fun load() {
|
||||||
|
load(effectsFile, defaultAtlasName, initialEndPoints.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun ParticleEffectData.configure() {
|
||||||
|
startPoint = Companion.startPoint
|
||||||
|
endPoint = if (index in initialEndPoints.indices) initialEndPoints[index]
|
||||||
|
else Vector2(offsetX + amplitudeX * Random.nextFloat(), offsetY + amplitudeY * Random.nextFloat())
|
||||||
|
delay = if (index in initialEndPoints.indices) index * initialDelay
|
||||||
|
else Random.nextFloat() * amplitudeDelay
|
||||||
|
travelTime = Companion.travelTime
|
||||||
|
interpolation = Interpolation.fastSlow
|
||||||
|
|
||||||
|
// The file definition has a whole bunch of "explosions" - a "rainbow" and six "shower-color" ones.
|
||||||
|
// Show either "rainbow" alone or a random selection of "shower" emitters.
|
||||||
|
// It also has some "dazzler" emitters that shouldn't be included in most runs.
|
||||||
|
val type = Random.nextInt(-1, 5)
|
||||||
|
if (type < 0) {
|
||||||
|
// Leave only rainbow emitter
|
||||||
|
effect.removeEmitters { it.startsWith("shower") }
|
||||||
|
} else {
|
||||||
|
// remove rainbow emitter and [type] "shower-color" emitters
|
||||||
|
val names = effect.emitters.asSequence()
|
||||||
|
.map { it.name }.filter { it.startsWith("shower") }
|
||||||
|
.shuffled().take(type)
|
||||||
|
.toSet() + "rainbow"
|
||||||
|
effect.removeEmitters { it in names }
|
||||||
|
}
|
||||||
|
if (Random.nextInt(4) > 0)
|
||||||
|
effect.removeEmitters { it.startsWith("dazzler") }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onComplete(effectData: ParticleEffectData): Int {
|
||||||
|
if (Random.nextInt(4) > 0) return 1
|
||||||
|
if (activeCount() <= minEffects) return Random.nextInt(1, 3)
|
||||||
|
if (activeCount() >= maxEffects) return Random.nextInt(0, 2)
|
||||||
|
return Random.nextInt(0, 3)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
package com.unciv.ui.components
|
||||||
|
|
||||||
|
import com.badlogic.gdx.math.Rectangle
|
||||||
|
import com.unciv.UncivGame
|
||||||
|
import com.unciv.ui.components.tilegroups.CityTileGroup
|
||||||
|
import com.unciv.ui.components.widgets.ZoomableScrollPane
|
||||||
|
import com.unciv.ui.screens.cityscreen.CityMapHolder
|
||||||
|
|
||||||
|
// todo sound
|
||||||
|
// todo moddability (refactor media search first)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display fireworks using the Gdx ParticleEffect system, over a map view, centered on a specific tile.
|
||||||
|
* - Use the [create] factory for instantiation - it handles checking the continuousRendering setting and asset existence.
|
||||||
|
* - Repeats endlessly
|
||||||
|
* - Handles the zooming and panning of the map
|
||||||
|
* - Intentionally exceeds the bounds of the passed (TileGroup) actor bounds, but not by much
|
||||||
|
* @param mapHolder the CityMapHolder (or WorldMapHolder) this should draw over
|
||||||
|
* @property setActorBounds Informs this where, relative to the TileGroupMap that is zoomed and panned through the ZoomableScrollPane, to draw - can be constant over lifetime
|
||||||
|
*/
|
||||||
|
class ParticleEffectMapFireworks(
|
||||||
|
private val mapHolder: ZoomableScrollPane
|
||||||
|
) : ParticleEffectFireworks() {
|
||||||
|
companion object {
|
||||||
|
fun create(game: UncivGame, mapScrollPane: CityMapHolder): ParticleEffectMapFireworks? {
|
||||||
|
if (!isEnabled(game, defaultAtlasName)) return null
|
||||||
|
return ParticleEffectMapFireworks(mapScrollPane).apply { load() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val actorBounds = Rectangle()
|
||||||
|
private val tempViewport = Rectangle()
|
||||||
|
|
||||||
|
// The factors below are just fine-tuning the looks, and avoid lengthy particle effect file edits
|
||||||
|
fun setActorBounds(tileGroup: CityTileGroup) {
|
||||||
|
tileGroup.run { actorBounds.set(x + (width - hexagonImageWidth) / 2, y + height / 4, hexagonImageWidth, height * 1.667f) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getScale() = mapHolder.scaleX * 0.667f
|
||||||
|
|
||||||
|
override fun getTargetBounds(bounds: Rectangle) {
|
||||||
|
// Empiric math - any attempts to ask Gdx via localToStageCoordinates were way off
|
||||||
|
val scale = mapHolder.scaleX // just assume scaleX==scaleY
|
||||||
|
mapHolder.getViewport(tempViewport)
|
||||||
|
bounds.x = (actorBounds.x - tempViewport.x) * scale
|
||||||
|
bounds.y = (actorBounds.y - tempViewport.y) * scale
|
||||||
|
bounds.width = actorBounds.width * scale
|
||||||
|
bounds.height = actorBounds.height * scale
|
||||||
|
}
|
||||||
|
}
|
@ -24,7 +24,7 @@ enum class CityTileState {
|
|||||||
BLOCKADED
|
BLOCKADED
|
||||||
}
|
}
|
||||||
|
|
||||||
class CityTileGroup(val city: City, tile: Tile, tileSetStrings: TileSetStrings) : TileGroup(tile,tileSetStrings) {
|
class CityTileGroup(val city: City, tile: Tile, tileSetStrings: TileSetStrings, private val nightMode: Boolean) : TileGroup(tile,tileSetStrings) {
|
||||||
|
|
||||||
var tileState = CityTileState.NONE
|
var tileState = CityTileState.NONE
|
||||||
|
|
||||||
@ -40,11 +40,20 @@ class CityTileGroup(val city: City, tile: Tile, tileSetStrings: TileSetStrings)
|
|||||||
layerMisc.removeWorkedIcon()
|
layerMisc.removeWorkedIcon()
|
||||||
var icon: Actor? = null
|
var icon: Actor? = null
|
||||||
|
|
||||||
|
val setDimmed = if (nightMode) fun(factor: Float) {
|
||||||
|
layerTerrain.dim(0.25f * factor)
|
||||||
|
} else fun(factor: Float) {
|
||||||
|
layerTerrain.dim(0.5f * factor)
|
||||||
|
}
|
||||||
|
val setUndimmed = if (nightMode) fun() {
|
||||||
|
layerTerrain.dim(0.5f)
|
||||||
|
} else fun() {}
|
||||||
|
|
||||||
when {
|
when {
|
||||||
|
|
||||||
// Does not belong to us
|
// Does not belong to us
|
||||||
tile.getOwner() != city.civ -> {
|
tile.getOwner() != city.civ -> {
|
||||||
layerTerrain.dim(0.3f)
|
setDimmed(0.6f)
|
||||||
layerMisc.setYieldVisible(UncivGame.Current.settings.showTileYields)
|
layerMisc.setYieldVisible(UncivGame.Current.settings.showTileYields)
|
||||||
layerMisc.dimYields(true)
|
layerMisc.dimYields(true)
|
||||||
|
|
||||||
@ -70,31 +79,34 @@ class CityTileGroup(val city: City, tile: Tile, tileSetStrings: TileSetStrings)
|
|||||||
|
|
||||||
// Out of city range
|
// Out of city range
|
||||||
tile !in city.tilesInRange -> {
|
tile !in city.tilesInRange -> {
|
||||||
layerTerrain.dim(0.5f)
|
setDimmed(1f)
|
||||||
layerMisc.dimYields(true)
|
layerMisc.dimYields(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Worked by another city
|
// Worked by another city
|
||||||
tile.isWorked() && tile.getWorkingCity() != city -> {
|
tile.isWorked() && tile.getWorkingCity() != city -> {
|
||||||
layerTerrain.dim(0.5f)
|
setDimmed(1f)
|
||||||
layerMisc.dimYields(true)
|
layerMisc.dimYields(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// City Center
|
// City Center
|
||||||
tile.isCityCenter() -> {
|
tile.isCityCenter() -> {
|
||||||
icon = ImageGetter.getImage("TileIcons/CityCenter")
|
icon = ImageGetter.getImage("TileIcons/CityCenter")
|
||||||
|
// Night mode does not apply to the city tile itself
|
||||||
layerMisc.dimYields(false)
|
layerMisc.dimYields(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Does not provide yields
|
// Does not provide yields
|
||||||
tile.stats.getTileStats(city, city.civ).isEmpty() -> {
|
tile.stats.getTileStats(city, city.civ).isEmpty() -> {
|
||||||
// Do nothing
|
// Do nothing except night-mode dimming
|
||||||
|
setUndimmed()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blockaded
|
// Blockaded
|
||||||
tile.isBlockaded() -> {
|
tile.isBlockaded() -> {
|
||||||
icon = ImageGetter.getImage("TileIcons/Blockaded")
|
icon = ImageGetter.getImage("TileIcons/Blockaded")
|
||||||
tileState = CityTileState.BLOCKADED
|
tileState = CityTileState.BLOCKADED
|
||||||
|
setUndimmed()
|
||||||
layerMisc.dimYields(true)
|
layerMisc.dimYields(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,6 +114,7 @@ class CityTileGroup(val city: City, tile: Tile, tileSetStrings: TileSetStrings)
|
|||||||
tile.isLocked() -> {
|
tile.isLocked() -> {
|
||||||
icon = ImageGetter.getImage("TileIcons/Locked")
|
icon = ImageGetter.getImage("TileIcons/Locked")
|
||||||
tileState = CityTileState.WORKABLE
|
tileState = CityTileState.WORKABLE
|
||||||
|
setUndimmed()
|
||||||
layerMisc.dimYields(false)
|
layerMisc.dimYields(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,18 +122,21 @@ class CityTileGroup(val city: City, tile: Tile, tileSetStrings: TileSetStrings)
|
|||||||
tile.isWorked() -> {
|
tile.isWorked() -> {
|
||||||
icon = ImageGetter.getImage("TileIcons/Worked")
|
icon = ImageGetter.getImage("TileIcons/Worked")
|
||||||
tileState = CityTileState.WORKABLE
|
tileState = CityTileState.WORKABLE
|
||||||
|
setUndimmed()
|
||||||
layerMisc.dimYields(false)
|
layerMisc.dimYields(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provides yield without worker assigned (isWorked already tested above)
|
// Provides yield without worker assigned (isWorked already tested above)
|
||||||
tile.providesYield() -> {
|
tile.providesYield() -> {
|
||||||
// defaults are OK
|
// defaults are OK
|
||||||
|
setUndimmed()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not-worked
|
// Not-worked
|
||||||
else -> {
|
else -> {
|
||||||
icon = ImageGetter.getImage("TileIcons/NotWorked")
|
icon = ImageGetter.getImage("TileIcons/NotWorked")
|
||||||
tileState = CityTileState.WORKABLE
|
tileState = CityTileState.WORKABLE
|
||||||
|
setUndimmed()
|
||||||
layerMisc.dimYields(true)
|
layerMisc.dimYields(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -396,18 +396,20 @@ open class ZoomableScrollPane(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return the currently scrolled-to viewport of the whole scrollable area */
|
/** Overwrite [rect] with the currently scrolled-to viewport of the whole scrollable area */
|
||||||
private fun getViewport(): Rectangle {
|
fun getViewport(rect: Rectangle) {
|
||||||
val viewportFromLeft = scrollX
|
val viewportFromLeft = scrollX
|
||||||
/** In the default coordinate system, the y origin is at the bottom, but scrollY is from the top, so we need to invert. */
|
/** In the default coordinate system, the y origin is at the bottom, but scrollY is from the top, so we need to invert. */
|
||||||
val viewportFromBottom = maxY - scrollY
|
val viewportFromBottom = maxY - scrollY
|
||||||
return Rectangle(
|
rect.x = viewportFromLeft - horizontalPadding
|
||||||
viewportFromLeft - horizontalPadding,
|
rect.y = viewportFromBottom - verticalPadding
|
||||||
viewportFromBottom - verticalPadding,
|
rect.width = width
|
||||||
width,
|
rect.height = height
|
||||||
height)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return the currently scrolled-to viewport of the whole scrollable area */
|
||||||
|
private fun getViewport() = Rectangle().also { getViewport(it) }
|
||||||
|
|
||||||
private fun onViewportChanged() {
|
private fun onViewportChanged() {
|
||||||
onViewportChangedListener?.invoke(maxX, maxY, getViewport())
|
onViewportChangedListener?.invoke(maxX, maxY, getViewport())
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,8 @@ object ImageGetter {
|
|||||||
private val textureRegionDrawables = HashMap<String, TextureRegionDrawable>()
|
private val textureRegionDrawables = HashMap<String, TextureRegionDrawable>()
|
||||||
private val ninePatchDrawables = HashMap<String, NinePatchDrawable>()
|
private val ninePatchDrawables = HashMap<String, NinePatchDrawable>()
|
||||||
|
|
||||||
|
fun getSpecificAtlas(name: String): TextureAtlas? = atlases[name]
|
||||||
|
|
||||||
fun resetAtlases() {
|
fun resetAtlases() {
|
||||||
atlases.values.forEach { it.dispose() }
|
atlases.values.forEach { it.dispose() }
|
||||||
atlases.clear()
|
atlases.clear()
|
||||||
|
@ -21,6 +21,7 @@ import com.unciv.models.stats.Stat
|
|||||||
import com.unciv.models.translations.tr
|
import com.unciv.models.translations.tr
|
||||||
import com.unciv.ui.audio.CityAmbiencePlayer
|
import com.unciv.ui.audio.CityAmbiencePlayer
|
||||||
import com.unciv.ui.audio.SoundPlayer
|
import com.unciv.ui.audio.SoundPlayer
|
||||||
|
import com.unciv.ui.components.ParticleEffectMapFireworks
|
||||||
import com.unciv.ui.components.extensions.colorFromRGB
|
import com.unciv.ui.components.extensions.colorFromRGB
|
||||||
import com.unciv.ui.components.extensions.disable
|
import com.unciv.ui.components.extensions.disable
|
||||||
import com.unciv.ui.components.extensions.packIfNeeded
|
import com.unciv.ui.components.extensions.packIfNeeded
|
||||||
@ -137,16 +138,20 @@ class CityScreen(
|
|||||||
|
|
||||||
private var cityAmbiencePlayer: CityAmbiencePlayer? = ambiencePlayer ?: CityAmbiencePlayer(city)
|
private var cityAmbiencePlayer: CityAmbiencePlayer? = ambiencePlayer ?: CityAmbiencePlayer(city)
|
||||||
|
|
||||||
|
/** Particle effects for WLTK day decoration */
|
||||||
|
private val isWLTKday = city.isWeLoveTheKingDayActive()
|
||||||
|
private val fireworks: ParticleEffectMapFireworks?
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (city.isWeLoveTheKingDayActive() && UncivGame.Current.settings.citySoundsVolume > 0) {
|
if (isWLTKday && UncivGame.Current.settings.citySoundsVolume > 0) {
|
||||||
SoundPlayer.play(UncivSound("WLTK"))
|
SoundPlayer.play(UncivSound("WLTK"))
|
||||||
}
|
}
|
||||||
|
fireworks = if (isWLTKday) ParticleEffectMapFireworks.create(game, mapScrollPane) else null
|
||||||
|
|
||||||
UncivGame.Current.settings.addCompletedTutorialTask("Enter city screen")
|
UncivGame.Current.settings.addCompletedTutorialTask("Enter city screen")
|
||||||
|
|
||||||
addTiles()
|
addTiles()
|
||||||
|
|
||||||
//stage.setDebugTableUnderMouse(true)
|
|
||||||
stage.addActor(cityStatsTable)
|
stage.addActor(cityStatsTable)
|
||||||
// If we are spying then we shoulden't be able to see their construction screen.
|
// If we are spying then we shoulden't be able to see their construction screen.
|
||||||
constructionsTable.addActorsToStage()
|
constructionsTable.addActorsToStage()
|
||||||
@ -251,6 +256,7 @@ class CityScreen(
|
|||||||
else -> Color.GREEN to 0.5f
|
else -> Color.GREEN to 0.5f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (tileGroup in tileGroups) {
|
for (tileGroup in tileGroups) {
|
||||||
tileGroup.update()
|
tileGroup.update()
|
||||||
tileGroup.layerMisc.removeHexOutline()
|
tileGroup.layerMisc.removeHexOutline()
|
||||||
@ -268,6 +274,9 @@ class CityScreen(
|
|||||||
getPickImprovementColor(tileGroup.tile).run {
|
getPickImprovementColor(tileGroup.tile).run {
|
||||||
tileGroup.layerMisc.addHexOutline(first.cpy().apply { this.a = second }) }
|
tileGroup.layerMisc.addHexOutline(first.cpy().apply { this.a = second }) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fireworks == null || tileGroup.tile.position != city.location) continue
|
||||||
|
fireworks.setActorBounds(tileGroup)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,7 +286,7 @@ class CityScreen(
|
|||||||
fun addWltkIcon(name: String, apply: Image.()->Unit = {}) =
|
fun addWltkIcon(name: String, apply: Image.()->Unit = {}) =
|
||||||
razeCityButtonHolder.add(ImageGetter.getImage(name).apply(apply)).size(wltkIconSize)
|
razeCityButtonHolder.add(ImageGetter.getImage(name).apply(apply)).size(wltkIconSize)
|
||||||
|
|
||||||
if (city.isWeLoveTheKingDayActive()) {
|
if (isWLTKday && fireworks == null) {
|
||||||
addWltkIcon("OtherIcons/WLTK LR") { color = Color.GOLD }
|
addWltkIcon("OtherIcons/WLTK LR") { color = Color.GOLD }
|
||||||
addWltkIcon("OtherIcons/WLTK 1") { color = Color.FIREBRICK }.padRight(10f)
|
addWltkIcon("OtherIcons/WLTK 1") { color = Color.FIREBRICK }.padRight(10f)
|
||||||
}
|
}
|
||||||
@ -309,7 +318,7 @@ class CityScreen(
|
|||||||
razeCityButtonHolder.add(stopRazingCityButton) //.colspan(cityPickerTable.columns)
|
razeCityButtonHolder.add(stopRazingCityButton) //.colspan(cityPickerTable.columns)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (city.isWeLoveTheKingDayActive()) {
|
if (isWLTKday && fireworks == null) {
|
||||||
addWltkIcon("OtherIcons/WLTK 2") { color = Color.FIREBRICK }.padLeft(10f)
|
addWltkIcon("OtherIcons/WLTK 2") { color = Color.FIREBRICK }.padLeft(10f)
|
||||||
addWltkIcon("OtherIcons/WLTK LR") {
|
addWltkIcon("OtherIcons/WLTK LR") {
|
||||||
color = Color.GOLD
|
color = Color.GOLD
|
||||||
@ -329,7 +338,7 @@ class CityScreen(
|
|||||||
val tileSetStrings = TileSetStrings()
|
val tileSetStrings = TileSetStrings()
|
||||||
val cityTileGroups = city.getCenterTile().getTilesInDistance(5)
|
val cityTileGroups = city.getCenterTile().getTilesInDistance(5)
|
||||||
.filter { selectedCiv.hasExplored(it) }
|
.filter { selectedCiv.hasExplored(it) }
|
||||||
.map { CityTileGroup(city, it, tileSetStrings) }
|
.map { CityTileGroup(city, it, tileSetStrings, fireworks != null) }
|
||||||
|
|
||||||
for (tileGroup in cityTileGroups) {
|
for (tileGroup in cityTileGroups) {
|
||||||
tileGroup.onClick { tileGroupOnClick(tileGroup, city) }
|
tileGroup.onClick { tileGroupOnClick(tileGroup, city) }
|
||||||
@ -531,6 +540,12 @@ class CityScreen(
|
|||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
cityAmbiencePlayer?.dispose()
|
cityAmbiencePlayer?.dispose()
|
||||||
|
fireworks?.dispose()
|
||||||
super.dispose()
|
super.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun render(delta: Float) {
|
||||||
|
super.render(delta)
|
||||||
|
fireworks?.render(stage, delta)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -826,3 +826,22 @@ The following audio is from https://pixabay.com/ [Pixabay License](https://pixab
|
|||||||
- [Cinematic Boom](https://pixabay.com/sound-effects/cinematic-boom-6872/) by Rizzard for final boom
|
- [Cinematic Boom](https://pixabay.com/sound-effects/cinematic-boom-6872/) by Rizzard for final boom
|
||||||
- [Cymbal Swell 2](https://pixabay.com/sound-effects/cymbal-swell-2-74766/) by rubberduckie for cymbal swells
|
- [Cymbal Swell 2](https://pixabay.com/sound-effects/cymbal-swell-2-74766/) by rubberduckie for cymbal swells
|
||||||
- [hit of orchestral cymbals and bass drum](https://pixabay.com/sound-effects/hit-of-orchestral-cymbals-and-bass-drum-14471/) by Selector for intro crash
|
- [hit of orchestral cymbals and bass drum](https://pixabay.com/sound-effects/hit-of-orchestral-cymbals-and-bass-drum-14471/) by Selector for intro crash
|
||||||
|
|
||||||
|
## Visual effects
|
||||||
|
|
||||||
|
The fireworks on the City screen of a WLTK-celebrating city are loosely based on the Fireworks.p file included in [Particle Park](https://github.com/raeleus/Particle-Park).
|
||||||
|
All differences and edits done by the Unciv team.
|
||||||
|
License quoted:
|
||||||
|
```
|
||||||
|
Particle Park Fireworks License
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Copyright © 2019 Raymond Buckley
|
||||||
|
|
||||||
|
Particle Park Fireworks can be used under the Creative Commons Attribution 4.0 International license.
|
||||||
|
|
||||||
|
See a human readable version here: https://creativecommons.org/licenses/by/4.0/
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
```
|
||||||
|
@ -4,6 +4,7 @@ import com.badlogic.gdx.Game
|
|||||||
import com.badlogic.gdx.Gdx
|
import com.badlogic.gdx.Gdx
|
||||||
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application
|
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application
|
||||||
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration
|
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration
|
||||||
|
import com.badlogic.gdx.files.FileHandle
|
||||||
import com.badlogic.gdx.graphics.Color
|
import com.badlogic.gdx.graphics.Color
|
||||||
import com.badlogic.gdx.graphics.Pixmap
|
import com.badlogic.gdx.graphics.Pixmap
|
||||||
import com.badlogic.gdx.math.Vector2
|
import com.badlogic.gdx.math.Vector2
|
||||||
@ -13,7 +14,9 @@ import com.badlogic.gdx.scenes.scene2d.utils.ClickListener
|
|||||||
import com.badlogic.gdx.scenes.scene2d.utils.Layout
|
import com.badlogic.gdx.scenes.scene2d.utils.Layout
|
||||||
import com.unciv.UncivGame
|
import com.unciv.UncivGame
|
||||||
import com.unciv.dev.FasterUIDevelopment.DevElement
|
import com.unciv.dev.FasterUIDevelopment.DevElement
|
||||||
|
import com.unciv.json.json
|
||||||
import com.unciv.logic.files.UncivFiles
|
import com.unciv.logic.files.UncivFiles
|
||||||
|
import com.unciv.models.metadata.GameSettings
|
||||||
import com.unciv.ui.components.extensions.center
|
import com.unciv.ui.components.extensions.center
|
||||||
import com.unciv.ui.components.extensions.toLabel
|
import com.unciv.ui.components.extensions.toLabel
|
||||||
import com.unciv.ui.components.fonts.FontFamilyData
|
import com.unciv.ui.components.fonts.FontFamilyData
|
||||||
@ -31,11 +34,13 @@ import java.awt.image.BufferedImage
|
|||||||
/** Creates a basic GDX application that mimics [UncivGame] as closely as possible,
|
/** Creates a basic GDX application that mimics [UncivGame] as closely as possible,
|
||||||
* starts up fast and shows one UI element, to be returned by [DevElement.createDevElement].
|
* starts up fast and shows one UI element, to be returned by [DevElement.createDevElement].
|
||||||
*
|
*
|
||||||
* * The parent will not size your Widget as the Gdx [Layout] contract promises,
|
* - The parent will not size your Widget as the Gdx [Layout] contract promises,
|
||||||
* you'll need to do it yourself. E.g, if you're a [WidgetGroup], call [pack()][WidgetGroup.pack].
|
* you'll need to do it yourself. E.g, if you're a [WidgetGroup], call [pack()][WidgetGroup.pack].
|
||||||
* If you forget, you'll see an orange dot in the window center.
|
* If you forget, you'll see an orange dot in the window center.
|
||||||
* * Resizing the window is not supported. You might lose interactivity.
|
* - Resizing the window is not supported. You might lose interactivity.
|
||||||
* * The middle mouse button toggles Scene2D debug mode, like the full game offers in the Debug Options.
|
* - However, settings including window size are saved separately from main Unciv, so you **can** test with different sizes.
|
||||||
|
* - Language is default English and there's no UI to change it - edit the settings file by hand, set once in the debugger, or hardcode in createDevElement if needed.
|
||||||
|
* - The middle mouse button toggles Scene2D debug mode, like the full game offers in the Debug Options.
|
||||||
*/
|
*/
|
||||||
object FasterUIDevelopment {
|
object FasterUIDevelopment {
|
||||||
|
|
||||||
@ -60,7 +65,7 @@ object FasterUIDevelopment {
|
|||||||
|
|
||||||
val config = Lwjgl3ApplicationConfiguration()
|
val config = Lwjgl3ApplicationConfiguration()
|
||||||
|
|
||||||
val settings = UncivFiles.getSettingsForPlatformLaunchers()
|
val settings = Settings.load()
|
||||||
if (!settings.isFreshlyCreated) {
|
if (!settings.isFreshlyCreated) {
|
||||||
val (width, height) = settings.windowState.coerceIn()
|
val (width, height) = settings.windowState.coerceIn()
|
||||||
config.setWindowedMode(width, height)
|
config.setWindowedMode(width, height)
|
||||||
@ -77,7 +82,7 @@ object FasterUIDevelopment {
|
|||||||
Fonts.fontImplementation = FontDesktop()
|
Fonts.fontImplementation = FontDesktop()
|
||||||
UncivGame.Current = game
|
UncivGame.Current = game
|
||||||
UncivGame.Current.files = UncivFiles(Gdx.files)
|
UncivGame.Current.files = UncivFiles(Gdx.files)
|
||||||
game.settings = UncivGame.Current.files.getGeneralSettings()
|
game.settings = Settings.load()
|
||||||
ImageGetter.resetAtlases()
|
ImageGetter.resetAtlases()
|
||||||
ImageGetter.reloadImages()
|
ImageGetter.reloadImages()
|
||||||
BaseScreen.setSkin()
|
BaseScreen.setSkin()
|
||||||
@ -88,6 +93,27 @@ object FasterUIDevelopment {
|
|||||||
override fun render() {
|
override fun render() {
|
||||||
game.render()
|
game.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun pause() {
|
||||||
|
Settings.save(UncivGame.Current.settings)
|
||||||
|
super.pause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Persist window size over invocations, but separately from main Unciv */
|
||||||
|
private object Settings {
|
||||||
|
const val SETTINGS_FILE_NAME = "FasterUIDevSettings.json"
|
||||||
|
val file: FileHandle = FileHandle(".").child(SETTINGS_FILE_NAME)
|
||||||
|
fun load(): GameSettings {
|
||||||
|
if (!file.exists()) return GameSettings().apply { isFreshlyCreated = true }
|
||||||
|
return json().fromJson(GameSettings::class.java, file)
|
||||||
|
}
|
||||||
|
fun save(settings: GameSettings) {
|
||||||
|
settings.isFreshlyCreated = false
|
||||||
|
// settings.refreshWindowSize() - No, we don't have the platform-dependent helpers initialized
|
||||||
|
settings.windowState = GameSettings.WindowState.current()
|
||||||
|
file.writeString(json().toJson(settings), false, Charsets.UTF_8.name())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UIDevScreen : BaseScreen() {
|
class UIDevScreen : BaseScreen() {
|
||||||
|
Loading…
Reference in New Issue
Block a user