diff options
8 files changed, 347 insertions, 10 deletions
diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/Application.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/Application.kt index 68ec1409..5e602ae5 100644 --- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/Application.kt +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/Application.kt @@ -20,7 +20,7 @@ fun RBuilder.application() = hashRouter { switch { route("/games") { gameBrowser() } route<IdProps>("/game/:id") { props -> gameScene(props.match.params.id) } - route<IdProps>("/lobby/:id") { props -> lobby(props.match.params.id) } + route<IdProps>("/lobby") { lobby() } route("/", exact = true) { home() } redirect(from = "*", to = "/") } diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Lobby.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Lobby.kt index 9804f954..360937ed 100644 --- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Lobby.kt +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Lobby.kt @@ -1,13 +1,69 @@ package org.luxons.sevenwonders.ui.components.lobby +import kotlinx.html.js.onClickFunction +import org.luxons.sevenwonders.model.api.LobbyDTO +import org.luxons.sevenwonders.model.api.PlayerDTO +import org.luxons.sevenwonders.ui.redux.RequestStartGameAction +import org.luxons.sevenwonders.ui.redux.connect import react.RBuilder +import react.RComponent +import react.RProps +import react.RState import react.dom.* -fun RBuilder.lobby(lobbyId: Long) = div { - h1 { - +"Lobby $lobbyId" - } - p { - +"Can't wait to play!" +interface LobbyStateProps : RProps { + var currentGame: LobbyDTO? + var currentPlayer: PlayerDTO? + var players: List<PlayerDTO> +} + +interface LobbyDispatchProps : RProps { + var startGame: () -> Unit +} + +interface LobbyProps : LobbyDispatchProps, LobbyStateProps + +class LobbyPresenter(props: LobbyProps) : RComponent<LobbyProps, RState>(props) { + + override fun RBuilder.render() { + val currentGame = props.currentGame + val currentPlayer = props.currentPlayer + if (currentGame == null || currentPlayer == null) { + div { +"Error: no current game." } + return + } + div { + h2 { +"${currentGame.name} — Lobby" } + radialPlayerList(props.players) + if (currentPlayer.isGameOwner) { + // <Button text="START" + // className={Classes.LARGE} + // intent={Intent.PRIMARY} + // icon='play' + // onClick={startGame} + // disabled={players.size < 3}/> + button { + attrs { + onClickFunction = { props.startGame() } + disabled = props.players.size < 3 + } + +"START" + } + } + } } } + +fun RBuilder.lobby() = lobby {} + +val lobby = connect<LobbyStateProps, LobbyDispatchProps, LobbyProps>( + clazz = LobbyPresenter::class, + mapStateToProps = { state, _ -> + currentGame = state.lobby + currentPlayer = state.player + players = state.lobby?.players ?: emptyList() + }, + mapDispatchToProps = { dispatch, _ -> + startGame = { dispatch(RequestStartGameAction()) } + } +) diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialList.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialList.kt new file mode 100644 index 00000000..be3bb1de --- /dev/null +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialList.kt @@ -0,0 +1,121 @@ +package org.luxons.sevenwonders.ui.components.lobby + +import kotlinx.css.CSSBuilder +import kotlinx.css.Display +import kotlinx.css.ListStyleType +import kotlinx.css.Position +import kotlinx.css.display +import kotlinx.css.height +import kotlinx.css.left +import kotlinx.css.listStyleType +import kotlinx.css.margin +import kotlinx.css.padding +import kotlinx.css.pct +import kotlinx.css.position +import kotlinx.css.properties.Timing +import kotlinx.css.properties.ms +import kotlinx.css.properties.transform +import kotlinx.css.properties.transition +import kotlinx.css.properties.translate +import kotlinx.css.px +import kotlinx.css.top +import kotlinx.css.width +import kotlinx.css.zIndex +import react.RBuilder +import react.ReactElement +import react.dom.* +import styled.css +import styled.styledDiv +import styled.styledLi +import styled.styledUl + +typealias ElementBuilder = RBuilder.() -> ReactElement + +fun RBuilder.radialList( + itemBuilders: List<ElementBuilder>, + centerElementBuilder: ElementBuilder, + itemWidth: Int, + itemHeight: Int, + options: RadialConfig = RadialConfig() +): ReactElement { + val containerWidth = options.diameter + itemWidth + val containerHeight = options.diameter + itemHeight + + return styledDiv { + css { + zeroMargins() + position = Position.relative + width = containerWidth.px + height = containerHeight.px + } + radialListItems(itemBuilders, options) + radialListCenter(centerElementBuilder) + } +} + +private fun RBuilder.radialListItems(itemBuilders: List<ElementBuilder>, radialConfig: RadialConfig): ReactElement { + val offsets = offsetsFromCenter(itemBuilders.size, radialConfig) + return styledUl { + css { + zeroMargins() + transition(property = "all", duration = 500.ms, timing = Timing.easeInOut) + zIndex = 1 + width = radialConfig.diameter.px + height = radialConfig.diameter.px + absoluteCenter() + } + itemBuilders.forEachIndexed { i, itemBuilder -> + radialListItem(itemBuilder, i, offsets[i]) + } + } +} + +private fun RBuilder.radialListItem(itemBuilder: ElementBuilder, i: Int, offset: CartesianCoords): ReactElement { + return styledLi { + css { + display = Display.block + position = Position.absolute + top = 50.pct + left = 50.pct + zeroMargins() + listStyleType = ListStyleType.unset + transition("all", 500.ms, Timing.easeInOut) + zIndex = 1 + transform { + translate(offset.x.px, offset.y.px) + translate((-50).pct, (-50).pct) + } + } + attrs { + key = "$i" + } + itemBuilder() + } +} + +private fun RBuilder.radialListCenter(centerElement: ElementBuilder?): ReactElement? { + if (centerElement == null) { + return null + } + return styledDiv { + css { + zIndex = 0 + absoluteCenter() + } + centerElement() + } +} + +private fun CSSBuilder.absoluteCenter() { + position = Position.absolute + left = 50.pct + top = 50.pct + transform { + translate((-50).pct, (-50).pct) + } +} + +private fun CSSBuilder.zeroMargins() { + margin = "0" + padding = "0" +} diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialMath.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialMath.kt new file mode 100644 index 00000000..e4d51cf8 --- /dev/null +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialMath.kt @@ -0,0 +1,54 @@ +package org.luxons.sevenwonders.ui.components.lobby + +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.roundToInt +import kotlin.math.sin + +data class CartesianCoords( + val x: Int, + val y: Int +) + +data class PolarCoords( + val radius: Int, + val angleDeg: Int +) + +private fun Int.toRadians() = (this * PI / 180.0) +private fun Double.project(angleRad: Double, trigFn: (Double) -> Double) = (this * trigFn(angleRad)).roundToInt() +private fun Double.xProjection(angleRad: Double) = project(angleRad, ::cos) +private fun Double.yProjection(angleRad: Double) = project(angleRad, ::sin) + +private fun PolarCoords.toCartesian() = CartesianCoords( + x = radius.toDouble().xProjection(angleDeg.toRadians()), + y = radius.toDouble().yProjection(angleDeg.toRadians()) +) + +enum class Direction(private val value: Int) { + CLOCKWISE(-1), + COUNTERCLOCKWISE(1); + + fun toOrientedDegrees(deg: Int) = value * deg +} + +data class RadialConfig( + val radius: Int = 120, + val arcDegrees: Int = 360, // full circle + val offsetDegrees: Int = 0, // 12 o'clock + val direction: Direction = Direction.CLOCKWISE +) { + val diameter: Int = radius * 2 +} + +private const val DEFAULT_START = 90; // Up + +fun offsetsFromCenter(nbItems: Int, radialConfig: RadialConfig = RadialConfig()): List<CartesianCoords> = + (0 until nbItems).map { itemCartesianOffsets(it, nbItems, radialConfig) } + +private fun itemCartesianOffsets(index: Int, nbItems: Int, config: RadialConfig): CartesianCoords { + val startAngle = DEFAULT_START + config.direction.toOrientedDegrees(config.offsetDegrees) + val angleStep = config.arcDegrees / nbItems + val itemAngle = startAngle + config.direction.toOrientedDegrees(angleStep) * index + return PolarCoords(config.radius, itemAngle).toCartesian() +} diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialPlayerList.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialPlayerList.kt new file mode 100644 index 00000000..de6eabbd --- /dev/null +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialPlayerList.kt @@ -0,0 +1,106 @@ +package org.luxons.sevenwonders.ui.components.lobby + +import kotlinx.css.Align +import kotlinx.css.FlexDirection +import kotlinx.css.alignContent +import kotlinx.css.flexDirection +import kotlinx.css.margin +import kotlinx.css.opacity +import org.luxons.sevenwonders.model.api.PlayerDTO +import react.RBuilder +import react.ReactElement +import react.dom.* +import styled.css +import styled.styledDiv +import styled.styledH5 + +fun RBuilder.radialPlayerList(players: List<PlayerDTO>): ReactElement { + val playerItemBuilders = players + .withUserFirst() + .growTo(targetSize = 3) + .map { p -> p.elementBuilder() } + + val tableImgBuilder: ElementBuilder = { roundTableImg() } + + return radialList( + itemBuilders = playerItemBuilders, + centerElementBuilder = tableImgBuilder, + itemWidth = 120, + itemHeight = 100, + options = RadialConfig( + radius = 175, + offsetDegrees = 180 + ) + ) +} + +private fun RBuilder.roundTableImg(): ReactElement = img { + attrs { + src = "images/round-table.png" + alt = "Round table" + width = "200" + height = "200" + } +} + +private fun List<PlayerDTO>.withUserFirst(): List<PlayerDTO> { + val nonUsersBeginning = takeWhile { !it.isUser } + val userToEnd = subList(nonUsersBeginning.size, size) + return userToEnd + nonUsersBeginning +} + +private fun <T> List<T>.growTo(targetSize: Int): List<T?> { + if (size >= targetSize) return this + return this + List(targetSize - size) { null } +} + +private fun PlayerDTO?.elementBuilder(): ElementBuilder { + if (this == null) { + return { playerPlaceholder() } + } else { + return { playerItem(this@elementBuilder) } + } +} + +private fun RBuilder.playerItem(player: PlayerDTO): ReactElement = styledDiv { + css { + flexDirection = FlexDirection.column + alignContent = Align.center + } + val title = if (player.isGameOwner) "Game owner" else null + userIcon(isUser = player.isUser, isOwner = player.isGameOwner, title = title) + styledH5 { + css { + margin = "0" + } + +player.displayName + } +} + +private fun RBuilder.playerPlaceholder(): ReactElement = styledDiv { + css { + flexDirection = FlexDirection.column + alignContent = Align.center + opacity = 0.3 + } + userIcon(isUser = false, isOwner = false, title = "Waiting for player...") + styledH5 { + css { + margin = "0" + } + +"?" + } +} + +private fun RBuilder.userIcon(isUser: Boolean, isOwner: Boolean, title: String?): ReactElement { + // TODO + // const icon: IconName = isOwner ? 'badge' : 'user'; + // const intent: Intent = isUser ? Intent.WARNING : Intent.NONE; + // return <Icon icon={icon} iconSize={50} intent={intent} title={title}/>; + val owner = if (isOwner) "(owner)" else "" + val user = if (isUser) "(me)" else "" + val hint = listOf(owner, user).joinToString(" ") + return span { + +hint + } +} 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 02c8f5f0..f2dd4e0f 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 @@ -52,6 +52,6 @@ private suspend fun SwSagaContext.handleGameJoined( dispatch(EnterLobbyAction(lobby.id)) coroutineScope { launch { lobbySaga(session, lobby.id) } - Router.lobby(lobby.id) + Router.lobby() } } diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/router/Router.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/router/Router.kt index fa768cbf..25cb2ec1 100644 --- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/router/Router.kt +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/router/Router.kt @@ -12,8 +12,8 @@ object Router { push("/game/$id") } - fun lobby(id: Long) { - push("/lobby/$id") + fun lobby() { + push("/lobby") } private fun push(path: String) { diff --git a/sw-ui-kt/src/main/resources/images/round-table.png b/sw-ui-kt/src/main/resources/images/round-table.png Binary files differnew file mode 100644 index 00000000..f277376d --- /dev/null +++ b/sw-ui-kt/src/main/resources/images/round-table.png |