From 325f2e44f632776af4be464303a75e72b31b3b05 Mon Sep 17 00:00:00 2001 From: Janis Date: Sat, 1 Nov 2025 21:35:08 +0100 Subject: [PATCH 1/2] feat(ui): add main menu navbar and join game functionality --- bruno/KnockOutWhist/Game/Get Game.bru | 2 +- bruno/KnockOutWhist/Game/Start Game.bru | 2 +- knockoutwhist | 2 +- .../app/controllers/MainMenuController.scala | 18 +++++- .../app/controllers/UserController.scala | 2 +- .../app/logic/game/GameLobby.scala | 3 + .../app/views/ingame/ingame.scala.html | 2 +- .../app/views/mainmenu/navbar.scala.html | 56 ++++++++++++++++++ .../app/views/mainmenu/rules.scala.html | 2 +- knockoutwhistweb/conf/routes | 5 +- knockoutwhistweb/public/images/profile.png | Bin 0 -> 37198 bytes 11 files changed, 84 insertions(+), 10 deletions(-) create mode 100644 knockoutwhistweb/app/views/mainmenu/navbar.scala.html create mode 100644 knockoutwhistweb/public/images/profile.png diff --git a/bruno/KnockOutWhist/Game/Get Game.bru b/bruno/KnockOutWhist/Game/Get Game.bru index 6b6d6af..8bc5076 100644 --- a/bruno/KnockOutWhist/Game/Get Game.bru +++ b/bruno/KnockOutWhist/Game/Get Game.bru @@ -11,7 +11,7 @@ get { } params:path { - id: uZDNZA + id: BZvtJ3 } settings { diff --git a/bruno/KnockOutWhist/Game/Start Game.bru b/bruno/KnockOutWhist/Game/Start Game.bru index cc231bf..fa798f6 100644 --- a/bruno/KnockOutWhist/Game/Start Game.bru +++ b/bruno/KnockOutWhist/Game/Start Game.bru @@ -11,7 +11,7 @@ post { } params:path { - id: uZDNZA + id: nR1o3n } settings { diff --git a/knockoutwhist b/knockoutwhist index e0e45c4..b9a7b0a 160000 --- a/knockoutwhist +++ b/knockoutwhist @@ -1 +1 @@ -Subproject commit e0e45c4b431fff6740e38a59906f5e217fcd801f +Subproject commit b9a7b0a2af7cef7225bf1a0388ebf58171a173f2 diff --git a/knockoutwhistweb/app/controllers/MainMenuController.scala b/knockoutwhistweb/app/controllers/MainMenuController.scala index a7a1637..7032b52 100644 --- a/knockoutwhistweb/app/controllers/MainMenuController.scala +++ b/knockoutwhistweb/app/controllers/MainMenuController.scala @@ -21,7 +21,7 @@ class MainMenuController @Inject()( // Pass the request-handling function directly to authAction (no nested Action) def mainMenu(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => - Ok("Main Menu for user: " + request.user.name) + Ok(views.html.mainmenu.navbar(Some(request.user))) } def index(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => @@ -36,6 +36,22 @@ class MainMenuController @Inject()( ) Redirect(routes.IngameController.game(gameLobby.id)) } + + def joinGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => + val postData = request.body.asFormUrlEncoded + if (postData.isDefined) { + val gameId = postData.get.get("gameId").flatMap(_.headOption).getOrElse("") + val game = podManager.getGame(gameId) + game match { + case Some(g) => + Redirect(routes.IngameController.joinGame(gameId)) + case None => + NotFound("Game not found") + } + } else { + BadRequest("Invalid form submission") + } + } def rules(): Action[AnyContent] = { Action { implicit request => diff --git a/knockoutwhistweb/app/controllers/UserController.scala b/knockoutwhistweb/app/controllers/UserController.scala index 361826f..d3e5ebf 100644 --- a/knockoutwhistweb/app/controllers/UserController.scala +++ b/knockoutwhistweb/app/controllers/UserController.scala @@ -64,7 +64,7 @@ class UserController @Inject()( if (sessionCookie.isDefined) { sessionManager.invalidateSession(sessionCookie.get.value) } - NoContent.discardingCookies(DiscardingCookie("sessionId")) + Redirect(routes.UserController.login()).discardingCookies(DiscardingCookie("sessionId")) } } \ No newline at end of file diff --git a/knockoutwhistweb/app/logic/game/GameLobby.scala b/knockoutwhistweb/app/logic/game/GameLobby.scala index 7dadf4a..6ee102d 100644 --- a/knockoutwhistweb/app/logic/game/GameLobby.scala +++ b/knockoutwhistweb/app/logic/game/GameLobby.scala @@ -70,6 +70,9 @@ class GameLobby private( if (!sessionOpt.get.host) { throw new NotHostException("Only the host can start the game!") } + if (logic.getCurrentState != Lobby) { + throw new IllegalStateException("The game has already started!") + } val playerNamesList = ListBuffer[AbstractPlayer]() users.values.foreach { player => playerNamesList += PlayerFactory.createPlayer(player.name, player.id, HUMAN) diff --git a/knockoutwhistweb/app/views/ingame/ingame.scala.html b/knockoutwhistweb/app/views/ingame/ingame.scala.html index cb24ac0..f587dbc 100644 --- a/knockoutwhistweb/app/views/ingame/ingame.scala.html +++ b/knockoutwhistweb/app/views/ingame/ingame.scala.html @@ -17,7 +17,7 @@ @if(logic.getCurrentTrick.get.firstCard.isDefined) { @util.WebUIUtils.cardtoImage(logic.getCurrentTrick.get.firstCard.get) } else { - @views.html.render.card.apply("../../../public/images/cards/1B.png")("Blank Card") + @views.html.render.card.apply("images/cards/1B.png")("Blank Card") } diff --git a/knockoutwhistweb/app/views/mainmenu/navbar.scala.html b/knockoutwhistweb/app/views/mainmenu/navbar.scala.html new file mode 100644 index 0000000..a5f9cc4 --- /dev/null +++ b/knockoutwhistweb/app/views/mainmenu/navbar.scala.html @@ -0,0 +1,56 @@ +@(user: Option[model.users.User]) +@main("Knockout Whist - Main Menu") { + +} diff --git a/knockoutwhistweb/app/views/mainmenu/rules.scala.html b/knockoutwhistweb/app/views/mainmenu/rules.scala.html index a6a1b90..08a968d 100644 --- a/knockoutwhistweb/app/views/mainmenu/rules.scala.html +++ b/knockoutwhistweb/app/views/mainmenu/rules.scala.html @@ -1,7 +1,7 @@ @() @main("Rules") { -
+
diff --git a/knockoutwhistweb/conf/routes b/knockoutwhistweb/conf/routes index c122c96..f1867c7 100644 --- a/knockoutwhistweb/conf/routes +++ b/knockoutwhistweb/conf/routes @@ -13,6 +13,7 @@ GET /mainmenu controllers.MainMenuController.mainMenu() GET /rules controllers.MainMenuController.rules() POST /createGame controllers.MainMenuController.createGame() +POST /joinGame controllers.MainMenuController.joinGame() # User authentication routes GET /login controllers.UserController.login() @@ -22,9 +23,7 @@ GET /logout controllers.UserController.logout() # In-game routes GET /game/:id controllers.IngameController.game(id: String) -POST /game/:id/join controllers.IngameController.joinGame(id: String) - +GET /game/:id/join controllers.IngameController.joinGame(id: String) POST /game/:id/start controllers.IngameController.startGame(id: String) - POST /game/:id/playCard controllers.IngameController.playCard(id: String) \ No newline at end of file diff --git a/knockoutwhistweb/public/images/profile.png b/knockoutwhistweb/public/images/profile.png new file mode 100644 index 0000000000000000000000000000000000000000..941c0dce378c752ba3e21fdad32784563422f65f GIT binary patch literal 37198 zcmeFZWmJ`0_XkQ!Nk}SP3Ifv7DJ?B64bq@=hoFR@bPGs}(jk&cN(xAKNrQlNbLT$q zId_cv{eJv2#u*10u=n$~;>gS0-A zxgZQB$dK6n*k+FsNP#9nS?>9tfUk3HPqRqyE zU!_HcpQ;U%UJ3Ejph$;*e=iydA2Ty^pEhg_<$rHP3%_fR z`rpfVN+6-9V@OJq{rA3^ndTJ#y-X7-3iFx>CVkz%7a@;?PBr!K_0l3!6G%v|N)r4+ zhpYMLubJ1zZ~XT%yDyQjh46#I*#Emm3CRw=|6b-L23)=ufwU{~|L!FkezzKNQ~&(+ z|J~~UR^tDj>c4yE|2FXdj>-S;XZ2;!t+0rQ!i1QZU(5^)Iiu&tJNis_?~dFctNQX~ zJuQv0D~?H3DDUG(yW!7TifU?Vv}!s!*JD!Ux(0i~sHm@6NFO{E(bCc)!3lm8D9=c^ zwz;{Qnv&AU&BfJKRaO=@IyTm?#>uN|$e1|{Cq6;wVy!`hNM2r^)BpU)A+DgH;71gV z`1Ad}<(?bH-+XOi($ee^?~-lvl$kU_3k@Yu4PN|pD*I)LtU9rlfdLy*c}Qt?b~d4o zAqh!>nX>ZFx9)!?8^2any|taGJN#|=GgU_a z>C187V4B5v0x{^$}LSTtvnN_rxvg8lvsF$H5yRfTwt%g zaiuJltHQ#_ex`)W(M3l`M)rk>jJTug2 z^88YqvWC!|yBCkd49S==l!KO%WVZmLirVvB-3GH3C<^`TU&cZ+_|A^3qs5~Qc?_FdrMs(@2RBKH2NHG zbD*P0(#fb>^m7gKVpc_=5kniTLqWqTWDbBk)V|(bc=h*_#>XF9bKe6NbL66gINR~P zr=&3zUL;&ZUg&$@rf8$O`q7YaFlrZzl+qu%Gf(u1X1!c$Cb@O{*|+qrSG9aE4OC@W zrRi_Y6sYImYve0U@YoI)Fg$o#6_(G;^8KEl?3BF3Ev(E;RloN?Xzp!7;8wB#o$|I+{3sO1r^CqjXwvi;*!B2Z@clSs`wL0>- zXK^-fnv971cB=WUcgoI|nYQ)`3JYVnZjEz2QB~z23O2l>k9lTr3wAU~*6T55s=(e} zrB|Juo%w-*fkO=R^goD+iPxep$CZAm3vqJ0*A&Tk^nO9h#o!>u{*Ff@cF~ZN^Scku zhv=`-Q56}}Bqz=?fnX*mIlFYy7|#@t@Ptm`*cWOQY72f454f@ll*q)v8Is43rF=$@ zA2IT5-9tH{JU-`AdJ*&*rIQ_1=xbWpEK+~I0W|3a;UYP6sAXv&7b(350F$Xi6DHBF_ z6Q!`ZfSASE!MfZ^Zwe9AdRASL<9<(3|LB*5h{v-ypR;*QAg`pvTkG^&_QOb_PQ94V zQIRNP==}&>4bN|1#3^dEBatet@H|lTE9yRM^!`r+uAXBs@>=591#){LMwj#!C`%n<4N9iUj>xEjg5`EM#UudW2f6j?oQ3I z#1U|8%-V76SW!`;?JnVh-}PsR3b*w4_p^(NHrhBjmCJia3AlXcO}4lvx4?4v`Q~)O zCVh8mwaJgH2F==~yoiG(#p53xHMO-=6_u4!LlqW1{K;-AB~nod=79iLan#D?PwyfX zj&T^&42W}b4qWx63(uGa)Lx8|+G(q!yk+x6cO>X7(yKhWxY+Nv+8isT(fP%_E5w+& z2Bn!qG)VUvo9JY3IU*q{s$WUql8uR}1I1^8M3!lc%+|&R%h}oa@kT*T4nt>mH=(ty zZGIH_!;OO=bR51ao1uG8N~oH;>5%W-l8{WgCC?`7kJW_C!vB1Ft=9d|(^XgrTmnqA z$vu7jB0UQ%dDR5YZH|rc^50ads{O^SA-MGt>Y7FtJwG;MUed!$=;T_P8nP-Qg^WXI z5WPoBYa;9F>MHz3nm(xUXITZgwwihV*T)PAg%rsVOUdizDG_&m{`@)DoBGi1Lwfp* zm;n}t&O&Q@_xSC6pA5!FyKVp_{4g7&&*Tu6~{`3TiBO_>PIp1nkTLJ z;?wU1T~<`|^{0pg&^VG7+8%4YLc;cbIKNQ5CrA6>fik~{i0>0e$Fe`r#OL4c#nQQ3 zs^Z19bQ({8)?iqFExsh;ZrPi1-{;-ix3fY*Le%cAuG7y=O>gN_O?h$dML}la;IDj^4dSXG-r<(&x1`DJiT-M#J(P={Fmb;=*o> zs^vyf3hoWVO8OQT70q@B^XOtRswu-Oj-Z8o?ZF7Fsi`@mrl#&W+L;rVWh7kZ!at0T zB2P5N@hqq1pm8J#BT#VZZYV4onI3fMR0=e|>Sd$pX4@1y5p}^v*^nf~jonNM;NVz~ zXxO(L=CGP?3MBLM^=%Mx-#SqW+<2!YgJMAl+$$_X$_IH=YVS>x^c_ zA!Xa$p8C4uhklG+Sh|G3eGSOWb>{(>hhz8=WE@7`%iSat4K_nyV6ObJ7y74xeGMoyREq`ODGb6hgtieivs_cQQMx9*3tgj{YE zY8O8hhBu7Zn>H$YE zCa2Gz`JAdXoo6pZ$M1ejOgJ{96-C3pzQ=Ksq&y1~H=--cYkvhFTHZI)#kM!0#97+_%U5 zj73z`Q26fMc1rJKFE6i_++4<2ZEe!~Z{OaGYiW7(pe15Q7&+Mvr86S-&6_S^9i1^M z#4}AAbANUC2*o0@h>nyoQy$HIYuxlJ!0L7^EG%U@)Z3>oqnp1=)^w*PCF;=R9e41M zeUB;Dfj3Uc&(AM^N5OwJohkXsT0>(f{su0210ZuHEe(wxPT&fFMNb|}$x?-9+m4l_ z-zCO&>Dim;>?~LsD=#nKI6XZ*v$eH#H8rJ6NJ`RS<>ArbfA~;~iHqwOEgxUC4OEwA zz@X&Pa&qqhwhu#*=QqJSmYN~4;g{4C>BPZF$)I!S=ddags+Iiyr{!Rk|^QYZYPHnIQ zHZe5Y=p$ZTRpsmO{CP%xPEH)MS#frDECA2scFXzLxVSIvX63TapO;V{ti;8|9YTAb zUTS`YLA5>GICXY;e*7~cJp5q12vx^|X9~-OE32gB`PSLtrjGIlF+X7)H%VdnOd;6L zg;MxC%WsM{Elgsn~s@2s^BrQ$N@S8Vp zig|1duSF}^-4*!eUs<&Zbw|$ScCcfF&*pSl4$fFBz zj&U|A;_WiqmDB5qa-_!@QjbdL>gxKAS3A5P>KDI$t3OGcWy<12r9O}#a1uU$))7TR zag%~?YXHiDaA83K=JEn^|C08snDbf|p{Z2{65_czI5=E74i~6f!2V7wrI&*A7$i~-VF2%Vcz^Z zHaNI<3b1txQCh!dcVKPy8$%htcvsxwb9B0Ib@_&fiTb60f<8d-HpE~@-5 z&&pwG-pSC%*m^OL_EKTz4G~%@o~!UuIo<%0+F0EFTT5!0A+n}a&}nQYp?Y&vtloWhg6%y&d#m_ zy8LESXADCW@AGMaZdjvAp(Elyv#(#H6$b^O5DCQ1j*WRdwYB}6keHbGSVcwU4QuZq z+JH+a8Snbg-=*f&!9V`7qs98yhaEHl7iMN=vCQ~~Ekd-&_YguPzOx+Cie)H1&9`e< z;qL{7g@w)0Z0i$Tq0g7^6H%r}aTLd)9fb-D32nPLI*tzkY4HrtUiyuj8Qwq{;7WTzq_2+yPe?REQ!0 zj9&qY&*ewf@DGG2v|E7OM}q;Ai&6BS;EC!}HL@1p~$n~VBzyAfIj8H!z*0K!YOOagEljK#lYw+67|J2xcc?LIqlNdYr z`bT3!($@-ie8<4xTBAoYGQrv!8j2!5M_X&KM`spV#3m(B<``ZEU5P$A@le4W;C-+$ zT0E5pwV*Odn*P4R(>@FQVm4E&(LlkkIEO5TO$qCN{xo*a&idEK#Hi!RsPi(Fd0M~D zw<(6Q8Rh8YG!Y#eYo_$1x1}fkpb9%JA49>a#&dz8pvRUbn)SO(1gYwanYt00)giBO zW|D!s#IEQ!$!Q8j+%|?EfV}mU3_JMC-u^z_UBQp~gWW`~CWfO%`^NsK%hqaY0~bL4 zTY~t5=cL&29k7iy7JvUfyu7^Bv7fH_g>^#_(`N(3!nD>PpS_j z73F=XU+HHTr#zH3^smv?cgt2-7}5?# z`PVV{?i_`Zy{&eb5}W7Z2EG`s8?@8ikW6`Q}S1e6+zj$`X-^WN|H{2M7vP8_c`O4xt35hXZuipL7=RZ?l zg~^DC!%TirR=oqJsxqqUYxj$hr;(#72Ci0iw$9D&C6q>%AGffE(U6G`QZpm@Bx^na z!6-f6-+!;Jt6Lxy%^5mbt3t`(_cs6Y=g-oHhN2^5V`FykcESJ_+XkFek&G@PX~e~* z`}&@{sH>~bKL)B2fdckRbr3n-?(tl} z_4wBpbFcLD^lr-YTD1a8>)oCZ#5gBkOJ z51?gO^ER1myI zJzQN4zPyW>B(DtWZGv$OYlrNO~C1T<>&jP z^^J{uts$+3bW{|_w(af&RlMAVbzvHH7KX$Gn;tKsBIiJP<@$=_YEV=X& zy-Lfw;eu8_CGUku3a^~JUWi)_5hM80E+Oxhlhp_p_)>W zu%U2Bp^VVK3^ID)>**qcm7AZx@fN7#03hod67&K!QLO4oMkhZ&I~gACr51MI3Uu2X zeZbGdqgYZP?wNi;@Un43zuI1pfRb`6EhXhszs1jF{mD*Bv8v5M$^$|>yUlY1V9eo0JCLUM?D`}dg|?g|c?UR7>ZR?Hlz+&y%3bP;eB$3bqG)-T;q zcILi+)vdOl_#74%76Y4OvWVi@ZQATUB?U$A4ImsGpf(G?fNRsKDI%JDA_jY+o6JQ; zG-S56zP?@+iv0l;n^${%HI{`>o*Bi3d9O=V7eM>)INqB0{j1hx6~lhwhjT5{F1$`5 zJhS3@XlMlBd{EfZiqf}D^Y^*qDo*+Ifxw1GCE_`Ng@0iBzQ#EzDyU@ouhq;sD21d% z_msw|?%bIO=K88@mQ>hAV-jInU!$HQ8zm(rC3*&!Gu;iz`lSm>sqy#c=5}`VM?ex# z842GAX}(PHi+(%kIC3R1Ou=V=8?Hy?D*`*OP-2p6lEd9RCbHe8dhm1X+w;SX5gs&@ z=I@OzHl|jUmID;8L|dLE&vkaL8A?br?fw1TL1Q_@o@iK!_I&Q!%+Tnldn@2Ap>Sz> zx+-LAoM%a>U7Q_92l(dxq8)vG+f5){tS<6Zr#evhR!0W)t*(uhy}+jyDa#P?+KU8q z?i&1=Q}rVY>oUvx5p*A7nLIYbI$qEAa1t?MndNdD{GJjo6o3a+#_)Ld+S=M~3^F`Q zO-&7I5IK5}#N!kLq_VQCj57*amEmVEAzf545Fl2~HJ95yqT`Sj%~aXM<)|_9GQ-y$ zvrr65g|4pU>Mx6ni)o0Xhk5g+fzj9JGyTp%n1#*sfmKz!h8!HF%b?000onA<#eKAs zR#+|Py;hd^Dt|oT5rV&pO);qc;(<2syKI{p_Ra2ewdL3Q}@-K1nz2Vng2_ zc<^ni3lXYKgVr~dm6e49?dgo^{n5h^DenKW0A=>4^t)hYkb=K(RO@@XXW~piaLc7c zhN_eI#q0%gk>}1}UXSQ0eejnBR4DIa(&~7Ay5Id9L%)9g3L2pCPlFN%g%#caBw1}%o_x5E`fa%}E6cR5xY(gH1eYT539(EM z+5Mt-|ElKQrk4w0Jy70WT<`abJHXC~rqsvoP~=`^uU(_F0o4!$%s;wN%Wc#6lUjeY zNG8k^&;nM~p`oEk0v_i*fVa@vu7_e0TIEvlc@_j^cTdk67icLT9OuPS)#wpBjo7}# z05q-Qb7)dCU|#(kuh!EZ75owLNd5Zivj4?Y_0M9Pp?sQ&5}ZVi?`VWXM9KyR2EMRo z1Y~ImhChEweKbYoVMD>Vf z)!??Ny$Xe>A(7^hE|CmEXr*O{fkhM4VQOz#B<8ZcEvl2EA;H>*LsJpzz{X}_4OTv)`(td zL(!wnj;JUkFSHx;Y@RLw79b-a;3NupnAcBb)hZyuxWMp`kk)3bWabjeX5Hb|1d+~{ zN@OF+9^b`Mv0o}KHy3uD$);l@I;k(JQ2%CEl#z&A2TC!y#P{^Q{lj;ywDjWUn#>c`+ zsdZf+3|?Pf$DJT`U1<^KekgtQqRMsz+enBG{jWoNPT0dlIk<(r+a&148oIjUuK4)) z{lLF*N_d*N_MFKM|4bcU02N)~wHaiTl~gzTqQ@AWkYFJIrT$$Odb!n zrfYn#T&uvyI7QU_iZyo9Vrhiz$2@Ldml_|hIn~~7aT$n;u?2LMZ5op}Krb=MgHSSjeG5k6Hgq8m~SfarKLOM2)VAEA?)EwYEydzS*U=3zzLAV+Pf+AW-263 zP7R;s8N2nM0^We)L}G9rb#x*J*B!vMJe>p7*_@xB{~F4$>g!&|UXn(8osccj&{?li zqsBftY3cDrzK7MH_H%qw^B;oKMMZe)7FRw9!K9c1zA;M92F0iV{sgIjua3gWz`vl4 zFd8OCKx6uThI-h)XEs0H-q{ob)hR@<0kfq~#_Y~o76k=Gz*7X`hFe&i7L1xCohoU{ zdQe?mO<`_st{+V;NOu2Cia%@_KP;%RLN0$Xvf#jj|}V%$67V! zVMdg`;Ao;%=I~h9mMfEh#^vTSj8q==Q84f-7vkUMoSz3^;!hgU{`nbpXrWAco`!}D z9ND?0ot^J*vkT~Fn`Gn!;E?tSI{V-0?d|1F=CO)2hT>KhBHQ66B`=&8a7FEEVtl_sG=M2aB@au*+ zF#CO;k~FWwwO=_ZczAgJQPiRviq)TDFzCo4!kd19dVJ}%+LumOQ;}X-w%@Jx=uyxp z$Q-J(^&X|A;o;%fY#U2^s8ScCq@=1~O5iG3vb)$&IQ#``N$C4xS1mtKDDvM+OG`9Q?CzJwc?r>>_A`}?HwxOx5Vh=|ov}_tc>|crSY-|ftj2<-=6vwgwa26wtYMm_#;Qq<& z4S#1R^;K~JnI!^RX!LSwEWQ%AyTy*!RZmTGko)TDd?yn0o=w1Fb4h|qdVN^dTG?$| z>r%D5J0LT6zIre)Fd&ccUfc!fzdR29tt|gi(gcJWm&S$$Qmbdrl4FfsxZX)0)g!b= z8%M|83j`);ZlVqR4B4_$?1v2>UpIlICMzf)V6UX8Sg5MK85`_WLGEmI@7QGW-8&QD;|Q=7>nbPj15nH0P`+)InlZ58+KJN}Zw5PtUgF(aV`20D5zct=D$RNDN> zkG;jQ>tb&hkv=~^JM(6HT4hajQ|IwIC&=svr!&v5pQabQQY$9{NR}kD1rB?tWZ4wy?6IfIoks z48`?r#gk{<&f|u4Y`D0%x7}6$?(I#MJ?kQz_9J?kVc45DpT=M#Dn?@`;$T0s4X=W_w{Hb{hzFksd(Y74PAPm+0unN^Rb24R(b&R%cN`>Lr0S)XToXKj|e!Q&N;p*b_s_b^Ma*jn_OflR*ckEq7 z?fTkU2h$?lx1q8XC^10*Jf*;&*<8UsDm~9v|8SK(ZGR8WDwh zgjM8gHRNJFHrR1h z+xFbvTvZ7PT1x=TC(U@Hk@V3d26n(KFr+0YD6>HAwkYADTbuy=;o$B1E(H=bsfJ`pmPTdx^!HCsG^1!`B_$(DA=l1NkmKR6sd(ppA_?V)#%XCd0&JC|53t+S;O}_Yz1Kj- z4{B)%c=82uS9#!&OV9|Z3lBrx~ zZDE+z4(6v9K=%4dJ)IYS!WNQKO5wJUg96f}9%moW`Pu8l2t2(uDq;8DJL3KqT-ATf zPP-N7K{_^+jimHq6BMkao+8ezWWf%89cAV z?LI9xYbOuS4j-?MTHX(ZA|GJZ^@a&>Ly2y=*{82xkDshe^S%F(ay|i7NBo=rWnElG zhLO;FzH)m;6I0W_^g=@9+&{{X(&8VpXjDX?iXk3H`STeeRq#!u-=Yx~Fe8tW+sd%M z9J@~%2!>l6uuU(-LzYbZWcXbMS^K&rNtZ2AS5Hs0gJe5=#FF?+HR_FC)A880%&sn><5wJaG2dt*2}Lp4(D1qr`^*Umtw^lrQp8$tHqGi8{_VJ!3~p zPtgSwP7D=ewFQC@a-BaY*wC@!RhycdP3@pIX_o6=e25zxw>|E4K0jFBKZgaMc?vrW z+x#^e%VQ+;#_ZhPI>LWA>dN-^wg>n!ruL%a$ptCrt|KEOE?a;8NWFUXYSmOA8fgmc z;^KnT)7zUpkhsj^Fn7ZKTKr!@BDdOq41sDGc#BkhZf{E<=jX&SMdkj!^qra@I zOwh2-)vHtXRQKM$9lywn1Q^|qU%#A&AOLgbRE|MKNf)sQd4@qiV~boM1app3wgtsV z+z$lh68APV(<|WLu(G?qOKMAhWh%#g7ow) z)ez+&)Ab<6WflzthE%mN+jyP_aH+1hnq5(nDLU^sd#uFpd>@jU9}%dlVh{rbSrdX7 z-ofe%!4mTznfTc|I7of-Jso_DL*J{l4NB7+1ac08x;^*1g>O>rISO3=3KW-bA5v2E zq67o*G@o5^vx;(*f@I~qG5pC!{PKV(Que+FPfXb@^f=_%YSLa z+*8u7Z9Rs!>zMoIHw9RPFXlEDV-sH>aEf&JrVpWo#;20pcGWktnV3snBB zX+#{oIjaLF!rQ`n;Y-laN7d_#9**x@ z`8IWtMWIwZEj_;DckbMg`R;RE@*v-@J*1Na84|DskSz--V^$dzby<`WTpq;0qf8b6 z(estTX}CKvWH{>u91|B(eA#hBIfPmIzINLvW#uaM8uas1b|d zQDC6`9WxFPElNB1wfQghy8CSvWYEhWkyQ1p5Eh6nmU(2c5PVQdPEL-6z-xE#i_{ix zbY^Y-{rgvTbaXTd(xIDydXZm?Ef9FK8-b;r*0@ujI6{}krj{rv3_#>vzh9kg9)T)k zX;9>haorj53bg$}z#o$ASVY5~WltAOis8q5ACgGU{C zQ~e*rUrfQ7#7Rj>vFuIK(K{OM5lSTSZBueGkWCe@RvS1aQdAMCg~lF*06+f6+lD?R zA=AxxzK{wXf-LdF7cH-`zB?_lk*nLHzyhp21k}L*rocV>WoIUb_kwcx18#0^Ik1>! zFU-yNL!V$EryHCn)P{Ym7uT$EP<)k+pcxtA! zRpl^sRGKdA(YHQbyN{|j-ozab(WpA-Ha$xtSkm%lkQPR%;4C)uVqeNiDoK81{)~iI zC;*|=QiE|>if&~S;Oi8R9zCK2$Y+vlTH7@y9-g}cB!+SRzc1aQ zO|7b`x`TM*c)ctV=7IxBooR4;EJSlkKj{R3naM(*6#v&a~t zs(u#Uz8G6aC`rR-KVA+si`{-8v$?}oFp!^>1c+)Lw#Q+*G6c5|kB*G0EqX{jN~(*w zs9md4QYOx8Y6NtnlQjHZl%X-Ii3LKOL&Wd;s_~)n&?ADUCr$1T^-8Hsj|RH>fB$$l zh0uHRfc%x;W?)oPhxI188$6ak@G|s01om2h^Oq(ZzA9#GTSAtjE+{)YhCM3|Tk-5p z2pU$I3@s1OP$tO6_=JRnvBZ8F1cZHa*D3=I z4Xqbw+_rY#%|!Ab4C6nOUx*O$i;21UE1zGrcrOVje6B9M>ZwJ&)A~9)%S?tGEVIc8 zTm_m5WWO~uG>C|bijJuhANK2t_(w~i12EJ_>F5IE?45e!lOJVqS^<+hZ*6U*N+1xe>%A2SG2fzbFsT)xf>X=gQ|y({ z9|I~l3uLx+yxg3jrs`ewf92E#A7rmV|H&$w_lTgy`<`H@yhzfoc4ONM;Dh=vO*`=x z@S@$or}jMqEAHzpOf*{qfuBlDQ-FXvfnW;4HtERa2A&ZEJcAlNj#URH1jy^^>(430 z{e9VV_!M@6^KB;E+LjpD-hwAMVaNU_=fN^tl$aH)Y3m%A#2Wn>bi*VkCZ-$iN0k$ zx0dt5G-&X2ns0kUB&G4>iLN1m4z)VSn=|ltilFN)CWH!kv*5r}ugENTZBbo38)ndhy2M;M}a-|z^MYzv@Aa=^j;;DFTo`r$LX{Ls*jr=Z}z z=zQB*?Z@`P7NyckGzWZmQ(n=1|Wi(lK*Vqct5IHEHgEgmP zV8NpX)MCxNV**e{hi^UWCdxfWW9 zo%Aq;?n`|R#aoqO0_)iF6wD*{@@+P9HF6ZtZYaQ!Sc(RSf1`rj;`7Z0h~OGeK`*(0 zaxGfY>*x7^5D8X#9PhI<#MmEtczBcoe`JH)0&$mDyS*U^IF&+flmp_U0O z2$jCRzAI?2FP$too@%!y3_j{h%Xu+bwO%>(^~Ka}3JNAige*h_Qi2v7cHiG%_!yd$ zDR^s!p@!ZCSvz`vC;sdu(j>k-BW`f}Br*KB_f&wT z_moM6;*mi}W-g{7zS}l_-RGN>{tbPgv)2x=A~=Xl^>L%m?v8vW8{#puf0m);gx#(MvU`i#Not^DK%yNOxd8xB9?Y0a<1N|<*v`bFl(`Do1 zx|^xaMTkcPTeLhA^!J+UhWwx=)cSELn9H=A3`S^=B_I|+kXt>p&hh?QUTv8R0v=z0 z-px~%WHeMPS-v&ItK2-h57cXd1ST4sCRj0^mmP*ioXW2de3EAjGCDI22nb+;)T4+Br5ppc0+RP?NlD2S z_>4m^U~rfV0TypMEgc=zBS8D^PEJ^w^3l3BwIGnx~Ecek~-kARi6kq8FWWy@jo zU6=O;uWsNP0SGm1hDGUTWmhf|oI(S5UFr^BQ-BC3QXKRLtulCq$PlT}6~u%Bl%eaF zf4ogjFA&=M^=+4x1%PM(3%A@iRPhzPj+PoSaOdHLuty!xa zrSteK<)*iIfUH^2)YO!KKxQ93iOYZdJMfa7=@#TW4r5GM5Cpi5(V^;`z!iDAr*`Io z@FqEeLqdcQp1@;CCor&Rch`rQDxpoir~~VEx22^e7Cp^AAX7Q$WowXde&+GU$fhey zkEMYLCs%2R!DcK7IEqy+O>o~C0iXe9kpkaMb~23*^IG?xja~3Kv9__fTVGRSuMd-h zWH5cmz7fQ?^x@_A@7EV_B5N%zEmK|8_19xqDoivXE@2M&`uEx;25+oBxZJ|LTOJrx z!Wg*H{S&x7Z*^rQjz1Ju33hGlBsU|WUX7L=sBzcFkbjs4D(acedi^W%g3$A5OV184 z#Y?OHj6)W7c2ei^zE}6xOz``JULM&vIQYE)N#MEo)yZ$a)4L%LHF%*ohLduk7l1dd zsN*o(Fma!QL-j*MvE5RqwgiKluiZA}PVrwr;3W$LfC`k%R+b8BT6Xr5M4%@dPy|$8 z#Ubl)YdkNfU7=l~U89d-?&#{eZx6AgZAcY68sWF!SV)qT{d z1d~36SV14>i@O)f#>UqXAn$ljulBezF4bRv6!O;1+4*S-oEOSTx#Fi^&}wV9J}NVv zPjzj$TjxN4#jh5e zH>P^Aajw_n9l03s^-T4vtp9QXl%b=n{ET!`7S4SSp%36e!8!0b*)`<|&(>4`V~^B< zd#VU@V(vSa6FO)4`!Jh5tPvMDP{jl+07Qo5^W^^>lB#$C^ z0#+RD^sl1nr1~J?#a>=f;SGbJx+NF4!XhL2AP12$2KMIy z!jq7)yad4WwVUQaQOtj)zRe{MH}?UPQgYd!#r6mW1p7%Id3Ns=UF&cAI^-kf@W&8^ z#oPyi<6vSE-IXuOrZ0v_R>JekL9FjZT3TA&00R?))GpOZW$i`5r%wt%n;qA|M;aoB zG)aG+LscFnl&u7YzK4qzVA>J`FJ;bm2_}>k!ECZ-wg2ZqM zj80XUJ?HpshR^9luFbo;yc{#DCkaL%d+6)eY-Ip8^vq2LIPK74V`G)TS=(`g0N`K9 zLQYE8nEt%+%#Oh!s!+yh9%1aI0qeWBm0n5e&UTxWoB^G&>0aQ{(z8F{KX1JW3p>$c zB>dDoAeIT|DvUgMVfSp)5Rrd#&#I`LkH6NNQ^V zHK;EyI;q-C0ivX()S!1#W_a~E_^9#Ofiy(=mwfm5>zqHu=E_R7(EaV%$j$}&(olcOLZhRVF5O~4-I$>N=ohO(o(V{D6pYZisS-^sze<9 zq!(1AY^P^uB`HuGRKl~b1WRzmv8CqWXqtofP+I~@b4*>GkW6)s(?cD0QE~~{Oppo( z!4`1`kj9?J96qJIgH0q_RKNxc*%Ny&@lY9LwlU~=W6c2c$6-3&&8ta^<)jJ`*g$wr ztGy|e5!nR+_l*BC9HZgEGQsG#%_Z1N6k+XVbYtheDGU<5S?-`I0c=#>o`WE40mOep zreC#T?*)ND!O>D6pfcUyRe28mzZHf9Qk|U$^<_Ha#o}#XI=>2xSNlD18X?qfB(vRa zXB&c~Jj8;Gj64Jls1}r?R^p1580y*s6nS=A`98r=K3s9gj>}>|4XcEq$BtmXsz5ZD zKo+V-Rl#a{H3mf03i==qNIi!&x81K3Up@1T4+{(lkxfJly8_SiQ72CLd7qM3n-0a1 z*cJ*)18l;64mLJk+I}vACwmLr#f1k|k)V9dLbU`P)y)=LWkHiU*E8lVBI|<5NUGt4 zQr3t_;*lYQWIdQd|NhajJtBHNK^ij*Q;j%57{cp*{{H?EPBohO1%-yXuC7&(IYD+C%Su{6QP_x`fG@Kfuq zBa9w$L7rFX-%o+PHFwE)w8euj|>K&K7ACrK3UPrrbn#O^DqIF2v>+# z2gDW$=DM^>@U>09QNHwSwZ=&5bo%`Z2#iujj8I?yH z*o}bqB0LRJKM?G+>>5%&3=pIaf%%9UUtOIaBm{B5>qH@PH9rjBG&WssBxLFQwKEmE z$HLOx0+GlakP^LlzUIWJQ(7|^D0lhxsMw4)XT!wG-xZK4Lz;uS!+pYa=#eK70ig$~ zY{l4UK^m`? zX^jR#tWE@_-byihqeV*q4%Z5(+jHfhm+E~2p~rMBJwk9WTr*+GpQ%yGGx=5L1$^3r z2j~o&zU|-;wNs_iXoi28y-Js_n;v6pd z_%X>5%nb$DFFJ*CNISA9Bfu4m{6cht-eNY{*n?w>{OQvtw_`lzfIbP_G_L(2F;vA2vw9tn#CHIxVjmqcs>0L~O$+UKVoP_!Ph&e|I2w!4 zJsB%A!2|da4#yW%`T+Cgh98A7NWuV5CKkf;v}2m!<3Sqd36cPPPwvrgq~FS>3H*oDoh^S;xNX z;wHj!%a!}U=!{Tu3}k@-kS>9QW%bP+6PHS$Oe4I>5T2)>&;o&IAAUtZ)WB7~=^Oa! zx>`3|r+ba=BuSObeHJrcc3S`z&4K8K5ZSy)sjc<8u`PGSA=tT<7!#9q{`a?BArM#& zgjVhtD4}MKap=3lD`x3q-n$wBeaveabVDhwZwV(+?~*DI2_UyN>&yx?lbC7PzN(@S z4-`(Tp=>r4Kpw*LC*-nmye{yMM=dWO$f(Y5CFxlRVdYnMGe$28nBR@=lj5!X0)&?Q7c}{xpON`4KdW_vWG0 zUS?+5zaS?tX%Df21%&Jg*v`RgCY71WZl#CeX8q>cT3ZIRz%lqNgqdRb`X;+*q$GH_ zwYl%AS!Cux5^Mz_zDksXqdz`(a>;hbNrtt%mkRGVUgLWMO25DUo-QNI?UE2?WV~Dul*>Gw$vvOFGtH&^4Y znUav!Q{qmvT(9c#^*>ctyB{G)4Grkvw#sAZe;5K^03Po+$|67mBsU6;KB!Sc6XtXaqcUH39O9JV1L-7~akGKHRvA@EK(71y7s4 z{msqI6#@qkp>e&1PtO^-0FF6R0ZE1x7jvUslxMKBPIp^ zN8TA0SlW7xPd*iE2trqk+W3{zF+}5kvS13MF|*bHYsWxiHZ(RCkJ}X82B_!_iQO+l zgM$aZfB#lSXo*XR^b(j>=D<@oEu-0ri(&}#KQL+&Fjbfb7Wyq|IZ)(rb=`&pN;#Y* z;f4D?T6f0H8P9VOtaM`~krm#s3k#+Fr|(rf#3XRczjC=$eT*9N#rD-yHr=(O#io<=9p zlKO>~`fmx5cvl)GivOg+sFemo4{7XZi+0K^b(G2&BJR4Y?~VjaSO+q4adD-uf+5;h z<r>Q+y^D^{wq+2_JPW&?2ZKN%f$(EB?uJD6I6N4ymzRCC8Rvo9+yn9b z9aIvnS7()vRSISAiI2BZYj2jhj|^SdA7s0y-1I}@i4!N7(6YQ3zzoc>_hvzBOZHqF zcBB&V1GV*Us9&lUgS{L;<1%^9!QoRB&DG&MI?lXHiyk`WI{(y?#~q}}hcvG9epXsb zsUZ(lnw3gV)_s2YXy@vA8by(N=O(ZDhe>>R_RqR*e0*VC;^kQ*>f4vdo-7J6>5smS zplu}jH>RjB<3cg!w_=E2D=boLR7~}9QDx5*!M9NQr*=G)d6*NUEN;o_#K=1lZFBAg z_lD7#QZXjsY6k1j7Bcw>wIPcjBhznTR(^f*s}dn8ZX_EK>js}wJdrg+O~@FCAw8mT zmc?1;N?a{Iy`-w^wn3stNc^W^q%Owj>H}5!1-(c!7ELG7>2rRgZ7n=Drt!;d{;TQB z^2*ZXNA51al>30_o4?~*@6x3tmdCbfnMl_iu$h0D2Upc9Ee9o>-=l2e<|Jyr_#FYB8i6{wse)|?AU2Y8B13q6E&EkerHH# z;ofGzX}asx_x`pR-d?U4Qm!2fQ1kP1zQX&pBZ+z+0(`qv1MaI}$;k)02Zh&YPskcfp-7+~3+JwCkS zD#lxeFEC{kqRh}|IiI=5a8p0_(rJF{UQd;ub%ir20js%H7Zc>PjC-$#^2Y_QIpyCg)Z&v zZ$Y>#&Nx21SUBynOJ? z@UW*R8YUG9adG>#;e+d;6q{Ukw3+onZ&BD<{{#J|U0YY@sC)SEZxLZ(B(OL~P>7GY z`l_#&98piG^1Y?R!pBTQTU1oEI_nWQ?w}naO(w&h@lbmdNQFuo{H-peqen5dec(J~Acj!;E8X)nKpGRJ` zwT8VUs@x_pv7Rb)+e*sH& >LzA{C=!jgaX}OiMpE9_&tLr*JTx(+Aal;6B%al2@ zG(#^-6c6W#){X@eMKVvuRy5KFgb^$=m>IJ9h2`e7nc}|E*IQ8}eoFU@eu*(f3xe+s z7@eNp-GztPvtzfco^GIC=mP$ZHEz4*1sk&4;oFT|24Gzcg7JB?>i0PT`DWgi9iNO$ zU%alA#fFszzCL{i9i2}JD484fSFTkYBKrjAv9((8l`1kutQ&G8M_gyxJ1|WHZ{NI0 zMZfe!Oyg5o%n%=M&?=39ko%povWri1bH@juWwe1~XUGlq`E6JEN!2l6OnUnyd{t27 zmsM5$`Wp%`?Mx`Z_`<ho?PMlbFe(7t(#B@@HiAIAMoC^B)Y$=I{V`;+sZi@VW|4M|A^XOPgAXZ7kO+52 zPcUU-V!~!S9lPg1clUv@D4yUeoxVX`+)TFc=qca#DRVWVCekk!0Byg4tc!Br-o5hT zsGQ>B;wMB(n5z5Jdlo-dA9$J@_vlCA;kez+m7v!%P+~iY0Vg=_6unp|#W_TQT5V@6 zyPhh<2yVF?ewxqZl=O6Q(a{IkVw?Rj)bcCN+*fU~yTau^(REfpET=5b`q{TUov_vz;-w};G_Oy8z-JczPxVgh}U~y_iO?Zn`!y< zTndto+JVI z-1Sbg7smFyf+C1Pe-QW>D;wKPbhEwOhhO%;U*NR0>02y~^7`@@_`}-CZ`6Y6lo!lBjYYmhusskA{z{)P$X2lJDp^Y`;oz*a)Bo5LT%vMQx|w+ zw{H`bWQ;E~i(+tBd7^r}_Q3@T;TTa_d3hr`Dk^hnOl3ol&1eQADEgou>6Ek)uZy9O zhjeNhU?q}jo>-kYAsEKz!%ixsGSU?j!kLmJ2tfO90GVPmQN3n0qV{r&x| zCq=0|w8+F!L>;NcSuNzqFmXD~4iA_9D{0uAmbjxA4wzyqFK+brv&Uu4FCHHbEsTtM zVR+e4@5YTAOejxZ!RlP5IMsM6PHa#p05A2aV~mj5U0`Be^q%CIG>p7F+Y9E>#c%Q`kK4)QkRMJs%4u(2kST_hm z(K6;Twwv&U3wBfp91}D?j^*qUquS4ckJ=?NBBI7GARvH>g3OB1V=3#LyS_laUcZ^D z&<;+9B#LBa2xG%w5mZy5$l7i{N!qYqa$uZu1O6 zX4L-Eg+jfkd|l}WwZ4bGSjoPd7M~Rv(m(8`;EURpvdG6V*)p{=g*C_dc^@+N-1PLW z&k(tDb{9~9%*%*nsoGpLc)ZbAD}^76zi4dSG#9h!L8InCy!Z^FaPR!f!NK)sTwM#51b>NO`^J}Hoa?-s zp5+uqA<;D>lla@$E0?eCyJ0=kK)X9PBJ+-W#i2fGS{+qY-Sy^&00_TaqhwI$fE{tD zqeGm9t>fhH5LRm&7A*La*^z%v#E>it9t>PHeh;w?@WNjQW1C0&6GSyMB_pGhulH8@ zKk8`El(GQb4lH=$>J>zhY5`Ow1NvD4!amL!GmQnt|7zl{oCj;y-lCw?6;LxnL>x!J z`Gjgf0{URL-^73gM5j0jcnETH`~mE>o`P{E(p)@}LSu_NUfbHUy+a^KD4`Pbir$}M5tLRjXD~RM%wMu_jS?hIlbuWW0{S_S(GmXOY zP|N>(2lwttmb0&TkC(TQsqZJR`(?^^52_wA2w+@s>4j%1F68>me_e9}25fyAMalVN zes>aTNSy+q#mn}Wmb&}s#~C7zmWhqcabzkJ${DTR5wJyf|pT+#63h6OHi5~?&8RXuZ3Ym7mY4(2X{ z1qgBS2Yzjc)wJ^D5dVR5z1Qh-mz)#)3#4RqOq#LKIfx+^NIpX4*&YV6S1mSv5BScPH-|B<@{sr-K^iDKi1~b{)VIsWp z7l;WJA{X}{{%@}|!Y2!}va)UhmDamuY4Ygo!>VTqk0M$2M}9Trf4i8M4}HuZcpGX7 z9)1_A%93FDROB9jg-O?V7jtP!=YC+Q{00r?Db=9$a$Q7>wBEhIof(s8fx^O$zC{Wi zWq(-+cb>r`P~Dt##cSj(Fpw8%pg;(GKLDnn0&56quR*=oACtK4_8OU%|Cocs-S8Q7 z?qzuY_bOaIum6L-!x4U^@i4jm;r|`;b}`r)_5i2}4zrv^SjHUHzK0j>S5*!_oSYr; znyA0VDBjiHfmV%W+&%8mCYTL;jr07NI{+}zxs_=;>FzUfO~IKN?~4k*CP^8bmfi|3qQfvT6_f{5-IYVQf_Bt42Fe07!&g1u>KsFU+>nGg+``)>v^erOGqlE z(9gUKGo%#oq~5#kI0!2p`|yv z(uA&f;TeX)7pP)Yb$}py@OjX@dGjWIeBL8-tWw$9TO#yPNNvrZNq2YmZr~lHnZfIb zuj6ejHQE)LHRH3QKe<8Fii!7`TZpS@@c^jh!n`88$-N$LC}rh+CEd{VwcCN5gg;+d z1q{~h5w_QhF;xa+0*G7BVaYe$BE)?ixGP)4Af0;qb{BGn8`bE!Rr`K| zt4I`_-QX^>?C05b(sJ5jJtVmEZ_cncl(2U+RCJ)j$ZBcQ<~h;2x8lYEk}vTh$0vYr&?8ksW+ak!-1GMKxb0o-+v4cD$nUBz zd$`x9ZSFmIQuO3i==VL~c%?>jbMr70yZ-%n+c)?|m)Ed;ue%|jYaZ>rEVIo`&fgQ6 zi*Mov7Y7(Z=A!8C4lu{~BkrXGTT)h@q4m^m9knl&t!YrEFnJrqb~ra zp~GlL-nX@_hX<^U^>3qAj^po6S=_jlCe1lLGt-l{4#?D)r!e^Qx8K5vlZ-xPdNcXy zgvb%IRx@FW5u8Y<**_+&+o=|~<~_cA`;k(R|43GODlm~3Q^UiIle4pX-0bWwTIfC@ zb59C=iH|^C-E&xD2&5&w>c!EfQ|KTQ^4aU1dU_R%K02ftst+LO>awlvuuq$@IT>B` z!yAmDSxI+dB=wUK%aMNo%x(k#^Yz50G?nXGMo*$@s%5Jvf6UB0K_GRax>HxyHz4$M5s4bAxr57}p{FXT#f8#*}?>f_Cq}j(3KJ6Sj9oavCLRj+ZZS zx4d`0@(qfa2l&o*j;-TMP0Xjqn?KHE~}i8#1N zDoI3W&@%WE8md2$;o+;c5B0g-4BV|Xy2i}}q!*(czAT}YA+@u!D=x1}+GVgACiAX| z-L$6Y4v$J6?A)!`o$dqiW`Kr$|54Ef+f(%vPFEeZv|cNK7L;U@v|Np`7%Na}6NoDG zPE}%tbH5<3UJUY5Eyx$7#|=3$^fgJsB)9o;44lKH2L}5%x)eFhBC}I@)-(s%l(%f@ zlp;*BwIcHH;s)oCj560L!IjzDjEF{g(Z%<>&)Lvqx|6?bGmBG67v!i0y6i{kiL1J!#JS};kMA6D~ ze$p}go-|#8L2j{WZrcJnF`IFO>Q$iGRU|#yM$>&@0G(ofCE8ncY|Fb~)cKo&V0=1(5-&J=H-fcA5<77)x93K>@}%iL2k8b_Znd|>M+MH^n&E@S|9DL*;! zm_}CmWVvC^qQ_=2{rn)~Gs==0iF+GJT&(9rI$k60nWiAdH>ZGlUXa4dv+23rFh24f z*rXeTl$5^2s7lVan3VC0oNxBe4(*BhV;iJ|u!26wWh9^)d?!~Hc#UhEsT$cyjR=8% z4-@Jg2-QBV@;^>#JvuFZ{VPq7L3}ydlsoXRy^Xh!xjq|2t+QvntKy?`WlcA(`DPj=+O^ zm6aD&RP!%)&TTS2$?Md8$J0D|_$i1iZ@3aikyU+6>~~qC_K4drw-L>*cBkg!eSo03 zA-*OBO@DHVPm?nB_7&6q6J` zBOKKn8Q2>aF(udLqu7aUHlh?(-~0_ zk=A~MPkI{}(SMV=F~L}S`}W}8sdM)@bsi+$$Z?G>f$eO6;_m|5AG_KT_YuEzM#s3s zY2+3Z8zTt7>;3v#W^+;b&$DEU&(+!5lAb83X1S(n08u&I9UU)Uw@VJx%+B&;TxBDR ze=w4S=*e183o6nwG7{#A3A0~XxoA7Sz1U#+y63$=C~aW2Ku)ntm0TPiK!p zt11IYKNmrIjP)N#V#`iW`taIWyiENotzw*O1yWqb&}bKNM4H4p@LYPh4-G;+{h<|G z0=LA3&ytAj<;|eVCa1=l(N59H-!;eAmTkuzCoaIYgIuA`r4~kWT#d)z`0k9P?vM z1wQP)nq}B-BRf+*q$()b6zgzW;6S*g%>H+W?f|osuAh5${EmED1l0}wl zggSho!G}W&!QQ?ueYXvUgN@m&N9ro5<_Aapr@po;{|f=#8;mv2z>P{N^{6DgaGPKJ z4$UN8HLB@mhJzdX2_@zXRL!DZb!QKFo4D-F@4k|4tfHjU49YGJ2G@5?%oH?~q@+}( z;uexPgdqTSJVTHvC(0R(IvyU`Fex4FZN^}|fpk_bc3?}KJ?3PxBpfW%nn@-OR3l(9 zV{|aE+#;f`EsM-#zVhwU>hrfq*@)r{>8rD>@!E#kck?E`g691lVh4i4BpAE)k|sLD z!HaPozEsZ`VnF!jXfLiT;eJc`;LjbG7fu$M|6D_nq3r+ih3n(_ufm4b7+#hhGZgu2 zD-|@XP2B$%9UYziBTHH|&E0d|SW4>#CB4GNsrcm_&%q4tOM(+)&%Z+drqHy`cg4=oW@d>}ZJNc7Yl1V76 zY+stPHXuRmn6hn{!xvG@P3MA*k$sI>LE~7AtK%3J|IK_X@IEG*Y0E}YesmZG<0M3* z9ITw2Gnmb6Ti(|!-YnSX_G0O>Uz!zM=AR)dvLrkt`pL)G=&BxW)9w;#mP-2xuFVVu zp|t>R%n*aqMKSH~Bo8yBA8gR=zMBlkv7(ZsWM>U*aL%uHYP9n5?zjqx*n>+DK??vc z>y5Kx*LJ_97_Aa5_U*FebBAoTv7wM<@A0+-p04&tv{WM5ZVv)~qQEsfN>=M6I_$8t zvTB5DJ>5CL_2Tv|iG$^pTsT)G$J)$n#9yHzBBHqh&0aO~H#gHN=dx58Z_{1{mVOMu zg`9q9bLeGzbb^1f<;2wVglCrPhkY?a?e6j@}z(Yln{lW-TYl$XY(O;;ZBrNyRx0EpKlR5f%GkkF7@ngw!n#ID zh!`C-)_DAC9yX(Jn8*GeeTpx#jtzNQKr8PU9X-nu$u$zHQAQ}h`{CU7a)7R}9fR!a zaHD0WA|)0fX7ze5>*C8qcy*dTtbhSW0;qMYQ@!}6K6tVAa;iMl=7*`GUw3dI(7h90-Xld2&yq(6 zhMs1;o%Ry*z8I84>u|~5Y<>6c-}OCA0{7D? z@q?sP{y#X{a>48p>H+6^vgVybpni@t#B+%Llx;O0*M0Uyh zJTMKW+AMn_IOekzQ*kVP-UfE9v2QOfGd#oGEWXcdal8><&%v~m+2Uk^Nsgdp^Bb7zvq31*^#i#Us(reQqtW6G4w*z5x@+X8&V9BYzI}pXiv2r4j%?J^`+tC7 z9-GP#k^FHlc@ut#`Ekg!AxW~AY){9wpKf;?6of=V}ypi{ip)YK#3)iiy}RY3%6ZN)%?%Wi7`tZa7Y~6Hj*1o z>Da4h^qNI(PmCh8f^!cGi`)im`Uz`|-pd-i*(@$C>9ZW1oCi}BFOEJ2q$LT?r@6L9 z@g0eoPcjLY#~ya}RVyc_{P%d{Gt3Sr#c$}{7`gPcaXcw+G6LUSCa}T-=s_~*8hIVV z=_@6up3|G{t_l1(+AM|n@szj6A035c8;=*k~DW`wb!@- zEd#@?aSVEMM+7{WGtC?4zs7TT92$-*Dw@lHROLp|>1PuHxn*Yls*FBdg(HxZ{?6QQ z7q$hMC6&5vT<7ez0J}?KXPn-0?YF}i`Vn8E52RDE?2)aXG}AmeCF`#j3HpEmRju^F7_Ssf8TM zLO-jZt)(>q$c{Zq<+=%VX|@yL?pGs%=`dqGsYt$RDwRI(;-BH!;|wEIRU)XG1)MUa zdBn!{i4TFHe&09!sa*8;i-md%R&MV3M`lmNV#=7zypM@jHCW}EIv~qTRTKgT|4)xk zX2#Nxz0uIPN@1QTu`n~Eau!87gK#96PyLXkx9fHq7&;Vt_@CEy`aRWZa7qVOT=--y z%vi;*9hNm?Twx8F={w-Q+Qrs&>pTTtC#?B*E4T-q$4O522xHe7Q5&=#@rVDLjuC2G zISO9yoAcGQuAI)e+KVps)kVe2TY%de_Hc8n12l4{Huzrn@e1#h@`-0~yri)(F-gy3 zb@KFBCYe(z-j?t$^<+qbB3m8Gl+#LQ-_V6b+Jzs}$8*gt_?!Q0B0kTkCGHe3e9Lle_ToN-p`-FhA9lAX?iU zlTbr>x84=XfGa=ZEq_nce9)QbjC=P443;={UqBkVJ{2wP{XPg0Wz)k?N2XzG)Np`9 zLy;)%o5Y@a-QrH0g#FHSS-DL^ZGc2lAGE45;BGx6?<0PT>{Tc(pTSyq7^b=c3kdT^ zWI3yfj&yX_(6ETAVjuneXK|6L$2rU4r61Mv4zU!bRwAgv7W<|EaIE$_x4mO~nx?ba zR_LB0XtXY+vW!1s-{7(OsG)z>iQwF3?0ot)QH8ml*25bR+ zad%sgc<$wL=ui`Tq}|fFn{@M6L+vrLw0ESdd=5eX_2AQmmAB07NinAjk%_v;i-L$WFf(_tY3^`oUau0iT#)|3GNig@{ zF%rP;vA%q~ZTO+c82^r%aV)lGLY+nBKPl}l?s>F~B0p(uh^iOA;A(R z`QE?#1t393Pzlz=)MKsnqUBwyw}RxKm-}U9JLW*2*2DdC)%=U3k3?1=;}sRs>D*($ zJnhWQlivcad=f1*k@_GMGlJ?fAmt)xKUJ@;-{rwPzcKx=>w()q3_;!&LGS~p)ou}jS2A8W7LotjIgU5LH7cFvfjNvkw%1H z|AzE4@NFC?o9IKUcq?g)6L&A}S{7{^QoS^5z6ICF4K`&Dl|S1%4=$uxeCh|!5so9R zbg@FV?O>0%MnYA4kAy2z706%(nzjvj0_nP;|o+N(4xD>U~Uf%Dd5lV=$EAzRNY zip$Th9(vygG-Ln~Ig&&-dCNRyc$I;2x(tTcHAG~uG6TI^?i?!VUzA{EOnZr4r+#{< zVJi~z(Ia~*Wj4mG<_56mNw9{fw)Xe$eql{f@{>V^M00DubxvRazGX>lZ!~64KAmi0 zH+zsJJJ|Gzz#1-sweD1uH;EUG+R1oDicB`i-NU0n4%w2KXnKDJSeq{@msjMw?@B>_gwg~Q1?HUN4BIUb(OHFO`FH+?G0$Lrp&$R0cV(4P%W`Ug^Cd8?8k1M}hvY=R+cOAc-atXJ1)7h{6Y!`JkOyy#L@X=$~D^;qe$ zBv}sCn-^&>7q)bVL7CVD`1wUxVy`ret}e|CT+&A_T|hbv*B5p8d2CtnN+l@pCY@jg zNcs+STM;b{4Mj+#xAxx+zTp1O-6%tEkTuUMYM@R*;X$`VeNsX73!?NFcM2RXJb&6O zenfUt)Xaat@wEKsH*Y51f;nt8Qn>!gkWn~*0@ZsZ2tFo5YwJPXjoNvigWi;i&H*>m zZ88Au5q6+xgn2}+&A6Tl6eMu0TAIyClIZHp%%KkW3fDdy6*1jg{rZsfSM4l=1kqcp z;^zC`4-9bX3bp0Cy<}k=-lga@rgv=LzPwMEuvVNd+}vfcDWjb~NqMUH{(Xxk9Kv`$ zIVDBy@LbC^eD>Tc##7C}mr`*;`GN2>*QKa#thCKp%wn!bj&v|`*j*LlB_V5OOqO>`4S`te50p(bmm3$W z5?PLBI>%XDl%m4k@*diP!#L*jt?}`cRK|MtG$i>(`1E*1*q@OmMH5Cdt zDJ~fb#APA6MrNnR?QQ>CoSo;Ufo=q=0+8_o6Jy1eCd;QT=xyaL`sFPhTzv4zfuvp$6!~r`acH*gynJ(QY zE;c&3;t5FupPJlQiJ*D#i-d%+PydkSiO)9Q-!Rk2w{0Vp*j0^OTsP#+X2Kq`dnxMM z+CJgL-iH&hVQ-%Oc!x@^tEi;(FLcf&SIBC8hL0fTruHb zWvIae68|gcd~QB*Yu*A_kj!{o%EXbb-f#86R4pEdN1F-lYpbmSAcHEj`m3$CRQ4M z*G3*_lM*6Kvi?nlB-S!po<4)C{JmS)j%{&@kb(e`M4FE{!?HBg!**j z-#@k^AX_hrXulfABVgJI9`x-nqOK#lg)`ZY`)=%SCEU>bh!x92-z25YPqAOD*KdIL z-z745Hi%^e$TUW&B4#7Sh?96c$@ zqWzxX0BiF_r7??&3%>Taj|^-(^^fmUB#g-UBWCeCirYaDc~g747tdgccNh`wUifY9 z;_ZpMToP0kG?LT)!>_!&V(wAKJ^itb!s@ku-nNU_$hZ+Cyj#D{i^(V3e?0Z?fQV~? zK>`<0Xm2DDX)@V|I%8**V8mJW-cKRg&oV6#jgcGcysVR^VFncT z-DFT0#j;9Ss}>;sLGK7dzJMvTs4obGi-&`QujY|~(v^dt=cz3kfiGA<^NP*VjV9otT2Q3j;4F45Poy&03mqcq!8$l2 z=n22j6TsKpL_oeq^9L=9?1LMV?Z7GjOEf${Conh=-bFfXyvFDgEgF0Ch}E<6ag=Uy zBOhvM4ulkQaDNM2&SsfM)xY53?Cf_M-wrMIdIvIs4!_{;9;|@#hz_;uG*l>V{&K-? zLJL<7ITBK@J~+|*m?VRX>N!PM>pA4~#7H-M!bb3UV$XX-2yTB=mow zEcb6q*5-bGAH3AMui+n{;<)hgxTjbEP5X#a_Yjcim;6r`Sdx5AousO7B4Fhk6eC>+ z2ztTF>AvvR7PeZ$kD?$gR-zI8*M_G%sP*FIH`m%-Uq1qAq9lUV_m#U5RgPEkDm*=r zM3~RRGexYb+r&TH4Q~<{>l!fD9WH>79#@+*?#3=qiEQ4FkI;(78emTrE9Xqh`V~1u zOPodT1v$_6h{`x(plA|e#HZE_AwV`-(5qdEa%@Yf$;n@pf;X--fZW=++f`|z?mzX^ znY^{NRRV^j?+B~Cz*l};NB;rMsan>5X$?3sHyXm*GkpK|B5|CBI`ljTAexJ`cefIo z-d_t-YhaQTME`03_X6>KRLsB?VTu_{C;Qtlt+Ir*-D!C#b(~^{J@@bby~M?tk_LH{ zkZ~c*=E8{%gZg)-aUjH?TEOonYoD^=U$eIKR%BD>q|r&|;Q&K*W6RNg3wiR zR5ry}%=K07m0r;D7H>C815P1`!1)?2M=#NNW(Dw_V=$msI#$;4i{bna403lc zdEL;q>H5wYLN}6=rIDtoIKQUF6_2K^2??Qb1&{~?U3?HJhws8|7~QNK zEXLL6ph0YG-e005-rEqeH3!+?Zox>mJ`#KonJ*C@#TJCq&(tqeoau5@4GE%GsQ66u z3nL>V+yEJC1%k&M26D0ztSYBPIIOIvZ~8a{l7byHE)z-CMi1dd?}75Oj7e3!8D`5n_oef#*qE) z*L$tJ`$uz4%V3^9%?%OBX{P~V#KeC1>eZ|7d!dE2G&bgKlgVh1s|3=>h67&HP-23$ zQ>WUCPnw9nm>rc+N2P%>;5zb0FR?w3Di)cdYb?cf`plW0XNBy)OT@2am&Y5>%MO@o z?#5ZI-Q!qp9{A5xnrtP)DpbD905-v_i;J)(jbykA&e> zXz(g4`{yH%VQ==fy6T=i=(j`0K{fe=WyKnI@7~KhH7fI&I8J0Y&^xI!g+*sS8s^<0 zVe+jw^A2QLZ}YcrY6f9lc1*ZK3If68!jZPOXzGM-(ne%R2+n`hEHEqDJ?>rgZ}!>y z;uuEJBa5gfLQeF8Q;;EFuYvlH_CUuk5BdJ>JRBTKM3&w&_R9ZOZy*`?+!@5rdRsba zPMX%6hFj*n>g!t_!L0kd3Wp1y-e_sB)WI@Y3Bi_l7)^il8m{zAmqk$*k9$VFla45q z7kza^0HM}^&KkD+4~#@OWfum$`=SX3SSx>AM|!7?%LS)^6-Q(l8n1+zRZ{$%3U7s! z;yf{C)I%p=wRo?W>N*5U){tuPah_pLg7B^kxexn%$yn1UNEugc$slVec7Q}K)8Vvc z=Fg4t4?4&F1*hJF;Q3O6CNSw>-$hGfQL-?zO9iUO7n`B#a1+6$ZCLm>jRYJUBCR77BrJTjoTOHUw0^vqa0A`LnpV@I9L z%{BgA2nIL*7hYb~Dl`lX5LLc8;`A$ZptofZX6>0zNN6u6Hc5QQ4=K#Ei#T_0`f9mY zTXQRr={cU%=!)3BmAJ$=#T0tX$jDvZ_GM@swunb$4H|6qay{?N>I41i03>vIvj0Uy zVbIEtyiD~(UY}&#I%9Mwl#vqHU%;xa9654?Mj(ev_TyOv=$K=nK_br1Gvtq;kW!fLE+zaf?o?C@PB`(l>5>A|My2Y6a|bl;X0!R3B`YZ!fwj{&rgw%;pa6^ zX$bLC{LfE8Q}|%V|NK;FC>a(Oi*Ym+>Hqu`@$1O{->>_B5A**mmH*#=2&)JGW@aQE TE8WjVf`9aNj1CoR*@pceCRFI5 literal 0 HcmV?d00001 -- 2.52.0 From 8eec1fd38f38f655e2053394910f27cd6501974a Mon Sep 17 00:00:00 2001 From: Janis Date: Sun, 2 Nov 2025 16:50:51 +0100 Subject: [PATCH 2/2] feat(ui): enhance user session management with interaction reset and locking --- .../app/controllers/IngameController.scala | 23 +++++++++++++++---- .../app/logic/game/GameLobby.scala | 4 ++++ .../app/model/sessions/UserSession.scala | 4 ++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/knockoutwhistweb/app/controllers/IngameController.scala b/knockoutwhistweb/app/controllers/IngameController.scala index ef588a8..8569b30 100644 --- a/knockoutwhistweb/app/controllers/IngameController.scala +++ b/knockoutwhistweb/app/controllers/IngameController.scala @@ -111,7 +111,10 @@ class IngameController @Inject()( cardIdOpt match { case Some(cardId) => val result = Try { - g.playCard(g.getUserSession(request.user.id), cardId.toInt) + val session = g.getUserSession(request.user.id) + session.lock.lock() + g.playCard(session, cardId.toInt) + session.lock.unlock() } if (result.isSuccess) { NoContent @@ -146,9 +149,15 @@ class IngameController @Inject()( val result = Try { cardIdOpt match { case Some(cardId) if cardId == "skip" => - g.playDogCard(g.getUserSession(request.user.id), -1) + val session = g.getUserSession(request.user.id) + session.lock.lock() + g.playDogCard(session, -1) + session.lock.unlock() case Some(cardId) => - g.playDogCard(g.getUserSession(request.user.id), cardId.toInt) + val session = g.getUserSession(request.user.id) + session.lock.lock() + g.playDogCard(session, cardId.toInt) + session.lock.unlock() case None => throw new IllegalArgumentException("cardId parameter is missing") } @@ -184,7 +193,10 @@ class IngameController @Inject()( trumpOpt match { case Some(trump) => val result = Try { - g.selectTrump(g.getUserSession(request.user.id), trump.toInt) + val session = g.getUserSession(request.user.id) + session.lock.lock() + g.selectTrump(session, trump.toInt) + session.lock.unlock() } if (result.isSuccess) { NoContent @@ -216,7 +228,10 @@ class IngameController @Inject()( tieOpt match { case Some(tie) => val result = Try { + val session = g.getUserSession(request.user.id) + session.lock.lock() g.selectTie(g.getUserSession(request.user.id), tie.toInt) + session.lock.unlock() } if (result.isSuccess) { NoContent diff --git a/knockoutwhistweb/app/logic/game/GameLobby.scala b/knockoutwhistweb/app/logic/game/GameLobby.scala index 6ee102d..a43e41a 100644 --- a/knockoutwhistweb/app/logic/game/GameLobby.scala +++ b/knockoutwhistweb/app/logic/game/GameLobby.scala @@ -111,6 +111,7 @@ class GameLobby private( if (!PlayerUtil.canPlayCard(card, getRound, getTrick, player)) { throw new CantPlayCardException("You can't play this card!") } + userSession.resetCanInteract() logic.playerInputLogic.receivedCard(card) } @@ -132,6 +133,7 @@ class GameLobby private( } val hand = getHand(player) val card = hand.cards(cardIndex) + userSession.resetCanInteract() logic.playerInputLogic.receivedDog(Some(card)) } @@ -144,6 +146,7 @@ class GameLobby private( val player = getPlayerInteractable(userSession, InteractionType.TrumpSuit) val trumpSuits = Suit.values.toList val selectedTrump = trumpSuits(trumpIndex) + userSession.resetCanInteract() logic.playerInputLogic.receivedTrumpSuit(selectedTrump) } @@ -154,6 +157,7 @@ class GameLobby private( */ def selectTie(userSession: UserSession, tieNumber: Int): Unit = { val player = getPlayerInteractable(userSession, InteractionType.TieChoice) + userSession.resetCanInteract() logic.playerTieLogic.receivedTieBreakerCard(tieNumber) } diff --git a/knockoutwhistweb/app/model/sessions/UserSession.scala b/knockoutwhistweb/app/model/sessions/UserSession.scala index df45464..148ed50 100644 --- a/knockoutwhistweb/app/model/sessions/UserSession.scala +++ b/knockoutwhistweb/app/model/sessions/UserSession.scala @@ -28,4 +28,8 @@ class UserSession(user: User, val host: Boolean) extends PlayerSession { override def name: String = user.name + def resetCanInteract(): Unit = { + canInteract = None + } + } -- 2.52.0
Rules Overview and Equipment