From cdaae5279e3f53c146df2500a8d7a1d4eae2f674 Mon Sep 17 00:00:00 2001 From: Joffrey Bion Date: Thu, 6 Jul 2023 00:16:45 +0200 Subject: Convert sw-ui module to Kotlin Multiplatform gradle plugin --- sw-ui/build.gradle.kts | 22 +- .../org/luxons/sevenwonders/ui/SevenWondersUi.kt | 49 ++ .../sevenwonders/ui/components/Application.kt | 52 ++ .../sevenwonders/ui/components/GlobalStyles.kt | 48 ++ .../ui/components/errors/ErrorDialog.kt | 57 +++ .../sevenwonders/ui/components/game/Board.kt | 227 +++++++++ .../ui/components/game/BoardSummary.kt | 211 ++++++++ .../sevenwonders/ui/components/game/CardImage.kt | 78 +++ .../sevenwonders/ui/components/game/GameScene.kt | 310 ++++++++++++ .../sevenwonders/ui/components/game/GameStyles.kt | 86 ++++ .../luxons/sevenwonders/ui/components/game/Hand.kt | 276 +++++++++++ .../ui/components/game/HandRotationIndicator.kt | 56 +++ .../components/game/PlayerPreparedCardPresenter.kt | 80 +++ .../ui/components/game/PreparedMove.kt | 73 +++ .../sevenwonders/ui/components/game/ScoreTable.kt | 188 +++++++ .../sevenwonders/ui/components/game/Tokens.kt | 155 ++++++ .../ui/components/game/TransactionsSelector.kt | 265 ++++++++++ .../ui/components/gameBrowser/CreateGameForm.kt | 58 +++ .../ui/components/gameBrowser/GameBrowser.kt | 69 +++ .../ui/components/gameBrowser/GameList.kt | 213 ++++++++ .../ui/components/gameBrowser/PlayerInfo.kt | 105 ++++ .../ui/components/home/ChooseNameForm.kt | 65 +++ .../luxons/sevenwonders/ui/components/home/Home.kt | 22 + .../sevenwonders/ui/components/home/HomeStyles.kt | 15 + .../sevenwonders/ui/components/lobby/Lobby.kt | 272 ++++++++++ .../ui/components/lobby/LobbyStyles.kt | 20 + .../sevenwonders/ui/components/lobby/RadialList.kt | 117 +++++ .../sevenwonders/ui/components/lobby/RadialMath.kt | 57 +++ .../ui/components/lobby/RadialPlayerList.kt | 139 ++++++ .../sevenwonders/ui/components/lobby/Table.kt | 97 ++++ .../sevenwonders/ui/names/RandomNameGenerator.kt | 546 +++++++++++++++++++++ .../org/luxons/sevenwonders/ui/redux/Actions.kt | 32 ++ .../org/luxons/sevenwonders/ui/redux/ApiActions.kt | 34 ++ .../org/luxons/sevenwonders/ui/redux/Reducers.kt | 95 ++++ .../org/luxons/sevenwonders/ui/redux/Store.kt | 29 ++ .../org/luxons/sevenwonders/ui/redux/Utils.kt | 31 ++ .../sevenwonders/ui/redux/sagas/RouteBasedSagas.kt | 44 ++ .../luxons/sevenwonders/ui/redux/sagas/Sagas.kt | 131 +++++ .../sevenwonders/ui/redux/sagas/SagasFramework.kt | 106 ++++ .../org/luxons/sevenwonders/ui/router/Router.kt | 48 ++ .../sevenwonders/ui/utils/CoroutinesUtils.kt | 15 + .../org/luxons/sevenwonders/ui/utils/StyleUtils.kt | 43 ++ sw-ui/src/jsMain/kotlin/webpack/WebpackUtils.kt | 9 + sw-ui/src/jsMain/resources/favicon.ico | Bin 0 -> 24838 bytes .../resources/images/backgrounds/papyrus.jpg | Bin 0 -> 480677 bytes .../resources/images/backgrounds/zeus-temple.jpg | Bin 0 -> 571089 bytes .../src/jsMain/resources/images/cards/academy.png | Bin 0 -> 87620 bytes sw-ui/src/jsMain/resources/images/cards/altar.png | Bin 0 -> 80843 bytes .../jsMain/resources/images/cards/apothecary.png | Bin 0 -> 88905 bytes .../src/jsMain/resources/images/cards/aqueduct.png | Bin 0 -> 90765 bytes .../jsMain/resources/images/cards/archeryrange.png | Bin 0 -> 86327 bytes sw-ui/src/jsMain/resources/images/cards/arena.png | Bin 0 -> 84837 bytes .../src/jsMain/resources/images/cards/arsenal.png | Bin 0 -> 88257 bytes .../jsMain/resources/images/cards/back/age1.png | Bin 0 -> 67850 bytes .../jsMain/resources/images/cards/back/age2.png | Bin 0 -> 68501 bytes .../jsMain/resources/images/cards/back/age3.png | Bin 0 -> 63391 bytes .../resources/images/cards/back/placeholder.png | Bin 0 -> 9622 bytes .../src/jsMain/resources/images/cards/barracks.png | Bin 0 -> 83840 bytes sw-ui/src/jsMain/resources/images/cards/baths.png | Bin 0 -> 84236 bytes sw-ui/src/jsMain/resources/images/cards/bazar.png | Bin 0 -> 80862 bytes .../jsMain/resources/images/cards/brickyard.png | Bin 0 -> 79194 bytes .../resources/images/cards/buildersguild.png | Bin 0 -> 86054 bytes .../jsMain/resources/images/cards/caravansery.png | Bin 0 -> 85841 bytes .../resources/images/cards/chamberofcommerce.png | Bin 0 -> 89136 bytes sw-ui/src/jsMain/resources/images/cards/circus.png | Bin 0 -> 95879 bytes .../src/jsMain/resources/images/cards/claypit.png | Bin 0 -> 78992 bytes .../src/jsMain/resources/images/cards/claypool.png | Bin 0 -> 76294 bytes .../jsMain/resources/images/cards/courthouse.png | Bin 0 -> 82399 bytes .../resources/images/cards/craftsmensguild.png | Bin 0 -> 90528 bytes .../jsMain/resources/images/cards/dispensary.png | Bin 0 -> 86175 bytes .../resources/images/cards/easttradingpost.png | Bin 0 -> 88611 bytes .../jsMain/resources/images/cards/excavation.png | Bin 0 -> 82667 bytes .../jsMain/resources/images/cards/forestcave.png | Bin 0 -> 75845 bytes .../resources/images/cards/fortifications.png | Bin 0 -> 85633 bytes sw-ui/src/jsMain/resources/images/cards/forum.png | Bin 0 -> 85713 bytes .../src/jsMain/resources/images/cards/foundry.png | Bin 0 -> 78894 bytes .../src/jsMain/resources/images/cards/gardens.png | Bin 0 -> 85889 bytes .../jsMain/resources/images/cards/glassworks.png | Bin 0 -> 81916 bytes .../jsMain/resources/images/cards/guardtower.png | Bin 0 -> 77432 bytes sw-ui/src/jsMain/resources/images/cards/haven.png | Bin 0 -> 93143 bytes .../jsMain/resources/images/cards/laboratory.png | Bin 0 -> 87869 bytes .../src/jsMain/resources/images/cards/library.png | Bin 0 -> 80338 bytes .../jsMain/resources/images/cards/lighthouse.png | Bin 0 -> 79746 bytes sw-ui/src/jsMain/resources/images/cards/lodge.png | Bin 0 -> 76021 bytes sw-ui/src/jsMain/resources/images/cards/loom.png | Bin 0 -> 85480 bytes .../jsMain/resources/images/cards/lumberyard.png | Bin 0 -> 83067 bytes .../resources/images/cards/magistratesguild.png | Bin 0 -> 88073 bytes .../jsMain/resources/images/cards/marketplace.png | Bin 0 -> 89816 bytes sw-ui/src/jsMain/resources/images/cards/mine.png | Bin 0 -> 83500 bytes .../jsMain/resources/images/cards/observatory.png | Bin 0 -> 81745 bytes .../src/jsMain/resources/images/cards/orevein.png | Bin 0 -> 82176 bytes sw-ui/src/jsMain/resources/images/cards/palace.png | Bin 0 -> 85097 bytes .../src/jsMain/resources/images/cards/pantheon.png | Bin 0 -> 83290 bytes .../src/jsMain/resources/images/cards/pawnshop.png | Bin 0 -> 83440 bytes .../resources/images/cards/philosophersguild.png | Bin 0 -> 89645 bytes sw-ui/src/jsMain/resources/images/cards/press.png | Bin 0 -> 88277 bytes sw-ui/src/jsMain/resources/images/cards/quarry.png | Bin 0 -> 77177 bytes .../src/jsMain/resources/images/cards/sawmill.png | Bin 0 -> 80987 bytes sw-ui/src/jsMain/resources/images/cards/school.png | Bin 0 -> 80260 bytes .../resources/images/cards/scientistsguild.png | Bin 0 -> 86768 bytes .../jsMain/resources/images/cards/scriptorium.png | Bin 0 -> 84987 bytes sw-ui/src/jsMain/resources/images/cards/senate.png | Bin 0 -> 91055 bytes .../resources/images/cards/shipownersguild.png | Bin 0 -> 86836 bytes .../resources/images/cards/siegeworkshop.png | Bin 0 -> 89072 bytes .../jsMain/resources/images/cards/spiesguild.png | Bin 0 -> 83823 bytes .../src/jsMain/resources/images/cards/stables.png | Bin 0 -> 85649 bytes sw-ui/src/jsMain/resources/images/cards/statue.png | Bin 0 -> 83639 bytes .../src/jsMain/resources/images/cards/stockade.png | Bin 0 -> 70706 bytes .../src/jsMain/resources/images/cards/stonepit.png | Bin 0 -> 84418 bytes .../resources/images/cards/strategistsguild.png | Bin 0 -> 86575 bytes sw-ui/src/jsMain/resources/images/cards/study.png | Bin 0 -> 84016 bytes sw-ui/src/jsMain/resources/images/cards/tavern.png | Bin 0 -> 81229 bytes sw-ui/src/jsMain/resources/images/cards/temple.png | Bin 0 -> 78057 bytes .../src/jsMain/resources/images/cards/theater.png | Bin 0 -> 89703 bytes .../jsMain/resources/images/cards/timberyard.png | Bin 0 -> 82874 bytes .../src/jsMain/resources/images/cards/townhall.png | Bin 0 -> 84439 bytes .../jsMain/resources/images/cards/tradersguild.png | Bin 0 -> 88057 bytes .../resources/images/cards/trainingground.png | Bin 0 -> 84102 bytes .../src/jsMain/resources/images/cards/treefarm.png | Bin 0 -> 88252 bytes .../jsMain/resources/images/cards/university.png | Bin 0 -> 74203 bytes .../src/jsMain/resources/images/cards/vineyard.png | Bin 0 -> 81329 bytes sw-ui/src/jsMain/resources/images/cards/walls.png | Bin 0 -> 83027 bytes .../resources/images/cards/westtradingpost.png | Bin 0 -> 90680 bytes .../jsMain/resources/images/cards/workersguild.png | Bin 0 -> 84595 bytes .../src/jsMain/resources/images/cards/workshop.png | Bin 0 -> 82116 bytes sw-ui/src/jsMain/resources/images/gear-50.png | Bin 0 -> 6260 bytes sw-ui/src/jsMain/resources/images/hand-cards5.png | Bin 0 -> 5519 bytes .../src/jsMain/resources/images/logo-7-wonders.png | Bin 0 -> 301442 bytes sw-ui/src/jsMain/resources/images/tokens/coin.png | Bin 0 -> 4515 bytes .../jsMain/resources/images/tokens/laurel-blue.png | Bin 0 -> 13534 bytes .../resources/images/tokens/military/defeat1.png | Bin 0 -> 3215 bytes .../resources/images/tokens/military/shield.png | Bin 0 -> 17253 bytes .../resources/images/tokens/military/victory1.png | Bin 0 -> 3676 bytes .../resources/images/tokens/resources/clay.png | Bin 0 -> 19566 bytes .../resources/images/tokens/resources/glass.png | Bin 0 -> 20961 bytes .../resources/images/tokens/resources/loom.png | Bin 0 -> 21053 bytes .../resources/images/tokens/resources/ore.png | Bin 0 -> 21524 bytes .../resources/images/tokens/resources/papyrus.png | Bin 0 -> 22695 bytes .../resources/images/tokens/resources/stone.png | Bin 0 -> 21516 bytes .../resources/images/tokens/resources/wood.png | Bin 0 -> 21642 bytes .../jsMain/resources/images/tokens/science/cog.png | Bin 0 -> 13071 bytes .../resources/images/tokens/science/compass.png | Bin 0 -> 9720 bytes .../resources/images/tokens/science/tablet.png | Bin 0 -> 12438 bytes .../resources/images/wonder-upgrade-bright.png | Bin 0 -> 26860 bytes .../resources/images/wonders/alexandriaA.png | Bin 0 -> 500078 bytes .../resources/images/wonders/alexandriaB.png | Bin 0 -> 503329 bytes .../jsMain/resources/images/wonders/babylonA.png | Bin 0 -> 560943 bytes .../jsMain/resources/images/wonders/babylonB.png | Bin 0 -> 560104 bytes .../jsMain/resources/images/wonders/ephesosA.png | Bin 0 -> 547227 bytes .../jsMain/resources/images/wonders/ephesosB.png | Bin 0 -> 550456 bytes .../resources/images/wonders/extra/agrigentoA.jpg | Bin 0 -> 705403 bytes .../resources/images/wonders/extra/angkorwatA.jpg | Bin 0 -> 930685 bytes .../resources/images/wonders/extra/angkorwatB.jpg | Bin 0 -> 987688 bytes .../resources/images/wonders/extra/avalonA.jpg | Bin 0 -> 658280 bytes .../resources/images/wonders/extra/ctesiphonB.jpg | Bin 0 -> 692738 bytes .../resources/images/wonders/extra/iramA.jpg | Bin 0 -> 734054 bytes .../resources/images/wonders/extra/persepolisA.jpg | Bin 0 -> 1057711 bytes .../resources/images/wonders/extra/romaA.jpg | Bin 0 -> 200076 bytes .../resources/images/wonders/extra/sangri-laA.jpg | Bin 0 -> 682795 bytes .../resources/images/wonders/extra/spahanA.jpg | Bin 0 -> 774749 bytes .../images/wonders/extra/the-great-wallA.jpg | Bin 0 -> 72721 bytes .../resources/images/wonders/extra/veniseA.jpg | Bin 0 -> 945317 bytes .../resources/images/wonders/extra/veniseB.jpg | Bin 0 -> 941879 bytes .../src/jsMain/resources/images/wonders/gizahA.png | Bin 0 -> 490115 bytes .../src/jsMain/resources/images/wonders/gizahB.png | Bin 0 -> 500706 bytes .../resources/images/wonders/halikarnassusA.png | Bin 0 -> 470957 bytes .../resources/images/wonders/halikarnassusB.png | Bin 0 -> 487620 bytes .../jsMain/resources/images/wonders/olympiaA.png | Bin 0 -> 557107 bytes .../jsMain/resources/images/wonders/olympiaB.png | Bin 0 -> 558174 bytes .../jsMain/resources/images/wonders/rhodosA.png | Bin 0 -> 578363 bytes .../jsMain/resources/images/wonders/rhodosB.png | Bin 0 -> 575123 bytes sw-ui/src/jsMain/resources/index.html | 19 + .../ui/redux/sagas/SagasFrameworkTest.kt | 89 ++++ .../sevenwonders/ui/utils/CoroutineUtilsTest.kt | 24 + .../org/luxons/sevenwonders/ui/SevenWondersUi.kt | 47 -- .../sevenwonders/ui/components/Application.kt | 52 -- .../sevenwonders/ui/components/GlobalStyles.kt | 48 -- .../ui/components/errors/ErrorDialog.kt | 57 --- .../sevenwonders/ui/components/game/Board.kt | 227 --------- .../ui/components/game/BoardSummary.kt | 211 -------- .../sevenwonders/ui/components/game/CardImage.kt | 78 --- .../sevenwonders/ui/components/game/GameScene.kt | 310 ------------ .../sevenwonders/ui/components/game/GameStyles.kt | 86 ---- .../luxons/sevenwonders/ui/components/game/Hand.kt | 276 ----------- .../ui/components/game/HandRotationIndicator.kt | 56 --- .../components/game/PlayerPreparedCardPresenter.kt | 80 --- .../ui/components/game/PreparedMove.kt | 73 --- .../sevenwonders/ui/components/game/ScoreTable.kt | 188 ------- .../sevenwonders/ui/components/game/Tokens.kt | 155 ------ .../ui/components/game/TransactionsSelector.kt | 265 ---------- .../ui/components/gameBrowser/CreateGameForm.kt | 58 --- .../ui/components/gameBrowser/GameBrowser.kt | 69 --- .../ui/components/gameBrowser/GameList.kt | 213 -------- .../ui/components/gameBrowser/PlayerInfo.kt | 105 ---- .../ui/components/home/ChooseNameForm.kt | 65 --- .../luxons/sevenwonders/ui/components/home/Home.kt | 22 - .../sevenwonders/ui/components/home/HomeStyles.kt | 15 - .../sevenwonders/ui/components/lobby/Lobby.kt | 272 ---------- .../ui/components/lobby/LobbyStyles.kt | 20 - .../sevenwonders/ui/components/lobby/RadialList.kt | 117 ----- .../sevenwonders/ui/components/lobby/RadialMath.kt | 57 --- .../ui/components/lobby/RadialPlayerList.kt | 139 ------ .../sevenwonders/ui/components/lobby/Table.kt | 97 ---- .../sevenwonders/ui/names/RandomNameGenerator.kt | 546 --------------------- .../org/luxons/sevenwonders/ui/redux/Actions.kt | 32 -- .../org/luxons/sevenwonders/ui/redux/ApiActions.kt | 34 -- .../org/luxons/sevenwonders/ui/redux/Reducers.kt | 95 ---- .../org/luxons/sevenwonders/ui/redux/Store.kt | 29 -- .../org/luxons/sevenwonders/ui/redux/Utils.kt | 31 -- .../sevenwonders/ui/redux/sagas/RouteBasedSagas.kt | 44 -- .../luxons/sevenwonders/ui/redux/sagas/Sagas.kt | 131 ----- .../sevenwonders/ui/redux/sagas/SagasFramework.kt | 106 ---- .../org/luxons/sevenwonders/ui/router/Router.kt | 48 -- .../sevenwonders/ui/utils/CoroutinesUtils.kt | 15 - .../org/luxons/sevenwonders/ui/utils/StyleUtils.kt | 43 -- sw-ui/src/main/kotlin/webpack/WebpackUtils.kt | 9 - sw-ui/src/main/resources/favicon.ico | Bin 24838 -> 0 bytes .../main/resources/images/backgrounds/papyrus.jpg | Bin 480677 -> 0 bytes .../resources/images/backgrounds/zeus-temple.jpg | Bin 571089 -> 0 bytes sw-ui/src/main/resources/images/cards/academy.png | Bin 87620 -> 0 bytes sw-ui/src/main/resources/images/cards/altar.png | Bin 80843 -> 0 bytes .../src/main/resources/images/cards/apothecary.png | Bin 88905 -> 0 bytes sw-ui/src/main/resources/images/cards/aqueduct.png | Bin 90765 -> 0 bytes .../main/resources/images/cards/archeryrange.png | Bin 86327 -> 0 bytes sw-ui/src/main/resources/images/cards/arena.png | Bin 84837 -> 0 bytes sw-ui/src/main/resources/images/cards/arsenal.png | Bin 88257 -> 0 bytes .../src/main/resources/images/cards/back/age1.png | Bin 67850 -> 0 bytes .../src/main/resources/images/cards/back/age2.png | Bin 68501 -> 0 bytes .../src/main/resources/images/cards/back/age3.png | Bin 63391 -> 0 bytes .../resources/images/cards/back/placeholder.png | Bin 9622 -> 0 bytes sw-ui/src/main/resources/images/cards/barracks.png | Bin 83840 -> 0 bytes sw-ui/src/main/resources/images/cards/baths.png | Bin 84236 -> 0 bytes sw-ui/src/main/resources/images/cards/bazar.png | Bin 80862 -> 0 bytes .../src/main/resources/images/cards/brickyard.png | Bin 79194 -> 0 bytes .../main/resources/images/cards/buildersguild.png | Bin 86054 -> 0 bytes .../main/resources/images/cards/caravansery.png | Bin 85841 -> 0 bytes .../resources/images/cards/chamberofcommerce.png | Bin 89136 -> 0 bytes sw-ui/src/main/resources/images/cards/circus.png | Bin 95879 -> 0 bytes sw-ui/src/main/resources/images/cards/claypit.png | Bin 78992 -> 0 bytes sw-ui/src/main/resources/images/cards/claypool.png | Bin 76294 -> 0 bytes .../src/main/resources/images/cards/courthouse.png | Bin 82399 -> 0 bytes .../resources/images/cards/craftsmensguild.png | Bin 90528 -> 0 bytes .../src/main/resources/images/cards/dispensary.png | Bin 86175 -> 0 bytes .../resources/images/cards/easttradingpost.png | Bin 88611 -> 0 bytes .../src/main/resources/images/cards/excavation.png | Bin 82667 -> 0 bytes .../src/main/resources/images/cards/forestcave.png | Bin 75845 -> 0 bytes .../main/resources/images/cards/fortifications.png | Bin 85633 -> 0 bytes sw-ui/src/main/resources/images/cards/forum.png | Bin 85713 -> 0 bytes sw-ui/src/main/resources/images/cards/foundry.png | Bin 78894 -> 0 bytes sw-ui/src/main/resources/images/cards/gardens.png | Bin 85889 -> 0 bytes .../src/main/resources/images/cards/glassworks.png | Bin 81916 -> 0 bytes .../src/main/resources/images/cards/guardtower.png | Bin 77432 -> 0 bytes sw-ui/src/main/resources/images/cards/haven.png | Bin 93143 -> 0 bytes .../src/main/resources/images/cards/laboratory.png | Bin 87869 -> 0 bytes sw-ui/src/main/resources/images/cards/library.png | Bin 80338 -> 0 bytes .../src/main/resources/images/cards/lighthouse.png | Bin 79746 -> 0 bytes sw-ui/src/main/resources/images/cards/lodge.png | Bin 76021 -> 0 bytes sw-ui/src/main/resources/images/cards/loom.png | Bin 85480 -> 0 bytes .../src/main/resources/images/cards/lumberyard.png | Bin 83067 -> 0 bytes .../resources/images/cards/magistratesguild.png | Bin 88073 -> 0 bytes .../main/resources/images/cards/marketplace.png | Bin 89816 -> 0 bytes sw-ui/src/main/resources/images/cards/mine.png | Bin 83500 -> 0 bytes .../main/resources/images/cards/observatory.png | Bin 81745 -> 0 bytes sw-ui/src/main/resources/images/cards/orevein.png | Bin 82176 -> 0 bytes sw-ui/src/main/resources/images/cards/palace.png | Bin 85097 -> 0 bytes sw-ui/src/main/resources/images/cards/pantheon.png | Bin 83290 -> 0 bytes sw-ui/src/main/resources/images/cards/pawnshop.png | Bin 83440 -> 0 bytes .../resources/images/cards/philosophersguild.png | Bin 89645 -> 0 bytes sw-ui/src/main/resources/images/cards/press.png | Bin 88277 -> 0 bytes sw-ui/src/main/resources/images/cards/quarry.png | Bin 77177 -> 0 bytes sw-ui/src/main/resources/images/cards/sawmill.png | Bin 80987 -> 0 bytes sw-ui/src/main/resources/images/cards/school.png | Bin 80260 -> 0 bytes .../resources/images/cards/scientistsguild.png | Bin 86768 -> 0 bytes .../main/resources/images/cards/scriptorium.png | Bin 84987 -> 0 bytes sw-ui/src/main/resources/images/cards/senate.png | Bin 91055 -> 0 bytes .../resources/images/cards/shipownersguild.png | Bin 86836 -> 0 bytes .../main/resources/images/cards/siegeworkshop.png | Bin 89072 -> 0 bytes .../src/main/resources/images/cards/spiesguild.png | Bin 83823 -> 0 bytes sw-ui/src/main/resources/images/cards/stables.png | Bin 85649 -> 0 bytes sw-ui/src/main/resources/images/cards/statue.png | Bin 83639 -> 0 bytes sw-ui/src/main/resources/images/cards/stockade.png | Bin 70706 -> 0 bytes sw-ui/src/main/resources/images/cards/stonepit.png | Bin 84418 -> 0 bytes .../resources/images/cards/strategistsguild.png | Bin 86575 -> 0 bytes sw-ui/src/main/resources/images/cards/study.png | Bin 84016 -> 0 bytes sw-ui/src/main/resources/images/cards/tavern.png | Bin 81229 -> 0 bytes sw-ui/src/main/resources/images/cards/temple.png | Bin 78057 -> 0 bytes sw-ui/src/main/resources/images/cards/theater.png | Bin 89703 -> 0 bytes .../src/main/resources/images/cards/timberyard.png | Bin 82874 -> 0 bytes sw-ui/src/main/resources/images/cards/townhall.png | Bin 84439 -> 0 bytes .../main/resources/images/cards/tradersguild.png | Bin 88057 -> 0 bytes .../main/resources/images/cards/trainingground.png | Bin 84102 -> 0 bytes sw-ui/src/main/resources/images/cards/treefarm.png | Bin 88252 -> 0 bytes .../src/main/resources/images/cards/university.png | Bin 74203 -> 0 bytes sw-ui/src/main/resources/images/cards/vineyard.png | Bin 81329 -> 0 bytes sw-ui/src/main/resources/images/cards/walls.png | Bin 83027 -> 0 bytes .../resources/images/cards/westtradingpost.png | Bin 90680 -> 0 bytes .../main/resources/images/cards/workersguild.png | Bin 84595 -> 0 bytes sw-ui/src/main/resources/images/cards/workshop.png | Bin 82116 -> 0 bytes sw-ui/src/main/resources/images/gear-50.png | Bin 6260 -> 0 bytes sw-ui/src/main/resources/images/hand-cards5.png | Bin 5519 -> 0 bytes sw-ui/src/main/resources/images/logo-7-wonders.png | Bin 301442 -> 0 bytes sw-ui/src/main/resources/images/tokens/coin.png | Bin 4515 -> 0 bytes .../main/resources/images/tokens/laurel-blue.png | Bin 13534 -> 0 bytes .../resources/images/tokens/military/defeat1.png | Bin 3215 -> 0 bytes .../resources/images/tokens/military/shield.png | Bin 17253 -> 0 bytes .../resources/images/tokens/military/victory1.png | Bin 3676 -> 0 bytes .../resources/images/tokens/resources/clay.png | Bin 19566 -> 0 bytes .../resources/images/tokens/resources/glass.png | Bin 20961 -> 0 bytes .../resources/images/tokens/resources/loom.png | Bin 21053 -> 0 bytes .../main/resources/images/tokens/resources/ore.png | Bin 21524 -> 0 bytes .../resources/images/tokens/resources/papyrus.png | Bin 22695 -> 0 bytes .../resources/images/tokens/resources/stone.png | Bin 21516 -> 0 bytes .../resources/images/tokens/resources/wood.png | Bin 21642 -> 0 bytes .../main/resources/images/tokens/science/cog.png | Bin 13071 -> 0 bytes .../resources/images/tokens/science/compass.png | Bin 9720 -> 0 bytes .../resources/images/tokens/science/tablet.png | Bin 12438 -> 0 bytes .../resources/images/wonder-upgrade-bright.png | Bin 26860 -> 0 bytes .../main/resources/images/wonders/alexandriaA.png | Bin 500078 -> 0 bytes .../main/resources/images/wonders/alexandriaB.png | Bin 503329 -> 0 bytes .../src/main/resources/images/wonders/babylonA.png | Bin 560943 -> 0 bytes .../src/main/resources/images/wonders/babylonB.png | Bin 560104 -> 0 bytes .../src/main/resources/images/wonders/ephesosA.png | Bin 547227 -> 0 bytes .../src/main/resources/images/wonders/ephesosB.png | Bin 550456 -> 0 bytes .../resources/images/wonders/extra/agrigentoA.jpg | Bin 705403 -> 0 bytes .../resources/images/wonders/extra/angkorwatA.jpg | Bin 930685 -> 0 bytes .../resources/images/wonders/extra/angkorwatB.jpg | Bin 987688 -> 0 bytes .../resources/images/wonders/extra/avalonA.jpg | Bin 658280 -> 0 bytes .../resources/images/wonders/extra/ctesiphonB.jpg | Bin 692738 -> 0 bytes .../main/resources/images/wonders/extra/iramA.jpg | Bin 734054 -> 0 bytes .../resources/images/wonders/extra/persepolisA.jpg | Bin 1057711 -> 0 bytes .../main/resources/images/wonders/extra/romaA.jpg | Bin 200076 -> 0 bytes .../resources/images/wonders/extra/sangri-laA.jpg | Bin 682795 -> 0 bytes .../resources/images/wonders/extra/spahanA.jpg | Bin 774749 -> 0 bytes .../images/wonders/extra/the-great-wallA.jpg | Bin 72721 -> 0 bytes .../resources/images/wonders/extra/veniseA.jpg | Bin 945317 -> 0 bytes .../resources/images/wonders/extra/veniseB.jpg | Bin 941879 -> 0 bytes sw-ui/src/main/resources/images/wonders/gizahA.png | Bin 490115 -> 0 bytes sw-ui/src/main/resources/images/wonders/gizahB.png | Bin 500706 -> 0 bytes .../resources/images/wonders/halikarnassusA.png | Bin 470957 -> 0 bytes .../resources/images/wonders/halikarnassusB.png | Bin 487620 -> 0 bytes .../src/main/resources/images/wonders/olympiaA.png | Bin 557107 -> 0 bytes .../src/main/resources/images/wonders/olympiaB.png | Bin 558174 -> 0 bytes .../src/main/resources/images/wonders/rhodosA.png | Bin 578363 -> 0 bytes .../src/main/resources/images/wonders/rhodosB.png | Bin 575123 -> 0 bytes sw-ui/src/main/resources/index.html | 19 - .../ui/redux/sagas/SagasFrameworkTest.kt | 89 ---- .../sevenwonders/ui/utils/CoroutineUtilsTest.kt | 24 - 347 files changed, 4765 insertions(+), 4765 deletions(-) create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/SevenWondersUi.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/Application.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/GlobalStyles.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/errors/ErrorDialog.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/Board.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/BoardSummary.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/CardImage.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/GameStyles.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/HandRotationIndicator.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/PlayerPreparedCardPresenter.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/PreparedMove.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/ScoreTable.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/Tokens.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/TransactionsSelector.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/CreateGameForm.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameBrowser.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameList.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/PlayerInfo.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/home/ChooseNameForm.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/home/Home.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/home/HomeStyles.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/lobby/Lobby.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/lobby/LobbyStyles.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialList.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialMath.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialPlayerList.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/lobby/Table.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/names/RandomNameGenerator.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/Actions.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/ApiActions.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/Store.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/Utils.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/sagas/RouteBasedSagas.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/sagas/Sagas.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFramework.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/router/Router.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/utils/CoroutinesUtils.kt create mode 100644 sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/utils/StyleUtils.kt create mode 100644 sw-ui/src/jsMain/kotlin/webpack/WebpackUtils.kt create mode 100644 sw-ui/src/jsMain/resources/favicon.ico create mode 100644 sw-ui/src/jsMain/resources/images/backgrounds/papyrus.jpg create mode 100644 sw-ui/src/jsMain/resources/images/backgrounds/zeus-temple.jpg create mode 100644 sw-ui/src/jsMain/resources/images/cards/academy.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/altar.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/apothecary.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/aqueduct.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/archeryrange.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/arena.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/arsenal.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/back/age1.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/back/age2.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/back/age3.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/back/placeholder.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/barracks.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/baths.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/bazar.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/brickyard.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/buildersguild.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/caravansery.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/chamberofcommerce.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/circus.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/claypit.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/claypool.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/courthouse.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/craftsmensguild.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/dispensary.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/easttradingpost.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/excavation.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/forestcave.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/fortifications.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/forum.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/foundry.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/gardens.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/glassworks.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/guardtower.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/haven.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/laboratory.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/library.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/lighthouse.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/lodge.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/loom.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/lumberyard.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/magistratesguild.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/marketplace.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/mine.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/observatory.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/orevein.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/palace.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/pantheon.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/pawnshop.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/philosophersguild.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/press.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/quarry.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/sawmill.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/school.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/scientistsguild.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/scriptorium.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/senate.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/shipownersguild.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/siegeworkshop.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/spiesguild.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/stables.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/statue.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/stockade.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/stonepit.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/strategistsguild.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/study.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/tavern.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/temple.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/theater.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/timberyard.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/townhall.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/tradersguild.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/trainingground.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/treefarm.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/university.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/vineyard.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/walls.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/westtradingpost.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/workersguild.png create mode 100644 sw-ui/src/jsMain/resources/images/cards/workshop.png create mode 100644 sw-ui/src/jsMain/resources/images/gear-50.png create mode 100644 sw-ui/src/jsMain/resources/images/hand-cards5.png create mode 100644 sw-ui/src/jsMain/resources/images/logo-7-wonders.png create mode 100644 sw-ui/src/jsMain/resources/images/tokens/coin.png create mode 100644 sw-ui/src/jsMain/resources/images/tokens/laurel-blue.png create mode 100644 sw-ui/src/jsMain/resources/images/tokens/military/defeat1.png create mode 100644 sw-ui/src/jsMain/resources/images/tokens/military/shield.png create mode 100644 sw-ui/src/jsMain/resources/images/tokens/military/victory1.png create mode 100644 sw-ui/src/jsMain/resources/images/tokens/resources/clay.png create mode 100644 sw-ui/src/jsMain/resources/images/tokens/resources/glass.png create mode 100644 sw-ui/src/jsMain/resources/images/tokens/resources/loom.png create mode 100644 sw-ui/src/jsMain/resources/images/tokens/resources/ore.png create mode 100644 sw-ui/src/jsMain/resources/images/tokens/resources/papyrus.png create mode 100644 sw-ui/src/jsMain/resources/images/tokens/resources/stone.png create mode 100644 sw-ui/src/jsMain/resources/images/tokens/resources/wood.png create mode 100644 sw-ui/src/jsMain/resources/images/tokens/science/cog.png create mode 100644 sw-ui/src/jsMain/resources/images/tokens/science/compass.png create mode 100644 sw-ui/src/jsMain/resources/images/tokens/science/tablet.png create mode 100644 sw-ui/src/jsMain/resources/images/wonder-upgrade-bright.png create mode 100644 sw-ui/src/jsMain/resources/images/wonders/alexandriaA.png create mode 100644 sw-ui/src/jsMain/resources/images/wonders/alexandriaB.png create mode 100644 sw-ui/src/jsMain/resources/images/wonders/babylonA.png create mode 100644 sw-ui/src/jsMain/resources/images/wonders/babylonB.png create mode 100644 sw-ui/src/jsMain/resources/images/wonders/ephesosA.png create mode 100644 sw-ui/src/jsMain/resources/images/wonders/ephesosB.png create mode 100644 sw-ui/src/jsMain/resources/images/wonders/extra/agrigentoA.jpg create mode 100644 sw-ui/src/jsMain/resources/images/wonders/extra/angkorwatA.jpg create mode 100644 sw-ui/src/jsMain/resources/images/wonders/extra/angkorwatB.jpg create mode 100644 sw-ui/src/jsMain/resources/images/wonders/extra/avalonA.jpg create mode 100644 sw-ui/src/jsMain/resources/images/wonders/extra/ctesiphonB.jpg create mode 100644 sw-ui/src/jsMain/resources/images/wonders/extra/iramA.jpg create mode 100644 sw-ui/src/jsMain/resources/images/wonders/extra/persepolisA.jpg create mode 100644 sw-ui/src/jsMain/resources/images/wonders/extra/romaA.jpg create mode 100644 sw-ui/src/jsMain/resources/images/wonders/extra/sangri-laA.jpg create mode 100644 sw-ui/src/jsMain/resources/images/wonders/extra/spahanA.jpg create mode 100644 sw-ui/src/jsMain/resources/images/wonders/extra/the-great-wallA.jpg create mode 100644 sw-ui/src/jsMain/resources/images/wonders/extra/veniseA.jpg create mode 100644 sw-ui/src/jsMain/resources/images/wonders/extra/veniseB.jpg create mode 100644 sw-ui/src/jsMain/resources/images/wonders/gizahA.png create mode 100644 sw-ui/src/jsMain/resources/images/wonders/gizahB.png create mode 100644 sw-ui/src/jsMain/resources/images/wonders/halikarnassusA.png create mode 100644 sw-ui/src/jsMain/resources/images/wonders/halikarnassusB.png create mode 100644 sw-ui/src/jsMain/resources/images/wonders/olympiaA.png create mode 100644 sw-ui/src/jsMain/resources/images/wonders/olympiaB.png create mode 100644 sw-ui/src/jsMain/resources/images/wonders/rhodosA.png create mode 100644 sw-ui/src/jsMain/resources/images/wonders/rhodosB.png create mode 100644 sw-ui/src/jsMain/resources/index.html create mode 100644 sw-ui/src/jsTest/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFrameworkTest.kt create mode 100644 sw-ui/src/jsTest/kotlin/org/luxons/sevenwonders/ui/utils/CoroutineUtilsTest.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/SevenWondersUi.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/Application.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/GlobalStyles.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/errors/ErrorDialog.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Board.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/BoardSummary.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/CardImage.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameStyles.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/HandRotationIndicator.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PlayerPreparedCardPresenter.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PreparedMove.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/ScoreTable.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Tokens.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/TransactionsSelector.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/CreateGameForm.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameBrowser.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameList.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/PlayerInfo.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/ChooseNameForm.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/Home.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/HomeStyles.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Lobby.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/LobbyStyles.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialList.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialMath.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialPlayerList.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Table.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/names/RandomNameGenerator.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Actions.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/ApiActions.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Store.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Utils.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/RouteBasedSagas.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/Sagas.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFramework.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/router/Router.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/utils/CoroutinesUtils.kt delete mode 100644 sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/utils/StyleUtils.kt delete mode 100644 sw-ui/src/main/kotlin/webpack/WebpackUtils.kt delete mode 100644 sw-ui/src/main/resources/favicon.ico delete mode 100644 sw-ui/src/main/resources/images/backgrounds/papyrus.jpg delete mode 100644 sw-ui/src/main/resources/images/backgrounds/zeus-temple.jpg delete mode 100644 sw-ui/src/main/resources/images/cards/academy.png delete mode 100644 sw-ui/src/main/resources/images/cards/altar.png delete mode 100644 sw-ui/src/main/resources/images/cards/apothecary.png delete mode 100644 sw-ui/src/main/resources/images/cards/aqueduct.png delete mode 100644 sw-ui/src/main/resources/images/cards/archeryrange.png delete mode 100644 sw-ui/src/main/resources/images/cards/arena.png delete mode 100644 sw-ui/src/main/resources/images/cards/arsenal.png delete mode 100644 sw-ui/src/main/resources/images/cards/back/age1.png delete mode 100644 sw-ui/src/main/resources/images/cards/back/age2.png delete mode 100644 sw-ui/src/main/resources/images/cards/back/age3.png delete mode 100644 sw-ui/src/main/resources/images/cards/back/placeholder.png delete mode 100644 sw-ui/src/main/resources/images/cards/barracks.png delete mode 100644 sw-ui/src/main/resources/images/cards/baths.png delete mode 100644 sw-ui/src/main/resources/images/cards/bazar.png delete mode 100644 sw-ui/src/main/resources/images/cards/brickyard.png delete mode 100644 sw-ui/src/main/resources/images/cards/buildersguild.png delete mode 100644 sw-ui/src/main/resources/images/cards/caravansery.png delete mode 100644 sw-ui/src/main/resources/images/cards/chamberofcommerce.png delete mode 100644 sw-ui/src/main/resources/images/cards/circus.png delete mode 100644 sw-ui/src/main/resources/images/cards/claypit.png delete mode 100644 sw-ui/src/main/resources/images/cards/claypool.png delete mode 100644 sw-ui/src/main/resources/images/cards/courthouse.png delete mode 100644 sw-ui/src/main/resources/images/cards/craftsmensguild.png delete mode 100644 sw-ui/src/main/resources/images/cards/dispensary.png delete mode 100644 sw-ui/src/main/resources/images/cards/easttradingpost.png delete mode 100644 sw-ui/src/main/resources/images/cards/excavation.png delete mode 100644 sw-ui/src/main/resources/images/cards/forestcave.png delete mode 100644 sw-ui/src/main/resources/images/cards/fortifications.png delete mode 100644 sw-ui/src/main/resources/images/cards/forum.png delete mode 100644 sw-ui/src/main/resources/images/cards/foundry.png delete mode 100644 sw-ui/src/main/resources/images/cards/gardens.png delete mode 100644 sw-ui/src/main/resources/images/cards/glassworks.png delete mode 100644 sw-ui/src/main/resources/images/cards/guardtower.png delete mode 100644 sw-ui/src/main/resources/images/cards/haven.png delete mode 100644 sw-ui/src/main/resources/images/cards/laboratory.png delete mode 100644 sw-ui/src/main/resources/images/cards/library.png delete mode 100644 sw-ui/src/main/resources/images/cards/lighthouse.png delete mode 100644 sw-ui/src/main/resources/images/cards/lodge.png delete mode 100644 sw-ui/src/main/resources/images/cards/loom.png delete mode 100644 sw-ui/src/main/resources/images/cards/lumberyard.png delete mode 100644 sw-ui/src/main/resources/images/cards/magistratesguild.png delete mode 100644 sw-ui/src/main/resources/images/cards/marketplace.png delete mode 100644 sw-ui/src/main/resources/images/cards/mine.png delete mode 100644 sw-ui/src/main/resources/images/cards/observatory.png delete mode 100644 sw-ui/src/main/resources/images/cards/orevein.png delete mode 100644 sw-ui/src/main/resources/images/cards/palace.png delete mode 100644 sw-ui/src/main/resources/images/cards/pantheon.png delete mode 100644 sw-ui/src/main/resources/images/cards/pawnshop.png delete mode 100644 sw-ui/src/main/resources/images/cards/philosophersguild.png delete mode 100644 sw-ui/src/main/resources/images/cards/press.png delete mode 100644 sw-ui/src/main/resources/images/cards/quarry.png delete mode 100644 sw-ui/src/main/resources/images/cards/sawmill.png delete mode 100644 sw-ui/src/main/resources/images/cards/school.png delete mode 100644 sw-ui/src/main/resources/images/cards/scientistsguild.png delete mode 100644 sw-ui/src/main/resources/images/cards/scriptorium.png delete mode 100644 sw-ui/src/main/resources/images/cards/senate.png delete mode 100644 sw-ui/src/main/resources/images/cards/shipownersguild.png delete mode 100644 sw-ui/src/main/resources/images/cards/siegeworkshop.png delete mode 100644 sw-ui/src/main/resources/images/cards/spiesguild.png delete mode 100644 sw-ui/src/main/resources/images/cards/stables.png delete mode 100644 sw-ui/src/main/resources/images/cards/statue.png delete mode 100644 sw-ui/src/main/resources/images/cards/stockade.png delete mode 100644 sw-ui/src/main/resources/images/cards/stonepit.png delete mode 100644 sw-ui/src/main/resources/images/cards/strategistsguild.png delete mode 100644 sw-ui/src/main/resources/images/cards/study.png delete mode 100644 sw-ui/src/main/resources/images/cards/tavern.png delete mode 100644 sw-ui/src/main/resources/images/cards/temple.png delete mode 100644 sw-ui/src/main/resources/images/cards/theater.png delete mode 100644 sw-ui/src/main/resources/images/cards/timberyard.png delete mode 100644 sw-ui/src/main/resources/images/cards/townhall.png delete mode 100644 sw-ui/src/main/resources/images/cards/tradersguild.png delete mode 100644 sw-ui/src/main/resources/images/cards/trainingground.png delete mode 100644 sw-ui/src/main/resources/images/cards/treefarm.png delete mode 100644 sw-ui/src/main/resources/images/cards/university.png delete mode 100644 sw-ui/src/main/resources/images/cards/vineyard.png delete mode 100644 sw-ui/src/main/resources/images/cards/walls.png delete mode 100644 sw-ui/src/main/resources/images/cards/westtradingpost.png delete mode 100644 sw-ui/src/main/resources/images/cards/workersguild.png delete mode 100644 sw-ui/src/main/resources/images/cards/workshop.png delete mode 100644 sw-ui/src/main/resources/images/gear-50.png delete mode 100644 sw-ui/src/main/resources/images/hand-cards5.png delete mode 100644 sw-ui/src/main/resources/images/logo-7-wonders.png delete mode 100644 sw-ui/src/main/resources/images/tokens/coin.png delete mode 100644 sw-ui/src/main/resources/images/tokens/laurel-blue.png delete mode 100644 sw-ui/src/main/resources/images/tokens/military/defeat1.png delete mode 100644 sw-ui/src/main/resources/images/tokens/military/shield.png delete mode 100644 sw-ui/src/main/resources/images/tokens/military/victory1.png delete mode 100644 sw-ui/src/main/resources/images/tokens/resources/clay.png delete mode 100644 sw-ui/src/main/resources/images/tokens/resources/glass.png delete mode 100644 sw-ui/src/main/resources/images/tokens/resources/loom.png delete mode 100644 sw-ui/src/main/resources/images/tokens/resources/ore.png delete mode 100644 sw-ui/src/main/resources/images/tokens/resources/papyrus.png delete mode 100644 sw-ui/src/main/resources/images/tokens/resources/stone.png delete mode 100644 sw-ui/src/main/resources/images/tokens/resources/wood.png delete mode 100644 sw-ui/src/main/resources/images/tokens/science/cog.png delete mode 100644 sw-ui/src/main/resources/images/tokens/science/compass.png delete mode 100644 sw-ui/src/main/resources/images/tokens/science/tablet.png delete mode 100644 sw-ui/src/main/resources/images/wonder-upgrade-bright.png delete mode 100644 sw-ui/src/main/resources/images/wonders/alexandriaA.png delete mode 100644 sw-ui/src/main/resources/images/wonders/alexandriaB.png delete mode 100644 sw-ui/src/main/resources/images/wonders/babylonA.png delete mode 100644 sw-ui/src/main/resources/images/wonders/babylonB.png delete mode 100644 sw-ui/src/main/resources/images/wonders/ephesosA.png delete mode 100644 sw-ui/src/main/resources/images/wonders/ephesosB.png delete mode 100644 sw-ui/src/main/resources/images/wonders/extra/agrigentoA.jpg delete mode 100644 sw-ui/src/main/resources/images/wonders/extra/angkorwatA.jpg delete mode 100644 sw-ui/src/main/resources/images/wonders/extra/angkorwatB.jpg delete mode 100644 sw-ui/src/main/resources/images/wonders/extra/avalonA.jpg delete mode 100644 sw-ui/src/main/resources/images/wonders/extra/ctesiphonB.jpg delete mode 100644 sw-ui/src/main/resources/images/wonders/extra/iramA.jpg delete mode 100644 sw-ui/src/main/resources/images/wonders/extra/persepolisA.jpg delete mode 100644 sw-ui/src/main/resources/images/wonders/extra/romaA.jpg delete mode 100644 sw-ui/src/main/resources/images/wonders/extra/sangri-laA.jpg delete mode 100644 sw-ui/src/main/resources/images/wonders/extra/spahanA.jpg delete mode 100644 sw-ui/src/main/resources/images/wonders/extra/the-great-wallA.jpg delete mode 100644 sw-ui/src/main/resources/images/wonders/extra/veniseA.jpg delete mode 100644 sw-ui/src/main/resources/images/wonders/extra/veniseB.jpg delete mode 100644 sw-ui/src/main/resources/images/wonders/gizahA.png delete mode 100644 sw-ui/src/main/resources/images/wonders/gizahB.png delete mode 100644 sw-ui/src/main/resources/images/wonders/halikarnassusA.png delete mode 100644 sw-ui/src/main/resources/images/wonders/halikarnassusB.png delete mode 100644 sw-ui/src/main/resources/images/wonders/olympiaA.png delete mode 100644 sw-ui/src/main/resources/images/wonders/olympiaB.png delete mode 100644 sw-ui/src/main/resources/images/wonders/rhodosA.png delete mode 100644 sw-ui/src/main/resources/images/wonders/rhodosB.png delete mode 100644 sw-ui/src/main/resources/index.html delete mode 100644 sw-ui/src/test/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFrameworkTest.kt delete mode 100644 sw-ui/src/test/kotlin/org/luxons/sevenwonders/ui/utils/CoroutineUtilsTest.kt (limited to 'sw-ui') diff --git a/sw-ui/build.gradle.kts b/sw-ui/build.gradle.kts index 0b736e34..2efb54ab 100644 --- a/sw-ui/build.gradle.kts +++ b/sw-ui/build.gradle.kts @@ -1,7 +1,7 @@ import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpack plugins { - kotlin("js") + kotlin("multiplatform") } kotlin { @@ -11,12 +11,11 @@ kotlin { useCommonJs() } sourceSets { - main { + val jsMain by getting { dependencies { implementation(projects.swClient) implementation(libs.kotlinx.coroutines.core) - implementation(libs.kotlinx.coroutines.test) implementation(platform(libs.kotlin.wrappers.bom.get())) implementation(libs.kotlin.wrappers.react.base) @@ -28,23 +27,22 @@ kotlin { implementation(libs.kotlin.wrappers.emotion) } } - test { + val jsTest by getting { dependencies { implementation(kotlin("test-js")) + implementation(libs.kotlinx.coroutines.test) } } } } -tasks { - "processResources"(ProcessResources::class) { - val webpack = project.tasks.withType(KotlinWebpack::class).first() +tasks.named("jsProcessResources") { + val webpack = project.tasks.withType(KotlinWebpack::class).first() - val bundleFile = webpack.outputFileName - val publicPath = "./" // TODO get public path from webpack config + val bundleFile = webpack.outputFileName + val publicPath = "./" // TODO get public path from webpack config - filesMatching("*.html") { - expand("bundle" to bundleFile, "publicPath" to publicPath) - } + filesMatching("*.html") { + expand("bundle" to bundleFile, "publicPath" to publicPath) } } diff --git a/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/SevenWondersUi.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/SevenWondersUi.kt new file mode 100644 index 00000000..44066b49 --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/SevenWondersUi.kt @@ -0,0 +1,49 @@ +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 = StrictMode.create { + Provider { + this.store = store + Application() + } + } + createRoot(rootElement).render(connectedApp) +} + +@OptIn(DelicateCoroutinesApi::class) +private fun initRedux(): Store { + val sagaManager = SagaManager() + val store = configureStore(sagaManager = sagaManager) + GlobalScope.launch { + sagaManager.launchSaga(this) { + rootSaga() + } + } + return store +} diff --git a/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/Application.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/Application.kt new file mode 100644 index 00000000..2cf8b4f1 --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/Application.kt @@ -0,0 +1,52 @@ +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/jsMain/kotlin/org/luxons/sevenwonders/ui/components/GlobalStyles.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/GlobalStyles.kt new file mode 100644 index 00000000..ee9c17ab --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/GlobalStyles.kt @@ -0,0 +1,48 @@ +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() + 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() + 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/jsMain/kotlin/org/luxons/sevenwonders/ui/components/errors/ErrorDialog.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/errors/ErrorDialog.kt new file mode 100644 index 00000000..c728d405 --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/errors/ErrorDialog.kt @@ -0,0 +1,57 @@ +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() + + 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("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/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/Board.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/Board.kt new file mode 100644 index 00000000..1eb5f6f0 --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/Board.kt @@ -0,0 +1,227 @@ +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("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>) { + 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 +} + +private val TableCardColumn = FC("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("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() + 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.() -> Unit = {}) { + boardToken("military/victory1", points, block) +} + +private fun ChildrenBuilder.defeatTokenCount(nbDefeatTokens: Int, block: HTMLAttributes.() -> Unit = {}) { + boardToken("military/defeat1", nbDefeatTokens, block) +} + +private fun ChildrenBuilder.boardToken(tokenName: String, count: Int, block: HTMLAttributes.() -> 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("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/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/BoardSummary.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/BoardSummary.kt new file mode 100644 index 00000000..37de113c --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/BoardSummary.kt @@ -0,0 +1,211 @@ +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("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("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/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/CardImage.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/CardImage.kt new file mode 100644 index 00000000..cffd509f --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/CardImage.kt @@ -0,0 +1,78 @@ +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("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("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("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/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt new file mode 100644 index 00000000..622e3f6d --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt @@ -0,0 +1,310 @@ +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 + 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("GameScenePresenter") { props -> + var transactionSelectorState by useState() + + 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): Pair { + 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) { + 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) { + 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, players: List) { + 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) { + 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 + var sayReady: () -> Unit +} + +private val SayReadyButton = FC("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/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/GameStyles.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/GameStyles.kt new file mode 100644 index 00000000..f5ec475e --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/GameStyles.kt @@ -0,0 +1,86 @@ +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/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt new file mode 100644 index 00000000..da71ea0b --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt @@ -0,0 +1,276 @@ +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("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? = 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("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/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/HandRotationIndicator.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/HandRotationIndicator.kt new file mode 100644 index 00000000..72cb6b65 --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/HandRotationIndicator.kt @@ -0,0 +1,56 @@ +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/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/PlayerPreparedCardPresenter.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/PlayerPreparedCardPresenter.kt new file mode 100644 index 00000000..627693e1 --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/PlayerPreparedCardPresenter.kt @@ -0,0 +1,80 @@ +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("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("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 { 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/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/PreparedMove.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/PreparedMove.kt new file mode 100644 index 00000000..3ecdc741 --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/PreparedMove.kt @@ -0,0 +1,73 @@ +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.() -> 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/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/ScoreTable.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/ScoreTable.kt new file mode 100644 index 00000000..cd54446f --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/ScoreTable.kt @@ -0,0 +1,188 @@ +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, 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) { + 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/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/Tokens.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/Tokens.kt new file mode 100644 index 00000000..01975f7e --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/Tokens.kt @@ -0,0 +1,155 @@ +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.() -> 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.() -> 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 { 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/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/TransactionsSelector.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/TransactionsSelector.kt new file mode 100644 index 00000000..cdf97ad9 --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/game/TransactionsSelector.kt @@ -0,0 +1,265 @@ +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, + 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 { 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.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.transactionCellInnerCss() { + css { + display = Display.flex + flexDirection = FlexDirection.row + alignItems = AlignItems.center + } +} + +private fun ChildrenBuilder.resourceList(countedResources: List) { + 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.resourceRowCss() { + css { + display = Display.flex + flexDirection = FlexDirection.row + alignItems = AlignItems.center + margin = Margin(vertical = 0.px, horizontal = Auto.auto) + } +} + +private fun List.toRepeatedTypesList(): List = flatMap { cr -> List(cr.count) { cr.type } } diff --git a/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/CreateGameForm.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/CreateGameForm.kt new file mode 100644 index 00000000..e0f7dd21 --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/CreateGameForm.kt @@ -0,0 +1,58 @@ +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/jsMain/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameBrowser.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameBrowser.kt new file mode 100644 index 00000000..10fb9d81 --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameBrowser.kt @@ -0,0 +1,69 @@ +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/jsMain/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameList.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameList.kt new file mode 100644 index 00000000..2919b065 --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameList.kt @@ -0,0 +1,213 @@ +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 +} + +external interface GameListDispatchProps : Props { + var joinGame: (Long) -> Unit +} + +external interface GameListProps : GameListStateProps, GameListDispatchProps + +val GameList = connectStateAndDispatch( + 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(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/jsMain/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/PlayerInfo.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/PlayerInfo.kt new file mode 100644 index 00000000..d7a9a80f --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/PlayerInfo.kt @@ -0,0 +1,105 @@ +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(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/jsMain/kotlin/org/luxons/sevenwonders/ui/components/home/ChooseNameForm.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/home/ChooseNameForm.kt new file mode 100644 index 00000000..ba37c09d --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/home/ChooseNameForm.kt @@ -0,0 +1,65 @@ +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 { 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/jsMain/kotlin/org/luxons/sevenwonders/ui/components/home/Home.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/home/Home.kt new file mode 100644 index 00000000..81f4c736 --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/home/Home.kt @@ -0,0 +1,22 @@ +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/jsMain/kotlin/org/luxons/sevenwonders/ui/components/home/HomeStyles.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/home/HomeStyles.kt new file mode 100644 index 00000000..015e78d6 --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/home/HomeStyles.kt @@ -0,0 +1,15 @@ +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/jsMain/kotlin/org/luxons/sevenwonders/ui/components/lobby/Lobby.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/lobby/Lobby.kt new file mode 100644 index 00000000..0330a192 --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/lobby/Lobby.kt @@ -0,0 +1,272 @@ +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) -> Unit + var reassignWonders: (wonders: List) -> Unit +} + +private val LobbyPresenter = FC { 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() + // 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) -> Unit, + reassignWonders: (wonders: List) -> 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) -> 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) -> Unit +} + +private val WonderSettingsGroup = FC { 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 = + 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/jsMain/kotlin/org/luxons/sevenwonders/ui/components/lobby/LobbyStyles.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/lobby/LobbyStyles.kt new file mode 100644 index 00000000..6b5dbe48 --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/lobby/LobbyStyles.kt @@ -0,0 +1,20 @@ +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/jsMain/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialList.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialList.kt new file mode 100644 index 00000000..1f88bebe --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialList.kt @@ -0,0 +1,117 @@ +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 ChildrenBuilder.radialList( + items: List, + centerElement: ReactElement<*>, + renderItem: (T) -> ReactElement<*>, + getKey: (T) -> String, + itemWidth: Int, + itemHeight: Int, + options: RadialConfig = RadialConfig(), + block: HTMLAttributes.() -> 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 ChildrenBuilder.radialListItems( + items: List, + 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/jsMain/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialMath.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialMath.kt new file mode 100644 index 00000000..4b5eb509 --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialMath.kt @@ -0,0 +1,57 @@ +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 { + 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/jsMain/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialPlayerList.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialPlayerList.kt new file mode 100644 index 00000000..645cf5f3 --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialPlayerList.kt @@ -0,0 +1,139 @@ +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, + currentPlayer: PlayerDTO, + block: HTMLAttributes.() -> 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.growWithPlaceholders(targetSize: Int): List = when { + size < targetSize -> this + List(targetSize - size) { PlayerItem.Placeholder(size + it) } + else -> this +} + +private fun List.withUserFirst(me: PlayerDTO): List { + 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(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/jsMain/kotlin/org/luxons/sevenwonders/ui/components/lobby/Table.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/lobby/Table.kt new file mode 100644 index 00000000..bfa43aa4 --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/components/lobby/Table.kt @@ -0,0 +1,97 @@ +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("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("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/jsMain/kotlin/org/luxons/sevenwonders/ui/names/RandomNameGenerator.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/names/RandomNameGenerator.kt new file mode 100644 index 00000000..393df78d --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/names/RandomNameGenerator.kt @@ -0,0 +1,546 @@ +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/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/Actions.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/Actions.kt new file mode 100644 index 00000000..b0c56a79 --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/Actions.kt @@ -0,0 +1,32 @@ +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) : 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/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/ApiActions.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/ApiActions.kt new file mode 100644 index 00000000..87bacf62 --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/ApiActions.kt @@ -0,0 +1,34 @@ +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) : RAction + +data class RequestReassignWonders(val wonders: List) : 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/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt new file mode 100644 index 00000000..e79b063e --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt @@ -0,0 +1,95 @@ +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 = 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 = 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, action: RAction): Map = 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/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/Store.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/Store.kt new file mode 100644 index 00000000..71c5eec0 --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/Store.kt @@ -0,0 +1,29 @@ +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 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 +} + +fun configureStore( + sagaManager: SagaManager, + initialState: SwState = INITIAL_STATE, +): Store { + val sagaEnhancer = applyMiddleware(sagaManager.createMiddleware()) + return createStore(::rootReducer, initialState, composeWithDevTools(sagaEnhancer, rEnhancer())) +} diff --git a/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/Utils.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/Utils.kt new file mode 100644 index 00000000..eb182dc7 --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/Utils.kt @@ -0,0 +1,31 @@ +package org.luxons.sevenwonders.ui.redux + +import react.* +import react.redux.* +import redux.* +import kotlin.reflect.* + +fun useSwSelector(selector: (SwState) -> R) = useSelector(selector) +fun useSwDispatch() = useDispatch() + +fun connectStateAndDispatch( + clazz: KClass>, + mapStateToProps: SP.(SwState, Props) -> Unit, + mapDispatchToProps: DP.((RAction) -> WrapperAction, Props) -> Unit, +): ComponentClass = connectStateAndDispatch( + component = clazz.react, + mapStateToProps = mapStateToProps, + mapDispatchToProps = mapDispatchToProps, +) + +fun connectStateAndDispatch( + component: ComponentClass

, + mapStateToProps: SP.(SwState, Props) -> Unit, + mapDispatchToProps: DP.((RAction) -> WrapperAction, Props) -> Unit, +): ComponentClass { + val connect = rConnect( + mapStateToProps = mapStateToProps, + mapDispatchToProps = mapDispatchToProps, + ) + return connect.invoke(component) +} diff --git a/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/sagas/RouteBasedSagas.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/sagas/RouteBasedSagas.kt new file mode 100644 index 00000000..3343e62e --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/sagas/RouteBasedSagas.kt @@ -0,0 +1,44 @@ +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/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/sagas/Sagas.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/sagas/Sagas.kt new file mode 100644 index 00000000..2ad98c8e --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/sagas/Sagas.kt @@ -0,0 +1,131 @@ +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 + +suspend fun SwSagaContext.rootSaga() = try { + coroutineScope { + val action = next() + 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 { session.chooseName(it.playerName) } + + scope.launchOnEach { session.createGame(it.gameName) } + scope.launchOnEach { session.joinGame(it.gameId) } + scope.launchOnEach { session.leaveLobby() } + scope.launchOnEach { session.disbandLobby() } + + scope.launchOnEach { session.addBot(it.botDisplayName) } + scope.launchOnEach { session.reorderPlayers(it.orderedPlayers) } + scope.launchOnEach { session.reassignWonders(it.wonders) } + scope.launchOnEach { session.startGame() } + + scope.launchOnEach { session.sayReady() } + scope.launchOnEach { session.prepareMove(it.move) } + scope.launchOnEach { session.unprepareMove() } + scope.launchOnEach { 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/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFramework.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFramework.kt new file mode 100644 index 00000000..05c03b13 --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFramework.kt @@ -0,0 +1,106 @@ +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( + private val monitor: ((A) -> Unit)? = null, +) { + private lateinit var context: SagaContext + + private val actions = MutableSharedFlow(extraBufferCapacity = Channel.UNLIMITED) + + fun createMiddleware(): Middleware = ::sagasMiddleware + + private fun sagasMiddleware(api: MiddlewareApi): ((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.() -> Unit): Job { + checkMiddlewareApplied() + return coroutineScope.launch { + context.saga() + } + } + + suspend fun runSaga(saga: suspend SagaContext.() -> 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( + private val reduxApi: MiddlewareApi, + val reduxActions: SharedFlow, +) { + /** + * 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 onEach( + crossinline handle: suspend SagaContext.(T) -> Unit, + ) { + reduxActions.filterIsInstance().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 CoroutineScope.launchOnEach( + crossinline handle: suspend SagaContext.(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 next(): T = reduxActions.filterIsInstance().first() +} diff --git a/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/router/Router.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/router/Router.kt new file mode 100644 index 00000000..1a0840cf --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/router/Router.kt @@ -0,0 +1,48 @@ +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 { + window.location.hash = it.route.path + } +} diff --git a/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/utils/CoroutinesUtils.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/utils/CoroutinesUtils.kt new file mode 100644 index 00000000..600f08d3 --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/utils/CoroutinesUtils.kt @@ -0,0 +1,15 @@ +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 awaitFirst(f1: suspend () -> R, f2: suspend () -> R): R = coroutineScope { + val deferred1 = async { f1() } + val deferred2 = async { f2() } + select { + deferred1.onAwait { deferred2.cancel(); it } + deferred2.onAwait { deferred1.cancel(); it } + } +} diff --git a/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/utils/StyleUtils.kt b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/utils/StyleUtils.kt new file mode 100644 index 00000000..7ca67be4 --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/org/luxons/sevenwonders/ui/utils/StyleUtils.kt @@ -0,0 +1,43 @@ +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() + +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() + +operator fun FilterFunction.plus(other: FilterFunction) = "$this $other".unsafeCast() + +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/jsMain/kotlin/webpack/WebpackUtils.kt b/sw-ui/src/jsMain/kotlin/webpack/WebpackUtils.kt new file mode 100644 index 00000000..dde1140a --- /dev/null +++ b/sw-ui/src/jsMain/kotlin/webpack/WebpackUtils.kt @@ -0,0 +1,9 @@ +package webpack + +external val process: Process + +external interface Process { + val env: dynamic +} + +fun isProdEnv(): Boolean = process.env.NODE_ENV == "production" diff --git a/sw-ui/src/jsMain/resources/favicon.ico b/sw-ui/src/jsMain/resources/favicon.ico new file mode 100644 index 00000000..5c125de5 Binary files /dev/null and b/sw-ui/src/jsMain/resources/favicon.ico differ diff --git a/sw-ui/src/jsMain/resources/images/backgrounds/papyrus.jpg b/sw-ui/src/jsMain/resources/images/backgrounds/papyrus.jpg new file mode 100644 index 00000000..90350045 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/backgrounds/papyrus.jpg differ diff --git a/sw-ui/src/jsMain/resources/images/backgrounds/zeus-temple.jpg b/sw-ui/src/jsMain/resources/images/backgrounds/zeus-temple.jpg new file mode 100644 index 00000000..5a28e933 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/backgrounds/zeus-temple.jpg differ diff --git a/sw-ui/src/jsMain/resources/images/cards/academy.png b/sw-ui/src/jsMain/resources/images/cards/academy.png new file mode 100644 index 00000000..d2a75075 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/academy.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/altar.png b/sw-ui/src/jsMain/resources/images/cards/altar.png new file mode 100644 index 00000000..bbde8f2f Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/altar.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/apothecary.png b/sw-ui/src/jsMain/resources/images/cards/apothecary.png new file mode 100644 index 00000000..01804c0a Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/apothecary.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/aqueduct.png b/sw-ui/src/jsMain/resources/images/cards/aqueduct.png new file mode 100644 index 00000000..c29d9566 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/aqueduct.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/archeryrange.png b/sw-ui/src/jsMain/resources/images/cards/archeryrange.png new file mode 100644 index 00000000..15c6edda Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/archeryrange.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/arena.png b/sw-ui/src/jsMain/resources/images/cards/arena.png new file mode 100644 index 00000000..7dc76961 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/arena.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/arsenal.png b/sw-ui/src/jsMain/resources/images/cards/arsenal.png new file mode 100644 index 00000000..fc3f4a27 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/arsenal.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/back/age1.png b/sw-ui/src/jsMain/resources/images/cards/back/age1.png new file mode 100644 index 00000000..a06332d7 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/back/age1.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/back/age2.png b/sw-ui/src/jsMain/resources/images/cards/back/age2.png new file mode 100644 index 00000000..9b52aa4e Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/back/age2.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/back/age3.png b/sw-ui/src/jsMain/resources/images/cards/back/age3.png new file mode 100644 index 00000000..86c983ee Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/back/age3.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/back/placeholder.png b/sw-ui/src/jsMain/resources/images/cards/back/placeholder.png new file mode 100644 index 00000000..9bfcf9c6 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/back/placeholder.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/barracks.png b/sw-ui/src/jsMain/resources/images/cards/barracks.png new file mode 100644 index 00000000..f5a68c17 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/barracks.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/baths.png b/sw-ui/src/jsMain/resources/images/cards/baths.png new file mode 100644 index 00000000..3d99d59d Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/baths.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/bazar.png b/sw-ui/src/jsMain/resources/images/cards/bazar.png new file mode 100644 index 00000000..f36e25c2 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/bazar.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/brickyard.png b/sw-ui/src/jsMain/resources/images/cards/brickyard.png new file mode 100644 index 00000000..ae0b7e9b Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/brickyard.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/buildersguild.png b/sw-ui/src/jsMain/resources/images/cards/buildersguild.png new file mode 100644 index 00000000..f5402611 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/buildersguild.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/caravansery.png b/sw-ui/src/jsMain/resources/images/cards/caravansery.png new file mode 100644 index 00000000..997bb102 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/caravansery.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/chamberofcommerce.png b/sw-ui/src/jsMain/resources/images/cards/chamberofcommerce.png new file mode 100644 index 00000000..44b5af28 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/chamberofcommerce.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/circus.png b/sw-ui/src/jsMain/resources/images/cards/circus.png new file mode 100644 index 00000000..b1ec4d8b Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/circus.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/claypit.png b/sw-ui/src/jsMain/resources/images/cards/claypit.png new file mode 100644 index 00000000..5442248e Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/claypit.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/claypool.png b/sw-ui/src/jsMain/resources/images/cards/claypool.png new file mode 100644 index 00000000..873cad47 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/claypool.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/courthouse.png b/sw-ui/src/jsMain/resources/images/cards/courthouse.png new file mode 100644 index 00000000..394901f2 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/courthouse.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/craftsmensguild.png b/sw-ui/src/jsMain/resources/images/cards/craftsmensguild.png new file mode 100644 index 00000000..09bff60e Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/craftsmensguild.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/dispensary.png b/sw-ui/src/jsMain/resources/images/cards/dispensary.png new file mode 100644 index 00000000..4917166b Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/dispensary.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/easttradingpost.png b/sw-ui/src/jsMain/resources/images/cards/easttradingpost.png new file mode 100644 index 00000000..0c67cc78 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/easttradingpost.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/excavation.png b/sw-ui/src/jsMain/resources/images/cards/excavation.png new file mode 100644 index 00000000..0fe1b01f Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/excavation.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/forestcave.png b/sw-ui/src/jsMain/resources/images/cards/forestcave.png new file mode 100644 index 00000000..262fffc6 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/forestcave.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/fortifications.png b/sw-ui/src/jsMain/resources/images/cards/fortifications.png new file mode 100644 index 00000000..3e113473 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/fortifications.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/forum.png b/sw-ui/src/jsMain/resources/images/cards/forum.png new file mode 100644 index 00000000..d6262158 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/forum.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/foundry.png b/sw-ui/src/jsMain/resources/images/cards/foundry.png new file mode 100644 index 00000000..da95a48e Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/foundry.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/gardens.png b/sw-ui/src/jsMain/resources/images/cards/gardens.png new file mode 100644 index 00000000..9a49a0ad Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/gardens.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/glassworks.png b/sw-ui/src/jsMain/resources/images/cards/glassworks.png new file mode 100644 index 00000000..285d7d54 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/glassworks.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/guardtower.png b/sw-ui/src/jsMain/resources/images/cards/guardtower.png new file mode 100644 index 00000000..524b06f3 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/guardtower.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/haven.png b/sw-ui/src/jsMain/resources/images/cards/haven.png new file mode 100644 index 00000000..e0b345b2 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/haven.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/laboratory.png b/sw-ui/src/jsMain/resources/images/cards/laboratory.png new file mode 100644 index 00000000..4c29e81f Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/laboratory.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/library.png b/sw-ui/src/jsMain/resources/images/cards/library.png new file mode 100644 index 00000000..7495a2ca Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/library.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/lighthouse.png b/sw-ui/src/jsMain/resources/images/cards/lighthouse.png new file mode 100644 index 00000000..2124811b Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/lighthouse.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/lodge.png b/sw-ui/src/jsMain/resources/images/cards/lodge.png new file mode 100644 index 00000000..22758688 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/lodge.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/loom.png b/sw-ui/src/jsMain/resources/images/cards/loom.png new file mode 100644 index 00000000..70bdf375 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/loom.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/lumberyard.png b/sw-ui/src/jsMain/resources/images/cards/lumberyard.png new file mode 100644 index 00000000..8558af1a Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/lumberyard.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/magistratesguild.png b/sw-ui/src/jsMain/resources/images/cards/magistratesguild.png new file mode 100644 index 00000000..d7deabb3 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/magistratesguild.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/marketplace.png b/sw-ui/src/jsMain/resources/images/cards/marketplace.png new file mode 100644 index 00000000..cd3676d4 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/marketplace.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/mine.png b/sw-ui/src/jsMain/resources/images/cards/mine.png new file mode 100644 index 00000000..4062775c Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/mine.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/observatory.png b/sw-ui/src/jsMain/resources/images/cards/observatory.png new file mode 100644 index 00000000..1da3d7b4 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/observatory.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/orevein.png b/sw-ui/src/jsMain/resources/images/cards/orevein.png new file mode 100644 index 00000000..fabea674 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/orevein.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/palace.png b/sw-ui/src/jsMain/resources/images/cards/palace.png new file mode 100644 index 00000000..1a24890e Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/palace.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/pantheon.png b/sw-ui/src/jsMain/resources/images/cards/pantheon.png new file mode 100644 index 00000000..264bae02 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/pantheon.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/pawnshop.png b/sw-ui/src/jsMain/resources/images/cards/pawnshop.png new file mode 100644 index 00000000..30bb3807 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/pawnshop.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/philosophersguild.png b/sw-ui/src/jsMain/resources/images/cards/philosophersguild.png new file mode 100644 index 00000000..f72590f6 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/philosophersguild.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/press.png b/sw-ui/src/jsMain/resources/images/cards/press.png new file mode 100644 index 00000000..c932df06 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/press.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/quarry.png b/sw-ui/src/jsMain/resources/images/cards/quarry.png new file mode 100644 index 00000000..8cdbdb22 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/quarry.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/sawmill.png b/sw-ui/src/jsMain/resources/images/cards/sawmill.png new file mode 100644 index 00000000..5abff473 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/sawmill.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/school.png b/sw-ui/src/jsMain/resources/images/cards/school.png new file mode 100644 index 00000000..ab2218d0 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/school.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/scientistsguild.png b/sw-ui/src/jsMain/resources/images/cards/scientistsguild.png new file mode 100644 index 00000000..7ee639e3 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/scientistsguild.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/scriptorium.png b/sw-ui/src/jsMain/resources/images/cards/scriptorium.png new file mode 100644 index 00000000..36dca27a Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/scriptorium.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/senate.png b/sw-ui/src/jsMain/resources/images/cards/senate.png new file mode 100644 index 00000000..ee878ea6 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/senate.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/shipownersguild.png b/sw-ui/src/jsMain/resources/images/cards/shipownersguild.png new file mode 100644 index 00000000..3eecd2da Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/shipownersguild.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/siegeworkshop.png b/sw-ui/src/jsMain/resources/images/cards/siegeworkshop.png new file mode 100644 index 00000000..bacf8309 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/siegeworkshop.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/spiesguild.png b/sw-ui/src/jsMain/resources/images/cards/spiesguild.png new file mode 100644 index 00000000..85e28d9e Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/spiesguild.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/stables.png b/sw-ui/src/jsMain/resources/images/cards/stables.png new file mode 100644 index 00000000..48c963f0 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/stables.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/statue.png b/sw-ui/src/jsMain/resources/images/cards/statue.png new file mode 100644 index 00000000..55aaa5cb Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/statue.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/stockade.png b/sw-ui/src/jsMain/resources/images/cards/stockade.png new file mode 100644 index 00000000..37741429 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/stockade.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/stonepit.png b/sw-ui/src/jsMain/resources/images/cards/stonepit.png new file mode 100644 index 00000000..724900c7 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/stonepit.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/strategistsguild.png b/sw-ui/src/jsMain/resources/images/cards/strategistsguild.png new file mode 100644 index 00000000..ae186a4b Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/strategistsguild.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/study.png b/sw-ui/src/jsMain/resources/images/cards/study.png new file mode 100644 index 00000000..d8b9ebf9 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/study.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/tavern.png b/sw-ui/src/jsMain/resources/images/cards/tavern.png new file mode 100644 index 00000000..418b0fb2 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/tavern.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/temple.png b/sw-ui/src/jsMain/resources/images/cards/temple.png new file mode 100644 index 00000000..9a8d89dc Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/temple.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/theater.png b/sw-ui/src/jsMain/resources/images/cards/theater.png new file mode 100644 index 00000000..0d5b2b01 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/theater.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/timberyard.png b/sw-ui/src/jsMain/resources/images/cards/timberyard.png new file mode 100644 index 00000000..0f20547f Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/timberyard.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/townhall.png b/sw-ui/src/jsMain/resources/images/cards/townhall.png new file mode 100644 index 00000000..d0638739 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/townhall.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/tradersguild.png b/sw-ui/src/jsMain/resources/images/cards/tradersguild.png new file mode 100644 index 00000000..15777e77 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/tradersguild.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/trainingground.png b/sw-ui/src/jsMain/resources/images/cards/trainingground.png new file mode 100644 index 00000000..d59ef4f8 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/trainingground.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/treefarm.png b/sw-ui/src/jsMain/resources/images/cards/treefarm.png new file mode 100644 index 00000000..18cf228f Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/treefarm.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/university.png b/sw-ui/src/jsMain/resources/images/cards/university.png new file mode 100644 index 00000000..c9ca8a80 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/university.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/vineyard.png b/sw-ui/src/jsMain/resources/images/cards/vineyard.png new file mode 100644 index 00000000..58fa8ee1 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/vineyard.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/walls.png b/sw-ui/src/jsMain/resources/images/cards/walls.png new file mode 100644 index 00000000..3823c62f Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/walls.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/westtradingpost.png b/sw-ui/src/jsMain/resources/images/cards/westtradingpost.png new file mode 100644 index 00000000..b536269f Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/westtradingpost.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/workersguild.png b/sw-ui/src/jsMain/resources/images/cards/workersguild.png new file mode 100644 index 00000000..de4f452f Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/workersguild.png differ diff --git a/sw-ui/src/jsMain/resources/images/cards/workshop.png b/sw-ui/src/jsMain/resources/images/cards/workshop.png new file mode 100644 index 00000000..8f585d61 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/cards/workshop.png differ diff --git a/sw-ui/src/jsMain/resources/images/gear-50.png b/sw-ui/src/jsMain/resources/images/gear-50.png new file mode 100644 index 00000000..93a4a186 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/gear-50.png differ diff --git a/sw-ui/src/jsMain/resources/images/hand-cards5.png b/sw-ui/src/jsMain/resources/images/hand-cards5.png new file mode 100644 index 00000000..1e2199cd Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/hand-cards5.png differ diff --git a/sw-ui/src/jsMain/resources/images/logo-7-wonders.png b/sw-ui/src/jsMain/resources/images/logo-7-wonders.png new file mode 100644 index 00000000..96974d3e Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/logo-7-wonders.png differ diff --git a/sw-ui/src/jsMain/resources/images/tokens/coin.png b/sw-ui/src/jsMain/resources/images/tokens/coin.png new file mode 100644 index 00000000..f4813042 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/tokens/coin.png differ diff --git a/sw-ui/src/jsMain/resources/images/tokens/laurel-blue.png b/sw-ui/src/jsMain/resources/images/tokens/laurel-blue.png new file mode 100644 index 00000000..115bba91 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/tokens/laurel-blue.png differ diff --git a/sw-ui/src/jsMain/resources/images/tokens/military/defeat1.png b/sw-ui/src/jsMain/resources/images/tokens/military/defeat1.png new file mode 100644 index 00000000..1c61bf4c Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/tokens/military/defeat1.png differ diff --git a/sw-ui/src/jsMain/resources/images/tokens/military/shield.png b/sw-ui/src/jsMain/resources/images/tokens/military/shield.png new file mode 100644 index 00000000..3a0e1dea Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/tokens/military/shield.png differ diff --git a/sw-ui/src/jsMain/resources/images/tokens/military/victory1.png b/sw-ui/src/jsMain/resources/images/tokens/military/victory1.png new file mode 100644 index 00000000..6b9aff29 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/tokens/military/victory1.png differ diff --git a/sw-ui/src/jsMain/resources/images/tokens/resources/clay.png b/sw-ui/src/jsMain/resources/images/tokens/resources/clay.png new file mode 100644 index 00000000..72fc0b0e Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/tokens/resources/clay.png differ diff --git a/sw-ui/src/jsMain/resources/images/tokens/resources/glass.png b/sw-ui/src/jsMain/resources/images/tokens/resources/glass.png new file mode 100644 index 00000000..61fd2be5 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/tokens/resources/glass.png differ diff --git a/sw-ui/src/jsMain/resources/images/tokens/resources/loom.png b/sw-ui/src/jsMain/resources/images/tokens/resources/loom.png new file mode 100644 index 00000000..294adcb2 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/tokens/resources/loom.png differ diff --git a/sw-ui/src/jsMain/resources/images/tokens/resources/ore.png b/sw-ui/src/jsMain/resources/images/tokens/resources/ore.png new file mode 100644 index 00000000..c2149daa Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/tokens/resources/ore.png differ diff --git a/sw-ui/src/jsMain/resources/images/tokens/resources/papyrus.png b/sw-ui/src/jsMain/resources/images/tokens/resources/papyrus.png new file mode 100644 index 00000000..91a59221 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/tokens/resources/papyrus.png differ diff --git a/sw-ui/src/jsMain/resources/images/tokens/resources/stone.png b/sw-ui/src/jsMain/resources/images/tokens/resources/stone.png new file mode 100644 index 00000000..674c40db Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/tokens/resources/stone.png differ diff --git a/sw-ui/src/jsMain/resources/images/tokens/resources/wood.png b/sw-ui/src/jsMain/resources/images/tokens/resources/wood.png new file mode 100644 index 00000000..09a4ede8 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/tokens/resources/wood.png differ diff --git a/sw-ui/src/jsMain/resources/images/tokens/science/cog.png b/sw-ui/src/jsMain/resources/images/tokens/science/cog.png new file mode 100644 index 00000000..61250d8a Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/tokens/science/cog.png differ diff --git a/sw-ui/src/jsMain/resources/images/tokens/science/compass.png b/sw-ui/src/jsMain/resources/images/tokens/science/compass.png new file mode 100644 index 00000000..6497e34f Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/tokens/science/compass.png differ diff --git a/sw-ui/src/jsMain/resources/images/tokens/science/tablet.png b/sw-ui/src/jsMain/resources/images/tokens/science/tablet.png new file mode 100644 index 00000000..954fd9ef Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/tokens/science/tablet.png differ diff --git a/sw-ui/src/jsMain/resources/images/wonder-upgrade-bright.png b/sw-ui/src/jsMain/resources/images/wonder-upgrade-bright.png new file mode 100644 index 00000000..0f59c068 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonder-upgrade-bright.png differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/alexandriaA.png b/sw-ui/src/jsMain/resources/images/wonders/alexandriaA.png new file mode 100644 index 00000000..0d4135f3 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/alexandriaA.png differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/alexandriaB.png b/sw-ui/src/jsMain/resources/images/wonders/alexandriaB.png new file mode 100644 index 00000000..dd072f8a Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/alexandriaB.png differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/babylonA.png b/sw-ui/src/jsMain/resources/images/wonders/babylonA.png new file mode 100644 index 00000000..ae323c78 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/babylonA.png differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/babylonB.png b/sw-ui/src/jsMain/resources/images/wonders/babylonB.png new file mode 100644 index 00000000..3780dc9d Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/babylonB.png differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/ephesosA.png b/sw-ui/src/jsMain/resources/images/wonders/ephesosA.png new file mode 100644 index 00000000..307794ba Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/ephesosA.png differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/ephesosB.png b/sw-ui/src/jsMain/resources/images/wonders/ephesosB.png new file mode 100644 index 00000000..ec2e9cb7 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/ephesosB.png differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/extra/agrigentoA.jpg b/sw-ui/src/jsMain/resources/images/wonders/extra/agrigentoA.jpg new file mode 100644 index 00000000..76ba8195 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/extra/agrigentoA.jpg differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/extra/angkorwatA.jpg b/sw-ui/src/jsMain/resources/images/wonders/extra/angkorwatA.jpg new file mode 100644 index 00000000..32f52514 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/extra/angkorwatA.jpg differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/extra/angkorwatB.jpg b/sw-ui/src/jsMain/resources/images/wonders/extra/angkorwatB.jpg new file mode 100644 index 00000000..c3f4304e Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/extra/angkorwatB.jpg differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/extra/avalonA.jpg b/sw-ui/src/jsMain/resources/images/wonders/extra/avalonA.jpg new file mode 100644 index 00000000..7f7f0678 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/extra/avalonA.jpg differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/extra/ctesiphonB.jpg b/sw-ui/src/jsMain/resources/images/wonders/extra/ctesiphonB.jpg new file mode 100644 index 00000000..c00b40ac Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/extra/ctesiphonB.jpg differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/extra/iramA.jpg b/sw-ui/src/jsMain/resources/images/wonders/extra/iramA.jpg new file mode 100644 index 00000000..d2c24e95 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/extra/iramA.jpg differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/extra/persepolisA.jpg b/sw-ui/src/jsMain/resources/images/wonders/extra/persepolisA.jpg new file mode 100644 index 00000000..2caa4f89 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/extra/persepolisA.jpg differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/extra/romaA.jpg b/sw-ui/src/jsMain/resources/images/wonders/extra/romaA.jpg new file mode 100644 index 00000000..c54bc820 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/extra/romaA.jpg differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/extra/sangri-laA.jpg b/sw-ui/src/jsMain/resources/images/wonders/extra/sangri-laA.jpg new file mode 100644 index 00000000..1c5dad97 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/extra/sangri-laA.jpg differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/extra/spahanA.jpg b/sw-ui/src/jsMain/resources/images/wonders/extra/spahanA.jpg new file mode 100644 index 00000000..ab2cfc84 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/extra/spahanA.jpg differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/extra/the-great-wallA.jpg b/sw-ui/src/jsMain/resources/images/wonders/extra/the-great-wallA.jpg new file mode 100644 index 00000000..4aacd39b Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/extra/the-great-wallA.jpg differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/extra/veniseA.jpg b/sw-ui/src/jsMain/resources/images/wonders/extra/veniseA.jpg new file mode 100644 index 00000000..55ec00b5 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/extra/veniseA.jpg differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/extra/veniseB.jpg b/sw-ui/src/jsMain/resources/images/wonders/extra/veniseB.jpg new file mode 100644 index 00000000..e18f3a12 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/extra/veniseB.jpg differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/gizahA.png b/sw-ui/src/jsMain/resources/images/wonders/gizahA.png new file mode 100644 index 00000000..e66735fb Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/gizahA.png differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/gizahB.png b/sw-ui/src/jsMain/resources/images/wonders/gizahB.png new file mode 100644 index 00000000..ed55ed45 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/gizahB.png differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/halikarnassusA.png b/sw-ui/src/jsMain/resources/images/wonders/halikarnassusA.png new file mode 100644 index 00000000..659f706e Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/halikarnassusA.png differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/halikarnassusB.png b/sw-ui/src/jsMain/resources/images/wonders/halikarnassusB.png new file mode 100644 index 00000000..b6ae1f93 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/halikarnassusB.png differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/olympiaA.png b/sw-ui/src/jsMain/resources/images/wonders/olympiaA.png new file mode 100644 index 00000000..478ed503 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/olympiaA.png differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/olympiaB.png b/sw-ui/src/jsMain/resources/images/wonders/olympiaB.png new file mode 100644 index 00000000..a97a9524 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/olympiaB.png differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/rhodosA.png b/sw-ui/src/jsMain/resources/images/wonders/rhodosA.png new file mode 100644 index 00000000..0c11a71a Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/rhodosA.png differ diff --git a/sw-ui/src/jsMain/resources/images/wonders/rhodosB.png b/sw-ui/src/jsMain/resources/images/wonders/rhodosB.png new file mode 100644 index 00000000..43e5d594 Binary files /dev/null and b/sw-ui/src/jsMain/resources/images/wonders/rhodosB.png differ diff --git a/sw-ui/src/jsMain/resources/index.html b/sw-ui/src/jsMain/resources/index.html new file mode 100644 index 00000000..b43c1428 --- /dev/null +++ b/sw-ui/src/jsMain/resources/index.html @@ -0,0 +1,19 @@ + + + + + + + Seven Wonders + + + + + + + + +

+ + + diff --git a/sw-ui/src/jsTest/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFrameworkTest.kt b/sw-ui/src/jsTest/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFrameworkTest.kt new file mode 100644 index 00000000..f810c8b9 --- /dev/null +++ b/sw-ui/src/jsTest/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFrameworkTest.kt @@ -0,0 +1,89 @@ +package org.luxons.sevenwonders.ui.redux.sagas + +import kotlinx.coroutines.* +import kotlinx.coroutines.test.* +import redux.RAction +import redux.Store +import redux.WrapperAction +import redux.applyMiddleware +import redux.compose +import redux.createStore +import redux.rEnhancer +import kotlin.test.Test +import kotlin.test.assertEquals + +private data class State(val data: String) + +private data class UpdateData(val newData: String) : RAction +private class DuplicateData : RAction +private class SideEffectAction(val data: String) : RAction + +private fun reduce(state: State, action: RAction): State = when (action) { + is UpdateData -> State(action.newData) + is DuplicateData -> State(state.data + state.data) + else -> state +} + +private fun configureTestStore(initialState: State): TestRedux { + val sagaMiddlewareFactory = SagaManager() + val sagaMiddleware = sagaMiddlewareFactory.createMiddleware() + val enhancers = compose(applyMiddleware(sagaMiddleware), rEnhancer()) + val store = createStore(::reduce, initialState, enhancers) + return TestRedux(store, sagaMiddlewareFactory) +} + +private data class TestRedux( + val store: Store, + val sagas: SagaManager, +) + +@OptIn(ExperimentalCoroutinesApi::class) // for runTest +class SagaContextTest { + + @Test + fun dispatch_dispatchesToStore() = runTest { + val redux = configureTestStore(State("initial")) + + redux.sagas.runSaga { + dispatch(UpdateData("Bob")) + } + + assertEquals(State("Bob"), redux.store.getState(), "state is not as expected") + } + + @Test + fun next_waitsForNextAction() = runTest { + val redux = configureTestStore(State("initial")) + + val job = redux.sagas.launchSaga(this) { + val action = next() + dispatch(UpdateData("effect-${action.data}")) + } + advanceUntilIdle() // make sure the saga is launched + + assertEquals(State("initial"), redux.store.getState()) + + redux.store.dispatch(SideEffectAction("data")) + job.join() + assertEquals(State("effect-data"), redux.store.getState()) + } + + @Test + fun onEach() = runTest { + val redux = configureTestStore(State("initial")) + + val job = redux.sagas.launchSaga(this) { + onEach { + dispatch(UpdateData("effect-${it.data}")) + } + } + advanceUntilIdle() // make sure the saga is launched + + assertEquals(State("initial"), redux.store.getState()) + + redux.store.dispatch(SideEffectAction("data")) + yield() + assertEquals(State("effect-data"), redux.store.getState()) + job.cancel() + } +} diff --git a/sw-ui/src/jsTest/kotlin/org/luxons/sevenwonders/ui/utils/CoroutineUtilsTest.kt b/sw-ui/src/jsTest/kotlin/org/luxons/sevenwonders/ui/utils/CoroutineUtilsTest.kt new file mode 100644 index 00000000..ef8dfb62 --- /dev/null +++ b/sw-ui/src/jsTest/kotlin/org/luxons/sevenwonders/ui/utils/CoroutineUtilsTest.kt @@ -0,0 +1,24 @@ +package org.luxons.sevenwonders.ui.utils + +import kotlinx.coroutines.* +import kotlinx.coroutines.test.* +import kotlin.test.Test +import kotlin.test.assertEquals + +class CoroutineUtilsTest { + + @OptIn(ExperimentalCoroutinesApi::class) // for runTest + @Test + fun awaitFirstTest() = runTest { + val s = awaitFirst( + { delay(100); "1" }, + { delay(200); "2" }, + ) + assertEquals("1", s) + val s2 = awaitFirst( + { delay(150); "1" }, + { delay(50); "2" }, + ) + assertEquals("2", s2) + } +} 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 { - val sagaManager = SagaManager() - 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() - 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() - 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() - - 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("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("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>) { - 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 -} - -private val TableCardColumn = FC("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("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() - 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.() -> Unit = {}) { - boardToken("military/victory1", points, block) -} - -private fun ChildrenBuilder.defeatTokenCount(nbDefeatTokens: Int, block: HTMLAttributes.() -> Unit = {}) { - boardToken("military/defeat1", nbDefeatTokens, block) -} - -private fun ChildrenBuilder.boardToken(tokenName: String, count: Int, block: HTMLAttributes.() -> 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("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("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("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("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("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("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 - 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("GameScenePresenter") { props -> - var transactionSelectorState by useState() - - 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): Pair { - 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) { - 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) { - 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, players: List) { - 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) { - 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 - var sayReady: () -> Unit -} - -private val SayReadyButton = FC("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("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? = 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("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("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("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 { 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.() -> 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, 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) { - 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.() -> 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.() -> 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 { 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, - 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 { 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.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.transactionCellInnerCss() { - css { - display = Display.flex - flexDirection = FlexDirection.row - alignItems = AlignItems.center - } -} - -private fun ChildrenBuilder.resourceList(countedResources: List) { - 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.resourceRowCss() { - css { - display = Display.flex - flexDirection = FlexDirection.row - alignItems = AlignItems.center - margin = Margin(vertical = 0.px, horizontal = Auto.auto) - } -} - -private fun List.toRepeatedTypesList(): List = 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 -} - -external interface GameListDispatchProps : Props { - var joinGame: (Long) -> Unit -} - -external interface GameListProps : GameListStateProps, GameListDispatchProps - -val GameList = connectStateAndDispatch( - 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(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(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 { 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) -> Unit - var reassignWonders: (wonders: List) -> Unit -} - -private val LobbyPresenter = FC { 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() - // 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) -> Unit, - reassignWonders: (wonders: List) -> 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) -> 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) -> Unit -} - -private val WonderSettingsGroup = FC { 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 = - 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 ChildrenBuilder.radialList( - items: List, - centerElement: ReactElement<*>, - renderItem: (T) -> ReactElement<*>, - getKey: (T) -> String, - itemWidth: Int, - itemHeight: Int, - options: RadialConfig = RadialConfig(), - block: HTMLAttributes.() -> 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 ChildrenBuilder.radialListItems( - items: List, - 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 { - 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, - currentPlayer: PlayerDTO, - block: HTMLAttributes.() -> 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.growWithPlaceholders(targetSize: Int): List = when { - size < targetSize -> this + List(targetSize - size) { PlayerItem.Placeholder(size + it) } - else -> this -} - -private fun List.withUserFirst(me: PlayerDTO): List { - 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(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("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("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) : 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) : RAction - -data class RequestReassignWonders(val wonders: List) : 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 = 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 = 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, action: RAction): Map = 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 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 -} - -fun configureStore( - sagaManager: SagaManager, - initialState: SwState = INITIAL_STATE, -): Store { - 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 useSwSelector(selector: (SwState) -> R) = useSelector(selector) -fun useSwDispatch() = useDispatch() - -fun connectStateAndDispatch( - clazz: KClass>, - mapStateToProps: SP.(SwState, Props) -> Unit, - mapDispatchToProps: DP.((RAction) -> WrapperAction, Props) -> Unit, -): ComponentClass = connectStateAndDispatch( - component = clazz.react, - mapStateToProps = mapStateToProps, - mapDispatchToProps = mapDispatchToProps, -) - -fun connectStateAndDispatch( - component: ComponentClass

, - mapStateToProps: SP.(SwState, Props) -> Unit, - mapDispatchToProps: DP.((RAction) -> WrapperAction, Props) -> Unit, -): ComponentClass { - val connect = rConnect( - 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 - -suspend fun SwSagaContext.rootSaga() = try { - coroutineScope { - val action = next() - 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 { session.chooseName(it.playerName) } - - scope.launchOnEach { session.createGame(it.gameName) } - scope.launchOnEach { session.joinGame(it.gameId) } - scope.launchOnEach { session.leaveLobby() } - scope.launchOnEach { session.disbandLobby() } - - scope.launchOnEach { session.addBot(it.botDisplayName) } - scope.launchOnEach { session.reorderPlayers(it.orderedPlayers) } - scope.launchOnEach { session.reassignWonders(it.wonders) } - scope.launchOnEach { session.startGame() } - - scope.launchOnEach { session.sayReady() } - scope.launchOnEach { session.prepareMove(it.move) } - scope.launchOnEach { session.unprepareMove() } - scope.launchOnEach { 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( - private val monitor: ((A) -> Unit)? = null, -) { - private lateinit var context: SagaContext - - private val actions = MutableSharedFlow(extraBufferCapacity = Channel.UNLIMITED) - - fun createMiddleware(): Middleware = ::sagasMiddleware - - private fun sagasMiddleware(api: MiddlewareApi): ((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.() -> Unit): Job { - checkMiddlewareApplied() - return coroutineScope.launch { - context.saga() - } - } - - suspend fun runSaga(saga: suspend SagaContext.() -> 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( - private val reduxApi: MiddlewareApi, - val reduxActions: SharedFlow, -) { - /** - * 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 onEach( - crossinline handle: suspend SagaContext.(T) -> Unit, - ) { - reduxActions.filterIsInstance().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 CoroutineScope.launchOnEach( - crossinline handle: suspend SagaContext.(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 next(): T = reduxActions.filterIsInstance().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 { - 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 awaitFirst(f1: suspend () -> R, f2: suspend () -> R): R = coroutineScope { - val deferred1 = async { f1() } - val deferred2 = async { f2() } - select { - 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() - -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() - -operator fun FilterFunction.plus(other: FilterFunction) = "$this $other".unsafeCast() - -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" diff --git a/sw-ui/src/main/resources/favicon.ico b/sw-ui/src/main/resources/favicon.ico deleted file mode 100644 index 5c125de5..00000000 Binary files a/sw-ui/src/main/resources/favicon.ico and /dev/null differ diff --git a/sw-ui/src/main/resources/images/backgrounds/papyrus.jpg b/sw-ui/src/main/resources/images/backgrounds/papyrus.jpg deleted file mode 100644 index 90350045..00000000 Binary files a/sw-ui/src/main/resources/images/backgrounds/papyrus.jpg and /dev/null differ diff --git a/sw-ui/src/main/resources/images/backgrounds/zeus-temple.jpg b/sw-ui/src/main/resources/images/backgrounds/zeus-temple.jpg deleted file mode 100644 index 5a28e933..00000000 Binary files a/sw-ui/src/main/resources/images/backgrounds/zeus-temple.jpg and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/academy.png b/sw-ui/src/main/resources/images/cards/academy.png deleted file mode 100644 index d2a75075..00000000 Binary files a/sw-ui/src/main/resources/images/cards/academy.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/altar.png b/sw-ui/src/main/resources/images/cards/altar.png deleted file mode 100644 index bbde8f2f..00000000 Binary files a/sw-ui/src/main/resources/images/cards/altar.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/apothecary.png b/sw-ui/src/main/resources/images/cards/apothecary.png deleted file mode 100644 index 01804c0a..00000000 Binary files a/sw-ui/src/main/resources/images/cards/apothecary.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/aqueduct.png b/sw-ui/src/main/resources/images/cards/aqueduct.png deleted file mode 100644 index c29d9566..00000000 Binary files a/sw-ui/src/main/resources/images/cards/aqueduct.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/archeryrange.png b/sw-ui/src/main/resources/images/cards/archeryrange.png deleted file mode 100644 index 15c6edda..00000000 Binary files a/sw-ui/src/main/resources/images/cards/archeryrange.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/arena.png b/sw-ui/src/main/resources/images/cards/arena.png deleted file mode 100644 index 7dc76961..00000000 Binary files a/sw-ui/src/main/resources/images/cards/arena.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/arsenal.png b/sw-ui/src/main/resources/images/cards/arsenal.png deleted file mode 100644 index fc3f4a27..00000000 Binary files a/sw-ui/src/main/resources/images/cards/arsenal.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/back/age1.png b/sw-ui/src/main/resources/images/cards/back/age1.png deleted file mode 100644 index a06332d7..00000000 Binary files a/sw-ui/src/main/resources/images/cards/back/age1.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/back/age2.png b/sw-ui/src/main/resources/images/cards/back/age2.png deleted file mode 100644 index 9b52aa4e..00000000 Binary files a/sw-ui/src/main/resources/images/cards/back/age2.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/back/age3.png b/sw-ui/src/main/resources/images/cards/back/age3.png deleted file mode 100644 index 86c983ee..00000000 Binary files a/sw-ui/src/main/resources/images/cards/back/age3.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/back/placeholder.png b/sw-ui/src/main/resources/images/cards/back/placeholder.png deleted file mode 100644 index 9bfcf9c6..00000000 Binary files a/sw-ui/src/main/resources/images/cards/back/placeholder.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/barracks.png b/sw-ui/src/main/resources/images/cards/barracks.png deleted file mode 100644 index f5a68c17..00000000 Binary files a/sw-ui/src/main/resources/images/cards/barracks.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/baths.png b/sw-ui/src/main/resources/images/cards/baths.png deleted file mode 100644 index 3d99d59d..00000000 Binary files a/sw-ui/src/main/resources/images/cards/baths.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/bazar.png b/sw-ui/src/main/resources/images/cards/bazar.png deleted file mode 100644 index f36e25c2..00000000 Binary files a/sw-ui/src/main/resources/images/cards/bazar.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/brickyard.png b/sw-ui/src/main/resources/images/cards/brickyard.png deleted file mode 100644 index ae0b7e9b..00000000 Binary files a/sw-ui/src/main/resources/images/cards/brickyard.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/buildersguild.png b/sw-ui/src/main/resources/images/cards/buildersguild.png deleted file mode 100644 index f5402611..00000000 Binary files a/sw-ui/src/main/resources/images/cards/buildersguild.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/caravansery.png b/sw-ui/src/main/resources/images/cards/caravansery.png deleted file mode 100644 index 997bb102..00000000 Binary files a/sw-ui/src/main/resources/images/cards/caravansery.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/chamberofcommerce.png b/sw-ui/src/main/resources/images/cards/chamberofcommerce.png deleted file mode 100644 index 44b5af28..00000000 Binary files a/sw-ui/src/main/resources/images/cards/chamberofcommerce.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/circus.png b/sw-ui/src/main/resources/images/cards/circus.png deleted file mode 100644 index b1ec4d8b..00000000 Binary files a/sw-ui/src/main/resources/images/cards/circus.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/claypit.png b/sw-ui/src/main/resources/images/cards/claypit.png deleted file mode 100644 index 5442248e..00000000 Binary files a/sw-ui/src/main/resources/images/cards/claypit.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/claypool.png b/sw-ui/src/main/resources/images/cards/claypool.png deleted file mode 100644 index 873cad47..00000000 Binary files a/sw-ui/src/main/resources/images/cards/claypool.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/courthouse.png b/sw-ui/src/main/resources/images/cards/courthouse.png deleted file mode 100644 index 394901f2..00000000 Binary files a/sw-ui/src/main/resources/images/cards/courthouse.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/craftsmensguild.png b/sw-ui/src/main/resources/images/cards/craftsmensguild.png deleted file mode 100644 index 09bff60e..00000000 Binary files a/sw-ui/src/main/resources/images/cards/craftsmensguild.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/dispensary.png b/sw-ui/src/main/resources/images/cards/dispensary.png deleted file mode 100644 index 4917166b..00000000 Binary files a/sw-ui/src/main/resources/images/cards/dispensary.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/easttradingpost.png b/sw-ui/src/main/resources/images/cards/easttradingpost.png deleted file mode 100644 index 0c67cc78..00000000 Binary files a/sw-ui/src/main/resources/images/cards/easttradingpost.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/excavation.png b/sw-ui/src/main/resources/images/cards/excavation.png deleted file mode 100644 index 0fe1b01f..00000000 Binary files a/sw-ui/src/main/resources/images/cards/excavation.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/forestcave.png b/sw-ui/src/main/resources/images/cards/forestcave.png deleted file mode 100644 index 262fffc6..00000000 Binary files a/sw-ui/src/main/resources/images/cards/forestcave.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/fortifications.png b/sw-ui/src/main/resources/images/cards/fortifications.png deleted file mode 100644 index 3e113473..00000000 Binary files a/sw-ui/src/main/resources/images/cards/fortifications.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/forum.png b/sw-ui/src/main/resources/images/cards/forum.png deleted file mode 100644 index d6262158..00000000 Binary files a/sw-ui/src/main/resources/images/cards/forum.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/foundry.png b/sw-ui/src/main/resources/images/cards/foundry.png deleted file mode 100644 index da95a48e..00000000 Binary files a/sw-ui/src/main/resources/images/cards/foundry.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/gardens.png b/sw-ui/src/main/resources/images/cards/gardens.png deleted file mode 100644 index 9a49a0ad..00000000 Binary files a/sw-ui/src/main/resources/images/cards/gardens.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/glassworks.png b/sw-ui/src/main/resources/images/cards/glassworks.png deleted file mode 100644 index 285d7d54..00000000 Binary files a/sw-ui/src/main/resources/images/cards/glassworks.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/guardtower.png b/sw-ui/src/main/resources/images/cards/guardtower.png deleted file mode 100644 index 524b06f3..00000000 Binary files a/sw-ui/src/main/resources/images/cards/guardtower.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/haven.png b/sw-ui/src/main/resources/images/cards/haven.png deleted file mode 100644 index e0b345b2..00000000 Binary files a/sw-ui/src/main/resources/images/cards/haven.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/laboratory.png b/sw-ui/src/main/resources/images/cards/laboratory.png deleted file mode 100644 index 4c29e81f..00000000 Binary files a/sw-ui/src/main/resources/images/cards/laboratory.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/library.png b/sw-ui/src/main/resources/images/cards/library.png deleted file mode 100644 index 7495a2ca..00000000 Binary files a/sw-ui/src/main/resources/images/cards/library.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/lighthouse.png b/sw-ui/src/main/resources/images/cards/lighthouse.png deleted file mode 100644 index 2124811b..00000000 Binary files a/sw-ui/src/main/resources/images/cards/lighthouse.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/lodge.png b/sw-ui/src/main/resources/images/cards/lodge.png deleted file mode 100644 index 22758688..00000000 Binary files a/sw-ui/src/main/resources/images/cards/lodge.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/loom.png b/sw-ui/src/main/resources/images/cards/loom.png deleted file mode 100644 index 70bdf375..00000000 Binary files a/sw-ui/src/main/resources/images/cards/loom.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/lumberyard.png b/sw-ui/src/main/resources/images/cards/lumberyard.png deleted file mode 100644 index 8558af1a..00000000 Binary files a/sw-ui/src/main/resources/images/cards/lumberyard.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/magistratesguild.png b/sw-ui/src/main/resources/images/cards/magistratesguild.png deleted file mode 100644 index d7deabb3..00000000 Binary files a/sw-ui/src/main/resources/images/cards/magistratesguild.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/marketplace.png b/sw-ui/src/main/resources/images/cards/marketplace.png deleted file mode 100644 index cd3676d4..00000000 Binary files a/sw-ui/src/main/resources/images/cards/marketplace.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/mine.png b/sw-ui/src/main/resources/images/cards/mine.png deleted file mode 100644 index 4062775c..00000000 Binary files a/sw-ui/src/main/resources/images/cards/mine.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/observatory.png b/sw-ui/src/main/resources/images/cards/observatory.png deleted file mode 100644 index 1da3d7b4..00000000 Binary files a/sw-ui/src/main/resources/images/cards/observatory.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/orevein.png b/sw-ui/src/main/resources/images/cards/orevein.png deleted file mode 100644 index fabea674..00000000 Binary files a/sw-ui/src/main/resources/images/cards/orevein.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/palace.png b/sw-ui/src/main/resources/images/cards/palace.png deleted file mode 100644 index 1a24890e..00000000 Binary files a/sw-ui/src/main/resources/images/cards/palace.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/pantheon.png b/sw-ui/src/main/resources/images/cards/pantheon.png deleted file mode 100644 index 264bae02..00000000 Binary files a/sw-ui/src/main/resources/images/cards/pantheon.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/pawnshop.png b/sw-ui/src/main/resources/images/cards/pawnshop.png deleted file mode 100644 index 30bb3807..00000000 Binary files a/sw-ui/src/main/resources/images/cards/pawnshop.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/philosophersguild.png b/sw-ui/src/main/resources/images/cards/philosophersguild.png deleted file mode 100644 index f72590f6..00000000 Binary files a/sw-ui/src/main/resources/images/cards/philosophersguild.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/press.png b/sw-ui/src/main/resources/images/cards/press.png deleted file mode 100644 index c932df06..00000000 Binary files a/sw-ui/src/main/resources/images/cards/press.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/quarry.png b/sw-ui/src/main/resources/images/cards/quarry.png deleted file mode 100644 index 8cdbdb22..00000000 Binary files a/sw-ui/src/main/resources/images/cards/quarry.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/sawmill.png b/sw-ui/src/main/resources/images/cards/sawmill.png deleted file mode 100644 index 5abff473..00000000 Binary files a/sw-ui/src/main/resources/images/cards/sawmill.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/school.png b/sw-ui/src/main/resources/images/cards/school.png deleted file mode 100644 index ab2218d0..00000000 Binary files a/sw-ui/src/main/resources/images/cards/school.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/scientistsguild.png b/sw-ui/src/main/resources/images/cards/scientistsguild.png deleted file mode 100644 index 7ee639e3..00000000 Binary files a/sw-ui/src/main/resources/images/cards/scientistsguild.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/scriptorium.png b/sw-ui/src/main/resources/images/cards/scriptorium.png deleted file mode 100644 index 36dca27a..00000000 Binary files a/sw-ui/src/main/resources/images/cards/scriptorium.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/senate.png b/sw-ui/src/main/resources/images/cards/senate.png deleted file mode 100644 index ee878ea6..00000000 Binary files a/sw-ui/src/main/resources/images/cards/senate.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/shipownersguild.png b/sw-ui/src/main/resources/images/cards/shipownersguild.png deleted file mode 100644 index 3eecd2da..00000000 Binary files a/sw-ui/src/main/resources/images/cards/shipownersguild.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/siegeworkshop.png b/sw-ui/src/main/resources/images/cards/siegeworkshop.png deleted file mode 100644 index bacf8309..00000000 Binary files a/sw-ui/src/main/resources/images/cards/siegeworkshop.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/spiesguild.png b/sw-ui/src/main/resources/images/cards/spiesguild.png deleted file mode 100644 index 85e28d9e..00000000 Binary files a/sw-ui/src/main/resources/images/cards/spiesguild.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/stables.png b/sw-ui/src/main/resources/images/cards/stables.png deleted file mode 100644 index 48c963f0..00000000 Binary files a/sw-ui/src/main/resources/images/cards/stables.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/statue.png b/sw-ui/src/main/resources/images/cards/statue.png deleted file mode 100644 index 55aaa5cb..00000000 Binary files a/sw-ui/src/main/resources/images/cards/statue.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/stockade.png b/sw-ui/src/main/resources/images/cards/stockade.png deleted file mode 100644 index 37741429..00000000 Binary files a/sw-ui/src/main/resources/images/cards/stockade.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/stonepit.png b/sw-ui/src/main/resources/images/cards/stonepit.png deleted file mode 100644 index 724900c7..00000000 Binary files a/sw-ui/src/main/resources/images/cards/stonepit.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/strategistsguild.png b/sw-ui/src/main/resources/images/cards/strategistsguild.png deleted file mode 100644 index ae186a4b..00000000 Binary files a/sw-ui/src/main/resources/images/cards/strategistsguild.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/study.png b/sw-ui/src/main/resources/images/cards/study.png deleted file mode 100644 index d8b9ebf9..00000000 Binary files a/sw-ui/src/main/resources/images/cards/study.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/tavern.png b/sw-ui/src/main/resources/images/cards/tavern.png deleted file mode 100644 index 418b0fb2..00000000 Binary files a/sw-ui/src/main/resources/images/cards/tavern.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/temple.png b/sw-ui/src/main/resources/images/cards/temple.png deleted file mode 100644 index 9a8d89dc..00000000 Binary files a/sw-ui/src/main/resources/images/cards/temple.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/theater.png b/sw-ui/src/main/resources/images/cards/theater.png deleted file mode 100644 index 0d5b2b01..00000000 Binary files a/sw-ui/src/main/resources/images/cards/theater.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/timberyard.png b/sw-ui/src/main/resources/images/cards/timberyard.png deleted file mode 100644 index 0f20547f..00000000 Binary files a/sw-ui/src/main/resources/images/cards/timberyard.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/townhall.png b/sw-ui/src/main/resources/images/cards/townhall.png deleted file mode 100644 index d0638739..00000000 Binary files a/sw-ui/src/main/resources/images/cards/townhall.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/tradersguild.png b/sw-ui/src/main/resources/images/cards/tradersguild.png deleted file mode 100644 index 15777e77..00000000 Binary files a/sw-ui/src/main/resources/images/cards/tradersguild.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/trainingground.png b/sw-ui/src/main/resources/images/cards/trainingground.png deleted file mode 100644 index d59ef4f8..00000000 Binary files a/sw-ui/src/main/resources/images/cards/trainingground.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/treefarm.png b/sw-ui/src/main/resources/images/cards/treefarm.png deleted file mode 100644 index 18cf228f..00000000 Binary files a/sw-ui/src/main/resources/images/cards/treefarm.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/university.png b/sw-ui/src/main/resources/images/cards/university.png deleted file mode 100644 index c9ca8a80..00000000 Binary files a/sw-ui/src/main/resources/images/cards/university.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/vineyard.png b/sw-ui/src/main/resources/images/cards/vineyard.png deleted file mode 100644 index 58fa8ee1..00000000 Binary files a/sw-ui/src/main/resources/images/cards/vineyard.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/walls.png b/sw-ui/src/main/resources/images/cards/walls.png deleted file mode 100644 index 3823c62f..00000000 Binary files a/sw-ui/src/main/resources/images/cards/walls.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/westtradingpost.png b/sw-ui/src/main/resources/images/cards/westtradingpost.png deleted file mode 100644 index b536269f..00000000 Binary files a/sw-ui/src/main/resources/images/cards/westtradingpost.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/workersguild.png b/sw-ui/src/main/resources/images/cards/workersguild.png deleted file mode 100644 index de4f452f..00000000 Binary files a/sw-ui/src/main/resources/images/cards/workersguild.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/cards/workshop.png b/sw-ui/src/main/resources/images/cards/workshop.png deleted file mode 100644 index 8f585d61..00000000 Binary files a/sw-ui/src/main/resources/images/cards/workshop.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/gear-50.png b/sw-ui/src/main/resources/images/gear-50.png deleted file mode 100644 index 93a4a186..00000000 Binary files a/sw-ui/src/main/resources/images/gear-50.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/hand-cards5.png b/sw-ui/src/main/resources/images/hand-cards5.png deleted file mode 100644 index 1e2199cd..00000000 Binary files a/sw-ui/src/main/resources/images/hand-cards5.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/logo-7-wonders.png b/sw-ui/src/main/resources/images/logo-7-wonders.png deleted file mode 100644 index 96974d3e..00000000 Binary files a/sw-ui/src/main/resources/images/logo-7-wonders.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/tokens/coin.png b/sw-ui/src/main/resources/images/tokens/coin.png deleted file mode 100644 index f4813042..00000000 Binary files a/sw-ui/src/main/resources/images/tokens/coin.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/tokens/laurel-blue.png b/sw-ui/src/main/resources/images/tokens/laurel-blue.png deleted file mode 100644 index 115bba91..00000000 Binary files a/sw-ui/src/main/resources/images/tokens/laurel-blue.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/tokens/military/defeat1.png b/sw-ui/src/main/resources/images/tokens/military/defeat1.png deleted file mode 100644 index 1c61bf4c..00000000 Binary files a/sw-ui/src/main/resources/images/tokens/military/defeat1.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/tokens/military/shield.png b/sw-ui/src/main/resources/images/tokens/military/shield.png deleted file mode 100644 index 3a0e1dea..00000000 Binary files a/sw-ui/src/main/resources/images/tokens/military/shield.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/tokens/military/victory1.png b/sw-ui/src/main/resources/images/tokens/military/victory1.png deleted file mode 100644 index 6b9aff29..00000000 Binary files a/sw-ui/src/main/resources/images/tokens/military/victory1.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/tokens/resources/clay.png b/sw-ui/src/main/resources/images/tokens/resources/clay.png deleted file mode 100644 index 72fc0b0e..00000000 Binary files a/sw-ui/src/main/resources/images/tokens/resources/clay.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/tokens/resources/glass.png b/sw-ui/src/main/resources/images/tokens/resources/glass.png deleted file mode 100644 index 61fd2be5..00000000 Binary files a/sw-ui/src/main/resources/images/tokens/resources/glass.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/tokens/resources/loom.png b/sw-ui/src/main/resources/images/tokens/resources/loom.png deleted file mode 100644 index 294adcb2..00000000 Binary files a/sw-ui/src/main/resources/images/tokens/resources/loom.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/tokens/resources/ore.png b/sw-ui/src/main/resources/images/tokens/resources/ore.png deleted file mode 100644 index c2149daa..00000000 Binary files a/sw-ui/src/main/resources/images/tokens/resources/ore.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/tokens/resources/papyrus.png b/sw-ui/src/main/resources/images/tokens/resources/papyrus.png deleted file mode 100644 index 91a59221..00000000 Binary files a/sw-ui/src/main/resources/images/tokens/resources/papyrus.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/tokens/resources/stone.png b/sw-ui/src/main/resources/images/tokens/resources/stone.png deleted file mode 100644 index 674c40db..00000000 Binary files a/sw-ui/src/main/resources/images/tokens/resources/stone.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/tokens/resources/wood.png b/sw-ui/src/main/resources/images/tokens/resources/wood.png deleted file mode 100644 index 09a4ede8..00000000 Binary files a/sw-ui/src/main/resources/images/tokens/resources/wood.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/tokens/science/cog.png b/sw-ui/src/main/resources/images/tokens/science/cog.png deleted file mode 100644 index 61250d8a..00000000 Binary files a/sw-ui/src/main/resources/images/tokens/science/cog.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/tokens/science/compass.png b/sw-ui/src/main/resources/images/tokens/science/compass.png deleted file mode 100644 index 6497e34f..00000000 Binary files a/sw-ui/src/main/resources/images/tokens/science/compass.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/tokens/science/tablet.png b/sw-ui/src/main/resources/images/tokens/science/tablet.png deleted file mode 100644 index 954fd9ef..00000000 Binary files a/sw-ui/src/main/resources/images/tokens/science/tablet.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonder-upgrade-bright.png b/sw-ui/src/main/resources/images/wonder-upgrade-bright.png deleted file mode 100644 index 0f59c068..00000000 Binary files a/sw-ui/src/main/resources/images/wonder-upgrade-bright.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/alexandriaA.png b/sw-ui/src/main/resources/images/wonders/alexandriaA.png deleted file mode 100644 index 0d4135f3..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/alexandriaA.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/alexandriaB.png b/sw-ui/src/main/resources/images/wonders/alexandriaB.png deleted file mode 100644 index dd072f8a..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/alexandriaB.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/babylonA.png b/sw-ui/src/main/resources/images/wonders/babylonA.png deleted file mode 100644 index ae323c78..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/babylonA.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/babylonB.png b/sw-ui/src/main/resources/images/wonders/babylonB.png deleted file mode 100644 index 3780dc9d..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/babylonB.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/ephesosA.png b/sw-ui/src/main/resources/images/wonders/ephesosA.png deleted file mode 100644 index 307794ba..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/ephesosA.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/ephesosB.png b/sw-ui/src/main/resources/images/wonders/ephesosB.png deleted file mode 100644 index ec2e9cb7..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/ephesosB.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/extra/agrigentoA.jpg b/sw-ui/src/main/resources/images/wonders/extra/agrigentoA.jpg deleted file mode 100644 index 76ba8195..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/extra/agrigentoA.jpg and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/extra/angkorwatA.jpg b/sw-ui/src/main/resources/images/wonders/extra/angkorwatA.jpg deleted file mode 100644 index 32f52514..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/extra/angkorwatA.jpg and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/extra/angkorwatB.jpg b/sw-ui/src/main/resources/images/wonders/extra/angkorwatB.jpg deleted file mode 100644 index c3f4304e..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/extra/angkorwatB.jpg and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/extra/avalonA.jpg b/sw-ui/src/main/resources/images/wonders/extra/avalonA.jpg deleted file mode 100644 index 7f7f0678..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/extra/avalonA.jpg and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/extra/ctesiphonB.jpg b/sw-ui/src/main/resources/images/wonders/extra/ctesiphonB.jpg deleted file mode 100644 index c00b40ac..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/extra/ctesiphonB.jpg and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/extra/iramA.jpg b/sw-ui/src/main/resources/images/wonders/extra/iramA.jpg deleted file mode 100644 index d2c24e95..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/extra/iramA.jpg and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/extra/persepolisA.jpg b/sw-ui/src/main/resources/images/wonders/extra/persepolisA.jpg deleted file mode 100644 index 2caa4f89..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/extra/persepolisA.jpg and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/extra/romaA.jpg b/sw-ui/src/main/resources/images/wonders/extra/romaA.jpg deleted file mode 100644 index c54bc820..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/extra/romaA.jpg and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/extra/sangri-laA.jpg b/sw-ui/src/main/resources/images/wonders/extra/sangri-laA.jpg deleted file mode 100644 index 1c5dad97..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/extra/sangri-laA.jpg and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/extra/spahanA.jpg b/sw-ui/src/main/resources/images/wonders/extra/spahanA.jpg deleted file mode 100644 index ab2cfc84..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/extra/spahanA.jpg and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/extra/the-great-wallA.jpg b/sw-ui/src/main/resources/images/wonders/extra/the-great-wallA.jpg deleted file mode 100644 index 4aacd39b..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/extra/the-great-wallA.jpg and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/extra/veniseA.jpg b/sw-ui/src/main/resources/images/wonders/extra/veniseA.jpg deleted file mode 100644 index 55ec00b5..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/extra/veniseA.jpg and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/extra/veniseB.jpg b/sw-ui/src/main/resources/images/wonders/extra/veniseB.jpg deleted file mode 100644 index e18f3a12..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/extra/veniseB.jpg and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/gizahA.png b/sw-ui/src/main/resources/images/wonders/gizahA.png deleted file mode 100644 index e66735fb..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/gizahA.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/gizahB.png b/sw-ui/src/main/resources/images/wonders/gizahB.png deleted file mode 100644 index ed55ed45..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/gizahB.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/halikarnassusA.png b/sw-ui/src/main/resources/images/wonders/halikarnassusA.png deleted file mode 100644 index 659f706e..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/halikarnassusA.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/halikarnassusB.png b/sw-ui/src/main/resources/images/wonders/halikarnassusB.png deleted file mode 100644 index b6ae1f93..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/halikarnassusB.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/olympiaA.png b/sw-ui/src/main/resources/images/wonders/olympiaA.png deleted file mode 100644 index 478ed503..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/olympiaA.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/olympiaB.png b/sw-ui/src/main/resources/images/wonders/olympiaB.png deleted file mode 100644 index a97a9524..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/olympiaB.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/rhodosA.png b/sw-ui/src/main/resources/images/wonders/rhodosA.png deleted file mode 100644 index 0c11a71a..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/rhodosA.png and /dev/null differ diff --git a/sw-ui/src/main/resources/images/wonders/rhodosB.png b/sw-ui/src/main/resources/images/wonders/rhodosB.png deleted file mode 100644 index 43e5d594..00000000 Binary files a/sw-ui/src/main/resources/images/wonders/rhodosB.png and /dev/null differ diff --git a/sw-ui/src/main/resources/index.html b/sw-ui/src/main/resources/index.html deleted file mode 100644 index b43c1428..00000000 --- a/sw-ui/src/main/resources/index.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - Seven Wonders - - - - - - - - -

- - - diff --git a/sw-ui/src/test/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFrameworkTest.kt b/sw-ui/src/test/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFrameworkTest.kt deleted file mode 100644 index f810c8b9..00000000 --- a/sw-ui/src/test/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFrameworkTest.kt +++ /dev/null @@ -1,89 +0,0 @@ -package org.luxons.sevenwonders.ui.redux.sagas - -import kotlinx.coroutines.* -import kotlinx.coroutines.test.* -import redux.RAction -import redux.Store -import redux.WrapperAction -import redux.applyMiddleware -import redux.compose -import redux.createStore -import redux.rEnhancer -import kotlin.test.Test -import kotlin.test.assertEquals - -private data class State(val data: String) - -private data class UpdateData(val newData: String) : RAction -private class DuplicateData : RAction -private class SideEffectAction(val data: String) : RAction - -private fun reduce(state: State, action: RAction): State = when (action) { - is UpdateData -> State(action.newData) - is DuplicateData -> State(state.data + state.data) - else -> state -} - -private fun configureTestStore(initialState: State): TestRedux { - val sagaMiddlewareFactory = SagaManager() - val sagaMiddleware = sagaMiddlewareFactory.createMiddleware() - val enhancers = compose(applyMiddleware(sagaMiddleware), rEnhancer()) - val store = createStore(::reduce, initialState, enhancers) - return TestRedux(store, sagaMiddlewareFactory) -} - -private data class TestRedux( - val store: Store, - val sagas: SagaManager, -) - -@OptIn(ExperimentalCoroutinesApi::class) // for runTest -class SagaContextTest { - - @Test - fun dispatch_dispatchesToStore() = runTest { - val redux = configureTestStore(State("initial")) - - redux.sagas.runSaga { - dispatch(UpdateData("Bob")) - } - - assertEquals(State("Bob"), redux.store.getState(), "state is not as expected") - } - - @Test - fun next_waitsForNextAction() = runTest { - val redux = configureTestStore(State("initial")) - - val job = redux.sagas.launchSaga(this) { - val action = next() - dispatch(UpdateData("effect-${action.data}")) - } - advanceUntilIdle() // make sure the saga is launched - - assertEquals(State("initial"), redux.store.getState()) - - redux.store.dispatch(SideEffectAction("data")) - job.join() - assertEquals(State("effect-data"), redux.store.getState()) - } - - @Test - fun onEach() = runTest { - val redux = configureTestStore(State("initial")) - - val job = redux.sagas.launchSaga(this) { - onEach { - dispatch(UpdateData("effect-${it.data}")) - } - } - advanceUntilIdle() // make sure the saga is launched - - assertEquals(State("initial"), redux.store.getState()) - - redux.store.dispatch(SideEffectAction("data")) - yield() - assertEquals(State("effect-data"), redux.store.getState()) - job.cancel() - } -} diff --git a/sw-ui/src/test/kotlin/org/luxons/sevenwonders/ui/utils/CoroutineUtilsTest.kt b/sw-ui/src/test/kotlin/org/luxons/sevenwonders/ui/utils/CoroutineUtilsTest.kt deleted file mode 100644 index ef8dfb62..00000000 --- a/sw-ui/src/test/kotlin/org/luxons/sevenwonders/ui/utils/CoroutineUtilsTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package org.luxons.sevenwonders.ui.utils - -import kotlinx.coroutines.* -import kotlinx.coroutines.test.* -import kotlin.test.Test -import kotlin.test.assertEquals - -class CoroutineUtilsTest { - - @OptIn(ExperimentalCoroutinesApi::class) // for runTest - @Test - fun awaitFirstTest() = runTest { - val s = awaitFirst( - { delay(100); "1" }, - { delay(200); "2" }, - ) - assertEquals("1", s) - val s2 = awaitFirst( - { delay(150); "1" }, - { delay(50); "2" }, - ) - assertEquals("2", s2) - } -} -- cgit