diff options
author | Joffrey Bion <joffrey.bion@booking.com> | 2020-03-27 02:01:55 +0100 |
---|---|---|
committer | Joffrey Bion <joffrey.bion@booking.com> | 2020-03-27 10:59:39 +0100 |
commit | 8c666d14239bf50775ca97aae4b23191dfa73b1c (patch) | |
tree | b2abb26a84a6a8ff4b82b715aab76bd472ef348a /sw-ui-kt/src | |
parent | Add error handling saga printing errors in console (diff) | |
download | seven-wonders-8c666d14239bf50775ca97aae4b23191dfa73b1c.tar.gz seven-wonders-8c666d14239bf50775ca97aae4b23191dfa73b1c.tar.bz2 seven-wonders-8c666d14239bf50775ca97aae4b23191dfa73b1c.zip |
Add sagas and components for game scene
Diffstat (limited to 'sw-ui-kt/src')
11 files changed, 338 insertions, 30 deletions
diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt index d8b5ec7d..15c453a3 100644 --- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt @@ -1,8 +1,122 @@ package org.luxons.sevenwonders.ui.components.game +import com.palantir.blueprintjs.Intent +import com.palantir.blueprintjs.bpButton +import com.palantir.blueprintjs.bpNonIdealState +import kotlinx.css.CSSBuilder +import kotlinx.css.Overflow +import kotlinx.css.Position +import kotlinx.css.background +import kotlinx.css.backgroundSize +import kotlinx.css.bottom +import kotlinx.css.left +import kotlinx.css.overflow +import kotlinx.css.position +import kotlinx.css.px +import kotlinx.css.right +import kotlinx.css.top +import org.luxons.sevenwonders.model.PlayerMove +import org.luxons.sevenwonders.model.PlayerTurnInfo +import org.luxons.sevenwonders.model.api.PlayerDTO +import org.luxons.sevenwonders.ui.redux.RequestPrepareMove +import org.luxons.sevenwonders.ui.redux.RequestSayReady +import org.luxons.sevenwonders.ui.redux.connectStateAndDispatch +import org.luxons.sevenwonders.ui.utils.createElement import react.RBuilder +import react.RClass +import react.RComponent +import react.RProps +import react.RState import react.dom.* +import styled.css +import styled.styledDiv -fun RBuilder.gameScene() = div { - h1 { +"Game" } +interface GameSceneStateProps: RProps { + var players: List<PlayerDTO> + var turnInfo: PlayerTurnInfo? +} + +interface GameSceneDispatchProps: RProps { + var sayReady: () -> Unit + var prepareMove: (move: PlayerMove) -> Unit +} + +interface GameSceneProps : GameSceneStateProps, GameSceneDispatchProps + +private class GameScene(props: GameSceneProps) : RComponent<GameSceneProps, RState>(props) { + + override fun RBuilder.render() { + styledDiv { + css { + background = "url('images/background-papyrus.jpg')" + backgroundSize = "cover" + fullScreen() + } + val turnInfo = props.turnInfo + if (turnInfo == null) { + gamePreStart(props.sayReady) + } else { + turnInfoScene(turnInfo) + } + } + } + + private fun RBuilder.gamePreStart(onReadyClicked: () -> Unit) { + bpNonIdealState( + description = createElement { + p { +"Click 'ready' when you are"} + }, + action = createElement { + bpButton( + large = true, + intent = Intent.PRIMARY, + icon = "play", + onClick = { onReadyClicked() } + ) { + +"READY" + } + } + ) + } + + private fun RBuilder.turnInfoScene(turnInfo: PlayerTurnInfo) { + val bd = turnInfo.table.boards[turnInfo.playerIndex]; + div { + p { + turnInfo.message } +// boardComponent(board = bd) +// handComponent( +// cards = turnInfo.hand, +// wonderUpgradable = turnInfo.wonderBuildability.isBuildable, +// prepareMove = props.prepareMove +// ) + productionBar( + gold = bd.gold, + production = bd.production + ) + } + } +} + +fun RBuilder.gameScene() = gameScene {} + +private val gameScene: RClass<GameSceneProps> = connectStateAndDispatch<GameSceneStateProps, GameSceneDispatchProps, + GameSceneProps>( + clazz = GameScene::class, + mapDispatchToProps = { dispatch, _ -> + prepareMove = { move -> dispatch(RequestPrepareMove(move)) } + sayReady = { dispatch(RequestSayReady()) } + }, + mapStateToProps = { state, _ -> + players = state.currentLobby?.players ?: emptyList() + turnInfo = state.currentTurnInfo + } +) + +private fun CSSBuilder.fullScreen() { + position = Position.fixed + top = 0.px + left = 0.px + bottom = 0.px + right = 0.px + overflow = Overflow.hidden } diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/ProductionBar.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/ProductionBar.kt new file mode 100644 index 00000000..f435a572 --- /dev/null +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/ProductionBar.kt @@ -0,0 +1,165 @@ +package org.luxons.sevenwonders.ui.components.game + +import kotlinx.css.Align +import kotlinx.css.BorderStyle +import kotlinx.css.CSSBuilder +import kotlinx.css.Color +import kotlinx.css.Display +import kotlinx.css.Position +import kotlinx.css.VerticalAlign +import kotlinx.css.alignItems +import kotlinx.css.background +import kotlinx.css.bottom +import kotlinx.css.color +import kotlinx.css.display +import kotlinx.css.fontFamily +import kotlinx.css.fontSize +import kotlinx.css.height +import kotlinx.css.margin +import kotlinx.css.marginLeft +import kotlinx.css.position +import kotlinx.css.properties.borderTop +import kotlinx.css.properties.boxShadow +import kotlinx.css.px +import kotlinx.css.rem +import kotlinx.css.verticalAlign +import kotlinx.css.vw +import kotlinx.css.width +import kotlinx.css.zIndex +import kotlinx.html.DIV +import kotlinx.html.title +import org.luxons.sevenwonders.model.boards.Production +import org.luxons.sevenwonders.model.resources.CountedResource +import org.luxons.sevenwonders.model.resources.ResourceType +import react.RBuilder +import react.dom.* +import styled.StyledDOMBuilder +import styled.css +import styled.styledDiv +import styled.styledImg +import styled.styledSpan + +fun RBuilder.productionBar(gold: Int, production: Production) { + styledDiv { + css { + productionBarStyle() + } + goldIndicator(gold) + fixedResources(production.fixedResources) + alternativeResources(production.alternativeResources) + } +} + +private fun RBuilder.goldIndicator(amount: Int) { + tokenWithCount(tokenName = "coin", count = amount) +} + +private fun RBuilder.fixedResources(resources: List<CountedResource>) { + styledDiv { + css { + margin = "auto" + display = Display.flex + } + resources.forEach { + tokenWithCount(tokenName = getTokenName(it.type), count = it.count) { + attrs { key = it.type.toString() } + css { marginLeft = 1.rem } + } + } + } +} + +private fun RBuilder.alternativeResources(resources: Set<Set<ResourceType>>) { + styledDiv { + css { + margin = "auto" + display = Display.flex + } + resources.forEachIndexed { index, res -> + resourceChoice(types = res) { + attrs { + key = index.toString() + } + } + } + } +} + +private fun RBuilder.resourceChoice(types: Set<ResourceType>, block: StyledDOMBuilder<DIV>.() -> Unit = {}) { + styledDiv { + css { + marginLeft = (1.5).rem + } + block() + for ((i, t) in types.withIndex()) { + tokenImage(tokenName = getTokenName(t)) { + attrs { this.key = t.toString() } + } + if (i < types.indices.last) { + styledSpan { css { choiceSeparatorStyle() } } + } + } + } +} + +private fun RBuilder.tokenWithCount(tokenName: String, count: Int, block: StyledDOMBuilder<DIV>.() -> Unit = {}) { + styledDiv { + block() + tokenImage(tokenName) + styledSpan { + css { tokenCountStyle() } + + "× $count" + } + } +} + +private fun RBuilder.tokenImage(tokenName: String, block: StyledDOMBuilder<DIV>.() -> Unit = {}) { + styledImg(src = getTokenImagePath(tokenName)) { + css { + tokenImageStyle() + } + attrs { + this.title = tokenName + this.alt = tokenName + } + } +} + +private fun getTokenImagePath(tokenName: String)= "/images/tokens/${tokenName}.png" + +private fun getTokenName(resourceType: ResourceType)= "resources/${resourceType.toString().toLowerCase()}" + +private fun CSSBuilder.productionBarStyle() { + alignItems = Align.center + // background = "lightgray" + background = "linear-gradient(#eaeaea, #888 7%)" + bottom = 0.px + borderTop(width = 1.px, color = Color("#8b8b8b"), style = BorderStyle.solid) + boxShadow(blurRadius = 15.px, color = Color("#747474")) + display = Display.flex + height = (3.5).rem + width = 100.vw + position = Position.fixed + zIndex = 99 +} + +private fun CSSBuilder.choiceSeparatorStyle() { + fontSize = 2.rem + verticalAlign = VerticalAlign.middle + margin(all = 5.px) + color = Color("#c29929") + declarations["text-shadow"] = "0 0 1px black" +} + +private fun CSSBuilder.tokenImageStyle() { + height = 3.rem + width = 3.rem + verticalAlign = VerticalAlign.middle +} + +private fun CSSBuilder.tokenCountStyle() { + fontFamily = "fantasy" + fontSize = 1.5.rem + verticalAlign = VerticalAlign.middle + marginLeft = 0.2.rem +} diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/CreateGameForm.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/CreateGameForm.kt index 3b125df5..876a167e 100644 --- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/CreateGameForm.kt +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/CreateGameForm.kt @@ -3,8 +3,6 @@ package org.luxons.sevenwonders.ui.components.gameBrowser import com.palantir.blueprintjs.Intent import com.palantir.blueprintjs.bpButton import com.palantir.blueprintjs.bpInputGroup -import com.palantir.blueprintjs.org.luxons.sevenwonders.ui.components.gameBrowser.playerInfo -import com.palantir.blueprintjs.org.luxons.sevenwonders.ui.utils.createElement import kotlinx.css.Display import kotlinx.css.FlexDirection import kotlinx.css.JustifyContent @@ -14,6 +12,7 @@ import kotlinx.css.justifyContent import kotlinx.html.js.onSubmitFunction import org.luxons.sevenwonders.ui.redux.RequestCreateGame import org.luxons.sevenwonders.ui.redux.connectDispatch +import org.luxons.sevenwonders.ui.utils.createElement import org.w3c.dom.HTMLInputElement import org.w3c.dom.events.Event import react.RBuilder diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/PlayerInfo.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/PlayerInfo.kt index 1cb52b50..222d4329 100644 --- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/PlayerInfo.kt +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/PlayerInfo.kt @@ -1,4 +1,4 @@ -package com.palantir.blueprintjs.org.luxons.sevenwonders.ui.components.gameBrowser +package org.luxons.sevenwonders.ui.components.gameBrowser import org.luxons.sevenwonders.model.api.PlayerDTO import org.luxons.sevenwonders.ui.redux.connectState diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/ChooseNameForm.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/ChooseNameForm.kt index b0a7b5e2..1aa4be43 100644 --- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/ChooseNameForm.kt +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/ChooseNameForm.kt @@ -3,10 +3,10 @@ package org.luxons.sevenwonders.ui.components.home import com.palantir.blueprintjs.Intent import com.palantir.blueprintjs.bpButton import com.palantir.blueprintjs.bpInputGroup -import com.palantir.blueprintjs.org.luxons.sevenwonders.ui.utils.createElement import kotlinx.html.js.onSubmitFunction import org.luxons.sevenwonders.ui.redux.RequestChooseName import org.luxons.sevenwonders.ui.redux.connectDispatch +import org.luxons.sevenwonders.ui.utils.createElement import org.w3c.dom.HTMLInputElement import org.w3c.dom.events.Event import react.RBuilder diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Actions.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Actions.kt index 43b8aace..223cd5c1 100644 --- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Actions.kt +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Actions.kt @@ -1,7 +1,7 @@ package org.luxons.sevenwonders.ui.redux import org.luxons.sevenwonders.model.GameState -import org.luxons.sevenwonders.model.PlayerMove +import org.luxons.sevenwonders.model.PlayerTurnInfo import org.luxons.sevenwonders.model.api.LobbyDTO import org.luxons.sevenwonders.model.api.PlayerDTO import org.luxons.sevenwonders.model.cards.PreparedCard @@ -19,7 +19,7 @@ data class UpdatePlayers(val players: Map<String, PlayerDTO>): RAction data class EnterGameAction(val gameId: Long): RAction -data class TurnInfoEvent(val players: Map<String, PlayerDTO>): RAction +data class TurnInfoEvent(val turnInfo: PlayerTurnInfo): RAction data class PreparedCardEvent(val card: PreparedCard): RAction diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt index 4c75bd66..dd6dea98 100644 --- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt @@ -1,14 +1,18 @@ package org.luxons.sevenwonders.ui.redux +import org.luxons.sevenwonders.model.GameState +import org.luxons.sevenwonders.model.PlayerTurnInfo import org.luxons.sevenwonders.model.api.LobbyDTO import org.luxons.sevenwonders.model.api.PlayerDTO import redux.RAction data class SwState( + val playersByUsername: Map<String, PlayerDTO> = emptyMap(), + val gamesById: Map<Long, LobbyDTO> = emptyMap(), val currentPlayerUsername: String? = null, val currentLobbyId: Long? = null, - val playersByUsername: Map<String, PlayerDTO> = emptyMap(), - val gamesById: Map<Long, LobbyDTO> = emptyMap() + val currentTurnInfo: PlayerTurnInfo? = null, + val currentTable: GameState? = null ) { val games: List<LobbyDTO> = gamesById.values.toList() val currentLobby: LobbyDTO? = currentLobbyId?.let { gamesById[it] } @@ -16,12 +20,28 @@ data class SwState( } fun rootReducer(state: SwState, action: RAction): SwState = state.copy( + playersByUsername = playersReducer(state.playersByUsername, action), + gamesById = gamesReducer(state.gamesById, action), currentPlayerUsername = currentPlayerReducer(state.currentPlayerUsername, action), currentLobbyId = currentLobbyReducer(state.currentLobbyId, action), - gamesById = gamesReducer(state.gamesById, action), - playersByUsername = playersReducer(state.playersByUsername, action) + currentTurnInfo = currentTurnInfoReducer(state.currentTurnInfo, action), + currentTable = currentTableReducer(state.currentTable, action) ) +private fun playersReducer(playersByUsername: Map<String, PlayerDTO>, action: RAction): Map<String, PlayerDTO> = when (action) { + is UpdatePlayers -> playersByUsername + action.players + is UpdateLobbyAction -> playersByUsername + action.lobby.players.associateBy { it.username } + is SetCurrentPlayerAction -> playersByUsername + (action.player.username to action.player) + else -> playersByUsername +} + +private fun gamesReducer(games: Map<Long, LobbyDTO>, action: RAction): Map<Long, LobbyDTO> = when (action) { + is UpdateGameListAction -> action.games.associateBy { it.id } // replaces because should remove deleted games + is EnterLobbyAction -> games + (action.lobby.id to action.lobby) + is UpdateLobbyAction -> games + (action.lobby.id to action.lobby) + else -> games +} + private fun currentPlayerReducer(username: String?, action: RAction): String? = when (action) { is SetCurrentPlayerAction -> action.player.username else -> username @@ -32,16 +52,13 @@ private fun currentLobbyReducer(currentLobbyId: Long?, action: RAction): Long? = else -> currentLobbyId } -private fun gamesReducer(games: Map<Long, LobbyDTO>, action: RAction): Map<Long, LobbyDTO> = when (action) { - is UpdateGameListAction -> action.games.associateBy { it.id } // replaces because should remove deleted games - is EnterLobbyAction -> games + (action.lobby.id to action.lobby) - is UpdateLobbyAction -> games + (action.lobby.id to action.lobby) - else -> games +private fun currentTurnInfoReducer(currentTurnInfo: PlayerTurnInfo?, action: RAction): PlayerTurnInfo? = when (action) { + is TurnInfoEvent -> action.turnInfo + else -> currentTurnInfo } -private fun playersReducer(playersByUsername: Map<String, PlayerDTO>, action: RAction): Map<String, PlayerDTO> = when (action) { - is UpdatePlayers -> playersByUsername + action.players - is UpdateLobbyAction -> playersByUsername + action.lobby.players.associateBy { it.username } - is SetCurrentPlayerAction -> playersByUsername + (action.player.username to action.player) - else -> playersByUsername +private fun currentTableReducer(currentTable: GameState?, action: RAction): GameState? = when (action) { + is TurnInfoEvent -> action.turnInfo.table + is TableUpdateEvent -> action.table + else -> currentTable } diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameBrowserSagas.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameBrowserSagas.kt index d6555c26..7806bc98 100644 --- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameBrowserSagas.kt +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameBrowserSagas.kt @@ -1,6 +1,5 @@ package org.luxons.sevenwonders.ui.redux.sagas -import com.palantir.blueprintjs.org.luxons.sevenwonders.ui.utils.awaitFirst import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import org.hildan.krossbow.stomp.StompSubscription @@ -12,6 +11,7 @@ import org.luxons.sevenwonders.ui.redux.RequestJoinGame import org.luxons.sevenwonders.ui.redux.UpdateGameListAction import org.luxons.sevenwonders.ui.router.Navigate import org.luxons.sevenwonders.ui.router.Route +import org.luxons.sevenwonders.ui.utils.awaitFirst suspend fun SwSagaContext.gameBrowserSaga(session: SevenWondersSession) { GameBrowserSaga(session, this).run() diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameSagas.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameSagas.kt index eb4e3c13..3e810a28 100644 --- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameSagas.kt +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameSagas.kt @@ -4,6 +4,12 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import org.luxons.sevenwonders.client.SevenWondersSession import org.luxons.sevenwonders.model.api.State +import org.luxons.sevenwonders.ui.redux.PlayerReadyEvent +import org.luxons.sevenwonders.ui.redux.PreparedCardEvent +import org.luxons.sevenwonders.ui.redux.RequestPrepareMove +import org.luxons.sevenwonders.ui.redux.RequestSayReady +import org.luxons.sevenwonders.ui.redux.TableUpdateEvent +import org.luxons.sevenwonders.ui.redux.TurnInfoEvent suspend fun SwSagaContext.gameSaga(session: SevenWondersSession) { val lobby = getState().currentLobby ?: error("Game saga run without a current game") @@ -11,11 +17,18 @@ suspend fun SwSagaContext.gameSaga(session: SevenWondersSession) { error("Game saga run but the game hasn't started") } coroutineScope { - launch { watchPlayerReady(session, lobby.id) } + val playerReadySub = session.watchPlayerReady(lobby.id) + val preparedCardsSub = session.watchPreparedCards(lobby.id) + val tableUpdatesSub = session.watchTableUpdates(lobby.id) + val turnInfoSub = session.watchTurns() + val sayReadyJob = launch { onEach<RequestSayReady> { session.sayReady() } } + val prepareMoveJob = launch { onEach<RequestPrepareMove> { session.prepareMove(it.move) } } + launch { dispatchAll(playerReadySub.messages) { PlayerReadyEvent(it) } } + launch { dispatchAll(preparedCardsSub.messages) { PreparedCardEvent(it) } } + launch { dispatchAll(tableUpdatesSub.messages) { TableUpdateEvent(it) } } + launch { dispatchAll(turnInfoSub.messages) { TurnInfoEvent(it) } } + // TODO await game end + // TODO unsubscribe all subs, cancel all jobs } } -private suspend fun SwSagaContext.watchPlayerReady(session: SevenWondersSession, gameId: Long) { - session.watchPlayerReady(gameId) -} - diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/utils/CoroutinesUtils.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/utils/CoroutinesUtils.kt index c7e950c8..55f8e0f6 100644 --- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/utils/CoroutinesUtils.kt +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/utils/CoroutinesUtils.kt @@ -1,4 +1,4 @@ -package com.palantir.blueprintjs.org.luxons.sevenwonders.ui.utils +package org.luxons.sevenwonders.ui.utils import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/utils/ReactUtils.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/utils/ReactUtils.kt index 6aa7a438..07b3f2b5 100644 --- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/utils/ReactUtils.kt +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/utils/ReactUtils.kt @@ -1,4 +1,4 @@ -package com.palantir.blueprintjs.org.luxons.sevenwonders.ui.utils +package org.luxons.sevenwonders.ui.utils import kotlinx.html.SPAN import kotlinx.html.attributesMapOf |