mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-21 10:25:10 -04:00
Merge 63755fd8db79b13f7f12c27b66983f86181a0b50 into d51ef24c205b6b05330b3c4d7ce79c402db44447
This commit is contained in:
commit
af1d890725
52
core/src/com/unciv/logic/AlternatingStateManager.kt
Normal file
52
core/src/com/unciv/logic/AlternatingStateManager.kt
Normal file
@ -0,0 +1,52 @@
|
||||
package com.unciv.logic
|
||||
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.unciv.utils.Concurrency
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.ExperimentalTime
|
||||
|
||||
class AlternatingStateManager(
|
||||
private val name: String,
|
||||
private val originalState: () -> Unit,
|
||||
private val alternateState: () -> Unit
|
||||
) {
|
||||
private var job: Job? = null
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
fun start(
|
||||
duration: Duration = 5.seconds, interval: Duration = 500.milliseconds
|
||||
) {
|
||||
job?.cancel()
|
||||
job = Concurrency.run(name) {
|
||||
val startTime = Clock.System.now()
|
||||
|
||||
var isAlternateState = true
|
||||
while (true) {
|
||||
if (isAlternateState) {
|
||||
Gdx.app.postRunnable(alternateState)
|
||||
isAlternateState = false
|
||||
} else {
|
||||
Gdx.app.postRunnable(originalState)
|
||||
isAlternateState = true
|
||||
}
|
||||
|
||||
if (Clock.System.now() - startTime >= duration) {
|
||||
Gdx.app.postRunnable(originalState)
|
||||
return@run
|
||||
}
|
||||
|
||||
delay(interval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
job?.cancel()
|
||||
Gdx.app.postRunnable(originalState)
|
||||
}
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
package com.unciv.logic.multiplayer.chat
|
||||
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.ui.screens.worldscreen.chat.ChatPopup
|
||||
import com.unciv.utils.toUUIDOrNull
|
||||
import java.util.Collections.synchronizedMap
|
||||
import java.util.LinkedList
|
||||
import java.util.Queue
|
||||
@ -10,6 +12,8 @@ import java.util.UUID
|
||||
data class Chat(
|
||||
val gameId: UUID,
|
||||
) {
|
||||
var unreadCount = 0
|
||||
|
||||
// <civName, message> pairs
|
||||
private val messages: MutableList<Pair<String, String>> = mutableListOf(INITIAL_MESSAGE)
|
||||
|
||||
@ -52,6 +56,8 @@ object ChatStore {
|
||||
*/
|
||||
var chatPopup: ChatPopup? = null
|
||||
|
||||
var hasGlobalMessage = false
|
||||
|
||||
private var gameIdToChat: MutableMap<UUID, Chat> = synchronizedMap(mutableMapOf())
|
||||
|
||||
/** When no [ChatPopup] is open to receive these oddities, we keep them here.
|
||||
@ -72,21 +78,36 @@ object ChatStore {
|
||||
globalMessages = LinkedList()
|
||||
}
|
||||
|
||||
fun relayChatMessage(chat: Response.Chat) {
|
||||
fun relayChatMessage(incomingChatMsg: Response.Chat) {
|
||||
Gdx.app.postRunnable {
|
||||
if (chat.gameId == null || chat.gameId.isBlank()) {
|
||||
relayGlobalMessage(chat.message, chat.civName)
|
||||
if (incomingChatMsg.gameId == null || incomingChatMsg.gameId.isBlank()) {
|
||||
relayGlobalMessage(incomingChatMsg.message, incomingChatMsg.civName)
|
||||
} else {
|
||||
val gameId = try {
|
||||
UUID.fromString(chat.gameId)
|
||||
UUID.fromString(incomingChatMsg.gameId)
|
||||
} catch (_: Throwable) {
|
||||
// Discard messages with invalid UUID
|
||||
return@postRunnable
|
||||
}
|
||||
|
||||
getChatByGameId(gameId).addMessage(chat.civName, chat.message)
|
||||
if (chatPopup?.chat?.gameId == gameId) {
|
||||
chatPopup?.addMessage(chat.civName, chat.message)
|
||||
val chat = chatPopup?.chat ?: getChatByGameId(gameId)
|
||||
chat.addMessage(incomingChatMsg.civName, incomingChatMsg.message)
|
||||
if (gameId.equals(chatPopup?.chat?.gameId)) {
|
||||
chatPopup?.addMessage(incomingChatMsg.civName, incomingChatMsg.message)
|
||||
}
|
||||
|
||||
if (chatPopup == null && incomingChatMsg.civName != "System") {
|
||||
if (gameId.equals(UncivGame.Current.worldScreen?.gameInfo?.gameId?.toUUIDOrNull())) {
|
||||
// ensures that you are not getting notified for your own messages
|
||||
if (UncivGame.Current.worldScreen?.gameInfo?.currentPlayer != incomingChatMsg.civName) {
|
||||
chat.unreadCount++
|
||||
UncivGame.Current.worldScreen?.chatButton?.triggerChatIndication()
|
||||
}
|
||||
} else {
|
||||
// user is out of world screen or
|
||||
// some other game not currently on screen has a message
|
||||
chat.unreadCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -103,7 +124,13 @@ object ChatStore {
|
||||
|
||||
fun relayGlobalMessage(message: String, civName: String = "System") {
|
||||
Gdx.app.postRunnable {
|
||||
chatPopup?.addMessage(civName, message, suffix = "one time") ?: globalMessages.add(Pair(civName, message))
|
||||
if (civName != "System") {
|
||||
hasGlobalMessage = chatPopup == null
|
||||
if (hasGlobalMessage) UncivGame.Current.worldScreen?.chatButton?.triggerChatIndication()
|
||||
}
|
||||
|
||||
chatPopup?.addMessage(civName, message, suffix = "one time")
|
||||
?: globalMessages.add(Pair(civName, message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,8 +7,8 @@ import com.badlogic.gdx.scenes.scene2d.ui.Cell
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Label
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.Constants
|
||||
import com.unciv.ui.components.fonts.Fonts
|
||||
import com.unciv.ui.components.extensions.toLabel
|
||||
import com.unciv.ui.components.fonts.Fonts
|
||||
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||
|
||||
/**
|
||||
@ -23,10 +23,11 @@ open class IconTextButton(
|
||||
text: String,
|
||||
val icon: Actor? = null,
|
||||
fontSize: Int = Constants.defaultFontSize,
|
||||
fontColor: Color = Color.WHITE
|
||||
): Button(BaseScreen.skin) {
|
||||
val fontColor: Color = Color.WHITE
|
||||
) : Button(BaseScreen.skin) {
|
||||
/** [Label] instance produced by, and with content and formatting as specified in [String.toLabel]. */
|
||||
val label = text.toLabel(fontColor, fontSize, hideIcons = true) // Since by definition we already have an icon
|
||||
|
||||
/** Table cell containing the [icon] if any, or `null` (that is, when no [icon] was supplied, the Cell will exist but have no Actor). */
|
||||
val iconCell: Cell<Actor> =
|
||||
if (icon != null) {
|
||||
@ -37,6 +38,7 @@ open class IconTextButton(
|
||||
} else {
|
||||
add().padRight(fontSize / 2f)
|
||||
}
|
||||
|
||||
/** Table cell instance containing the [label]. */
|
||||
val labelCell: Cell<Label> = add(label)
|
||||
|
||||
|
@ -1,22 +1,80 @@
|
||||
package com.unciv.ui.screens.worldscreen.chat
|
||||
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.AlternatingStateManager
|
||||
import com.unciv.logic.multiplayer.chat.ChatStore
|
||||
import com.unciv.logic.multiplayer.chat.ChatWebSocket
|
||||
import com.unciv.logic.multiplayer.chat.Message
|
||||
import com.unciv.ui.components.SmallButtonStyle
|
||||
import com.unciv.ui.components.extensions.disable
|
||||
import com.unciv.ui.components.extensions.toTextButton
|
||||
import com.unciv.ui.components.input.onClick
|
||||
import com.unciv.ui.images.IconTextButton
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.screens.worldscreen.WorldScreen
|
||||
|
||||
private val smallButtonStyle = SmallButtonStyle()
|
||||
|
||||
class ChatButton(val worldScreen: WorldScreen) : IconTextButton(
|
||||
"Chat", ImageGetter.getImage("OtherIcons/Chat"), 23
|
||||
) {
|
||||
private val chat = ChatStore.getChatByGameId(worldScreen.gameInfo.gameId)
|
||||
|
||||
private val badge = "".toTextButton(smallButtonStyle).apply {
|
||||
disable()
|
||||
label.setColor(Color.WHITE)
|
||||
label.setAlignment(Align.center)
|
||||
label.setFontScale(0.2f)
|
||||
}
|
||||
|
||||
private val flash = AlternatingStateManager(
|
||||
name = "ChatButton color flash",
|
||||
originalState = {
|
||||
icon?.color = fontColor
|
||||
label.color = fontColor
|
||||
}, alternateState = {
|
||||
icon?.color = Color.ORANGE
|
||||
label.color = Color.ORANGE
|
||||
}
|
||||
)
|
||||
|
||||
private fun updateBadge() {
|
||||
badge.height = height / 3
|
||||
badge.setPosition(
|
||||
width - badge.width / 1.5f,
|
||||
height - badge.height / 1.5f
|
||||
)
|
||||
|
||||
badge.isVisible = chat.unreadCount > 0 || ChatStore.hasGlobalMessage
|
||||
|
||||
if (badge.isVisible) {
|
||||
var text = chat.unreadCount.toString()
|
||||
if (ChatStore.hasGlobalMessage) {
|
||||
text += '+'
|
||||
}
|
||||
badge.setText(text)
|
||||
} else flash.stop()
|
||||
}
|
||||
|
||||
fun triggerChatIndication() {
|
||||
updateBadge()
|
||||
flash.start()
|
||||
}
|
||||
|
||||
init {
|
||||
width = 95f
|
||||
iconCell.pad(3f).center()
|
||||
addActor(badge)
|
||||
updateBadge()
|
||||
|
||||
onClick {
|
||||
ChatPopup(worldScreen).open()
|
||||
chat.unreadCount = 0
|
||||
ChatStore.hasGlobalMessage = false
|
||||
updateBadge()
|
||||
|
||||
ChatPopup(chat, worldScreen).open()
|
||||
}
|
||||
|
||||
refreshVisibility()
|
||||
@ -34,6 +92,7 @@ class ChatButton(val worldScreen: WorldScreen) : IconTextButton(
|
||||
Message.Join(listOf(worldScreen.gameInfo.gameId)),
|
||||
)
|
||||
updatePosition()
|
||||
updateBadge()
|
||||
true
|
||||
} else {
|
||||
ChatWebSocket.stop()
|
||||
|
@ -11,6 +11,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.multiplayer.chat.Chat
|
||||
import com.unciv.logic.multiplayer.chat.ChatStore
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.components.extensions.coerceLightnessAtLeast
|
||||
@ -28,6 +29,7 @@ private val civChatColorsMap = mapOf<String, Color>(
|
||||
)
|
||||
|
||||
class ChatPopup(
|
||||
val chat: Chat,
|
||||
private val worldScreen: WorldScreen,
|
||||
) : Popup(screen = worldScreen, scrollable = Scrollability.None) {
|
||||
companion object {
|
||||
@ -35,8 +37,6 @@ class ChatPopup(
|
||||
const val CIVNAME_COLOR_MIN_LIGHTNESS = 0.60f
|
||||
}
|
||||
|
||||
val chat = ChatStore.getChatByGameId(worldScreen.gameInfo.gameId)
|
||||
|
||||
private val chatTable = Table(skin)
|
||||
private val scrollPane = ScrollPane(chatTable, skin)
|
||||
private val messageField = UncivTextField(
|
||||
|
@ -5,17 +5,17 @@ import com.badlogic.gdx.scenes.scene2d.Actor
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Button
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Cell
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.HorizontalGroup
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Image
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Label
|
||||
import com.badlogic.gdx.utils.Disposable
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.AlternatingStateManager
|
||||
import com.unciv.logic.event.EventBus
|
||||
import com.unciv.logic.multiplayer.HasMultiplayerGameName
|
||||
import com.unciv.logic.multiplayer.MultiplayerGameNameChanged
|
||||
import com.unciv.logic.multiplayer.MultiplayerGamePreview
|
||||
import com.unciv.logic.multiplayer.MultiplayerGameUpdateEnded
|
||||
import com.unciv.logic.multiplayer.MultiplayerGameUpdateStarted
|
||||
import com.unciv.logic.multiplayer.MultiplayerGameUpdated
|
||||
import com.unciv.logic.multiplayer.MultiplayerGamePreview
|
||||
import com.unciv.logic.multiplayer.isUsersTurn
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.components.extensions.setSize
|
||||
@ -24,20 +24,20 @@ import com.unciv.ui.components.widgets.LoadingImage
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||
import com.unciv.utils.Concurrency
|
||||
import com.unciv.utils.launchOnGLThread
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class MultiplayerStatusButton(
|
||||
screen: BaseScreen,
|
||||
curGame: MultiplayerGamePreview?
|
||||
) : Button(BaseScreen.skin), Disposable {
|
||||
private var curGameName = curGame?.name
|
||||
private val loadingImage = LoadingImage(style = LoadingImage.Style(
|
||||
idleImageName = "OtherIcons/Multiplayer",
|
||||
idleIconColor = Color.WHITE,
|
||||
minShowTime = 500
|
||||
))
|
||||
private val loadingImage = LoadingImage(
|
||||
style = LoadingImage.Style(
|
||||
idleImageName = "OtherIcons/Multiplayer",
|
||||
idleIconColor = Color.WHITE,
|
||||
minShowTime = 500
|
||||
)
|
||||
)
|
||||
private val turnIndicator = TurnIndicator()
|
||||
private val turnIndicatorCell: Cell<Actor>
|
||||
private val gameNamesWithCurrentTurn = getInitialGamesWithCurrentTurn()
|
||||
@ -96,7 +96,7 @@ class MultiplayerStatusButton(
|
||||
}
|
||||
|
||||
private fun updateTurnIndicator(flash: Boolean = true) {
|
||||
if (gameNamesWithCurrentTurn.size == 0) {
|
||||
if (gameNamesWithCurrentTurn.isEmpty()) {
|
||||
turnIndicatorCell.clearActor()
|
||||
} else {
|
||||
turnIndicatorCell.setActor(turnIndicator)
|
||||
@ -105,7 +105,7 @@ class MultiplayerStatusButton(
|
||||
|
||||
// flash so the user sees an better update
|
||||
if (flash) {
|
||||
turnIndicator.flash()
|
||||
turnIndicator.flash.start(3.seconds)
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,10 +118,9 @@ class MultiplayerStatusButton(
|
||||
|
||||
private class TurnIndicator : HorizontalGroup(), Disposable {
|
||||
val gameAmount = Label("2", BaseScreen.skin)
|
||||
val image: Image
|
||||
private var job: Job? = null
|
||||
val image = ImageGetter.getImage("OtherIcons/ExclamationMark")
|
||||
|
||||
init {
|
||||
image = ImageGetter.getImage("OtherIcons/ExclamationMark")
|
||||
image.setSize(30f)
|
||||
addActor(image)
|
||||
}
|
||||
@ -135,23 +134,18 @@ private class TurnIndicator : HorizontalGroup(), Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
fun flash() {
|
||||
// using a gdx Action would be nicer, but we don't necessarily have continuousRendering on and we still want to flash
|
||||
flash(6, Color.WHITE, Color.ORANGE)
|
||||
}
|
||||
private fun flash(alternations: Int, curColor: Color, nextColor: Color) {
|
||||
if (alternations == 0) return
|
||||
gameAmount.color = nextColor
|
||||
image.color = nextColor
|
||||
job = Concurrency.run("StatusButton color flash") {
|
||||
delay(500)
|
||||
launchOnGLThread {
|
||||
flash(alternations - 1, nextColor, curColor)
|
||||
}
|
||||
val flash = AlternatingStateManager(
|
||||
name = "StatusButton color flash",
|
||||
originalState = {
|
||||
image.color = Color.WHITE
|
||||
gameAmount.color = Color.WHITE
|
||||
}, alternateState = {
|
||||
image.color = Color.ORANGE
|
||||
gameAmount.color = Color.ORANGE
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
override fun dispose() {
|
||||
job?.cancel()
|
||||
flash.stop()
|
||||
}
|
||||
}
|
||||
|
@ -4,12 +4,18 @@ import yairm210.purity.annotations.Pure
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Checks if a [String] is a valid UUID
|
||||
* Tries to convert a [String] to a valid [UUID]
|
||||
* and returns `null` upon failure
|
||||
*/
|
||||
@Pure
|
||||
fun String.isUUID(): Boolean = try {
|
||||
fun String.toUUIDOrNull(): UUID? = try {
|
||||
UUID.fromString(this)
|
||||
true
|
||||
} catch (_: Throwable) {
|
||||
false
|
||||
null
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a [String] is a valid [UUID]
|
||||
*/
|
||||
@Pure
|
||||
fun String.isUUID(): Boolean = toUUIDOrNull() != null
|
||||
|
Loading…
x
Reference in New Issue
Block a user