diff options
author | Joffrey Bion <joffrey.bion@gmail.com> | 2023-07-06 00:16:45 +0200 |
---|---|---|
committer | Joffrey Bion <joffrey.bion@gmail.com> | 2023-07-06 00:30:55 +0200 |
commit | cdaae5279e3f53c146df2500a8d7a1d4eae2f674 (patch) | |
tree | 55347f2839caaf4688f4f989e9568ffd7102db63 /sw-ui/src/main/kotlin | |
parent | Upgrade Kotlin JS wrappers to 1.0.0-pre.585 (diff) | |
download | seven-wonders-main.tar.gz seven-wonders-main.tar.bz2 seven-wonders-main.zip |
Convert sw-ui module to Kotlin Multiplatform gradle pluginmain
Diffstat (limited to 'sw-ui/src/main/kotlin')
42 files changed, 0 insertions, 4621 deletions
diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/SevenWondersUi.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/SevenWondersUi.kt deleted file mode 100644 index 0bd3400e..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/SevenWondersUi.kt +++ /dev/null @@ -1,47 +0,0 @@ -package org.luxons.sevenwonders.ui - -import kotlinx.browser.window -import kotlinx.coroutines.* -import org.luxons.sevenwonders.ui.components.* -import org.luxons.sevenwonders.ui.redux.* -import org.luxons.sevenwonders.ui.redux.sagas.* -import react.* -import react.dom.client.* -import react.redux.* -import redux.* -import web.dom.document -import web.html.* - -fun main() { - window.onload = { init() } -} - -private fun init() { - val rootElement = document.getElementById("root") - if (rootElement == null) { - console.error("Element with ID 'root' was not found, cannot bootstrap react app") - return - } - renderRoot(rootElement) -} - -private fun renderRoot(rootElement: HTMLElement) { - val store = initRedux() - val connectedApp = Provider.create { - this.store = store - Application() - } - createRoot(rootElement).render(connectedApp) -} - -@OptIn(DelicateCoroutinesApi::class) -private fun initRedux(): Store<SwState, RAction, WrapperAction> { - val sagaManager = SagaManager<SwState, RAction, WrapperAction>() - val store = configureStore(sagaManager = sagaManager) - GlobalScope.launch { - sagaManager.launchSaga(this) { - rootSaga() - } - } - return store -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/Application.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/Application.kt deleted file mode 100644 index 2cf8b4f1..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/Application.kt +++ /dev/null @@ -1,52 +0,0 @@ -package org.luxons.sevenwonders.ui.components - -import js.core.jso -import org.luxons.sevenwonders.ui.components.errors.* -import org.luxons.sevenwonders.ui.components.game.* -import org.luxons.sevenwonders.ui.components.gameBrowser.* -import org.luxons.sevenwonders.ui.components.home.* -import org.luxons.sevenwonders.ui.components.lobby.* -import org.luxons.sevenwonders.ui.router.* -import react.* -import react.router.* -import react.router.dom.* - -val Application = FC("Application") { - ErrorDialog() - RouterProvider { - router = hashRouter - } -} - -// Using plain jso objects instead of createRoutesFromElements -// because of a broken Route external interface (no properties) -// See https://github.com/JetBrains/kotlin-wrappers/issues/2024 -private val hashRouter = createHashRouter( - routes = arrayOf( - jso { - path = SwRoute.GAME_BROWSER.path - Component = GameBrowser - }, - jso { - path = SwRoute.GAME.path - Component = GameScene - }, - jso { - path = SwRoute.LOBBY.path - Component = Lobby - }, - jso { - path = SwRoute.HOME.path - Component = Home - }, - jso { - path = "*" - Component = FC { - Navigate { - to = "/" - replace = true - } - } - }, - ), -) diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/GlobalStyles.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/GlobalStyles.kt deleted file mode 100644 index ee9c17ab..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/GlobalStyles.kt +++ /dev/null @@ -1,48 +0,0 @@ -package org.luxons.sevenwonders.ui.components - -import emotion.css.* -import org.luxons.sevenwonders.ui.utils.* -import web.cssom.* - - -object GlobalStyles { - - val preGameWidth = 60.rem - - val zeusBackground = ClassName { - background = "url('images/backgrounds/zeus-temple.jpg') center no-repeat".unsafeCast<Background>() - backgroundSize = BackgroundSize.cover - } - - val fullscreen = ClassName { - position = Position.fixed - top = 0.px - left = 0.px - bottom = 0.px - right = 0.px - overflow = Overflow.hidden - } - - val papyrusBackground = ClassName { - background = "url('images/backgrounds/papyrus.jpg')".unsafeCast<Background>() - backgroundSize = BackgroundSize.cover - } - - val centerLeftTopTransform = ClassName { - left = 50.pct - top = 50.pct - transform = translate((-50).pct, (-50).pct) - } - - val fixedCenter = ClassName(centerLeftTopTransform) { - position = Position.fixed - } - - val centerInPositionedParent = ClassName(centerLeftTopTransform) { - position = Position.absolute - } - - val noPadding = ClassName { - padding = Padding(all = 0.px) - } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/errors/ErrorDialog.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/errors/ErrorDialog.kt deleted file mode 100644 index c728d405..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/errors/ErrorDialog.kt +++ /dev/null @@ -1,57 +0,0 @@ -package org.luxons.sevenwonders.ui.components.errors - -import blueprintjs.core.* -import blueprintjs.icons.* -import kotlinx.browser.* -import org.luxons.sevenwonders.ui.redux.* -import org.luxons.sevenwonders.ui.router.* -import react.* -import react.dom.html.ReactHTML.p -import react.redux.* -import redux.* - -val ErrorDialog = FC { - val dispatch = useDispatch<RAction, WrapperAction>() - - ErrorDialogPresenter { - errorMessage = useSwSelector { it.fatalError } - goHome = { dispatch(Navigate(SwRoute.HOME)) } - } -} - -private external interface ErrorDialogProps : Props { - var errorMessage: String? - var goHome: () -> Unit -} - -private val ErrorDialogPresenter = FC<ErrorDialogProps>("ErrorDialogPresenter") { props -> - val errorMessage = props.errorMessage - BpDialog { - isOpen = errorMessage != null - titleText = "Oops!" - icon = BpIcon.create { - icon = IconNames.ERROR - intent = Intent.DANGER - } - onClose = { goHomeAndRefresh() } - - BpDialogBody { - p { - +(errorMessage ?: "fatal error") - } - } - BpDialogFooter { - BpButton { - icon = IconNames.LOG_OUT - onClick = { goHomeAndRefresh() } - - +"HOME" - } - } - } -} - -private fun goHomeAndRefresh() { - // we don't use a redux action here because we actually want to redirect and refresh the page - window.location.href = SwRoute.HOME.path -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Board.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Board.kt deleted file mode 100644 index 1eb5f6f0..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Board.kt +++ /dev/null @@ -1,227 +0,0 @@ -package org.luxons.sevenwonders.ui.components.game - -import csstype.* -import emotion.react.* -import org.luxons.sevenwonders.model.boards.* -import org.luxons.sevenwonders.model.cards.* -import org.luxons.sevenwonders.model.wonders.* -import react.* -import react.dom.html.* -import react.dom.html.ReactHTML.div -import react.dom.html.ReactHTML.img -import web.cssom.* -import web.html.* - -// card offsets in % of their size when displayed in columns -private const val xOffset = 20 -private const val yOffset = 21 - -external interface BoardComponentProps : PropsWithClassName { - var board: Board -} - -val BoardComponent = FC<BoardComponentProps>("Board") { props -> - div { - className = props.className - tableCards(cardColumns = props.board.playedCards) - wonderComponent(wonder = props.board.wonder, military = props.board.military) - } -} - -private fun ChildrenBuilder.tableCards(cardColumns: List<List<TableCard>>) { - div { - css { - display = Display.flex - justifyContent = JustifyContent.spaceAround - height = 45.pct - width = 100.pct - } - cardColumns.forEach { cards -> - TableCardColumn { - this.key = cards.first().color.toString() - this.cards = cards - } - } - } -} - -private external interface TableCardColumnProps : PropsWithClassName { - var cards: List<TableCard> -} - -private val TableCardColumn = FC<TableCardColumnProps>("TableCardColumn") { props -> - div { - css { - height = 100.pct - width = 13.pct - marginRight = 4.pct - position = Position.relative - } - props.cards.forEachIndexed { index, card -> - TableCard { - this.card = card - this.indexInColumn = index - this.key = card.name - } - } - } -} - -private external interface TableCardProps : PropsWithClassName { - var card: TableCard - var indexInColumn: Int -} - -private val TableCard = FC<TableCardProps>("TableCard") { props -> - val highlightColor = if (props.card.playedDuringLastMove) NamedColor.gold else null - CardImage { - this.card = props.card - this.highlightColor = highlightColor - - css { - position = Position.absolute - zIndex = integer(props.indexInColumn + 2) // go above the board and the built wonder cards - transform = translate( - tx = (props.indexInColumn * xOffset).pct, - ty = (props.indexInColumn * yOffset).pct, - ) - maxWidth = 100.pct - maxHeight = 70.pct - - hover { - zIndex = integer(1000) - maxWidth = 110.pct - maxHeight = 75.pct - hoverHighlightStyle() - } - } - } -} - -private fun ChildrenBuilder.wonderComponent(wonder: ApiWonder, military: Military) { - div { - css { - position = Position.relative - width = 100.pct - height = 40.pct - } - div { - css { - position = Position.absolute - left = 50.pct - top = 0.px - transform = translatex((-50).pct) - height = 100.pct - maxWidth = 95.pct // same as wonder - - // bring to the foreground on hover - hover { zIndex = integer(1000) } - } - img { - src = "/images/wonders/${wonder.image}" - title = wonder.name - alt = "Wonder ${wonder.name}" - - css { - borderRadius = "0.5%/1.5%".unsafeCast<BorderRadius>() - boxShadow = BoxShadow(color = NamedColor.black, offsetX = 0.2.rem, offsetY = 0.2.rem, blurRadius = 0.5.rem) - maxHeight = 100.pct - maxWidth = 100.pct - zIndex = integer(1) // go above the built wonder cards, but below the table cards - - hover { hoverHighlightStyle() } - } - } - div { - css { - position = Position.absolute - top = 20.pct - right = (-80).px - display = Display.flex - flexDirection = FlexDirection.column - alignItems = AlignItems.start - } - victoryPoints(military.victoryPoints) { - css { - marginBottom = 5.px - } - } - defeatTokenCount(military.nbDefeatTokens) { - css { - marginTop = 5.px - } - } - } - wonder.stages.forEachIndexed { index, stage -> - WonderStageElement { - this.stage = stage - css { - wonderCardStyle(index, wonder.stages.size) - } - } - } - } - } -} - -private fun ChildrenBuilder.victoryPoints(points: Int, block: HTMLAttributes<HTMLDivElement>.() -> Unit = {}) { - boardToken("military/victory1", points, block) -} - -private fun ChildrenBuilder.defeatTokenCount(nbDefeatTokens: Int, block: HTMLAttributes<HTMLDivElement>.() -> Unit = {}) { - boardToken("military/defeat1", nbDefeatTokens, block) -} - -private fun ChildrenBuilder.boardToken(tokenName: String, count: Int, block: HTMLAttributes<HTMLDivElement>.() -> Unit) { - tokenWithCount( - tokenName = tokenName, - count = count, - countPosition = TokenCountPosition.RIGHT, - brightText = true, - ) { - css { - filter = dropShadow(0.2.rem, 0.2.rem, 0.5.rem, NamedColor.black) - height = 15.pct - } - block() - } -} - -private external interface WonderStageElementProps : PropsWithClassName { - var stage: ApiWonderStage -} - -private val WonderStageElement = FC<WonderStageElementProps>("WonderStageElement") { props -> - val back = props.stage.cardBack - if (back != null) { - val highlightColor = if (props.stage.builtDuringLastMove) NamedColor.gold else null - CardBackImage { - this.cardBack = back - this.highlightColor = highlightColor - this.className = props.className - } - } else { - CardPlaceholderImage { - this.className = props.className - } - } -} - -private fun PropertiesBuilder.wonderCardStyle(stageIndex: Int, nbStages: Int) { - position = Position.absolute - top = 60.pct // makes the cards stick out of the bottom of the wonder - left = stagePositionPercent(stageIndex, nbStages).pct - maxWidth = 24.pct // ratio of card width to wonder width - maxHeight = 90.pct // ratio of card height to wonder height - zIndex = integer(-1) // below wonder (somehow 0 is not sufficient) -} - -private fun stagePositionPercent(stageIndex: Int, nbStages: Int): Double = when (nbStages) { - 2 -> 37.5 + stageIndex * 29.8 // 37.5 (29.8) 67.3 - 4 -> -1.5 + stageIndex * 26.7 // -1.5 (26.6) 25.1 (26.8) 51.9 (26.7) 78.6 - else -> 7.9 + stageIndex * 30.0 -} - -private fun PropertiesBuilder.hoverHighlightStyle() { - highlightStyle(NamedColor.palegoldenrod) -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/BoardSummary.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/BoardSummary.kt deleted file mode 100644 index 37de113c..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/BoardSummary.kt +++ /dev/null @@ -1,211 +0,0 @@ -package org.luxons.sevenwonders.ui.components.game - -import blueprintjs.core.* -import csstype.* -import emotion.css.* -import emotion.react.* -import org.luxons.sevenwonders.model.api.* -import org.luxons.sevenwonders.model.boards.* -import org.luxons.sevenwonders.ui.components.gameBrowser.* -import org.luxons.sevenwonders.ui.utils.* -import react.* -import react.dom.html.ReactHTML.div -import react.dom.html.ReactHTML.hr -import web.cssom.* - -enum class BoardSummarySide( - val tokenCountPosition: TokenCountPosition, - val alignment: AlignItems, - val popoverPosition: PopoverPosition, -) { - LEFT(TokenCountPosition.RIGHT, AlignItems.flexStart, PopoverPosition.RIGHT), - TOP(TokenCountPosition.OVER, AlignItems.flexStart, PopoverPosition.BOTTOM), - RIGHT(TokenCountPosition.LEFT, AlignItems.flexEnd, PopoverPosition.LEFT), - BOTTOM(TokenCountPosition.OVER, AlignItems.flexStart, PopoverPosition.TOP), -} - -external interface BoardSummaryWithPopoverProps : PropsWithClassName { - var player: PlayerDTO - var board: Board - var side: BoardSummarySide -} - -val BoardSummaryWithPopover = FC<BoardSummaryWithPopoverProps>("BoardSummaryWithPopover") { props -> - BpPopover { - content = BoardComponent.create { - className = GameStyles.fullBoardPreview - board = props.board - } - position = props.side.popoverPosition - interactionKind = PopoverInteractionKind.HOVER - popoverClassName = ClassName { - val bgColor = GameStyles.sandBgColor.withAlpha(0.7) - backgroundColor = bgColor - borderRadius = 0.5.rem - padding = Padding(all = 0.5.rem) - - children(".bp4-popover-content") { - background = None.none // overrides default white background - } - descendants(".bp4-popover-arrow-fill") { - set(Variable("fill"), bgColor.toString()) // overrides default white arrow - } - descendants(".bp4-popover-arrow::before") { - // The popover arrow is implemented with a simple square rotated 45 degrees (like a rhombus). - // Since we use a semi-transparent background, we can see the box shadow of the rest of the arrow through - // the popover, and thus we see the square. This boxShadow(transparent) is to avoid that. - boxShadow = BoxShadow(0.px, 0.px, 0.px, 0.px, NamedColor.transparent) - } - }.toString() - - BoardSummary { - this.className = props.className - this.player = props.player - this.board = props.board - this.side = props.side - } - } -} - -external interface BoardSummaryProps : PropsWithClassName { - var player: PlayerDTO - var board: Board - var side: BoardSummarySide - var showPreparationStatus: Boolean? -} - -val BoardSummary = FC<BoardSummaryProps>("BoardSummary") { props -> - div { - css(props.className) { - display = Display.flex - flexDirection = FlexDirection.column - alignItems = props.side.alignment - padding = Padding(all = 0.5.rem) - backgroundColor = NamedColor.palegoldenrod.withAlpha(0.5) - zIndex = integer(50) // above table cards - - hover { - backgroundColor = NamedColor.palegoldenrod - } - } - - val showPreparationStatus = props.showPreparationStatus ?: true - topBar(props.player, props.side, showPreparationStatus) - hr { - css { - margin = Margin(vertical = 0.5.rem, horizontal = 0.rem) - width = 100.pct - } - } - bottomBar(props.side, props.board, props.player, showPreparationStatus) - } -} - -private fun ChildrenBuilder.topBar(player: PlayerDTO, side: BoardSummarySide, showPreparationStatus: Boolean) { - val playerIconSize = 25 - if (showPreparationStatus && side == BoardSummarySide.TOP) { - div { - css { - display = Display.flex - flexDirection = FlexDirection.row - justifyContent = JustifyContent.spaceBetween - width = 100.pct - } - PlayerInfo { - this.player = player - this.iconSize = playerIconSize - } - PlayerPreparedCard { - this.playerDisplayName = player.displayName - this.username = player.username - } - } - } else { - PlayerInfo { - this.player = player - this.iconSize = playerIconSize - } - } -} - -private fun ChildrenBuilder.bottomBar(side: BoardSummarySide, board: Board, player: PlayerDTO, showPreparationStatus: Boolean) { - div { - css { - display = Display.flex - flexDirection = if (side == BoardSummarySide.TOP || side == BoardSummarySide.BOTTOM) FlexDirection.row else FlexDirection.column - alignItems = side.alignment - if (side != BoardSummarySide.TOP) { - width = 100.pct - } - } - val tokenSize = 2.rem - generalCounts(board, tokenSize, side.tokenCountPosition) - BpDivider() - scienceTokens(board, tokenSize, side.tokenCountPosition) - if (showPreparationStatus && side != BoardSummarySide.TOP) { - BpDivider() - div { - css { - width = 100.pct - alignItems = AlignItems.center - display = Display.flex - flexDirection = FlexDirection.column - } - PlayerPreparedCard { - this.playerDisplayName = player.displayName - this.username = player.username - } - } - } - } -} - -private fun ChildrenBuilder.generalCounts( - board: Board, - tokenSize: Length, - countPosition: TokenCountPosition, -) { - goldIndicator(amount = board.gold, imgSize = tokenSize, amountPosition = countPosition) - tokenWithCount( - tokenName = "laurel-blue", - count = board.bluePoints, - imgSize = tokenSize, - countPosition = countPosition, - brightText = countPosition == TokenCountPosition.OVER, - ) - tokenWithCount( - tokenName = "military/shield", - count = board.military.nbShields, - imgSize = tokenSize, - countPosition = countPosition, - brightText = countPosition == TokenCountPosition.OVER, - ) -} - -private fun ChildrenBuilder.scienceTokens( - board: Board, - tokenSize: Length, - sciencePosition: TokenCountPosition, -) { - tokenWithCount( - tokenName = "science/compass", - count = board.science.nbCompasses, - imgSize = tokenSize, - countPosition = sciencePosition, - brightText = sciencePosition == TokenCountPosition.OVER, - ) - tokenWithCount( - tokenName = "science/cog", - count = board.science.nbWheels, - imgSize = tokenSize, - countPosition = sciencePosition, - brightText = sciencePosition == TokenCountPosition.OVER, - ) - tokenWithCount( - tokenName = "science/tablet", - count = board.science.nbTablets, - imgSize = tokenSize, - countPosition = sciencePosition, - brightText = sciencePosition == TokenCountPosition.OVER, - ) -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/CardImage.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/CardImage.kt deleted file mode 100644 index cffd509f..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/CardImage.kt +++ /dev/null @@ -1,78 +0,0 @@ -package org.luxons.sevenwonders.ui.components.game - -import csstype.* -import emotion.react.* -import org.luxons.sevenwonders.model.cards.* -import react.* -import react.dom.html.ReactHTML.img -import web.cssom.* -import web.cssom.Color - -external interface CardImageProps : PropsWithClassName { - var card: Card - var faceDown: Boolean? - var highlightColor: Color? -} - -val CardImage = FC<CardImageProps>("CardImage") { props -> - if (props.faceDown == true) { - CardBackImage { - cardBack = props.card.back - highlightColor = props.highlightColor - } - } else { - img { - src = "/images/cards/${props.card.image}" - title = props.card.name - alt = "Card ${props.card.name}" - - css(props.className) { - cardImageStyle(props.highlightColor) - } - } - } -} - -external interface CardBackImageProps : PropsWithClassName { - var cardBack: CardBack - var highlightColor: Color? -} - -val CardBackImage = FC<CardBackImageProps>("CardBackImage") { props -> - img { - src = "/images/cards/back/${props.cardBack.image}" - alt = "Card back (${props.cardBack.image})" - css(props.className) { - cardImageStyle(props.highlightColor) - } - } -} - -val CardPlaceholderImage = FC<PropsWithClassName>("CardPlaceholderImage") { props -> - img { - src = "/images/cards/back/placeholder.png" - alt = "Card placeholder" - css(props.className) { - opacity = number(0.20) - borderRadius = 5.pct - } - } -} - -private fun PropertiesBuilder.cardImageStyle(highlightColor: Color?) { - borderRadius = 5.pct - boxShadow = BoxShadow(offsetX = 2.px, offsetY = 2.px, blurRadius = 5.px, color = NamedColor.black) - highlightStyle(highlightColor) -} - -internal fun PropertiesBuilder.highlightStyle(highlightColor: Color?) { - if (highlightColor != null) { - boxShadow = BoxShadow( - offsetX = 0.px, - offsetY = 0.px, - blurRadius = 1.rem, - spreadRadius = 0.1.rem, - color = highlightColor, - ) - } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt deleted file mode 100644 index 622e3f6d..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt +++ /dev/null @@ -1,310 +0,0 @@ -package org.luxons.sevenwonders.ui.components.game - -import blueprintjs.core.* -import blueprintjs.icons.* -import emotion.react.* -import org.luxons.sevenwonders.client.* -import org.luxons.sevenwonders.model.* -import org.luxons.sevenwonders.model.api.* -import org.luxons.sevenwonders.model.boards.* -import org.luxons.sevenwonders.model.cards.* -import org.luxons.sevenwonders.model.resources.* -import org.luxons.sevenwonders.ui.components.* -import org.luxons.sevenwonders.ui.redux.* -import org.luxons.sevenwonders.ui.utils.* -import org.luxons.sevenwonders.ui.utils.Padding -import react.* -import react.dom.html.ReactHTML.div -import web.cssom.* -import web.cssom.Position - -external interface GameSceneProps : Props { - var currentPlayer: PlayerDTO? - var players: List<PlayerDTO> - var game: GameState - var preparedMove: PlayerMove? - var preparedCard: HandCard? - var sayReady: () -> Unit - var prepareMove: (move: PlayerMove) -> Unit - var unprepareMove: () -> Unit - var leaveGame: () -> Unit -} - -data class TransactionSelectorState( - val moveType: MoveType, - val card: HandCard, - val transactionsOptions: ResourceTransactionOptions, -) - -val GameScene = FC("GameScene") { - - val player = useSwSelector { it.currentPlayer } - val gameState = useSwSelector { it.gameState } - val dispatch = useSwDispatch() - - div { - css(GlobalStyles.papyrusBackground, GlobalStyles.fullscreen) {} - - if (gameState == null) { - BpNonIdealState { - icon = IconNames.ERROR - titleText = "Error: no game data" - } - } else { - GameScenePresenter { - currentPlayer = player - players = gameState.players - game = gameState - preparedMove = gameState.currentPreparedMove - preparedCard = gameState.currentPreparedCard - - prepareMove = { move -> dispatch(RequestPrepareMove(move)) } - unprepareMove = { dispatch(RequestUnprepareMove()) } - sayReady = { dispatch(RequestSayReady()) } - leaveGame = { dispatch(RequestLeaveGame()) } - } - } - } -} - -private val GameScenePresenter = FC<GameSceneProps>("GameScenePresenter") { props -> - var transactionSelectorState by useState<TransactionSelectorState>() - - val game = props.game - val board = game.getOwnBoard() - div { - val maybeRed = GameStyles.pulsatingRed.takeIf { game.everyoneIsWaitingForMe() } - css(maybeRed) { - height = 100.pct - } - val action = game.action - if (action is TurnAction.WatchScore) { - scoreTableOverlay(action.scoreBoard, props.players, props.leaveGame) - } - actionInfo(game.action.message) - BoardComponent { - this.board = board - css { - padding = Padding(vertical = 7.rem, horizontal = 7.rem) // to fit the action info message & board summaries - width = 100.pct - height = 100.pct - } - } - transactionsSelectorDialog( - state = transactionSelectorState, - neighbours = playerNeighbours(props.currentPlayer, props.players), - prepareMove = { move -> - props.prepareMove(move) - transactionSelectorState = null - }, - cancelTransactionSelection = { transactionSelectorState = null }, - ) - boardSummaries(game) - handRotationIndicator(game.handRotationDirection) - handCards( - game = game, - prepareMove = props.prepareMove, - startTransactionsSelection = { - transactionSelectorState = it - } - ) - val card = props.preparedCard - val move = props.preparedMove - if (card != null && move != null) { - BpOverlay { - isOpen = true - onClose = { props.unprepareMove() } - - preparedMove(card, move, props.unprepareMove) { - css(GlobalStyles.fixedCenter) {} - } - } - } - if (game.action is TurnAction.SayReady) { - SayReadyButton { - currentPlayer = props.currentPlayer - players = props.players - sayReady = props.sayReady - } - } - } -} - -private fun GameState.everyoneIsWaitingForMe(): Boolean { - val onlyMeInTheGame = players.count { it.isHuman } == 1 - if (onlyMeInTheGame || currentPreparedMove != null) { - return false - } - return preparedCardsByUsername.values.count { it != null } == players.size - 1 -} - -private fun playerNeighbours(currentPlayer: PlayerDTO?, players: List<PlayerDTO>): Pair<PlayerDTO, PlayerDTO> { - val me = currentPlayer?.username ?: error("we shouldn't be trying to display this if there is no player") - val size = players.size - val myIndex = players.indexOfFirst { it.username == me } - return players[(myIndex - 1 + size) % size] to players[(myIndex + 1) % size] -} - -private fun ChildrenBuilder.actionInfo(message: String) { - div { - css(ClassName(Classes.DARK)) { - position = Position.fixed - top = 0.pct - left = 0.pct - margin = Margin(vertical = 0.4.rem, horizontal = 0.4.rem) - maxWidth = 25.pct // leave space for 4 board summaries when there are 7 players - } - BpCard { - elevation = Elevation.TWO - css { - padding = Padding(all = 0.px) - } - - BpCallout { - intent = Intent.PRIMARY - icon = IconNames.INFO_SIGN - +message - } - } - } -} - -private fun ChildrenBuilder.boardSummaries(game: GameState) { - val leftBoard = game.getBoard(RelativeBoardPosition.LEFT) - val rightBoard = game.getBoard(RelativeBoardPosition.RIGHT) - val topBoards = game.getNonNeighbourBoards().reversed() - selfBoardSummary(game.getOwnBoard(), game.players) - leftPlayerBoardSummary(leftBoard, game.players) - rightPlayerBoardSummary(rightBoard, game.players) - if (topBoards.isNotEmpty()) { - topPlayerBoardsSummaries(topBoards, game.players) - } -} - -private fun ChildrenBuilder.leftPlayerBoardSummary(board: Board, players: List<PlayerDTO>) { - div { - css { - position = Position.absolute - left = 0.px - bottom = 40.pct - } - BoardSummaryWithPopover { - this.player = players[board.playerIndex] - this.board = board - this.side = BoardSummarySide.LEFT - - css { - borderTopRightRadius = 0.4.rem - borderBottomRightRadius = 0.4.rem - } - } - } -} - -private fun ChildrenBuilder.rightPlayerBoardSummary(board: Board, players: List<PlayerDTO>) { - div { - css { - position = Position.absolute - right = 0.px - bottom = 40.pct - } - BoardSummaryWithPopover { - this.player = players[board.playerIndex] - this.board = board - this.side = BoardSummarySide.RIGHT - - css { - borderTopLeftRadius = 0.4.rem - borderBottomLeftRadius = 0.4.rem - } - } - } -} - -private fun ChildrenBuilder.topPlayerBoardsSummaries(boards: List<Board>, players: List<PlayerDTO>) { - div { - css { - position = Position.absolute - top = 0.px - left = 50.pct - transform = translate((-50).pct) - display = Display.flex - flexDirection = FlexDirection.row - } - boards.forEach { board -> - BoardSummaryWithPopover { - this.player = players[board.playerIndex] - this.board = board - this.side = BoardSummarySide.TOP - - css { - borderBottomLeftRadius = 0.4.rem - borderBottomRightRadius = 0.4.rem - margin = Margin(vertical = 0.rem, horizontal = 2.rem) - } - } - } - } -} - -private fun ChildrenBuilder.selfBoardSummary(board: Board, players: List<PlayerDTO>) { - div { - css { - position = Position.absolute - bottom = 0.px - left = 0.px - } - BoardSummary { - this.player = players[board.playerIndex] - this.board = board - this.side = BoardSummarySide.BOTTOM - this.showPreparationStatus = false - - css { - borderTopLeftRadius = 0.4.rem - borderTopRightRadius = 0.4.rem - margin = Margin(vertical = 0.rem, horizontal = 2.rem) - } - } - } -} - -private external interface SayReadyButtonProps : Props { - var currentPlayer: PlayerDTO? - var players: List<PlayerDTO> - var sayReady: () -> Unit -} - -private val SayReadyButton = FC<SayReadyButtonProps>("SayReadyButton") { props -> - val isReady = props.currentPlayer?.isReady == true - val intent = if (isReady) Intent.SUCCESS else Intent.PRIMARY - div { - css { - position = Position.absolute - bottom = 6.rem - left = 50.pct - transform = translate(tx = (-50).pct) - zIndex = integer(2) // go above the wonder (1) and wonder-upgrade cards (0) - } - BpButtonGroup { - BpButton { - this.large = true - this.disabled = isReady - this.intent = intent - this.icon = if (isReady) IconNames.TICK_CIRCLE else IconNames.PLAY - this.onClick = { props.sayReady() } - - +"READY" - } - // not really a button, but nice for style - BpButton { - this.large = true - this.icon = IconNames.PEOPLE - this.disabled = isReady - this.intent = intent - - +"${props.players.count { it.isReady }}/${props.players.size}" - } - } - } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameStyles.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameStyles.kt deleted file mode 100644 index f5ec475e..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameStyles.kt +++ /dev/null @@ -1,86 +0,0 @@ -package org.luxons.sevenwonders.ui.components.game - -import emotion.css.* -import org.luxons.sevenwonders.ui.utils.* -import web.cssom.* - -object GameStyles { - - val totalScore = ClassName { - fontWeight = FontWeight.bold - } - - val civilScore = scoreTagColorCss(Color("#2a73c9")) - val scienceScore = scoreTagColorCss(Color("#0f9960")) - val militaryScore = scoreTagColorCss(Color("#d03232")) - val tradeScore = scoreTagColorCss(Color("#e2c11b")) - val guildScore = scoreTagColorCss(Color("#663399")) - val wonderScore = scoreTagColorCss(NamedColor.darkcyan) - val goldScore = scoreTagColorCss(NamedColor.goldenrod) - - val sandBgColor = NamedColor.palegoldenrod - - - val fullBoardPreview = ClassName { - width = 40.vw - height = 50.vh - } - - val dimmedCard = ClassName { - filter = brightness(60.pct) + grayscale(50.pct) - } - - val transactionsSelector = ClassName { - backgroundColor = sandBgColor - width = 40.rem // default is 500px, we want to fit players on the side - - children(".bp4-dialog-header") { - background = None.none // overrides default white background - } - } - - val bestPrice = ClassName { - fontWeight = FontWeight.bold - color = rgb(50, 120, 50) - transform = rotate((-20).deg) - } - - val discardMoveText = ClassName { - display = Display.flex - alignItems = AlignItems.center - height = 3.rem - fontSize = 2.rem - color = NamedColor.goldenrod - fontWeight = FontWeight.bold - borderTop = Border(0.2.rem, LineStyle.solid, NamedColor.goldenrod) - borderBottom = Border(0.2.rem, LineStyle.solid, NamedColor.goldenrod) - } - - val scoreBoard = ClassName { - backgroundColor = sandBgColor - } - - private fun scoreTagColorCss(color: Color) = ClassName { - backgroundColor = color - } - - val pulsatingRed = ClassName { - animation = Animation( - name = keyframes { - to { - boxShadow = BoxShadow( - inset = BoxShadowInset.inset, - offsetX = 0.px, - offsetY = 0.px, - blurRadius = 20.px, - spreadRadius = 8.px, - color = NamedColor.red, - ) - } - }, - duration = 2.s, - ) - animationIterationCount = AnimationIterationCount.infinite - animationDirection = AnimationDirection.alternate - } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt deleted file mode 100644 index da71ea0b..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt +++ /dev/null @@ -1,276 +0,0 @@ -package org.luxons.sevenwonders.ui.components.game - -import blueprintjs.core.* -import blueprintjs.icons.* -import csstype.* -import emotion.react.* -import org.luxons.sevenwonders.client.* -import org.luxons.sevenwonders.model.* -import org.luxons.sevenwonders.model.boards.* -import org.luxons.sevenwonders.model.cards.* -import org.luxons.sevenwonders.model.resources.* -import org.luxons.sevenwonders.model.wonders.* -import org.luxons.sevenwonders.ui.utils.* -import react.* -import react.dom.html.ReactHTML.div -import web.cssom.* -import web.cssom.Position -import kotlin.math.* - -fun ChildrenBuilder.handCards( - game: GameState, - prepareMove: (PlayerMove) -> Unit, - startTransactionsSelection: (TransactionSelectorState) -> Unit, -) { - HandCards { - this.action = game.action - this.ownBoard = game.getOwnBoard() - this.preparedMove = game.currentPreparedMove - this.prepareMove = { moveType: MoveType, card: HandCard, transactionOptions: ResourceTransactionOptions -> - when (transactionOptions.size) { - 1 -> prepareMove(PlayerMove(moveType, card.name, transactionOptions.single())) - else -> startTransactionsSelection(TransactionSelectorState(moveType, card, transactionOptions)) - } - } - } -} - -private enum class HandAction( - val buttonTitle: String, - val moveType: MoveType, - val icon: IconName, -) { - PLAY("PLAY", MoveType.PLAY, "play"), - PLAY_FREE("Play as this age's free card", MoveType.PLAY_FREE, "star"), - PLAY_FREE_DISCARDED("Play discarded card", MoveType.PLAY_FREE_DISCARDED, "star"), - COPY_GUILD("Copy this guild card", MoveType.COPY_GUILD, "duplicate") -} - -external interface HandCardsProps : Props { - var action: TurnAction - var ownBoard: Board - var preparedMove: PlayerMove? - var prepareMove: (MoveType, HandCard, ResourceTransactionOptions) -> Unit -} - -private val HandCards = FC<HandCardsProps>("HandCards") { props -> - val hand = props.action.cardsToPlay() ?: return@FC - div { - css { - handStyle() - } - hand.filter { it.name != props.preparedMove?.cardName }.forEachIndexed { index, c -> - HandCard { - card = c - action = props.action - ownBoard = props.ownBoard - prepareMove = props.prepareMove - key = index.toString() - } - } - } -} - -private fun TurnAction.cardsToPlay(): List<HandCard>? = when (this) { - is TurnAction.PlayFromHand -> hand - is TurnAction.PlayFromDiscarded -> discardedCards - is TurnAction.PickNeighbourGuild -> neighbourGuildCards - is TurnAction.SayReady, - is TurnAction.Wait, - is TurnAction.WatchScore -> null -} - -private external interface HandCardProps : Props { - var card: HandCard - var action: TurnAction - var ownBoard: Board - var prepareMove: (MoveType, HandCard, ResourceTransactionOptions) -> Unit -} - -private val HandCard = FC<HandCardProps>("HandCard") { props -> - div { - css(ClassName("hand-card")) { - alignItems = AlignItems.flexEnd - display = Display.grid - margin = Margin(all = 0.2.rem) - } - CardImage { - css { - val isPlayable = props.card.playability.isPlayable || props.ownBoard.canPlayAnyCardForFree - handCardImgStyle(isPlayable) - } - this.card = props.card - } - actionButtons(props.card, props.action, props.ownBoard, props.prepareMove) - } -} - -private fun ChildrenBuilder.actionButtons( - card: HandCard, - action: TurnAction, - ownBoard: Board, - prepareMove: (MoveType, HandCard, ResourceTransactionOptions) -> Unit, -) { - div { - css { - justifyContent = JustifyContent.center - alignItems = AlignItems.flexEnd - display = None.none - gridRow = integer(1) - gridColumn = integer(1) - - ancestorHover(".hand-card") { - display = Display.flex - } - } - BpButtonGroup { - when (action) { - is TurnAction.PlayFromHand -> { - playCardButton(card, HandAction.PLAY, prepareMove) - if (ownBoard.canPlayAnyCardForFree) { - playCardButton(card.copy(playability = CardPlayability.SPECIAL_FREE), HandAction.PLAY_FREE, prepareMove) - } - } - is TurnAction.PlayFromDiscarded -> playCardButton(card, HandAction.PLAY_FREE_DISCARDED, prepareMove) - is TurnAction.PickNeighbourGuild -> playCardButton(card, HandAction.COPY_GUILD, prepareMove) - is TurnAction.SayReady, - is TurnAction.Wait, - is TurnAction.WatchScore -> error("unsupported action in hand card: $action") - } - - if (action.allowsBuildingWonder()) { - upgradeWonderButton(card, ownBoard.wonder.buildability, prepareMove) - } - if (action.allowsDiscarding()) { - discardButton(card, prepareMove) - } - } - } -} - -private fun ChildrenBuilder.playCardButton( - card: HandCard, - handAction: HandAction, - prepareMove: (MoveType, HandCard, ResourceTransactionOptions) -> Unit, -) { - BpButton { - title = "${handAction.buttonTitle} (${cardPlayabilityInfo(card.playability)})" - large = true - intent = Intent.SUCCESS - disabled = !card.playability.isPlayable - onClick = { prepareMove(handAction.moveType, card, card.playability.transactionOptions) } - - BpIcon { icon = handAction.icon } - - if (card.playability.isPlayable && !card.playability.isFree) { - priceInfo(card.playability.minPrice) - } - } -} - -private fun ChildrenBuilder.upgradeWonderButton( - card: HandCard, - wonderBuildability: WonderBuildability, - prepareMove: (MoveType, HandCard, ResourceTransactionOptions) -> Unit, -) { - BpButton { - title = "UPGRADE WONDER (${wonderBuildabilityInfo(wonderBuildability)})" - large = true - intent = Intent.PRIMARY - disabled = !wonderBuildability.isBuildable - onClick = { prepareMove(MoveType.UPGRADE_WONDER, card, wonderBuildability.transactionsOptions) } - - BpIcon { icon = IconNames.KEY_SHIFT } - if (wonderBuildability.isBuildable && !wonderBuildability.isFree) { - priceInfo(wonderBuildability.minPrice) - } - } -} - -private fun ChildrenBuilder.discardButton(card: HandCard, prepareMove: (MoveType, HandCard, ResourceTransactionOptions) -> Unit) { - BpButton { - title = "DISCARD (+3 coins)" // TODO remove hardcoded value - large = true - intent = Intent.DANGER - icon = IconNames.CROSS - onClick = { prepareMove(MoveType.DISCARD, card, singleOptionNoTransactionNeeded()) } - } -} - -private fun cardPlayabilityInfo(playability: CardPlayability) = when (playability.isPlayable) { - true -> priceText(-playability.minPrice) - false -> playability.playabilityLevel.message -} - -private fun wonderBuildabilityInfo(buildability: WonderBuildability) = when (buildability.isBuildable) { - true -> priceText(-buildability.minPrice) - false -> buildability.playabilityLevel.message -} - -private fun priceText(amount: Int) = when (amount.absoluteValue) { - 0 -> "free" - 1 -> "${pricePrefix(amount)}$amount coin" - else -> "${pricePrefix(amount)}$amount coins" -} - -private fun pricePrefix(amount: Int) = when { - amount > 0 -> "+" - else -> "" -} - -private fun ChildrenBuilder.priceInfo(amount: Int) { - goldIndicator( - amount = amount, - amountPosition = TokenCountPosition.OVER, - imgSize = 1.rem, - customCountStyle = { - fontFamily = FontFamily.sansSerif - fontSize = 0.8.rem - }, - ) { - css { - position = Position.absolute - top = (-0.2).rem - left = (-0.2).rem - } - } -} - -private fun PropertiesBuilder.handStyle() { - alignItems = AlignItems.center - bottom = 0.px - display = Display.flex - height = 345.px - left = 50.pct - maxHeight = 25.vw - position = Position.absolute - transform = translate(tx = (-50).pct, ty = 65.pct) - transition = Transition(TransitionProperty.all, duration = 0.5.s, timingFunction = TransitionTimingFunction.ease) - zIndex = integer(30) - - hover { - bottom = 1.rem - zIndex = integer(60) - transform = translate(tx = (-50).pct, ty = 0.pct) - } -} - -private fun PropertiesBuilder.handCardImgStyle(isPlayable: Boolean) { - gridRow = integer(1) - gridColumn = integer(1) - maxWidth = 13.vw - maxHeight = 60.vh - transition = Transition(TransitionProperty.all, duration = 0.1.s, timingFunction = TransitionTimingFunction.ease) - width = 11.rem - - ancestorHover(".hand-card") { - boxShadow = BoxShadow(offsetX = 0.px, offsetY = 10.px, blurRadius = 40.px, color = NamedColor.black) - width = 14.rem - maxWidth = 15.vw - maxHeight = 90.vh - } - - if (!isPlayable) { - filter = grayscale(50.pct) + contrast(50.pct) - } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/HandRotationIndicator.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/HandRotationIndicator.kt deleted file mode 100644 index 72cb6b65..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/HandRotationIndicator.kt +++ /dev/null @@ -1,56 +0,0 @@ -package org.luxons.sevenwonders.ui.components.game - -import blueprintjs.core.* -import blueprintjs.icons.* -import csstype.* -import emotion.react.* -import org.luxons.sevenwonders.model.cards.* -import react.* -import react.dom.html.ReactHTML.div -import react.dom.html.ReactHTML.img -import web.cssom.* -import web.cssom.Position - -fun ChildrenBuilder.handRotationIndicator(direction: HandRotationDirection) { - div { - css { - position = Position.absolute - display = Display.flex - alignItems = AlignItems.center - bottom = 25.vh - val sideDistance = 2.rem - when (direction) { - HandRotationDirection.LEFT -> left = sideDistance - HandRotationDirection.RIGHT -> right = sideDistance - } - } - - title = "Your hand will be passed to the player on your $direction after playing this card." - - when (direction) { - HandRotationDirection.LEFT -> { - BpIcon { - icon = IconNames.ARROW_LEFT - size = 25 - } - handCardsImg() - } - HandRotationDirection.RIGHT -> { - handCardsImg() - BpIcon { - icon = IconNames.ARROW_RIGHT - size = 25 - } - } - } - } -} - -private fun ChildrenBuilder.handCardsImg() { - img { - src = "images/hand-cards5.png" - css { - width = 4.rem - } - } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PlayerPreparedCardPresenter.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PlayerPreparedCardPresenter.kt deleted file mode 100644 index 627693e1..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PlayerPreparedCardPresenter.kt +++ /dev/null @@ -1,80 +0,0 @@ -package org.luxons.sevenwonders.ui.components.game - -import csstype.* -import emotion.css.* -import emotion.react.* -import org.luxons.sevenwonders.model.cards.* -import org.luxons.sevenwonders.ui.redux.* -import org.luxons.sevenwonders.ui.utils.* -import react.* -import react.dom.html.ReactHTML.div -import react.dom.html.ReactHTML.img -import web.cssom.* - -external interface PlayerPreparedCardProps : Props { - var playerDisplayName: String - var username: String -} - -val PlayerPreparedCard = FC<PlayerPreparedCardProps>("PlayerPreparedCard") { props -> - val cardBack = useSwSelector { it.gameState?.preparedCardsByUsername?.get(props.username) } - - PlayerPreparedCardPresenter { - this.playerDisplayName = props.playerDisplayName - this.cardBack = cardBack - } -} - -external interface PlayerPreparedCardPresenterProps : Props { - var playerDisplayName: String - var cardBack: CardBack? -} - -private val PlayerPreparedCardPresenter = FC<PlayerPreparedCardPresenterProps>("PlayerPreparedCardPresenter") { props -> - val cardBack = props.cardBack - val sideSize = 30.px - div { - css { - width = sideSize - height = sideSize - } - title = if (cardBack == null) { - "${props.playerDisplayName} is still thinking…" - } else { - "${props.playerDisplayName} is ready to play this turn" - } - - if (cardBack != null) { - CardBackImage { - this.cardBack = cardBack - css { - maxHeight = sideSize - } - } - } else { - RotatingGear { - css { - maxHeight = sideSize - } - } - } - } -} - -private val RotatingGear = FC<PropsWithClassName> { props -> - img { - src = "images/gear-50.png" - css(props.className) { - animation = Animation( - name = keyframes { - to { - transform = rotate(360.deg) - } - }, - duration = 1.5.s, - timingFunction = cubicBezier(0.2, 0.9, 0.7, 1.3), - ) - animationIterationCount = AnimationIterationCount.infinite - } - } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PreparedMove.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PreparedMove.kt deleted file mode 100644 index 3ecdc741..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PreparedMove.kt +++ /dev/null @@ -1,73 +0,0 @@ -package org.luxons.sevenwonders.ui.components.game - -import blueprintjs.core.* -import blueprintjs.icons.* -import csstype.* -import emotion.react.* -import org.luxons.sevenwonders.model.* -import org.luxons.sevenwonders.model.cards.* -import org.luxons.sevenwonders.ui.components.* -import react.* -import react.dom.html.* -import react.dom.html.ReactHTML.div -import react.dom.html.ReactHTML.img -import web.cssom.* -import web.cssom.Position -import web.html.* - -fun ChildrenBuilder.preparedMove( - card: HandCard, - move: PlayerMove, - unprepareMove: () -> Unit, - block: HTMLAttributes<HTMLDivElement>.() -> Unit, -) { - div { - block() - CardImage { - this.card = card - if (move.type == MoveType.DISCARD || move.type == MoveType.UPGRADE_WONDER) { - this.className = GameStyles.dimmedCard - } - } - if (move.type == MoveType.DISCARD) { - discardText() - } - if (move.type == MoveType.UPGRADE_WONDER) { - upgradeWonderSymbol() - } - div { - css { - position = web.cssom.Position.absolute - top = 0.px - right = 0.px - } - BpButton { - icon = IconNames.CROSS - title = "Cancel prepared move" - small = true - intent = Intent.DANGER - onClick = { unprepareMove() } - } - } - } -} - -private fun ChildrenBuilder.discardText() { - div { - css(GlobalStyles.centerInPositionedParent, GameStyles.discardMoveText) {} - +"DISCARD" - } -} - -private fun ChildrenBuilder.upgradeWonderSymbol() { - img { - src = "/images/wonder-upgrade-bright.png" - css { - width = 8.rem - position = Position.absolute - left = 10.pct - top = 50.pct - transform = translate((-50).pct, (-50).pct) - } - } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/ScoreTable.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/ScoreTable.kt deleted file mode 100644 index cd54446f..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/ScoreTable.kt +++ /dev/null @@ -1,188 +0,0 @@ -package org.luxons.sevenwonders.ui.components.game - -import blueprintjs.core.* -import blueprintjs.icons.* -import csstype.* -import emotion.react.* -import org.luxons.sevenwonders.model.api.* -import org.luxons.sevenwonders.model.score.* -import org.luxons.sevenwonders.ui.components.* -import org.luxons.sevenwonders.ui.utils.* -import react.* -import react.dom.html.* -import react.dom.html.ReactHTML.div -import react.dom.html.ReactHTML.h1 -import react.dom.html.ReactHTML.sup -import react.dom.html.ReactHTML.tbody -import react.dom.html.ReactHTML.td -import react.dom.html.ReactHTML.th -import react.dom.html.ReactHTML.thead -import react.dom.html.ReactHTML.tr -import web.cssom.* - -fun ChildrenBuilder.scoreTableOverlay(scoreBoard: ScoreBoard, players: List<PlayerDTO>, leaveGame: () -> Unit) { - BpOverlay { - isOpen = true - - BpCard { - css(GlobalStyles.fixedCenter, GameStyles.scoreBoard) {} - - div { - // FIXME this doesn't look right, the scoreBoard class is applied at both levels - css(GameStyles.scoreBoard) { // loads the styles so that they can be picked up by bpCard - display = Display.flex - flexDirection = FlexDirection.column - alignItems = AlignItems.center - } - h1 { - css { - marginTop = 0.px - } - +"Score Board" - } - scoreTable(scoreBoard, players) - div { - css { - marginTop = 1.rem - } - BpButton { - intent = Intent.WARNING - rightIcon = "log-out" - large = true - onClick = { leaveGame() } - - +"LEAVE" - } - } - } - } - } -} - -private fun ChildrenBuilder.scoreTable(scoreBoard: ScoreBoard, players: List<PlayerDTO>) { - BpHTMLTable { - bordered = false - interactive = true - - thead { - tr { - th { - fullCenterInlineStyle() - +"Rank" - } - th { - fullCenterInlineStyle() - colSpan = 2 - - +"Player" - } - th { - fullCenterInlineStyle() - +"Score" - } - ScoreCategory.values().forEach { - th { - fullCenterInlineStyle() - +it.title - } - } - } - } - tbody { - scoreBoard.scores.forEachIndexed { index, score -> - val player = players[score.playerIndex] - tr { - td { - fullCenterInlineStyle() - ordinal(scoreBoard.ranks[index]) - } - td { - fullCenterInlineStyle() - BpIcon { - icon = player.icon?.name ?: IconNames.USER - size = 25 - } - } - td { - inlineStyles { - verticalAlign = VerticalAlign.middle - } - +player.displayName - } - td { - fullCenterInlineStyle() - BpTag { - large = true - round = true - minimal = true - className = GameStyles.totalScore - - +"${score.totalPoints}" - } - } - ScoreCategory.values().forEach { cat -> - td { - fullCenterInlineStyle() - BpTag { - large = true - round = true - fill = true - icon = cat.icon - className = classNameForCategory(cat) - - +"${score.pointsByCategory[cat]}" - } - } - } - } - } - } - } -} - -private fun ChildrenBuilder.ordinal(value: Int) { - +"$value" - sup { +value.ordinalIndicator() } -} - -private fun Int.ordinalIndicator() = when { - this % 10 == 1 && this != 11 -> "st" - this % 10 == 2 && this != 12 -> "nd" - this % 10 == 3 && this != 13 -> "rd" - else -> "th" -} - -private fun HTMLAttributes<*>.fullCenterInlineStyle() { - // inline styles necessary to overcome blueprintJS overrides - inlineStyles { - textAlign = TextAlign.center - verticalAlign = VerticalAlign.middle - } -} - -private fun classNameForCategory(cat: ScoreCategory): ClassName = when (cat) { - ScoreCategory.CIVIL -> GameStyles.civilScore - ScoreCategory.SCIENCE -> GameStyles.scienceScore - ScoreCategory.MILITARY -> GameStyles.militaryScore - ScoreCategory.TRADE -> GameStyles.tradeScore - ScoreCategory.GUILD -> GameStyles.guildScore - ScoreCategory.WONDER -> GameStyles.wonderScore - ScoreCategory.GOLD -> GameStyles.goldScore -} - -private val ScoreCategory.icon: String - get() = when (this) { - ScoreCategory.CIVIL -> IconNames.OFFICE - ScoreCategory.SCIENCE -> IconNames.LAB_TEST - ScoreCategory.MILITARY -> IconNames.CUT - ScoreCategory.TRADE -> IconNames.SWAP_HORIZONTAL - ScoreCategory.GUILD -> IconNames.CLEAN // stars - ScoreCategory.WONDER -> IconNames.SYMBOL_TRIANGLE_UP - ScoreCategory.GOLD -> IconNames.DOLLAR - } - -// Potentially useful emojis: -// Greek temple: 🏛 -// Cog (science): ⚙️ -// Swords (war): ⚔️ -// Gold bag: 💰 diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Tokens.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Tokens.kt deleted file mode 100644 index 01975f7e..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Tokens.kt +++ /dev/null @@ -1,155 +0,0 @@ -package org.luxons.sevenwonders.ui.components.game - -import csstype.* -import emotion.react.* -import org.luxons.sevenwonders.model.resources.* -import org.luxons.sevenwonders.ui.components.* -import react.* -import react.dom.html.* -import react.dom.html.ReactHTML.div -import react.dom.html.ReactHTML.img -import react.dom.html.ReactHTML.span -import web.cssom.* -import web.html.* - -private fun getResourceTokenName(resourceType: ResourceType) = "resources/${resourceType.toString().lowercase()}" - -private fun getTokenImagePath(tokenName: String) = "/images/tokens/$tokenName.png" - -enum class TokenCountPosition { - LEFT, - RIGHT, - OVER, -} - -fun ChildrenBuilder.goldIndicator( - amount: Int, - amountPosition: TokenCountPosition = TokenCountPosition.OVER, - imgSize: Length = 3.rem, - customCountStyle: PropertiesBuilder.() -> Unit = {}, - block: HTMLAttributes<HTMLDivElement>.() -> Unit = {}, -) { - tokenWithCount( - tokenName = "coin", - title = "$amount gold coins", - imgSize = imgSize, - count = amount, - countPosition = amountPosition, - customCountStyle = customCountStyle, - block = block, - ) -} - -fun ChildrenBuilder.resourceImage( - resourceType: ResourceType, - title: String = resourceType.toString(), - size: Length?, -) { - TokenImage { - this.tokenName = getResourceTokenName(resourceType) - this.title = title - this.size = size - } -} - -fun ChildrenBuilder.tokenWithCount( - tokenName: String, - count: Int, - title: String = tokenName, - imgSize: Length? = null, - countPosition: TokenCountPosition = TokenCountPosition.RIGHT, - brightText: Boolean = false, - customCountStyle: PropertiesBuilder.() -> Unit = {}, - block: HTMLAttributes<HTMLDivElement>.() -> Unit = {}, -) { - div { - block() - val tokenCountSize = if (imgSize != null) 0.6 * imgSize else 1.5.rem - when (countPosition) { - TokenCountPosition.RIGHT -> { - TokenImage { - this.tokenName = tokenName - this.title = title - this.size = imgSize - } - span { - css { - tokenCountStyle(tokenCountSize, brightText, customCountStyle) - marginLeft = 0.2.rem - } - +"× $count" - } - } - - TokenCountPosition.LEFT -> { - span { - css { - tokenCountStyle(tokenCountSize, brightText, customCountStyle) - marginRight = 0.2.rem - } - +"$count ×" - } - TokenImage { - this.tokenName = tokenName - this.title = title - this.size = imgSize - } - } - - TokenCountPosition.OVER -> { - div { - css { - position = Position.relative - // if container becomes large, this one stays small so that children stay on top of each other - width = Length.fitContent - } - TokenImage { - this.tokenName = tokenName - this.title = title - this.size = imgSize - } - span { - css(GlobalStyles.centerInPositionedParent) { - tokenCountStyle(tokenCountSize, brightText, customCountStyle) - } - +"$count" - } - } - } - } - } -} - -external interface TokenImageProps : Props { - var tokenName: String - var title: String? - var size: Length? -} - -val TokenImage = FC<TokenImageProps> { props -> - img { - src = getTokenImagePath(props.tokenName) - title = props.title ?: props.tokenName - alt = props.tokenName - - css { - height = props.size ?: 100.pct - if (props.size != null) { - width = props.size - } - verticalAlign = VerticalAlign.middle - } - } -} - -private fun PropertiesBuilder.tokenCountStyle( - size: Length, - brightText: Boolean, - customStyle: PropertiesBuilder.() -> Unit = {}, -) { - fontFamily = string("Acme") - fontSize = size - verticalAlign = VerticalAlign.middle - color = if (brightText) NamedColor.white else NamedColor.black - customStyle() -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/TransactionsSelector.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/TransactionsSelector.kt deleted file mode 100644 index cdf97ad9..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/TransactionsSelector.kt +++ /dev/null @@ -1,265 +0,0 @@ -package org.luxons.sevenwonders.ui.components.game - -import blueprintjs.core.* -import blueprintjs.icons.* -import csstype.* -import emotion.react.* -import org.luxons.sevenwonders.model.* -import org.luxons.sevenwonders.model.api.* -import org.luxons.sevenwonders.model.resources.* -import org.luxons.sevenwonders.model.resources.Provider -import org.luxons.sevenwonders.ui.components.gameBrowser.* -import org.luxons.sevenwonders.ui.utils.* -import org.luxons.sevenwonders.ui.utils.Margin -import react.* -import react.dom.html.* -import react.dom.html.ReactHTML.div -import react.dom.html.ReactHTML.p -import react.dom.html.ReactHTML.tbody -import react.dom.html.ReactHTML.td -import react.dom.html.ReactHTML.tr -import web.cssom.* -import web.html.* - -fun ChildrenBuilder.transactionsSelectorDialog( - state: TransactionSelectorState?, - neighbours: Pair<PlayerDTO, PlayerDTO>, - prepareMove: (PlayerMove) -> Unit, - cancelTransactionSelection: () -> Unit, -) { - BpDialog { - isOpen = state != null - titleText = "Trading time!" - canEscapeKeyClose = true - canOutsideClickClose = true - isCloseButtonShown = true - onClose = { cancelTransactionSelection() } - - className = GameStyles.transactionsSelector - - BpDialogBody { - p { - +"You don't have enough resources to perform this move, but you can buy them from neighbours. " - +"Please pick an option:" - } - if (state != null) { // should always be true when the dialog is rendered - div { - css { - margin = Margin(all = Auto.auto) - display = Display.flex - alignItems = AlignItems.center - } - neighbour(neighbours.first) - div { - css { - flexGrow = number(1.0) - margin = Margin(vertical = 0.rem, horizontal = 0.5.rem) - display = Display.flex - flexDirection = FlexDirection.column - alignItems = AlignItems.center - } - OptionsTable { - this.state = state - this.prepareMove = prepareMove - } - } - neighbour(neighbours.second) - } - } - } - } -} - -private fun ChildrenBuilder.neighbour(player: PlayerDTO) { - div { - css { - width = 12.rem - - // center the icon - display = Display.flex - flexDirection = FlexDirection.column - alignItems = AlignItems.center - } - PlayerInfo { - this.player = player - this.iconSize = 40 - this.orientation = FlexDirection.column - this.ellipsize = false - } - } -} - -private external interface OptionsTableProps : PropsWithChildren { - var state: TransactionSelectorState - var prepareMove: (PlayerMove) -> Unit -} - -private val OptionsTable = FC<OptionsTableProps> { props -> - val state = props.state - val prepareMove = props.prepareMove - - var expanded by useState { false } - - val bestPrice = state.transactionsOptions.bestPrice - val (cheapestOptions, otherOptions) = state.transactionsOptions.partition { it.totalPrice == bestPrice } - - BpHTMLTable { - interactive = true - tbody { - cheapestOptions.forEach { transactions -> - transactionsOptionRow( - transactions = transactions, - showBestPriceIndicator = expanded, - onClick = { prepareMove(PlayerMove(state.moveType, state.card.name, transactions)) }, - ) - } - if (expanded) { - otherOptions.forEach { transactions -> - transactionsOptionRow( - transactions = transactions, - showBestPriceIndicator = false, - onClick = { prepareMove(PlayerMove(state.moveType, state.card.name, transactions)) }, - ) - } - } - } - } - if (otherOptions.isNotEmpty()) { - val icon = if (expanded) "chevron-up" else "chevron-down" - val text = if (expanded) "Hide expensive options" else "Show more expensive options" - BpButton { - this.minimal = true - this.small = true - this.icon = icon - this.rightIcon = icon - this.onClick = { expanded = !expanded } - - +text - } - } -} - -private fun ChildrenBuilder.transactionsOptionRow( - transactions: PricedResourceTransactions, - showBestPriceIndicator: Boolean, - onClick: () -> Unit, -) { - tr { - css { - cursor = Cursor.pointer - alignItems = AlignItems.center - } - this.onClick = { onClick() } - // there should be at most one of each - val leftTr = transactions.firstOrNull { it.provider == Provider.LEFT_PLAYER } - val rightTr = transactions.firstOrNull { it.provider == Provider.RIGHT_PLAYER } - td { - transactionCellCss() - div { - css { opacity = number(if (leftTr == null) 0.5 else 1.0) } - transactionCellInnerCss() - BpIcon { - icon = IconNames.CARET_LEFT - size = IconSize.LARGE - } - goldIndicator(leftTr?.totalPrice ?: 0, imgSize = 2.5.rem) - } - } - td { - transactionCellCss() - if (leftTr != null) { - resourceList(leftTr.resources) - } - } - td { - transactionCellCss() - css { width = 1.5.rem } - if (showBestPriceIndicator) { - bestPriceIndicator() - } - } - td { - transactionCellCss() - if (rightTr != null) { - resourceList(rightTr.resources) - } - } - td { - transactionCellCss() - div { - css { opacity = number(if (rightTr == null) 0.5 else 1.0) } - transactionCellInnerCss() - goldIndicator(rightTr?.totalPrice ?: 0, imgSize = 2.5.rem) - BpIcon { - icon = IconNames.CARET_RIGHT - size = IconSize.LARGE - } - } - } - } -} - -private fun ChildrenBuilder.bestPriceIndicator() { - div { - css(GameStyles.bestPrice){} - +"Best\nprice!" - } -} - -private fun HTMLAttributes<HTMLTableCellElement>.transactionCellCss() { - // we need inline styles to win over BlueprintJS's styles (which are more specific than .class) - inlineStyles { - verticalAlign = VerticalAlign.middle - textAlign = TextAlign.center - } -} - -private fun HTMLAttributes<HTMLDivElement>.transactionCellInnerCss() { - css { - display = Display.flex - flexDirection = FlexDirection.row - alignItems = AlignItems.center - } -} - -private fun ChildrenBuilder.resourceList(countedResources: List<CountedResource>) { - val resources = countedResources.toRepeatedTypesList() - - // The biggest card is the Palace and requires 7 resources (1 of each). - // We always have at least 1 resource on our wonder, so we'll never need to buy more than 6. - // Therefore, 3 by row seems decent. When there are 4 items, it's visually better to have a 2x2 matrix, though. - val rows = resources.chunked(if (resources.size == 4) 2 else 3) - - val imgSize = 1.5 - div { - css { - display = Display.flex - flexDirection = FlexDirection.column - alignItems = AlignItems.center - justifyContent = JustifyContent.center - flexGrow = number(1.0) - // this ensures stable dimensions, no matter how many resources (up to 2x3 matrix) - width = (imgSize * 3).rem - height = (imgSize * 2).rem - } - rows.forEach { row -> - div { - resourceRowCss() - row.forEach { - resourceImage(it, size = imgSize.rem) - } - } - } - } -} - -private fun HTMLAttributes<HTMLDivElement>.resourceRowCss() { - css { - display = Display.flex - flexDirection = FlexDirection.row - alignItems = AlignItems.center - margin = Margin(vertical = 0.px, horizontal = Auto.auto) - } -} - -private fun List<CountedResource>.toRepeatedTypesList(): List<ResourceType> = flatMap { cr -> List(cr.count) { cr.type } } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/CreateGameForm.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/CreateGameForm.kt deleted file mode 100644 index e0f7dd21..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/CreateGameForm.kt +++ /dev/null @@ -1,58 +0,0 @@ -package org.luxons.sevenwonders.ui.components.gameBrowser - -import blueprintjs.core.* -import blueprintjs.icons.* -import emotion.react.* -import org.luxons.sevenwonders.ui.names.* -import org.luxons.sevenwonders.ui.redux.* -import react.* -import react.dom.html.ReactHTML.form -import web.cssom.* - -val CreateGameForm = FC { - var gameName by useState("") - - val dispatch = useSwDispatch() - val createGame = { dispatch(RequestCreateGame(gameName)) } - - form { - css { - display = Display.flex - flexDirection = FlexDirection.row - } - onSubmit = { e -> - e.preventDefault() - createGame() - } - - BpInputGroup { - large = true - placeholder = "Game name" - value = gameName - onChange = { e -> - val input = e.currentTarget - gameName = input.value - } - rightElement = BpButton.create { - title = "Generate random name" - icon = IconNames.RANDOM - minimal = true - onClick = { gameName = randomGameName() } - } - } - BpButton { - title = "Create the game" - intent = Intent.PRIMARY - icon = IconNames.ARROW_RIGHT - large = true - onClick = { e -> - e.preventDefault() // prevents refreshing the page when pressing Enter - createGame() - } - - css { - marginLeft = 0.2.rem - } - } - } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameBrowser.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameBrowser.kt deleted file mode 100644 index 10fb9d81..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameBrowser.kt +++ /dev/null @@ -1,69 +0,0 @@ -package org.luxons.sevenwonders.ui.components.gameBrowser - -import blueprintjs.core.* -import emotion.react.* -import org.luxons.sevenwonders.ui.components.* -import org.luxons.sevenwonders.ui.redux.* -import org.luxons.sevenwonders.ui.utils.* -import react.* -import react.dom.html.ReactHTML.div -import react.dom.html.ReactHTML.h1 -import react.dom.html.ReactHTML.h2 -import web.cssom.* - -val GameBrowser = FC { - div { - css(GlobalStyles.fullscreen, GlobalStyles.zeusBackground) { - padding = Padding(all = 1.rem) - } - div { - css(ClassName(Classes.DARK)) { - margin = Margin(vertical = 0.px, horizontal = Auto.auto) - maxWidth = GlobalStyles.preGameWidth - } - div { - css { - display = Display.flex - justifyContent = JustifyContent.spaceBetween - } - h1 { +"Games" } - CurrentPlayerInfo() - } - - BpCard { - css { - marginBottom = 1.rem - } - - h2 { - css { - marginTop = 0.px - } - +"Create a Game" - } - CreateGameForm() - } - - BpCard { - h2 { - css { - marginTop = 0.px - } - +"Join a Game" - } - GameList() - } - } - } -} - -val CurrentPlayerInfo = FC { - val connectedPlayer = useSwSelector { it.connectedPlayer } - PlayerInfo { - player = connectedPlayer - iconSize = 30 - showUsername = true - orientation = FlexDirection.row - ellipsize = false - } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameList.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameList.kt deleted file mode 100644 index 2919b065..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameList.kt +++ /dev/null @@ -1,213 +0,0 @@ -package org.luxons.sevenwonders.ui.components.gameBrowser - -import blueprintjs.core.* -import blueprintjs.icons.* -import csstype.* -import emotion.react.* -import org.luxons.sevenwonders.model.api.* -import org.luxons.sevenwonders.model.api.State -import org.luxons.sevenwonders.ui.redux.* -import org.luxons.sevenwonders.ui.utils.* -import react.* -import react.dom.html.ReactHTML.col -import react.dom.html.ReactHTML.colgroup -import react.dom.html.ReactHTML.div -import react.dom.html.ReactHTML.span -import react.dom.html.ReactHTML.tbody -import react.dom.html.ReactHTML.td -import react.dom.html.ReactHTML.th -import react.dom.html.ReactHTML.thead -import react.dom.html.ReactHTML.tr -import web.cssom.* -import react.State as RState - -external interface GameListStateProps : Props { - var connectedPlayer: ConnectedPlayer - var games: List<LobbyDTO> -} - -external interface GameListDispatchProps : Props { - var joinGame: (Long) -> Unit -} - -external interface GameListProps : GameListStateProps, GameListDispatchProps - -val GameList = connectStateAndDispatch<GameListStateProps, GameListDispatchProps, GameListProps>( - clazz = GameListPresenter::class, - mapStateToProps = { state, _ -> - connectedPlayer = state.connectedPlayer ?: error("there should be a connected player") - games = state.games - }, - mapDispatchToProps = { dispatch, _ -> - joinGame = { gameId -> dispatch(RequestJoinGame(gameId = gameId)) } - }, -) - -private class GameListPresenter(props: GameListProps) : Component<GameListProps, RState>(props) { - - override fun render() = Fragment.create { - if (props.games.isEmpty()) { - noGamesInfo() - } else { - gamesTable() - } - } - - private fun ChildrenBuilder.noGamesInfo() { - BpNonIdealState { - icon = IconNames.GEOSEARCH - titleText = "No games to join" - - div { - css(ClassName(Classes.RUNNING_TEXT)) { - maxWidth = 35.rem - } - +"Nobody seems to be playing at the moment. " - +"Don't be disappointed, you can always create your own game, and play with bots if you're alone." - } - } - } - - private fun ChildrenBuilder.gamesTable() { - BpHTMLTable { - css { - width = 100.pct - } - - columnWidthsSpec() - thead { - gameListHeaderRow() - } - tbody { - props.games.forEach { - gameListItemRow(it) - } - } - } - } - - private fun ChildrenBuilder.columnWidthsSpec() { - colgroup { - col { - css { - width = 40.rem - } - } - col { - css { - width = 5.rem - textAlign = TextAlign.center - } - } - col { - css { - width = 5.rem - textAlign = TextAlign.center // use inline style on th instead to overcome blueprint style - } - } - col { - css { - width = 3.rem - textAlign = TextAlign.center - } - } - } - } - - private fun ChildrenBuilder.gameListHeaderRow() = tr { - th { - +"Name" - } - th { - inlineStyles { gameTableHeaderCellStyle() } - +"Status" - } - th { - inlineStyles { gameTableHeaderCellStyle() } - +"Players" - } - th { - inlineStyles { gameTableHeaderCellStyle() } - +"Join" - } - } - - private fun ChildrenBuilder.gameListItemRow(lobby: LobbyDTO) = tr { - key = lobby.id.toString() - // inline styles necessary to overcome BlueprintJS's verticalAlign=top - td { - inlineStyles { gameTableCellStyle() } - +lobby.name - } - td { - inlineStyles { - textAlign = TextAlign.center - gameTableCellStyle() - } - gameStatus(lobby.state) - } - td { - inlineStyles { gameTableCellStyle() } - playerCount(lobby.players.size) - } - td { - inlineStyles { gameTableCellStyle() } - joinButton(lobby) - } - } - - private fun PropertiesBuilder.gameTableHeaderCellStyle() { - textAlign = TextAlign.center - } - - private fun PropertiesBuilder.gameTableCellStyle() { - verticalAlign = VerticalAlign.middle - } - - private fun ChildrenBuilder.gameStatus(state: State) { - val intent = when (state) { - State.LOBBY -> Intent.SUCCESS - State.PLAYING -> Intent.WARNING - State.FINISHED -> Intent.DANGER - } - BpTag { - this.minimal = true - this.intent = intent - - +state.toString() - } - } - - private fun ChildrenBuilder.playerCount(nPlayers: Int) { - div { - css { - display = Display.flex - flexDirection = FlexDirection.row - justifyContent = JustifyContent.center - } - title = "Number of players" - BpIcon { - icon = IconNames.PEOPLE - title = null - } - span { - css { - marginLeft = 0.3.rem - } - +nPlayers.toString() - } - } - } - - private fun ChildrenBuilder.joinButton(lobby: LobbyDTO) { - val joinability = lobby.joinability(props.connectedPlayer.displayName) - BpButton { - minimal = true - large = true - title = joinability.tooltip - icon = "arrow-right" - disabled = !joinability.canDo - onClick = { props.joinGame(lobby.id) } - } - } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/PlayerInfo.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/PlayerInfo.kt deleted file mode 100644 index d7a9a80f..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/PlayerInfo.kt +++ /dev/null @@ -1,105 +0,0 @@ -package org.luxons.sevenwonders.ui.components.gameBrowser - -import blueprintjs.core.* -import csstype.* -import emotion.react.* -import org.luxons.sevenwonders.model.api.* -import react.* -import react.State -import react.dom.html.ReactHTML.div -import react.dom.html.ReactHTML.span -import web.cssom.* - -external interface PlayerInfoProps : PropsWithChildren { - var player: BasicPlayerInfo? - var showUsername: Boolean? - var iconSize: Int? - var orientation: FlexDirection? - var ellipsize: Boolean? -} - -val PlayerInfo = PlayerInfoPresenter::class.react - -private class PlayerInfoPresenter(props: PlayerInfoProps) : Component<PlayerInfoProps, State>(props) { - - override fun render() = div.create { - val orientation = props.orientation ?: FlexDirection.row - css { - display = Display.flex - alignItems = AlignItems.center - flexDirection = orientation - } - props.player?.let { - BpIcon { - icon = it.icon?.name ?: "user" - size = props.iconSize ?: 30 - } - if (props.showUsername == true) { - playerNameWithUsername(it.displayName, it.username) { - iconSeparationMargin(orientation) - } - } else { - playerName(it.displayName) { - iconSeparationMargin(orientation) - } - } - } - } - - private fun ChildrenBuilder.playerName(displayName: String, style: PropertiesBuilder.() -> Unit = {}) { - span { - css { - fontSize = 1.rem - if (props.orientation == FlexDirection.column) { - textAlign = TextAlign.center - } - style() - } - // TODO replace by BlueprintJS's Text elements (built-in ellipsize based on width) - val maxDisplayNameLength = 15 - val ellipsize = props.ellipsize ?: true - if (ellipsize && displayName.length > maxDisplayNameLength) { - title = displayName - +displayName.ellipsize(maxDisplayNameLength) - } else { - +displayName - } - } - } - - private fun String.ellipsize(maxLength: Int) = take(maxLength - 1) + "…" - - private fun PropertiesBuilder.iconSeparationMargin(orientation: FlexDirection) { - val margin = 0.4.rem - when (orientation) { - FlexDirection.row -> marginLeft = margin - FlexDirection.column -> marginTop = margin - FlexDirection.rowReverse -> marginRight = margin - FlexDirection.columnReverse -> marginBottom = margin - else -> error("Unsupported orientation '$orientation' for player info component") - } - } - - private fun ChildrenBuilder.playerNameWithUsername( - displayName: String, - username: String, - style: PropertiesBuilder.() -> Unit = {} - ) { - div { - css { - display = Display.flex - flexDirection = FlexDirection.column - style() - } - playerName(displayName) - span { - css { - marginTop = 0.1.rem - color = NamedColor.lightgray - fontSize = 0.8.rem - } - +"($username)" - } - } - } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/ChooseNameForm.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/ChooseNameForm.kt deleted file mode 100644 index ba37c09d..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/ChooseNameForm.kt +++ /dev/null @@ -1,65 +0,0 @@ -package org.luxons.sevenwonders.ui.components.home - -import blueprintjs.core.* -import blueprintjs.icons.* -import emotion.react.* -import org.luxons.sevenwonders.ui.names.* -import org.luxons.sevenwonders.ui.redux.* -import react.* -import react.dom.html.ReactHTML.form -import web.cssom.* - -val ChooseNameForm = FC { - val dispatch = useSwDispatch() - ChooseNameFormPresenter { - chooseUsername = { name -> dispatch(RequestChooseName(name)) } - } -} - -private external interface ChooseNameFormPresenterProps : PropsWithChildren { - var chooseUsername: (String) -> Unit -} - -private val ChooseNameFormPresenter = FC<ChooseNameFormPresenterProps> { props -> - var usernameState by useState("") - - form { - css { - display = Display.flex - flexDirection = FlexDirection.row - } - onSubmit = { e -> - e.preventDefault() - props.chooseUsername(usernameState) - } - BpInputGroup { - large = true - placeholder = "Username" - value = usernameState - onChange = { e -> - val input = e.currentTarget - usernameState = input.value - } - rightElement = BpButton.create { - title = "Generate random name" - icon = IconNames.RANDOM - minimal = true - onClick = { usernameState = randomGreekName() } - } - } - BpButton { - title = "Start" - icon = IconNames.ARROW_RIGHT - intent = Intent.PRIMARY - large = true - onClick = { e -> - e.preventDefault() - props.chooseUsername(usernameState) - } - - css { - marginLeft = 0.2.rem - } - } - } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/Home.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/Home.kt deleted file mode 100644 index 81f4c736..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/Home.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.luxons.sevenwonders.ui.components.home - -import emotion.react.* -import org.luxons.sevenwonders.ui.components.* -import react.* -import react.dom.html.ReactHTML.div -import react.dom.html.ReactHTML.img - -private const val LOGO = "images/logo-7-wonders.png" - -val Home = FC("Home") { - div { - css(GlobalStyles.fullscreen, GlobalStyles.zeusBackground, HomeStyles.centerChildren) {} - - img { - src = LOGO - alt = "Seven Wonders" - } - - ChooseNameForm() - } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/HomeStyles.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/HomeStyles.kt deleted file mode 100644 index 015e78d6..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/HomeStyles.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.luxons.sevenwonders.ui.components.home - -import csstype.* -import emotion.css.* -import web.cssom.* - -object HomeStyles { - - val centerChildren = ClassName { - display = Display.flex - flexDirection = FlexDirection.column - alignItems = AlignItems.center - justifyContent = JustifyContent.center - } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Lobby.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Lobby.kt deleted file mode 100644 index 0330a192..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Lobby.kt +++ /dev/null @@ -1,272 +0,0 @@ -package org.luxons.sevenwonders.ui.components.lobby - -import blueprintjs.core.* -import blueprintjs.icons.* -import emotion.react.* -import org.luxons.sevenwonders.model.api.* -import org.luxons.sevenwonders.model.wonders.* -import org.luxons.sevenwonders.ui.components.* -import org.luxons.sevenwonders.ui.redux.* -import org.luxons.sevenwonders.ui.utils.* -import react.* -import react.dom.html.ReactHTML.div -import react.dom.html.ReactHTML.h1 -import react.dom.html.ReactHTML.h2 -import react.dom.html.ReactHTML.h3 -import react.dom.html.ReactHTML.h4 -import web.cssom.* -import web.cssom.Position - -private val BOT_NAMES = listOf("Wall-E", "B-Max", "Sonny", "T-800", "HAL", "GLaDOS", "R2-D2", "Bender", "AWESOM-O") - -val Lobby = FC(displayName = "Lobby") { - val lobby = useSwSelector { it.currentLobby } - val player = useSwSelector { it.currentPlayer } - - val dispatch = useSwDispatch() - - if (lobby == null || player == null) { - BpNonIdealState { - icon = IconNames.ERROR - titleText = "Error: no current game" - } - } else { - LobbyPresenter { - currentGame = lobby - currentPlayer = player - - startGame = { dispatch(RequestStartGame()) } - addBot = { name -> dispatch(RequestAddBot(name)) } - leaveLobby = { dispatch(RequestLeaveLobby()) } - disbandLobby = { dispatch(RequestDisbandLobby()) } - reorderPlayers = { orderedPlayers -> dispatch(RequestReorderPlayers(orderedPlayers)) } - reassignWonders = { wonders -> dispatch(RequestReassignWonders(wonders)) } - } - } -} - -private external interface LobbyPresenterProps : Props { - var currentGame: LobbyDTO - var currentPlayer: PlayerDTO - var startGame: () -> Unit - var addBot: (displayName: String) -> Unit - var leaveLobby: () -> Unit - var disbandLobby: () -> Unit - var reorderPlayers: (orderedPlayers: List<String>) -> Unit - var reassignWonders: (wonders: List<AssignedWonder>) -> Unit -} - -private val LobbyPresenter = FC<LobbyPresenterProps> { props -> - div { - css(GlobalStyles.fullscreen, GlobalStyles.zeusBackground) { - padding = Padding(all = 1.rem) - } - div { - css(ClassName(Classes.DARK), LobbyStyles.contentContainer) { - margin = Margin(vertical = 0.rem, horizontal = Auto.auto) - maxWidth = GlobalStyles.preGameWidth - } - h1 { +"${props.currentGame.name} — Lobby" } - - radialPlayerList(props.currentGame.players, props.currentPlayer) { - css { - // to make players more readable on the background - background = "radial-gradient(closest-side, black 20%, transparent)".unsafeCast<Gradient>() - // make it bigger so the background covers more ground - width = 40.rem - height = 40.rem - } - } - actionButtons(props.currentPlayer, props.currentGame, props.startGame, props.leaveLobby, props.disbandLobby, props.addBot) - - if (props.currentPlayer.isGameOwner) { - setupPanel(props.currentGame, props.reorderPlayers, props.reassignWonders) - } - } - } -} - -private fun ChildrenBuilder.actionButtons( - currentPlayer: PlayerDTO, - currentGame: LobbyDTO, - startGame: () -> Unit, - leaveLobby: () -> Unit, - disbandLobby: () -> Unit, - addBot: (String) -> Unit, -) { - div { - css { - position = Position.absolute - bottom = 2.rem - left = 50.pct - transform = translate((-50).pct) - - width = 70.pct - display = Display.flex - justifyContent = JustifyContent.spaceAround - } - if (currentPlayer.isGameOwner) { - BpButtonGroup { - leaveButton(leaveLobby) - disbandButton(disbandLobby) - } - BpButtonGroup { - addBotButton(currentGame, addBot) - startButton(currentGame.startability(currentPlayer.username), startGame) - } - } else { - leaveButton(leaveLobby) - } - } -} - -private fun ChildrenBuilder.startButton(startability: Actionability, startGame: () -> Unit) { - BpButton { - large = true - intent = Intent.PRIMARY - icon = IconNames.PLAY - title = startability.tooltip - disabled = !startability.canDo - onClick = { startGame() } - - +"START" - } -} - -private fun ChildrenBuilder.setupPanel( - currentGame: LobbyDTO, - reorderPlayers: (usernames: List<String>) -> Unit, - reassignWonders: (wonders: List<AssignedWonder>) -> Unit, -) { - div { - className = LobbyStyles.setupPanel - - BpCard { - elevation = Elevation.TWO - className = ClassName(Classes.DARK) - - h2 { - css { - marginTop = 0.px - } - +"Game setup" - } - BpDivider() - h3 { - +"Players" - } - reorderPlayersButton(currentGame, reorderPlayers) - h3 { - +"Wonders" - } - WonderSettingsGroup { - this.currentGame = currentGame - this.reassignWonders = reassignWonders - } - } - } -} - -private fun ChildrenBuilder.addBotButton(currentGame: LobbyDTO, addBot: (String) -> Unit) { - BpButton { - large = true - icon = IconNames.PLUS - rightIcon = IconNames.DESKTOP - intent = Intent.PRIMARY - title = if (currentGame.maxPlayersReached) "Max players reached" else "Add a bot to this game" - disabled = currentGame.maxPlayersReached - onClick = { addBot(randomBotNameUnusedIn(currentGame)) } - } -} - -private fun randomBotNameUnusedIn(currentGame: LobbyDTO): String { - val availableBotNames = BOT_NAMES.filter { name -> - currentGame.players.none { it.displayName == name } - } - return availableBotNames.random() -} - -private fun ChildrenBuilder.reorderPlayersButton(currentGame: LobbyDTO, reorderPlayers: (usernames: List<String>) -> Unit) { - BpButton { - icon = IconNames.RANDOM - rightIcon = IconNames.PEOPLE - title = "Re-order players randomly" - onClick = { reorderPlayers(currentGame.players.map { it.username }.shuffled()) } - - +"Reorder players" - } -} - -private external interface WonderSettingsGroupProps : Props { - var currentGame: LobbyDTO - var reassignWonders: (List<AssignedWonder>) -> Unit -} - -private val WonderSettingsGroup = FC<WonderSettingsGroupProps> { props -> - val reassignWonders = props.reassignWonders - - BpButton { - icon = IconNames.RANDOM - title = "Re-assign wonders to players randomly" - onClick = { reassignWonders(randomWonderAssignments(props.currentGame)) } - - +"Randomize wonders" - } - h4 { - +"Select wonder sides:" - } - BpButtonGroup { - BpButton { - icon = IconNames.RANDOM - title = "Re-roll wonder sides randomly" - onClick = { reassignWonders(assignedWondersWithRandomSides(props.currentGame)) } - } - BpButton { - title = "Choose side A for everyone" - onClick = { reassignWonders(assignedWondersWithForcedSide(props.currentGame, WonderSide.A)) } - - +"A" - } - BpButton { - title = "Choose side B for everyone" - onClick = { reassignWonders(assignedWondersWithForcedSide(props.currentGame, WonderSide.B)) } - - +"B" - } - } -} - -private fun randomWonderAssignments(currentGame: LobbyDTO): List<AssignedWonder> = - currentGame.allWonders.deal(currentGame.players.size) - -private fun assignedWondersWithForcedSide( - currentGame: LobbyDTO, - side: WonderSide -) = currentGame.players.map { currentGame.findWonder(it.wonder.name).withSide(side) } - -private fun assignedWondersWithRandomSides(currentGame: LobbyDTO) = - currentGame.players.map { currentGame.findWonder(it.wonder.name) }.map { it.withRandomSide() } - -private fun ChildrenBuilder.leaveButton(leaveLobby: () -> Unit) { - BpButton { - large = true - intent = Intent.WARNING - icon = "arrow-left" - title = "Leave the lobby and go back to the game browser" - onClick = { leaveLobby() } - - +"LEAVE" - } -} - -private fun ChildrenBuilder.disbandButton(disbandLobby: () -> Unit) { - BpButton { - large = true - intent = Intent.DANGER - icon = IconNames.DELETE - title = "Disband the group and go back to the game browser" - onClick = { disbandLobby() } - - +"DISBAND" - } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/LobbyStyles.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/LobbyStyles.kt deleted file mode 100644 index 6b5dbe48..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/LobbyStyles.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.luxons.sevenwonders.ui.components.lobby - -import emotion.css.* -import org.luxons.sevenwonders.ui.components.* -import web.cssom.* - -object LobbyStyles { - - val contentContainer = ClassName { - margin = Margin(vertical = 0.px, horizontal = Auto.auto) - maxWidth = GlobalStyles.preGameWidth - } - - val setupPanel = ClassName { - position = Position.fixed - top = 2.rem - right = 1.rem - width = 20.rem - } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialList.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialList.kt deleted file mode 100644 index 1f88bebe..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialList.kt +++ /dev/null @@ -1,117 +0,0 @@ -package org.luxons.sevenwonders.ui.components.lobby - -import csstype.* -import emotion.react.* -import org.luxons.sevenwonders.ui.components.* -import react.* -import react.dom.html.* -import react.dom.html.ReactHTML.div -import react.dom.html.ReactHTML.li -import react.dom.html.ReactHTML.ul -import web.cssom.* -import web.html.* - -fun <T> ChildrenBuilder.radialList( - items: List<T>, - centerElement: ReactElement<*>, - renderItem: (T) -> ReactElement<*>, - getKey: (T) -> String, - itemWidth: Int, - itemHeight: Int, - options: RadialConfig = RadialConfig(), - block: HTMLAttributes<HTMLDivElement>.() -> Unit = {}, -) { - val containerWidth = options.diameter + itemWidth - val containerHeight = options.diameter + itemHeight - - div { - css(GlobalStyles.fixedCenter) { - zeroMargins() - width = containerWidth.px - height = containerHeight.px - } - block() - radialListItems(items, renderItem, getKey, options) - radialListCenter(centerElement) - } -} - -private fun <T> ChildrenBuilder.radialListItems( - items: List<T>, - renderItem: (T) -> ReactElement<*>, - getKey: (T) -> String, - radialConfig: RadialConfig, -) { - val offsets = offsetsFromCenter(items.size, radialConfig) - ul { - css { - zeroMargins() - transition = Transition( - property = TransitionProperty.all, - duration = 500.ms, - timingFunction = TransitionTimingFunction.easeInOut, - ) - zIndex = integer(1) - width = radialConfig.diameter.px - height = radialConfig.diameter.px - absoluteCenter() - } - // We ensure a stable order of the DOM elements so that position animations look nice. - // We still respect the order of the items in the list when placing them along the circle. - val indexByKey = buildMap { - items.forEachIndexed { index, item -> put(getKey(item), index) } - } - items.sortedBy { getKey(it) }.forEach { item -> - val key = getKey(item) - radialListItem(renderItem(item), key, offsets[indexByKey.getValue(key)]) - } - } -} - -private fun ChildrenBuilder.radialListItem(item: ReactElement<*>, key: String, offset: CartesianCoords) { - li { - css { - display = Display.block - position = Position.absolute - top = 50.pct - left = 50.pct - zeroMargins() - listStyleType = Globals.unset - transition = Transition( - property = TransitionProperty.all, - duration = 500.ms, - timingFunction = TransitionTimingFunction.easeInOut, - ) - zIndex = integer(1) - transform = translate(offset.x.px - 50.pct, offset.y.px - 50.pct) - } - this.key = key - - child(item) - } -} - -private fun ChildrenBuilder.radialListCenter(centerElement: ReactElement<*>?) { - if (centerElement == null) { - return - } - div { - css { - zIndex = integer(0) - absoluteCenter() - } - child(centerElement) - } -} - -private fun PropertiesBuilder.absoluteCenter() { - position = Position.absolute - left = 50.pct - top = 50.pct - transform = translate((-50).pct, (-50).pct) -} - -private fun PropertiesBuilder.zeroMargins() { - margin = Margin(vertical = 0.px, horizontal = 0.px) - padding = Padding(vertical = 0.px, horizontal = 0.px) -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialMath.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialMath.kt deleted file mode 100644 index 4b5eb509..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialMath.kt +++ /dev/null @@ -1,57 +0,0 @@ -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()), -) - -// Y-axis is pointing down in the browser, so the directions need to be reversed -// (positive angles are now clockwise) -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 spreadArcDegrees: Int = 360, // full circle - val firstItemAngleDegrees: Int = 0, // 12 o'clock - val direction: Direction = Direction.CLOCKWISE, -) { - val diameter: Int = radius * 2 -} - -private const val DEFAULT_START = -90 // Up, because Y-axis is reversed - -fun offsetsFromCenter(nbItems: Int, radialConfig: RadialConfig = RadialConfig()): List<CartesianCoords> { - val startAngle = DEFAULT_START + radialConfig.direction.toOrientedDegrees(radialConfig.firstItemAngleDegrees) - val angleStep = radialConfig.spreadArcDegrees / nbItems - return List(nbItems) { itemCartesianOffsets(startAngle, angleStep, it, radialConfig) } -} - -private fun itemCartesianOffsets(startAngle: Int, angleStep: Int, index: Int, config: RadialConfig): CartesianCoords { - val itemAngle = startAngle + config.direction.toOrientedDegrees(angleStep) * index - return PolarCoords(config.radius, itemAngle).toCartesian() -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialPlayerList.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialPlayerList.kt deleted file mode 100644 index 645cf5f3..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialPlayerList.kt +++ /dev/null @@ -1,139 +0,0 @@ -package org.luxons.sevenwonders.ui.components.lobby - -import blueprintjs.core.* -import blueprintjs.icons.* -import csstype.* -import emotion.react.* -import org.luxons.sevenwonders.model.api.* -import org.luxons.sevenwonders.model.api.actions.Icon -import org.luxons.sevenwonders.model.wonders.* -import org.luxons.sevenwonders.ui.utils.* -import react.* -import react.dom.html.* -import react.dom.html.ReactHTML.div -import react.dom.html.ReactHTML.span -import web.cssom.* -import web.html.* - -fun ChildrenBuilder.radialPlayerList( - players: List<PlayerDTO>, - currentPlayer: PlayerDTO, - block: HTMLAttributes<HTMLDivElement>.() -> Unit = {}, -) { - val playerItems = players // - .map { PlayerItem.Player(it) } - .growWithPlaceholders(targetSize = 3) - .withUserFirst(currentPlayer) - - radialList( - items = playerItems, - centerElement = LobbyWoodenTable.create { - diameter = 200.px - borderSize = 15.px - }, - renderItem = { PlayerElement.create { playerItem = it } }, - getKey = { it.key }, - itemWidth = 120, - itemHeight = 100, - options = RadialConfig( - radius = 175, - firstItemAngleDegrees = 180, // self at the bottom - direction = Direction.COUNTERCLOCKWISE, // new players sit to the right of last player - ), - block = block, - ) -} - -private fun List<PlayerItem>.growWithPlaceholders(targetSize: Int): List<PlayerItem> = when { - size < targetSize -> this + List(targetSize - size) { PlayerItem.Placeholder(size + it) } - else -> this -} - -private fun List<PlayerItem>.withUserFirst(me: PlayerDTO): List<PlayerItem> { - val nonUsersBeginning = takeWhile { (it as? PlayerItem.Player)?.player?.username != me.username } - val userToEnd = subList(nonUsersBeginning.size, size) - return userToEnd + nonUsersBeginning -} - -private sealed class PlayerItem { - abstract val key: String - abstract val playerText: String - abstract val opacity: Opacity - abstract val icon: ReactElement<*> - - data class Player(val player: PlayerDTO) : PlayerItem() { - override val key = player.username - override val playerText = player.displayName - override val opacity = number(1.0) - override val icon = createUserIcon( - icon = player.icon ?: when { - player.isGameOwner -> Icon(IconNames.BADGE) - else -> Icon(IconNames.USER) - }, - title = if (player.isGameOwner) "Game owner" else null, - ) - } - - data class Placeholder(val index: Int) : PlayerItem() { - override val key = "player-placeholder-$index" - override val playerText = "?" - override val opacity = number(0.4) - override val icon = createUserIcon( - icon = Icon(IconNames.USER), - title = "Waiting for player...", - ) - } -} - -private fun createUserIcon(icon: Icon, title: String?) = BpIcon.create { - this.icon = icon.name - this.size = 50 - this.title = title -} - -private external interface PlayerElementProps : Props { - var playerItem: PlayerItem -} - -private val PlayerElement = FC<PlayerElementProps>(displayName = "PlayerElement") { props -> - val playerItem = props.playerItem - div { - css { - display = Display.flex - flexDirection = FlexDirection.column - alignItems = AlignItems.center - opacity = playerItem.opacity - } - child(playerItem.icon) - span { - css { - fontSize = if (playerItem is PlayerItem.Placeholder) 1.5.rem else 0.9.rem - } - +playerItem.playerText - } - if (playerItem is PlayerItem.Player) { - div { - val wonder = playerItem.player.wonder - - css { - marginTop = 0.3.rem - - children(".wonder-tag") { - color = Color("#f5f8fa") // blueprintjs dark theme color (removed by .bp4-tag) - backgroundColor = when (wonder.side) { - WonderSide.A -> NamedColor.seagreen - WonderSide.B -> NamedColor.darkred - } - } - } - - BpTag { - round = true - className = ClassName("wonder-tag") - - +"${wonder.name} ${wonder.side}" - } - } - } - } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Table.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Table.kt deleted file mode 100644 index bfa43aa4..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Table.kt +++ /dev/null @@ -1,97 +0,0 @@ -package org.luxons.sevenwonders.ui.components.lobby - -import csstype.* -import emotion.css.* -import emotion.react.* -import emotion.styled.* -import org.luxons.sevenwonders.ui.utils.* -import react.* -import react.dom.html.ReactHTML.div -import web.cssom.* - -private val FIRE_REFLECTION_COLOR = Color("#b85e00") - -private external interface CircleProps : PropsWithChildren, PropsWithClassName { - var diameter: Length -} - -private val Circle = FC<CircleProps>("Circle") { props -> - div { - css(props.className) { - width = props.diameter - height = props.diameter - borderRadius = 50.pct - } - child(props.children) - } -} - -private val OverlayCircle = Circle.styled { - position = Position.absolute - top = 0.px - left = 0.px -} - -external interface LobbyWoodenTableProps : Props { - var diameter: Length - var borderSize: Length -} - -val LobbyWoodenTable = FC<LobbyWoodenTableProps>("LobbyWoodenTable") { props -> - Circle { - diameter = props.diameter - - css { - backgroundColor = Color("#3d1e0e") - } - - Circle { - diameter = props.diameter - props.borderSize - css { - position = Position.absolute - top = props.borderSize / 2 - left = props.borderSize / 2 - background = linearGradient(45.deg, Color("#88541e"), Color("#995645"), Color("#52251a")) - } - } - - // flame reflection coming from bottom-right - OverlayCircle { - diameter = props.diameter - - css { - background = - linearGradient((-45).deg, stop(FIRE_REFLECTION_COLOR, 10.pct), stop(NamedColor.transparent, 50.pct)) - opacityAnimation(duration = 1.3.s) - } - } - // flame reflection coming from bottom-left - OverlayCircle { - diameter = props.diameter - - css { - background = - linearGradient(45.deg, stop(FIRE_REFLECTION_COLOR, 20.pct), stop(NamedColor.transparent, 40.pct)) - opacityAnimation(duration = 0.8.s) - } - } - } -} - -private fun PropertiesBuilder.opacityAnimation(duration: Time) { - val keyframes = keyframes { - from { - opacity = number(0.0) - } - to { - opacity = number(0.35) - } - } - animation = Animation( - name = keyframes, - duration = duration, - timingFunction = cubicBezier(0.4, 0.4, 0.4, 2.0), - ) - animationDirection = AnimationDirection.alternate - animationIterationCount = AnimationIterationCount.infinite -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/names/RandomNameGenerator.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/names/RandomNameGenerator.kt deleted file mode 100644 index 393df78d..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/names/RandomNameGenerator.kt +++ /dev/null @@ -1,546 +0,0 @@ -package org.luxons.sevenwonders.ui.names - -import kotlin.random.Random - -internal fun randomGameName(): String = gameNames.random() - -internal fun randomGreekName(): String { - val randName = prefixes.random() + suffixes.random() - return if (Random.nextBoolean()) randName else "$randName of ${cities.random()}" -} - -private val gameNames = listOf( - "Age of Antiquity", - "Age of Civilization", - "Age of Discovery", - "Age of Empires", - "Age of Wonders", - "Ancient Capitals", - "Ancient Kingdoms", - "Ancient Wonders", - "Cities of Antiquity", - "City of Wonders", - "Empire Builders", - "Empires of the Past", - "Great Monuments", - "Legendary Cities", - "Legends of the Past", - "Lost Empires", - "Magnificent Monuments", - "Magnificent Seven", - "Monuments of the Past", - "Monuments of the World", - "Mythical Kingdoms", - "Secrets of the Past", - "Seven Ancient Wonders", - "Seven Colossi", - "Seven Kingdoms", - "Seven Marvels", - "Seven Wonders Adventures", - "Seven Wonders Chronicles", - "Seven Wonders Enigma", - "Seven Wonders Expedition", - "Seven Wonders Frontier", - "Seven Wonders Legacy", - "Seven Wonders Odyssey", - "Seven Wonders Quest", - "Seven Wonders Saga", - "Seven Wonders Treasures", - "Seven Wonders Voyage", - "Seven Wonders and Beyond", - "The Great Discoveries", - "The Legacy of Wonders", - "The Magic of Seven", - "The Marvelous Seven", - "The Mysteries of Antiquity", - "The Seven Continents", - "The Seven Kingdoms", - "The Seven Legends", - "The Seven Secrets", - "The Seven Treasures", - "Wonders of Nature", - "Wonders of the Ages", - "Wonders of the World", - "Wonders of Time", - "World Treasures", -) - -private val prefixes = - listOf( - "Aba", - "Abde", - "Abre", - "Aby", - "Aca", - "Acle", - "Acri", - "Acro", - "Adme", - "Adra", - "Aea", - "Aegi", - "Aei", - "Aeo", - "Aese", - "Aeto", - "Aga", - "Age", - "Agi", - "Agri", - "Aia", - "Aka", - "Akti", - "Ala", - "Alco", - "Ale", - "Alka", - "Alki", - "Alo", - "Alphi", - "Ama", - "Ame", - "Ami", - "Amphi", - "Ana", - "Anchi", - "Andro", - "Ane", - "Anta", - "Anthe", - "Anti", - "Ape", - "Aphi", - "Apo", - "Arca", - "Arche", - "Arci", - "Arga", - "Ari", - "Arra", - "Arte", - "Asca", - "Asta", - "Asty", - "Atro", - "Atta", - "Aute", - "Bace", - "Bae", - "Bali", - "Bio", - "Boe", - "Bria", - "Care", - "Carpo", - "Casto", - "Cea", - "Cebri", - "Cele", - "Cephi", - "Chae", - "Chare", - "Chari", - "Choe", - "Chromi", - "Chryso", - "Cine", - "Cisse", - "Clea", - "Cleo", - "Clyto", - "Cnoe", - "Coe", - "Cordy", - "Cory", - "Crati", - "Creti", - "Croe", - "Ctea", - "Cyre", - "Dae", - "Dami", - "Damo", - "Dana", - "Daphi", - "Davo", - "Dei", - "Dema", - "Demo", - "Deo", - "Derky", - "Dexi", - "Dia", - "Dio", - "Dithy", - "Dore", - "Dori", - "Doro", - "Drya", - "Dymno", - "Eche", - "Eio", - "Ela", - "Elpe", - "Empe", - "Endy", - "Enge", - "Epa", - "Epe", - "Ephi", - "Era", - "Ere", - "Ergi", - "Erxa", - "Euca", - "Euche", - "Eudo", - "Eue", - "Euge", - "Euma", - "Eune", - "Eury", - "Euthy", - "Eva", - "Eve", - "Fae", - "Gale", - "Gany", - "Gaua", - "Genna", - "Gera", - "Glau", - "Gorgo", - "Gyra", - "Hae", - "Hagi", - "Hali", - "Harma", - "Harmo", - "Harpa", - "Hege", - "Heira", - "Heiro", - "Helge", - "Heli", - "Hera", - "Hermo", - "Hiero", - "Hippo", - "Hya", - "Hype", - "Hyrca", - "Iatro", - "Iby", - "Ica", - "Ido", - "Illy", - "Ina", - "Iphi", - "Iro", - "Isa", - "Isma", - "Iso", - "Ithe", - "Kae", - "Kale", - "Kalli", - "Kame", - "Kapa", - "Kari", - "Karo", - "Kau", - "Keo", - "Kera", - "Kleo", - "Krini", - "Krito", - "Labo", - "Lae", - "Lama", - "Lamu", - "Lao", - "Laso", - "Lea", - "Lei", - "Leo", - "Linu", - "Luko", - "Lyca", - "Lyco", - "Lysa", - "Lysi", - "Maca", - "Macha", - "Mae", - "Maia", - "Maka", - "Male", - "Mante", - "Marci", - "Marsy", - "Mega", - "Megi", - "Mela", - "Mele", - "Metho", - "Midy", - "Mise", - "Mono", - "Morsi", - "Myrsi", - "Naste", - "Nausi", - "Nea", - "Nele", - "Neri", - "Nica", - "Nico", - "Nire", - "Nomi", - "Nycti", - "Oche", - "Ocho", - "Oea", - "Oene", - "Oeno", - "Oile", - "Ona", - "One", - "Ophe", - "Ori", - "Orsi", - "Ory", - "Pae", - "Pala", - "Pana", - "Pandi", - "Pani", - "Panta", - "Para", - "Pata", - "Peiri", - "Pele", - "Peli", - "Peri", - "Phae", - "Phala", - "Philo", - "Phyla", - "Poe", - "Poly", - "Praxi", - "Prota", - "Pryta", - "Saby", - "Saty", - "Scama", - "Scytha", - "Sele", - "Sila", - "Simo", - "Sisy", - "Sopho", - "Stesa", - "Sya", - "Sylo", - "Syne", - "Tala", - "Teba", - "Tele", - "Tene", - "Theo", - "Therse", - "Thrasy", - "Tima", - "Tiry", - "Trio", - "Xanthi", - "Xena", - "Xeno", - ) - -private val suffixesMale = - listOf( - "ndros", - "bios", - "bulos", - "chus", - "cles", - "cydes", - "damos", - "dides", - "don", - "doros", - "dotus", - "gnis", - "goras", - "kles", - "kos", - "krates", - "laktos", - "laus", - "leon", - "llias", - "llos", - "llus", - "machos", - "machus", - "menes", - "menos", - "mos", - "ndius", - "nes", - "neus", - "nidas", - "nides", - "nos", - "nthius", - "patros", - "phanes", - "phantes", - "phimus", - "phnus", - "phon", - "phoros", - "phorus", - "phus", - "pides", - "pompos", - "pompus", - "pon", - "ppos", - "rax", - "reas", - "rides", - "ros", - "sias", - "sides", - "sius", - "stius", - "stor", - "stos", - "stus", - "talos", - "thenes", - "theus", - "tios", - ) - -private val suffixesFemale = - listOf( - "ndria", - "boea", - "casta", - "caste", - "cheia", - "chis", - "cleia", - "dee", - "deia", - "dike", - "dina", - "doce", - "dora", - "dusa", - "gaea", - "kia", - "laia", - "lea", - "line", - "llis", - "lope", - "mache", - "mathe", - "meda", - "mede", - "meia", - "mela", - "mene", - "mere", - "mia", - "mina", - "mpias", - "ndra", - "ne", - "neira", - "nessa", - "nia", - "nice", - "niera", - "nike", - "nippe", - "nna", - "nome", - "nope", - "nta", - "nthia", - "pe", - "phae", - "phana", - "phane", - "phile", - "phobe", - "phone", - "pia", - "polis", - "pris", - "pyle", - "reia", - "rine", - "ris", - "rista", - "rpia", - "sia", - "ssa", - "steia", - "stis", - "syne", - "ta", - "tea", - "thea", - "theia", - "thia", - "thippe", - "thra", - "thusa", - "thyia", - "tis", - "trite", - ) - -private val suffixes = suffixesMale + suffixesFemale - -private val cities = - listOf( - "Argos", - "Assos", - "Astypalaia", - "Carystus", - "Chalcis", - "Chios", - "Corfu", - "Corinth", - "Eretria", - "Erythrae", - "Karpathos", - "Kasos", - "Kos", - "Leros", - "Lindos", - "Marathon", - "Megara", - "Miletus", - "Mytilene", - "Naxos", - "Oenoe", - "Paros", - "Patmos", - "Patras", - "Phocis", - "Rhodes", - "Salamis", - "Skiathos", - "Sparta", - "Thasos", - "Thebes", - ) diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Actions.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Actions.kt deleted file mode 100644 index b0c56a79..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Actions.kt +++ /dev/null @@ -1,32 +0,0 @@ -package org.luxons.sevenwonders.ui.redux - -import org.luxons.sevenwonders.model.PlayerMove -import org.luxons.sevenwonders.model.PlayerTurnInfo -import org.luxons.sevenwonders.model.TurnAction -import org.luxons.sevenwonders.model.api.ConnectedPlayer -import org.luxons.sevenwonders.model.api.LobbyDTO -import org.luxons.sevenwonders.model.api.events.GameListEvent -import org.luxons.sevenwonders.model.cards.PreparedCard -import redux.RAction - -data class FatalError(val message: String) : RAction - -data class SetCurrentPlayerAction(val player: ConnectedPlayer) : RAction - -data class UpdateGameListAction(val event: GameListEvent) : RAction - -data class UpdateLobbyAction(val lobby: LobbyDTO) : RAction - -data class EnterLobbyAction(val lobby: LobbyDTO) : RAction - -object LeaveLobbyAction : RAction - -data class EnterGameAction(val lobby: LobbyDTO, val turnInfo: PlayerTurnInfo<TurnAction.SayReady>) : RAction - -data class TurnInfoEvent(val turnInfo: PlayerTurnInfo<*>) : RAction - -data class PreparedMoveEvent(val move: PlayerMove) : RAction - -data class PreparedCardEvent(val card: PreparedCard) : RAction - -data class PlayerReadyEvent(val username: String) : RAction diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/ApiActions.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/ApiActions.kt deleted file mode 100644 index 87bacf62..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/ApiActions.kt +++ /dev/null @@ -1,34 +0,0 @@ -package org.luxons.sevenwonders.ui.redux - -import org.luxons.sevenwonders.model.PlayerMove -import org.luxons.sevenwonders.model.Settings -import org.luxons.sevenwonders.model.wonders.AssignedWonder -import redux.RAction - -data class RequestChooseName(val playerName: String) : RAction - -data class RequestCreateGame(val gameName: String) : RAction - -data class RequestJoinGame(val gameId: Long) : RAction - -data class RequestAddBot(val botDisplayName: String) : RAction - -data class RequestReorderPlayers(val orderedPlayers: List<String>) : RAction - -data class RequestReassignWonders(val wonders: List<AssignedWonder>) : RAction - -data class RequestUpdateSettings(val settings: Settings) : RAction - -class RequestStartGame : RAction - -class RequestLeaveLobby : RAction - -class RequestDisbandLobby : RAction - -class RequestLeaveGame : RAction - -class RequestSayReady : RAction - -data class RequestPrepareMove(val move: PlayerMove) : RAction - -class RequestUnprepareMove : RAction diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt deleted file mode 100644 index e79b063e..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt +++ /dev/null @@ -1,95 +0,0 @@ -package org.luxons.sevenwonders.ui.redux - -import org.luxons.sevenwonders.client.GameState -import org.luxons.sevenwonders.model.api.ConnectedPlayer -import org.luxons.sevenwonders.model.api.LobbyDTO -import org.luxons.sevenwonders.model.api.PlayerDTO -import org.luxons.sevenwonders.model.api.events.GameListEvent -import redux.RAction - -data class SwState( - val connectedPlayer: ConnectedPlayer? = null, - // they must be by ID to support updates to a sublist - val gamesById: Map<Long, LobbyDTO> = emptyMap(), - val currentLobby: LobbyDTO? = null, - val gameState: GameState? = null, - val fatalError: String? = null, -) { - val currentPlayer: PlayerDTO? = (gameState?.players ?: currentLobby?.players)?.first { - it.username == connectedPlayer?.username - } - val games: List<LobbyDTO> = gamesById.values.toList() -} - -fun rootReducer(state: SwState, action: RAction): SwState = state.copy( - gamesById = gamesReducer(state.gamesById, action), - connectedPlayer = currentPlayerReducer(state.connectedPlayer, action), - currentLobby = currentLobbyReducer(state.currentLobby, action), - gameState = gameStateReducer(state.gameState, action), - fatalError = connectionLostReducer(action), -) - -private fun gamesReducer(games: Map<Long, LobbyDTO>, action: RAction): Map<Long, LobbyDTO> = when (action) { - is UpdateGameListAction -> when (action.event) { - is GameListEvent.ReplaceList -> action.event.lobbies.associateBy { it.id } - is GameListEvent.CreateOrUpdate -> games + (action.event.lobby.id to action.event.lobby) - is GameListEvent.Delete -> games - action.event.lobbyId - } - else -> games -} - -private fun currentPlayerReducer(currentPlayer: ConnectedPlayer?, action: RAction): ConnectedPlayer? = when (action) { - is SetCurrentPlayerAction -> action.player - else -> currentPlayer -} - -private fun currentLobbyReducer(currentLobby: LobbyDTO?, action: RAction): LobbyDTO? = when (action) { - is EnterLobbyAction -> action.lobby - is LeaveLobbyAction -> null - is UpdateLobbyAction -> action.lobby - is PlayerReadyEvent -> currentLobby?.let { l -> - l.copy(players = l.players.map { p -> if (p.username == action.username) p.copy(isReady = true) else p }) - } - else -> currentLobby -} - -private fun gameStateReducer(gameState: GameState?, action: RAction): GameState? = when (action) { - is EnterGameAction -> GameState( - gameId = action.lobby.id, - players = action.lobby.players, - playerIndex = action.turnInfo.playerIndex, - currentAge = action.turnInfo.table.currentAge, - boards = action.turnInfo.table.boards, - handRotationDirection = action.turnInfo.table.handRotationDirection, - action = action.turnInfo.action, - preparedCardsByUsername = emptyMap(), - currentPreparedMove = null, - ) - is PreparedMoveEvent -> gameState?.copy(currentPreparedMove = action.move) - is RequestUnprepareMove -> gameState?.copy(currentPreparedMove = null) - is PreparedCardEvent -> gameState?.copy( - preparedCardsByUsername = gameState.preparedCardsByUsername + (action.card.username to action.card.cardBack), - ) - is PlayerReadyEvent -> gameState?.copy( - players = gameState.players.map { p -> - if (p.username == action.username) p.copy(isReady = true) else p - }, - ) - is TurnInfoEvent -> gameState?.copy( - players = gameState.players.map { p -> p.copy(isReady = false) }, - playerIndex = action.turnInfo.playerIndex, - currentAge = action.turnInfo.table.currentAge, - boards = action.turnInfo.table.boards, - handRotationDirection = action.turnInfo.table.handRotationDirection, - action = action.turnInfo.action, - preparedCardsByUsername = emptyMap(), - currentPreparedMove = null, - ) - is LeaveLobbyAction -> null - else -> gameState -} - -private fun connectionLostReducer(action: RAction): String? = when (action) { - is FatalError -> action.message - else -> null -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Store.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Store.kt deleted file mode 100644 index 71c5eec0..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Store.kt +++ /dev/null @@ -1,29 +0,0 @@ -package org.luxons.sevenwonders.ui.redux - -import kotlinx.browser.window -import org.luxons.sevenwonders.ui.redux.sagas.SagaManager -import redux.RAction -import redux.Store -import redux.WrapperAction -import redux.applyMiddleware -import redux.compose -import redux.createStore -import redux.rEnhancer - -val INITIAL_STATE = SwState() - -private fun <A, T1, R> composeWithDevTools(function1: (T1) -> R, function2: (A) -> T1): (A) -> R { - val reduxDevtoolsExtensionCompose = window.asDynamic().__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ - if (reduxDevtoolsExtensionCompose == undefined) { - return compose(function1, function2) - } - return reduxDevtoolsExtensionCompose(function1, function2) as Function1<A, R> -} - -fun configureStore( - sagaManager: SagaManager<SwState, RAction, WrapperAction>, - initialState: SwState = INITIAL_STATE, -): Store<SwState, RAction, WrapperAction> { - val sagaEnhancer = applyMiddleware(sagaManager.createMiddleware()) - return createStore(::rootReducer, initialState, composeWithDevTools(sagaEnhancer, rEnhancer())) -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Utils.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Utils.kt deleted file mode 100644 index eb182dc7..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Utils.kt +++ /dev/null @@ -1,31 +0,0 @@ -package org.luxons.sevenwonders.ui.redux - -import react.* -import react.redux.* -import redux.* -import kotlin.reflect.* - -fun <R> useSwSelector(selector: (SwState) -> R) = useSelector(selector) -fun useSwDispatch() = useDispatch<RAction, WrapperAction>() - -fun <SP : Props, DP : Props, P : Props> connectStateAndDispatch( - clazz: KClass<out Component<P, out State>>, - mapStateToProps: SP.(SwState, Props) -> Unit, - mapDispatchToProps: DP.((RAction) -> WrapperAction, Props) -> Unit, -): ComponentClass<Props> = connectStateAndDispatch( - component = clazz.react, - mapStateToProps = mapStateToProps, - mapDispatchToProps = mapDispatchToProps, -) - -fun <SP : Props, DP : Props, P : Props> connectStateAndDispatch( - component: ComponentClass<P>, - mapStateToProps: SP.(SwState, Props) -> Unit, - mapDispatchToProps: DP.((RAction) -> WrapperAction, Props) -> Unit, -): ComponentClass<Props> { - val connect = rConnect<SwState, RAction, WrapperAction, Props, SP, DP, P>( - mapStateToProps = mapStateToProps, - mapDispatchToProps = mapDispatchToProps, - ) - return connect.invoke(component) -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/RouteBasedSagas.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/RouteBasedSagas.kt deleted file mode 100644 index 3343e62e..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/RouteBasedSagas.kt +++ /dev/null @@ -1,44 +0,0 @@ -package org.luxons.sevenwonders.ui.redux.sagas - -import kotlinx.coroutines.flow.map -import org.luxons.sevenwonders.client.SevenWondersSession -import org.luxons.sevenwonders.ui.redux.* -import org.luxons.sevenwonders.ui.router.Navigate -import org.luxons.sevenwonders.ui.router.SwRoute - -suspend fun SwSagaContext.gameBrowserSaga(session: SevenWondersSession) { - // browser navigation could have brought us here: we should leave the game/lobby - ensureNoCurrentGameNorLobby(session) - session.watchGames() - .map { UpdateGameListAction(it) } - .collect { dispatch(it) } -} - -private suspend fun SwSagaContext.ensureNoCurrentGameNorLobby(session: SevenWondersSession) { - if (reduxState.gameState != null) { - console.warn("User left a game via browser navigation, telling the server...") - session.leaveGame() - } else if (reduxState.currentLobby != null) { - console.warn("User left the lobby via browser navigation, telling the server...") - session.leaveLobby() - } -} - -suspend fun SwSagaContext.lobbySaga(session: SevenWondersSession) { - if (reduxState.gameState != null) { - console.warn("User left a game via browser navigation, telling the server...") - session.leaveGame() - } else if (reduxState.currentLobby == null) { - console.warn("User went to lobby page via browser navigation, redirecting to game browser...") - dispatch(Navigate(SwRoute.GAME_BROWSER)) - } -} - -suspend fun SwSagaContext.gameSaga(session: SevenWondersSession) { - if (reduxState.gameState == null) { - // TODO properly redirect somewhere - error("Game saga run without a current game") - } - // notifies the server that the client is ready to receive the first hand - session.sayReady() -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/Sagas.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/Sagas.kt deleted file mode 100644 index 2ad98c8e..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/Sagas.kt +++ /dev/null @@ -1,131 +0,0 @@ -package org.luxons.sevenwonders.ui.redux.sagas - -import kotlinx.browser.window -import kotlinx.coroutines.* -import org.hildan.krossbow.stomp.ConnectionException -import org.hildan.krossbow.stomp.MissingHeartBeatException -import org.hildan.krossbow.stomp.WebSocketClosedUnexpectedly -import org.luxons.sevenwonders.client.* -import org.luxons.sevenwonders.model.api.events.GameEvent -import org.luxons.sevenwonders.ui.redux.* -import org.luxons.sevenwonders.ui.router.Navigate -import org.luxons.sevenwonders.ui.router.SwRoute -import org.luxons.sevenwonders.ui.router.routerSaga -import redux.RAction -import redux.WrapperAction -import webpack.isProdEnv - -typealias SwSagaContext = SagaContext<SwState, RAction, WrapperAction> - -suspend fun SwSagaContext.rootSaga() = try { - coroutineScope { - val action = next<RequestChooseName>() - val serverUrl = sevenWondersWebSocketUrl() - console.info("Connecting to Seven Wonders web socket API...") - val session = SevenWondersClient().connect(serverUrl) - console.info("Connected!") - - launch(start = CoroutineStart.UNDISPATCHED) { - serverErrorSaga(session) - } - - launchApiActionHandlersIn(this, session) - launchApiEventHandlersIn(this, session) - - val player = session.chooseNameAndAwait(action.playerName) - dispatch(SetCurrentPlayerAction(player)) - - routerSaga(SwRoute.GAME_BROWSER) { - when (it) { - SwRoute.HOME -> Unit - SwRoute.LOBBY -> lobbySaga(session) - SwRoute.GAME_BROWSER -> gameBrowserSaga(session) - SwRoute.GAME -> gameSaga(session) - } - } - } -} catch (e: Exception) { - console.error(e) - dispatchFatalError(e) -} - -private fun SwSagaContext.dispatchFatalError(throwable: Throwable) { - when (throwable) { - is ConnectionException -> dispatch(FatalError(throwable.message ?: "Couldn't connect to the server.")) - is MissingHeartBeatException -> dispatch(FatalError("The server doesn't seem to be responding.")) - is WebSocketClosedUnexpectedly -> dispatch(FatalError("The connection to the server was closed unexpectedly.")) - else -> dispatch(FatalError("An unexpected error occurred: ${throwable.message}")) - } -} - -private fun sevenWondersWebSocketUrl(): String { - if (!isProdEnv()) { - return "ws://localhost:8000" - } - // prevents mixed content requests - val scheme = if (window.location.protocol.startsWith("https")) "wss" else "ws" - return "$scheme://${window.location.host}" -} - -private suspend fun serverErrorSaga(session: SevenWondersSession) { - session.watchErrors().collect { err -> - // These are not an error for the user, but rather for the programmer - console.error("${err.code}: ${err.message}") - console.error(JSON.stringify(err)) - } -} - -private fun SwSagaContext.launchApiActionHandlersIn(scope: CoroutineScope, session: SevenWondersSession) { - scope.launchOnEach<RequestChooseName> { session.chooseName(it.playerName) } - - scope.launchOnEach<RequestCreateGame> { session.createGame(it.gameName) } - scope.launchOnEach<RequestJoinGame> { session.joinGame(it.gameId) } - scope.launchOnEach<RequestLeaveLobby> { session.leaveLobby() } - scope.launchOnEach<RequestDisbandLobby> { session.disbandLobby() } - - scope.launchOnEach<RequestAddBot> { session.addBot(it.botDisplayName) } - scope.launchOnEach<RequestReorderPlayers> { session.reorderPlayers(it.orderedPlayers) } - scope.launchOnEach<RequestReassignWonders> { session.reassignWonders(it.wonders) } - scope.launchOnEach<RequestStartGame> { session.startGame() } - - scope.launchOnEach<RequestSayReady> { session.sayReady() } - scope.launchOnEach<RequestPrepareMove> { session.prepareMove(it.move) } - scope.launchOnEach<RequestUnprepareMove> { session.unprepareMove() } - scope.launchOnEach<RequestLeaveGame> { session.leaveGame() } -} - -private fun SwSagaContext.launchApiEventHandlersIn(scope: CoroutineScope, session: SevenWondersSession) { - scope.launch { - session.watchGameEvents().collect { event -> - when (event) { - is GameEvent.NameChosen -> { - dispatch(SetCurrentPlayerAction(event.player)) - dispatch(Navigate(SwRoute.GAME_BROWSER)) - } - is GameEvent.LobbyJoined -> { - dispatch(EnterLobbyAction(event.lobby)) - dispatch(Navigate(SwRoute.LOBBY)) - } - is GameEvent.LobbyUpdated -> { - dispatch(UpdateLobbyAction(event.lobby)) - } - GameEvent.LobbyLeft -> { - dispatch(LeaveLobbyAction) - dispatch(Navigate(SwRoute.GAME_BROWSER)) - } - is GameEvent.GameStarted -> { - val currentLobby = reduxState.currentLobby ?: error("Received game started event without being in a lobby") - dispatch(EnterGameAction(currentLobby, event.turnInfo)) - dispatch(Navigate(SwRoute.GAME)) - } - is GameEvent.NewTurnStarted -> dispatch(TurnInfoEvent(event.turnInfo)) - is GameEvent.MovePrepared -> dispatch(PreparedMoveEvent(event.move)) - is GameEvent.CardPrepared -> dispatch(PreparedCardEvent(event.preparedCard)) - is GameEvent.PlayerIsReady -> dispatch(PlayerReadyEvent(event.username)) - // Currently the move is already unprepared when launching the unprepare request - // TODO add a "unpreparing" state and only update redux when the move is successfully unprepared - GameEvent.MoveUnprepared -> {} - } - } - } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFramework.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFramework.kt deleted file mode 100644 index 05c03b13..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFramework.kt +++ /dev/null @@ -1,106 +0,0 @@ -package org.luxons.sevenwonders.ui.redux.sagas - -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* -import kotlinx.coroutines.flow.* -import redux.Middleware -import redux.MiddlewareApi -import redux.RAction - -class SagaManager<S, A : RAction, R>( - private val monitor: ((A) -> Unit)? = null, -) { - private lateinit var context: SagaContext<S, A, R> - - private val actions = MutableSharedFlow<A>(extraBufferCapacity = Channel.UNLIMITED) - - fun createMiddleware(): Middleware<S, A, R, A, R> = ::sagasMiddleware - - private fun sagasMiddleware(api: MiddlewareApi<S, A, R>): ((A) -> R) -> (A) -> R { - context = SagaContext(api, actions) - return { nextDispatch -> - { action -> - onActionDispatched(action) - val result = nextDispatch(action) - handleAction(action) - result - } - } - } - - private fun onActionDispatched(action: A) { - monitor?.invoke(action) - } - - private fun handleAction(action: A) { - val emitted = actions.tryEmit(action) - if (!emitted) { - // should never happen since our buffer is 'unlimited' (in reality it's Int.MAX_VALUE) - error("Couldn't dispatch redux action, buffer is full") - } - } - - fun launchSaga(coroutineScope: CoroutineScope, saga: suspend SagaContext<S, A, R>.() -> Unit): Job { - checkMiddlewareApplied() - return coroutineScope.launch { - context.saga() - } - } - - suspend fun runSaga(saga: suspend SagaContext<S, A, R>.() -> Unit) { - checkMiddlewareApplied() - context.saga() - } - - private fun checkMiddlewareApplied() { - check(::context.isInitialized) { - "Before running a Saga, you must mount the Saga middleware on the Store using applyMiddleware" - } - } -} - -class SagaContext<S, A : RAction, R>( - private val reduxApi: MiddlewareApi<S, A, R>, - val reduxActions: SharedFlow<A>, -) { - /** - * The current redux state. - */ - val reduxState: S - get() = reduxApi.getState() - - /** - * Dispatches the given redux [action]. - */ - fun dispatch(action: A) { - reduxApi.dispatch(action) - } - - /** - * Executes [handle] on every action dispatched of the type [T]. This runs forever until the current coroutine is - * cancelled. - */ - suspend inline fun <reified T : A> onEach( - crossinline handle: suspend SagaContext<S, A, R>.(T) -> Unit, - ) { - reduxActions.filterIsInstance<T>().collect { handle(it) } - } - - /** - * Launches a coroutine in the receiver scope that executes [handle] on every action dispatched of the type [T]. - * The returned [Job] can be used to cancel that coroutine (just like a regular [launch]) - */ - inline fun <reified T : A> CoroutineScope.launchOnEach( - crossinline handle: suspend SagaContext<S, A, R>.(T) -> Unit, - ): Job = launch { onEach(handle) } - - /** - * Suspends until the next action matching the given [predicate] is dispatched, and returns that action. - */ - suspend fun next(predicate: (A) -> Boolean): A = reduxActions.first { predicate(it) } - - /** - * Suspends until the next action of type [T] is dispatched, and returns that action. - */ - suspend inline fun <reified T : A> next(): T = reduxActions.filterIsInstance<T>().first() -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/router/Router.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/router/Router.kt deleted file mode 100644 index 1a0840cf..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/router/Router.kt +++ /dev/null @@ -1,48 +0,0 @@ -package org.luxons.sevenwonders.ui.router - -import kotlinx.browser.window -import kotlinx.coroutines.Job -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import org.luxons.sevenwonders.ui.redux.sagas.SwSagaContext -import redux.RAction - -enum class SwRoute(val path: String) { - HOME("/"), - GAME_BROWSER("/games"), - LOBBY("/lobby"), - GAME("/game"); - - companion object { - private val all = values().associateBy { it.path } - - fun from(path: String) = all.getValue(path) - } -} - -data class Navigate(val route: SwRoute) : RAction - -suspend fun SwSagaContext.routerSaga( - startRoute: SwRoute, - runRouteSaga: suspend SwSagaContext.(SwRoute) -> Unit, -) { - coroutineScope { - window.location.hash = startRoute.path - launch { changeRouteOnNavigateAction() } - var currentSaga: Job = launch { runRouteSaga(startRoute) } - window.onhashchange = { event -> - val route = SwRoute.from(event.newURL.substringAfter("#")) - currentSaga.cancel() - currentSaga = this@coroutineScope.launch { - runRouteSaga(route) - } - Unit - } - } -} - -suspend fun SwSagaContext.changeRouteOnNavigateAction() { - onEach<Navigate> { - window.location.hash = it.route.path - } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/utils/CoroutinesUtils.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/utils/CoroutinesUtils.kt deleted file mode 100644 index 600f08d3..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/utils/CoroutinesUtils.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.luxons.sevenwonders.ui.utils - -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.selects.select - -// Cannot inline or it crashes for some reason -suspend fun <R> awaitFirst(f1: suspend () -> R, f2: suspend () -> R): R = coroutineScope { - val deferred1 = async { f1() } - val deferred2 = async { f2() } - select<R> { - deferred1.onAwait { deferred2.cancel(); it } - deferred2.onAwait { deferred1.cancel(); it } - } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/utils/StyleUtils.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/utils/StyleUtils.kt deleted file mode 100644 index 7ca67be4..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/utils/StyleUtils.kt +++ /dev/null @@ -1,43 +0,0 @@ -package org.luxons.sevenwonders.ui.utils - -import csstype.* -import js.core.* -import react.dom.html.* -import web.cssom.* - -/** - * The cubic-bezier() function defines a Cubic Bezier curve. - * - * A Cubic Bezier curve is defined by four points P0, P1, P2, and P3. P0 and P3 are the start and the end of the curve - * and, in CSS these points are fixed as the coordinates are ratios. P0 is (0, 0) and represents the initial time and - * the initial state, P3 is (1, 1) and represents the final time and the final state. - * - * The x coordinates provided here must be between 0 and 1 (the bezier curve points should be between the start time - * and end time, giving other values would make the curve go back in the past or further into the future). - * - * The y coordinates may be any value: the intermediate states can be below or above the start (0) or end (1) values. - */ -fun cubicBezier(x1: Double, y1: Double, x2: Double, y2: Double) = - "cubic-bezier($x1, $y1, $x2, $y2)".unsafeCast<AnimationTimingFunction>() - -fun Margin(all: AutoLength) = Margin(vertical = all, horizontal = all) - -fun Padding(all: Length) = Padding(vertical = all, horizontal = all) - -// this should work because NamedColor is ultimately a hex string in JS, not the actual name -fun NamedColor.withAlpha(alpha: Double) = "$this${(alpha * 255).toInt().toString(16)}".unsafeCast<BackgroundColor>() - -operator fun FilterFunction.plus(other: FilterFunction) = "$this $other".unsafeCast<FilterFunction>() - -fun PropertiesBuilder.ancestorHover(selector: String, block: PropertiesBuilder.() -> Unit) = - "$selector:hover &".invoke(block) - -fun PropertiesBuilder.children(selector: String, block: PropertiesBuilder.() -> Unit) = - "& > $selector".invoke(block) - -fun PropertiesBuilder.descendants(selector: String, block: PropertiesBuilder.() -> Unit) = - "& $selector".invoke(block) - -fun HTMLAttributes<*>.inlineStyles(block: PropertiesBuilder.() -> Unit) { - style = jso(block) -} diff --git a/sw-ui/src/main/kotlin/webpack/WebpackUtils.kt b/sw-ui/src/main/kotlin/webpack/WebpackUtils.kt deleted file mode 100644 index dde1140a..00000000 --- a/sw-ui/src/main/kotlin/webpack/WebpackUtils.kt +++ /dev/null @@ -1,9 +0,0 @@ -package webpack - -external val process: Process - -external interface Process { - val env: dynamic -} - -fun isProdEnv(): Boolean = process.env.NODE_ENV == "production" |