@ -0,0 +1,21 @@ |
||||
[*.{css,scss,less,js,json,ts,sass,html,hbs,mustache,phtml,html.twig,md,yml}] |
||||
charset = utf-8 |
||||
indent_style = space |
||||
indent_size = 2 |
||||
end_of_line = lf |
||||
insert_final_newline = true |
||||
trim_trailing_whitespace = true |
||||
|
||||
[*.md] |
||||
indent_size = 4 |
||||
trim_trailing_whitespace = false |
||||
|
||||
[site/templates/**.php] |
||||
indent_size = 2 |
||||
|
||||
[site/snippets/**.php] |
||||
indent_size = 2 |
||||
|
||||
[package.json,.{babelrc,editorconfig,eslintrc,lintstagedrc,stylelintrc}] |
||||
indent_style = space |
||||
indent_size = 2 |
@ -0,0 +1,50 @@ |
||||
# System files |
||||
# ------------ |
||||
|
||||
Icon |
||||
.DS_Store |
||||
|
||||
# Temporary files |
||||
# --------------- |
||||
|
||||
/media/* |
||||
!/media/index.html |
||||
|
||||
# Lock files |
||||
# --------------- |
||||
|
||||
.lock |
||||
|
||||
# Editors |
||||
# (sensitive workspace files) |
||||
# --------------------------- |
||||
*.sublime-workspace |
||||
/.vscode |
||||
/.idea |
||||
|
||||
# -------------SECURITY------------- |
||||
# NEVER publish these files via Git! |
||||
# -------------SECURITY------------- |
||||
|
||||
# Cache Files |
||||
# --------------- |
||||
|
||||
/site/cache/* |
||||
!/site/cache/index.html |
||||
|
||||
# Accounts |
||||
# --------------- |
||||
|
||||
/site/accounts/* |
||||
!/site/accounts/index.html |
||||
|
||||
# Sessions |
||||
# --------------- |
||||
|
||||
/site/sessions/* |
||||
!/site/sessions/index.html |
||||
|
||||
# License |
||||
# --------------- |
||||
|
||||
/site/config/.license |
@ -0,0 +1,67 @@ |
||||
# Kirby .htaccess |
||||
# revision 2023-07-22 |
||||
|
||||
# rewrite rules |
||||
<IfModule mod_rewrite.c> |
||||
|
||||
# enable awesome urls. i.e.: |
||||
# http://yourdomain.com/about-us/team |
||||
RewriteEngine on |
||||
|
||||
# make sure to set the RewriteBase correctly |
||||
# if you are running the site in a subfolder; |
||||
# otherwise links or the entire site will break. |
||||
# |
||||
# If your homepage is http://yourdomain.com/mysite, |
||||
# set the RewriteBase to: |
||||
# |
||||
# RewriteBase /mysite |
||||
|
||||
# In some environments it's necessary to |
||||
# set the RewriteBase to: |
||||
# |
||||
# RewriteBase / |
||||
|
||||
# block files and folders beginning with a dot, such as .git |
||||
# except for the .well-known folder, which is used for Let's Encrypt and security.txt |
||||
RewriteRule (^|/)\.(?!well-known\/) index.php [L] |
||||
|
||||
# block all files in the content folder from being accessed directly |
||||
RewriteRule ^content/(.*) index.php [L] |
||||
|
||||
# block all files in the site folder from being accessed directly |
||||
RewriteRule ^site/(.*) index.php [L] |
||||
|
||||
# block direct access to Kirby and the Panel sources |
||||
RewriteRule ^kirby/(.*) index.php [L] |
||||
|
||||
# make site links work |
||||
RewriteCond %{REQUEST_FILENAME} !-f |
||||
RewriteCond %{REQUEST_FILENAME} !-d |
||||
RewriteRule ^(.*) index.php [L] |
||||
|
||||
</IfModule> |
||||
|
||||
# pass the Authorization header to PHP |
||||
SetEnvIf Authorization "(.+)" HTTP_AUTHORIZATION=$1 |
||||
|
||||
# compress text file responses |
||||
<IfModule mod_deflate.c> |
||||
AddOutputFilterByType DEFLATE text/plain |
||||
AddOutputFilterByType DEFLATE text/html |
||||
AddOutputFilterByType DEFLATE text/css |
||||
AddOutputFilterByType DEFLATE text/javascript |
||||
AddOutputFilterByType DEFLATE application/json |
||||
AddOutputFilterByType DEFLATE application/javascript |
||||
AddOutputFilterByType DEFLATE application/x-javascript |
||||
</IfModule> |
||||
|
||||
# set security headers in all responses |
||||
<IfModule mod_headers.c> |
||||
|
||||
# serve files as plain text if the actual content type is not known |
||||
# (hardens against attacks from malicious file uploads) |
||||
Header set Content-Type "text/plain" "expr=-z %{CONTENT_TYPE}" |
||||
Header set X-Content-Type-Options "nosniff" |
||||
|
||||
</IfModule> |
@ -0,0 +1,34 @@ |
||||
<img src="http://getkirby.com/assets/images/github/plainkit.jpg" width="300"> |
||||
|
||||
|
||||
**Kirby: the CMS that adapts to any project, loved by developers and editors alike.** |
||||
The Plainkit is a minimal Kirby setup with the basics you need to start a project from scratch. It is the ideal choice if you are already familiar with Kirby and want to start step-by-step. |
||||
|
||||
You can learn more about Kirby at [getkirby.com](https://getkirby.com). |
||||
|
||||
### Try Kirby for free |
||||
You can try Kirby and the Plainkit on your local machine or on a test server as long as you need to make sure it is the right tool for your next project. … and when you’re convinced, [buy your license](https://getkirby.com/buy). |
||||
|
||||
### Get going |
||||
Read our guide on [how to get started with Kirby](https://getkirby.com/docs/guide/quickstart). |
||||
|
||||
You can [download the latest version](https://github.com/getkirby/plainkit/archive/main.zip) of the Plainkit. |
||||
If you are familiar with Git, you can clone Kirby's Plainkit repository from Github. |
||||
|
||||
git clone https://github.com/getkirby/plainkit.git |
||||
|
||||
## What's Kirby? |
||||
- **[getkirby.com](https://getkirby.com)** – Get to know the CMS. |
||||
- **[Try it](https://getkirby.com/try)** – Take a test ride with our online demo. Or download one of our kits to get started. |
||||
- **[Documentation](https://getkirby.com/docs/guide)** – Read the official guide, reference and cookbook recipes. |
||||
- **[Issues](https://github.com/getkirby/kirby/issues)** – Report bugs and other problems. |
||||
- **[Feedback](https://feedback.getkirby.com)** – You have an idea for Kirby? Share it. |
||||
- **[Forum](https://forum.getkirby.com)** – Whenever you get stuck, don't hesitate to reach out for questions and support. |
||||
- **[Discord](https://chat.getkirby.com)** – Hang out and meet the community. |
||||
- **[Mastodon](https://mastodon.social/@getkirby)** – Spread the word. |
||||
- **[Instagram](https://www.instagram.com/getkirby/)** – Share your creations: #madewithkirby. |
||||
|
||||
--- |
||||
|
||||
© 2009-2022 Bastian Allgeier |
||||
[getkirby.com](https://getkirby.com) · [License agreement](https://getkirby.com/license) |
@ -0,0 +1,107 @@ |
||||
/* open-sans-300 - latin */ |
||||
@font-face { |
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ |
||||
font-family: 'Open Sans'; |
||||
font-style: normal; |
||||
font-weight: 300; |
||||
src: url('../fonts/opensans/open-sans-v40-latin-300.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ |
||||
} |
||||
|
||||
/* open-sans-300italic - latin */ |
||||
@font-face { |
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ |
||||
font-family: 'Open Sans'; |
||||
font-style: italic; |
||||
font-weight: 300; |
||||
src: url('../fonts/opensans/open-sans-v40-latin-300italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ |
||||
} |
||||
|
||||
/* open-sans-regular - latin */ |
||||
@font-face { |
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ |
||||
font-family: 'Open Sans'; |
||||
font-style: normal; |
||||
font-weight: 400; |
||||
src: url('../fonts/opensans/open-sans-v40-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ |
||||
} |
||||
|
||||
/* open-sans-italic - latin */ |
||||
@font-face { |
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ |
||||
font-family: 'Open Sans'; |
||||
font-style: italic; |
||||
font-weight: 400; |
||||
src: url('../fonts/opensans/open-sans-v40-latin-italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ |
||||
} |
||||
|
||||
/* open-sans-500 - latin */ |
||||
@font-face { |
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ |
||||
font-family: 'Open Sans'; |
||||
font-style: normal; |
||||
font-weight: 500; |
||||
src: url('../fonts/opensans/open-sans-v40-latin-500.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ |
||||
} |
||||
|
||||
/* open-sans-500italic - latin */ |
||||
@font-face { |
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ |
||||
font-family: 'Open Sans'; |
||||
font-style: italic; |
||||
font-weight: 500; |
||||
src: url('../fonts/opensans/open-sans-v40-latin-500italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ |
||||
} |
||||
|
||||
/* open-sans-600 - latin */ |
||||
@font-face { |
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ |
||||
font-family: 'Open Sans'; |
||||
font-style: normal; |
||||
font-weight: 600; |
||||
src: url('../fonts/opensans/open-sans-v40-latin-600.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ |
||||
} |
||||
|
||||
/* open-sans-600italic - latin */ |
||||
@font-face { |
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ |
||||
font-family: 'Open Sans'; |
||||
font-style: italic; |
||||
font-weight: 600; |
||||
src: url('../fonts/opensans/open-sans-v40-latin-600italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ |
||||
} |
||||
|
||||
/* open-sans-700 - latin */ |
||||
@font-face { |
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ |
||||
font-family: 'Open Sans'; |
||||
font-style: normal; |
||||
font-weight: 700; |
||||
src: url('../fonts/opensans/open-sans-v40-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ |
||||
} |
||||
|
||||
/* open-sans-700italic - latin */ |
||||
@font-face { |
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ |
||||
font-family: 'Open Sans'; |
||||
font-style: italic; |
||||
font-weight: 700; |
||||
src: url('../fonts/opensans/open-sans-v40-latin-700italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ |
||||
} |
||||
|
||||
/* open-sans-800 - latin */ |
||||
@font-face { |
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ |
||||
font-family: 'Open Sans'; |
||||
font-style: normal; |
||||
font-weight: 800; |
||||
src: url('../fonts/opensans/open-sans-v40-latin-800.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ |
||||
} |
||||
|
||||
/* open-sans-800italic - latin */ |
||||
@font-face { |
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ |
||||
font-family: 'Open Sans'; |
||||
font-style: italic; |
||||
font-weight: 800; |
||||
src: url('../fonts/opensans/open-sans-v40-latin-800italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ |
||||
} |
@ -0,0 +1,598 @@ |
||||
//////////////////////////////////////////////////////////////////////////////// |
||||
// SCSS |
||||
//////////////////////////////////////////////////////////////////////////////// |
||||
|
||||
$border: 0.5vh; |
||||
$orange: orange; |
||||
$shadow: 0.2rem 0.2rem 0.4rem #000; |
||||
$text-shadow: 0.1rem 0.1rem 0.2rem rgba(0,0,0,0.8); |
||||
$bg: #214d1d; |
||||
$darker: rgba(0,0,0,0.3); |
||||
$bgcolor: #2f6f2a; |
||||
$line: 0.2rem; |
||||
|
||||
@mixin border { |
||||
border-radius: 0.25em; |
||||
border: 0.3em black solid; |
||||
} |
||||
|
||||
@mixin button { |
||||
@include border; |
||||
cursor: pointer; |
||||
background-color: $darker; |
||||
} |
||||
|
||||
@mixin button-text{ |
||||
color: white; |
||||
font-weight: bold; |
||||
padding: 0.3em 0.5em; |
||||
text-align: center; |
||||
display: flex; |
||||
align-items: flex-end; |
||||
text-shadow: $shadow; |
||||
font-family: "Open Sans"; |
||||
} |
||||
//////////////////////////////////////////////////////////////////////////////// |
||||
// Meyerweb Reset |
||||
//////////////////////////////////////////////////////////////////////////////// |
||||
|
||||
html, body, div, span, applet, object, iframe, |
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre, |
||||
a, abbr, acronym, address, big, cite, code, |
||||
del, dfn, em, img, ins, kbd, q, s, samp, |
||||
small, strike, strong, sub, sup, tt, var, |
||||
b, u, i, center, |
||||
dl, dt, dd, ol, ul, li, |
||||
fieldset, form, label, legend, |
||||
table, caption, tbody, tfoot, thead, tr, th, td, |
||||
article, aside, canvas, details, embed, |
||||
figure, figcaption, footer, header, hgroup, |
||||
menu, nav, output, ruby, section, summary, |
||||
time, mark, audio, video { |
||||
margin: 0; |
||||
padding: 0; |
||||
border: 0; |
||||
font-size: 100%; |
||||
font: inherit; |
||||
vertical-align: baseline; |
||||
} |
||||
/* HTML5 display-role reset for older browsers */ |
||||
article, aside, details, figcaption, figure, |
||||
footer, header, hgroup, menu, nav, section { |
||||
display: block; |
||||
} |
||||
body { |
||||
// line-height: 1; |
||||
} |
||||
ol, ul { |
||||
list-style: none; |
||||
} |
||||
blockquote, q { |
||||
quotes: none; |
||||
} |
||||
blockquote:before, blockquote:after, |
||||
q:before, q:after { |
||||
content: ''; |
||||
content: none; |
||||
} |
||||
table { |
||||
border-collapse: collapse; |
||||
border-spacing: 0; |
||||
} |
||||
|
||||
//////////////////////////////////////////////////////////////////////////////// |
||||
// General |
||||
//////////////////////////////////////////////////////////////////////////////// |
||||
|
||||
@import "./font"; |
||||
|
||||
*{ |
||||
box-sizing: border-box; |
||||
margin: 0; |
||||
padding: 0; |
||||
} |
||||
|
||||
:root{ |
||||
font-size: Min(1.111111111vw, 1.481481481vh); |
||||
} |
||||
|
||||
body{ |
||||
margin: 0; |
||||
font-family: "Open Sans"; |
||||
background-color: black; |
||||
// background-color: $bgcolor; |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
height: 100vh; |
||||
color: white; |
||||
display: flex; |
||||
flex-direction: column; |
||||
overflow: hidden; |
||||
} |
||||
|
||||
input{ |
||||
font-family: "Open Sans"; |
||||
} |
||||
|
||||
main{ |
||||
border-left: 0.2rem solid black; |
||||
border-right: 0.2rem solid black; |
||||
height: 67.5rem; /* 1080"px" */ |
||||
width: 90rem; /* 1440"px" */ |
||||
background-color: $bgcolor; |
||||
display: flex; |
||||
flex-direction: column; |
||||
} |
||||
|
||||
h1,h2,h3,h4,h5,h6{ |
||||
font-weight: bold; |
||||
} |
||||
.game .body{ |
||||
height:48rem; |
||||
} |
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////// |
||||
// Simple class defs |
||||
//////////////////////////////////////////////////////////////////////////////// |
||||
|
||||
.center{ |
||||
display: flex; |
||||
justify-content: center; |
||||
} |
||||
|
||||
.hidden{ |
||||
display: none !important; |
||||
} |
||||
|
||||
.label{ |
||||
font-size: 1.8em; |
||||
padding: 0.3em 0; |
||||
font-weight: bold; |
||||
display: block; |
||||
} |
||||
|
||||
//////////////////////////////////////////////////////////////////////////////// |
||||
// Content |
||||
//////////////////////////////////////////////////////////////////////////////// |
||||
|
||||
.overlay{ |
||||
@include border; |
||||
position: absolute; |
||||
top: 50%; |
||||
left: 50%; |
||||
z-index: 1000; |
||||
transform: translate(-50%,-50%); |
||||
width: 45rem; |
||||
max-height: 60rem; |
||||
overflow-y: auto; |
||||
&::-webkit-scrollbar { |
||||
width: 0; /* Safari and Chrome */ |
||||
} |
||||
background-color: $bgcolor; |
||||
padding: 2em 1.7em; |
||||
} |
||||
|
||||
.dialog{ |
||||
flex-grow: 1; |
||||
display: flex; |
||||
flex-direction: column; |
||||
justify-content: center; |
||||
align-items: center; |
||||
width: auto; |
||||
h2{ |
||||
font-size: 2.4em; |
||||
margin-bottom: 0.5em; |
||||
text-shadow: $shadow; |
||||
} |
||||
p{ |
||||
font-size: 1.4em; |
||||
margin-bottom: 1em; |
||||
text-shadow: $shadow; |
||||
} |
||||
} |
||||
|
||||
body.home{ |
||||
header{ |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
height: 42%; |
||||
&>img{ |
||||
display: block; |
||||
width: 50%; |
||||
height: auto; |
||||
} |
||||
} |
||||
h1{ |
||||
text-align: center; |
||||
color: white; |
||||
font-size: 2.6rem; |
||||
text-shadow: $shadow; |
||||
} |
||||
.menu{ |
||||
flex-grow: 1; |
||||
display: flex; |
||||
align-items: center; |
||||
padding-bottom: 7.5rem; |
||||
|
||||
.mainmenu{ |
||||
width: 100%; |
||||
display: flex; |
||||
align-items: center; |
||||
flex-direction: column; |
||||
justify-content: center; |
||||
h1{ |
||||
margin-bottom: 1em; |
||||
} |
||||
} |
||||
|
||||
} |
||||
} |
||||
|
||||
|
||||
nav.list{ |
||||
display: flex; |
||||
justify-content: center; |
||||
flex-grow: 1; |
||||
|
||||
&.horizontal{ |
||||
flex-direction: row; |
||||
justify-content: space-evenly; |
||||
& > *+*{ |
||||
margin-left: 1em; |
||||
} |
||||
} |
||||
&.vertical{ |
||||
flex-direction: column; |
||||
& > *+*{ |
||||
margin-top: 1em; |
||||
} |
||||
} |
||||
} |
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////// |
||||
// Elements |
||||
//////////////////////////////////////////////////////////////////////////////// |
||||
|
||||
|
||||
.element{ |
||||
@include border; |
||||
border-width: 0.3rem; |
||||
background-color: $bg; |
||||
cursor: pointer; |
||||
|
||||
&:focus, &:hover{ |
||||
outline: none; |
||||
border-color: $orange; |
||||
} |
||||
|
||||
&.input{ |
||||
font-size: 2em; |
||||
padding: 0.3em; |
||||
background-color: white; |
||||
color: black; |
||||
display: block; |
||||
width: 100%; |
||||
} |
||||
|
||||
&.player{ |
||||
display: flex; |
||||
justify-content: flex-start; |
||||
align-items: center; |
||||
padding: 0.7em; |
||||
|
||||
h2{ |
||||
font-size: 1.3em; |
||||
} |
||||
|
||||
&>*{ |
||||
margin-right: 1em; |
||||
} |
||||
|
||||
img{ |
||||
height: 3em; |
||||
aspect-ratio: 1/1; |
||||
object-position: top; |
||||
object-fit: cover; |
||||
} |
||||
} |
||||
|
||||
&.plain{ |
||||
@include button-text; |
||||
padding: 1rem 2rem; |
||||
justify-content: center; |
||||
align-items: center; |
||||
box-shadow: 0.25rem 0.25rem 1rem rgba(0,0,0, 0.3); |
||||
font-size: 1.6em; |
||||
&:hover,&:focus{ |
||||
background-color: rgba(0, 0, 0, 0.2); |
||||
border-color: $orange; |
||||
outline: none; |
||||
} |
||||
&.back{ |
||||
background-color: #5E716A; |
||||
} |
||||
&.new{ |
||||
background-color: darken($bg, 5%); |
||||
} |
||||
} |
||||
&.player { |
||||
height: 4.5em; |
||||
} |
||||
&.square{ |
||||
font-size: 1rem; |
||||
width: 20em; |
||||
height: 20em; |
||||
padding: 2.5em 1.25em; |
||||
display: flex; |
||||
flex-direction: column; |
||||
justify-content: center; |
||||
align-items: center; |
||||
background-color: $bg; |
||||
box-shadow: 0.25em 0.25em 1em rgba(0,0,0, 0.3); |
||||
cursor: pointer; |
||||
h2{ |
||||
@include button-text; |
||||
font-size: 2em; |
||||
padding: 0.6em 0 0 0; |
||||
} |
||||
&:hover,&:focus{ |
||||
background-color: rgba(0, 0, 0, 0.2); |
||||
border-color: $orange; |
||||
outline: none; |
||||
} |
||||
.icon{ |
||||
width: 65%; |
||||
height: auto; |
||||
svg{ |
||||
width: 100%; |
||||
aspect-ratio: 1/1; |
||||
object-fit: cover; |
||||
fill: white; |
||||
filter: drop-shadow($shadow); |
||||
} |
||||
img{ |
||||
width: 100%; |
||||
aspect-ratio: 1/1; |
||||
object-fit: cover; |
||||
object-position: top; |
||||
filter: drop-shadow($shadow); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////// |
||||
// Game Setup |
||||
//////////////////////////////////////////////////////////////////////////////// |
||||
|
||||
body.xoi{ |
||||
main{ |
||||
display: flex; |
||||
justify-content: center; |
||||
.gamesetup{ |
||||
width: 65%; |
||||
margin: 0 auto; |
||||
h1{ |
||||
font-size: 3em; |
||||
text-align: center; |
||||
margin-bottom: 0.5em; |
||||
} |
||||
.menu{ |
||||
& > * { |
||||
margin-bottom: 1.3em; |
||||
} |
||||
} |
||||
} |
||||
.playerselect{ |
||||
& > h2{ |
||||
font-size: 1.8em; |
||||
margin-bottom: 0.7em; |
||||
text-align: center; |
||||
} |
||||
} |
||||
} |
||||
main .xoi{ |
||||
display: grid; |
||||
height: 100%; |
||||
grid-template-columns: 3fr 3fr 2fr 3fr 3fr; |
||||
grid-template-rows: 11.5rem 52rem 4rem; |
||||
grid-template-areas: |
||||
"player1 toGo1 score toGo2 player2" |
||||
"player1 game game game player2" |
||||
"navi navi navi navi navi" |
||||
; |
||||
background-color: black; |
||||
|
||||
.bigToGo{ |
||||
margin: $line; |
||||
background-color: white; |
||||
border: 0.7rem solid black; |
||||
color: black; |
||||
font-weight: bold; |
||||
font-size: 7.5em; |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
&.active{ |
||||
border-color: $orange; |
||||
} |
||||
&.one{ |
||||
grid-area: toGo1; |
||||
} |
||||
&.two{ |
||||
grid-area: toGo2; |
||||
} |
||||
} |
||||
.score{ |
||||
margin: $line 0; |
||||
grid-area: score; |
||||
color: white; |
||||
text-shadow: $text-shadow; |
||||
background-color: $bgcolor; |
||||
display: flex; |
||||
flex-direction: column; |
||||
justify-content: space-evenly; |
||||
&>div{ |
||||
/*sets and legs*/ |
||||
display: flex; |
||||
justify-content: space-evenly; |
||||
align-items: center; |
||||
h2,h3{ |
||||
margin:0; |
||||
text-align: center; |
||||
font-size: 1.3rem; |
||||
} |
||||
h3{ |
||||
font-size: 1.1rem; |
||||
} |
||||
&>h2{ |
||||
font-size: 3.2rem; |
||||
} |
||||
h2:nth-child(2){ |
||||
order:10; |
||||
} |
||||
} |
||||
} |
||||
.player{ |
||||
background-color: $bgcolor; |
||||
margin-bottom: $line/2; |
||||
|
||||
&.player1{ |
||||
grid-area: player1; |
||||
} |
||||
&.player2{ |
||||
grid-area: player2; |
||||
} |
||||
img{ |
||||
width: 100%; |
||||
aspect-ratio: 35/45; |
||||
object-fit: cover; |
||||
} |
||||
h2, h3{ |
||||
text-align: center; |
||||
margin: 0.2em; |
||||
text-shadow: $text-shadow; |
||||
} |
||||
h2{ |
||||
font-size: 1.8em; |
||||
} |
||||
h3{ |
||||
font-size: 1.2em; |
||||
} |
||||
.stats{ |
||||
display: grid; |
||||
grid-template-columns: auto 1fr 1fr 1fr; |
||||
grid-auto-rows: auto; |
||||
margin: 1em 0.6em; |
||||
text-shadow: $text-shadow; |
||||
|
||||
.row{ |
||||
display: contents; |
||||
font-size: 1.4em; |
||||
&.header{ |
||||
font-weight: bold; |
||||
} |
||||
:nth-child(2), :nth-child(3), :nth-child(4){ |
||||
text-align: center; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
.game{ |
||||
grid-area: game; |
||||
display: grid; |
||||
grid-template-columns: 1fr; |
||||
grid-template-rows: 4.72727272rem 47.272727273rem; |
||||
&>div{ |
||||
display: grid; |
||||
grid-template-columns: 4fr 4fr 2fr 4fr 4fr; |
||||
grid-auto-rows: 4.72727272rem; |
||||
overflow-y: scroll; |
||||
grid-auto-flow: dense; |
||||
height: 100%; |
||||
&::-webkit-scrollbar { |
||||
width: 0; /* Safari and Chrome */ |
||||
} |
||||
&>div{ |
||||
font-size: 2rem; |
||||
text-align: center; |
||||
background: white; |
||||
color: black; |
||||
margin: 0 0 $line $line; |
||||
width: calc(100% - $line); |
||||
height: calc(100% - $line); |
||||
border: 0.15em transparent solid; |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
&.headding{ |
||||
background-color: $bgcolor; |
||||
color: white; |
||||
text-shadow: $text-shadow; |
||||
} |
||||
&.rounds{ |
||||
grid-column: 3; |
||||
background-color: $bgcolor; |
||||
color: white; |
||||
text-shadow: $text-shadow; |
||||
} |
||||
&.player1.points{ |
||||
grid-column: 1; |
||||
} |
||||
&.player1.toGo{ |
||||
grid-column: 2; |
||||
font-size: 1.6rem; |
||||
} |
||||
&.player2.points{ |
||||
grid-column: 4; |
||||
} |
||||
&.player2.toGo{ |
||||
grid-column: 5; |
||||
margin-right: $line; |
||||
// width: calc(100% - 2*$line); |
||||
// height: calc(100% - $line); |
||||
font-size: 1.6rem; |
||||
} |
||||
&.input{ |
||||
padding: 0; |
||||
border: 0.15em orange solid; |
||||
input{ |
||||
text-align: center; |
||||
box-sizing: border-box; |
||||
font-family: inherit; |
||||
padding: 0.1em; |
||||
border: none; |
||||
width: 100%; |
||||
height: 100%; |
||||
font-size: 1em; |
||||
&:focus{ |
||||
outline: none; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
&>div.headding{ |
||||
&.rounds{ |
||||
font-size: 1.4rem; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
.nav{ |
||||
grid-area: navi; |
||||
background-color: $bgcolor; |
||||
margin-top: $line/2; |
||||
display: flex; |
||||
justify-content: space-evenly; |
||||
align-items: center; |
||||
span{ |
||||
font-size: 2em; |
||||
} |
||||
} |
||||
} |
||||
} |
After Width: | Height: | Size: 536 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 212 KiB |
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,105 @@ |
||||
|
||||
const html = (v) => { return v[0] }; |
||||
|
||||
export const renderer = { |
||||
props: ['stack'], |
||||
setup(props) { |
||||
|
||||
}, |
||||
template: html` |
||||
<Suspense> |
||||
<template v-for="states, i in stack"> |
||||
<component @keydown.esc.stop="states[states.length-1].reject(-1)" v-if="states.length > 0" :is="states[states.length-1].component" :stack="stack" v-bind="states[states.length-1].properties" @resolve="(e) => states[states.length-1].resolve(e)" @reject="(e) => states[states.length-1].reject(e)" :active="stack.length-1 == i"></component> |
||||
</template> |
||||
</Suspense> |
||||
` |
||||
} |
||||
|
||||
function reGet(stack) { |
||||
return safePromise(new Promise(function(resolve, reject) { |
||||
const lastState = stack[stack.length-1]; |
||||
lastState[lastState.length-1].resolve = resolve; |
||||
lastState[lastState.length-1].reject = reject; |
||||
})); |
||||
} |
||||
|
||||
export function popLastElem(stack) { |
||||
const elem = stack[stack.length-1].pop(); |
||||
if (stack[stack.length-1].length == 0) { |
||||
stack.pop(); |
||||
} |
||||
return elem; |
||||
} |
||||
|
||||
function safePromise(promise) { |
||||
return promise.then(data => [ data, undefined ]).catch(error => [ null, error != undefined ? error : -1 ]); |
||||
} |
||||
|
||||
export function overlayAndGet(component, properties, stack, reGetFlag=false) { |
||||
if (reGetFlag) return reGet(stack); |
||||
properties["stack"] = stack; |
||||
return safePromise(new Promise(function(resolve, reject) { |
||||
stack.push([{ |
||||
"component": component, |
||||
"properties": properties, |
||||
"resolve": resolve, |
||||
"reject": reject |
||||
}]) |
||||
})); |
||||
} |
||||
|
||||
export function replaceAndGet(component, properties, stack, reGetFlag=false) { |
||||
if (reGetFlag) return reGet(stack); |
||||
properties["stack"] = stack; |
||||
return safePromise(new Promise(function(resolve, reject) { |
||||
stack[stack.length-1].push({ |
||||
"component": component, |
||||
"properties": properties, |
||||
"resolve": resolve, |
||||
"reject": reject |
||||
}) |
||||
})); |
||||
} |
||||
|
||||
|
||||
export async function overlayAndPop(component, properties, stack){ |
||||
const ret = await overlayAndGet(component, properties, stack); |
||||
popLastElem(stack); |
||||
return ret; |
||||
} |
||||
|
||||
export async function replaceAndPop(component, properties, stack){ |
||||
const ret = await replaceAndGet(component, properties, stack); |
||||
popLastElem(stack); |
||||
return ret; |
||||
} |
||||
|
||||
|
||||
export const powerStateMachine = async (stateMachine, stack, initState=0, initInput=undefined) => { |
||||
let stateHistory = [initState]; |
||||
let resultHistory = [initInput]; |
||||
let reGet = false; |
||||
let newState, newResult; |
||||
while (stateHistory[stateHistory.length-1] != undefined) { |
||||
const state = stateHistory[stateHistory.length-1]; |
||||
const result = resultHistory[resultHistory.length-1]; |
||||
[newState, newResult] = await stateMachine[state](result, reGet); |
||||
if (newState == -1){ |
||||
reGet = true; |
||||
stateHistory.pop(); |
||||
resultHistory.pop(); |
||||
if (newResult != undefined) { |
||||
resultHistory[resultHistory.length-1] = newResult; |
||||
} |
||||
} else if (newState == state){ |
||||
reGet = true; |
||||
resultHistory[resultHistory.length-1] = newResult; |
||||
} else if (newState == undefined) { |
||||
return newResult; |
||||
} else { |
||||
reGet = false; |
||||
stateHistory.push(newState); |
||||
resultHistory.push(newResult); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,25 @@ |
||||
import { ref, computed } from "vue"; |
||||
import { getId } from "../handlers.js"; |
||||
import { handleActive, ArrowVerticalKeyHandler, ArrowHorizontalKeyHandler, NumberKeyHandler } from "../handlers.js"; |
||||
|
||||
const html = (v) => { return v[0] }; |
||||
|
||||
export default { |
||||
props: ['active', 'title', 'text', 'buttons', "withshortkey", "stack"], |
||||
setup(props, { emit }) { |
||||
const handlers = [ArrowHorizontalKeyHandler]; |
||||
if (props.withshortkey) { |
||||
handlers.push(NumberKeyHandler); |
||||
} |
||||
handleActive(props, handlers); |
||||
const elements = computed(() => props.buttons.map((i) => { i.onClick = () => emit('resolve', i.result ); return i; }) ); |
||||
return { elements } |
||||
}, |
||||
template: html` |
||||
<div class="dialog overlay" v-if="active"> |
||||
<h2>{{ title }}</h2> |
||||
<p>{{ text }}</p> |
||||
<d-list :elements="elements" type="horizontal" :withshortkey="withshortkey"></d-list> |
||||
</div> |
||||
` |
||||
} |
@ -0,0 +1,22 @@ |
||||
import list from "./list.js"; |
||||
|
||||
import playerElem from "./playerElem.js"; |
||||
import plainElem from "./plainElem.js"; |
||||
import squareElem from "./squareElem.js"; |
||||
import inputElem from "./inputElem.js"; |
||||
|
||||
import playerselect from "./playerselect.js"; |
||||
// import overlay from "./overlay.js";
|
||||
import dialog from "./dialog.js"; |
||||
|
||||
export default (app) => { |
||||
app.component("d-list", list); |
||||
|
||||
app.component("d-playerElem", playerElem); |
||||
app.component("d-plainElem", plainElem); |
||||
app.component("d-squareElem", squareElem); |
||||
app.component("d-inputElem", inputElem); |
||||
|
||||
app.component("d-playerselect", playerselect); |
||||
app.component("d-dialog", dialog); |
||||
} |
@ -0,0 +1,17 @@ |
||||
import { onMounted, ref, computed, watch, nextTick } from "vue"; |
||||
|
||||
const html = (v) => { return v[0] }; |
||||
|
||||
export default { |
||||
props: ['modelValue'], |
||||
setup(props) { |
||||
|
||||
return { window } |
||||
}, |
||||
template: html` |
||||
<input class="input element" :value="modelValue" |
||||
@input="$emit('update:modelValue', $event.target.value)" @focus="window.setTimeout (function(){ |
||||
$event.target.select(); |
||||
},1);"> |
||||
` |
||||
} |
@ -0,0 +1,17 @@ |
||||
import { ref, computed } from "vue"; |
||||
import { getId } from "../handlers.js"; |
||||
|
||||
const html = (v) => { return v[0] }; |
||||
|
||||
export default { |
||||
props: ['elements', 'type', "withshortkey"], |
||||
setup(props) { |
||||
return { } |
||||
}, |
||||
template: html` |
||||
<nav class="list" :class="{ horizontal: type == 'horizontal', vertical: type != 'horizontal'}"> |
||||
<component :is="item.component" v-for="(item, index) in elements" v-index :withshortkey="withshortkey" @click="item.onClick" v-bind="item.props"> |
||||
</component> |
||||
</nav> |
||||
` |
||||
} |
@ -0,0 +1,14 @@ |
||||
const html = (v) => { return v[0] }; |
||||
|
||||
// Not needed anymore
|
||||
export default { |
||||
props: ['component', "props", "active", "stack"], |
||||
setup(props) { |
||||
return { } |
||||
}, |
||||
template: html` |
||||
<div class="overlay"> |
||||
<component :is="component" v-bind="props" :active="active" :stack="stack" @resolve="(e) => $emit('resolve', e)" @reject="(e) => $emit('reject', e)"></component> |
||||
</div> |
||||
` |
||||
} |
@ -0,0 +1,16 @@ |
||||
import { computed, ref } from "vue"; |
||||
import { getId } from "../handlers.js"; |
||||
|
||||
const html = (v) => { return v[0] }; |
||||
|
||||
export default { |
||||
props: ['text', 'withshortkey', 'autofocus'], |
||||
setup(props) { |
||||
const el = ref(null); |
||||
const suffix = computed(() => props.withshortkey ? ` (${getId(el.value)})`: ""); |
||||
return { suffix, el } |
||||
}, |
||||
template: html` |
||||
<div class="plain element" ref="el" v-autofocus="autofocus">{{ text }}{{ suffix }}</div> |
||||
` |
||||
} |
@ -0,0 +1,23 @@ |
||||
import { onMounted, ref, computed, watch, nextTick } from "vue"; |
||||
|
||||
const html = (v) => { return v[0] }; |
||||
|
||||
export default { |
||||
props: ['player', 'id'], |
||||
setup(props) { |
||||
return { } |
||||
}, |
||||
template: html` |
||||
<div class="player element"> |
||||
<template v-if="player !== undefined"> |
||||
<img :src="player.img ? player.img : '/assets/img/placeholder_person.png'"> |
||||
<h2 class="name">{{ player.forename }} {{ player.surname }}</h2> |
||||
<h3 class="nickname">{{ player.nickname }}</h3> |
||||
</template> |
||||
<template v-if="player === undefined"> |
||||
<h2 class="name">No Player</h2> |
||||
<h3 class="nickname"> </h3> |
||||
</template> |
||||
</div> |
||||
` |
||||
} |
@ -0,0 +1,29 @@ |
||||
import { nextTick, reactive, ref, computed, onMounted, onUnmounted } from "vue"; |
||||
import { handleActive, ArrowVerticalKeyHandler, ArrowHorizontalKeyHandler, NumberKeyHandler } from "../handlers.js"; |
||||
|
||||
const html = (v) => { return v[0] }; |
||||
|
||||
export default { |
||||
props: ['players', 'stack', 'active', 'input'], |
||||
setup(props, context) { |
||||
handleActive(props, [ArrowVerticalKeyHandler]); |
||||
const children = computed(() => { |
||||
return props.players.map((p) => { |
||||
return { |
||||
component: "d-playerElem", |
||||
props: {"player": p}, |
||||
onClick: () => { |
||||
context.emit('resolve', p); |
||||
} |
||||
} |
||||
}) |
||||
}); |
||||
return { children } |
||||
}, |
||||
template: html` |
||||
<div class="playerselect"> |
||||
<!-- <h2>Select Player</h2> --> |
||||
<d-list title="Select Player" :withshortkey="true" :elements="children"/> |
||||
</div> |
||||
` |
||||
} |
@ -0,0 +1,22 @@ |
||||
import { computed, ref } from "vue"; |
||||
import { getId } from "../handlers.js"; |
||||
|
||||
const html = (v) => { return v[0] }; |
||||
|
||||
export default { |
||||
props: ["icon", "text", "withshortkey"], |
||||
setup(props) { |
||||
const el = ref(null); |
||||
const suffix = computed(() => props.withshortkey ? ` (${getId(el.value)})`: ""); |
||||
return { suffix, el } |
||||
}, |
||||
template: html` |
||||
<div class="square element" ref="el"> |
||||
<div class="icon"> |
||||
<inject-svg v-if="icon.endsWith('.svg')" :src="icon"></inject-svg> |
||||
<img v-if="!icon.endsWith('.svg')" :src="icon"> |
||||
</div> |
||||
<h2>{{ text }}{{ suffix }}</h2> |
||||
</div> |
||||
` |
||||
} |
@ -0,0 +1,155 @@ |
||||
import { ref, watch, nextTick, onBeforeUnmount } from 'vue'; |
||||
import { state } from "./stateMgr.js"; |
||||
|
||||
function getNumberFromKeyEvent(event) { |
||||
if (event.keyCode >= 96 && event.keyCode <= 105) { |
||||
return event.keyCode - 96; |
||||
} else if (event.keyCode >= 48 && event.keyCode <= 57) { |
||||
return event.keyCode - 48; |
||||
} |
||||
return null; |
||||
} |
||||
export const ArrowHorizontalKeyHandler = (event) => { |
||||
if (event.key == "ArrowRight") { |
||||
const idx = state.sortedElements.indexOf(String(state.activeIndex)); |
||||
state.activeIndex = state.sortedElements[(idx+1)%state.sortedElements.length]; |
||||
} else if (event.key == "ArrowLeft") { |
||||
const idx = state.sortedElements.indexOf(String(state.activeIndex)) |
||||
state.activeIndex = state.sortedElements[(idx+state.sortedElements.length-1)%state.sortedElements.length]; |
||||
} |
||||
} |
||||
export const ArrowVerticalKeyHandler = (event) => { |
||||
if (event.key == "ArrowDown") { |
||||
const idx = state.sortedElements.indexOf(String(state.activeIndex)) |
||||
state.activeIndex = state.sortedElements[(idx+1)%state.sortedElements.length]; |
||||
event.preventDefault(); |
||||
} else if (event.key == "ArrowUp") { |
||||
const idx = state.sortedElements.indexOf(String(state.activeIndex)) |
||||
state.activeIndex = state.sortedElements[(idx+state.sortedElements.length-1)%state.sortedElements.length]; |
||||
event.preventDefault(); |
||||
} |
||||
} |
||||
export const NumberKeyHandler = (event) => { |
||||
let i; |
||||
if ((i = getNumberFromKeyEvent(event)) != null) { |
||||
if (state.elements[i] != undefined) { |
||||
event.stopPropagation(); |
||||
event.preventDefault(); |
||||
state.activeIndex = i; |
||||
state.elements[i].focus(); |
||||
state.elements[i].click(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
|
||||
export const EnterHandler = (event) => { |
||||
if (event.key == "Enter"){ |
||||
if (state.hasOwnProperty("activeElement") && state.activeElement) { |
||||
event.stopPropagation(); |
||||
if (state.activeElement.tagName != "BUTTON"){ |
||||
event.preventDefault(); |
||||
} |
||||
state.activeElement.click(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
|
||||
export const handleActive = (props, keydownHandlers) => { |
||||
const ownCopy = keydownHandlers.map((f) => (e) => f(e)); |
||||
const currActive = ref(1); |
||||
state.activeIndex = 1; |
||||
const handle = () => { |
||||
if (props.active){ |
||||
state.activeIndex = currActive.value; |
||||
for (var i = 0; i < keydownHandlers.length; i++) { |
||||
document.addEventListener("keydown", ownCopy[i]); |
||||
} |
||||
} else { |
||||
currActive.value = state.activeIndex; |
||||
for (var i = 0; i < ownCopy.length; i++) { |
||||
document.removeEventListener("keydown", ownCopy[i]); |
||||
} |
||||
} |
||||
} |
||||
onBeforeUnmount(() => { |
||||
// Cleanup
|
||||
for (var i = 0; i < ownCopy.length; i++) { |
||||
document.removeEventListener("keydown", ownCopy[i]); |
||||
} |
||||
}); |
||||
watch(() => props.active, handle); |
||||
handle(); |
||||
} |
||||
|
||||
export const vAutofocus = { |
||||
mounted(el, binding, vnode, prevVnode) { |
||||
if (binding.value == undefined || binding.value) { |
||||
state.activeIndex = getId(el); |
||||
} |
||||
} |
||||
} |
||||
|
||||
export function getId(el) { |
||||
for (var key in state.elements) { |
||||
if (state.elements[key] == el){ |
||||
return key; |
||||
} |
||||
} |
||||
} |
||||
|
||||
|
||||
|
||||
const add = (el) => { |
||||
const focus = () => { |
||||
if (state.activeElement !== el) { |
||||
state.activeIndex = val; |
||||
state.activeElement = el; |
||||
} |
||||
} |
||||
|
||||
const blur = (e) => { |
||||
if (e.relatedTarget === null) { |
||||
state.activeElement = undefined; |
||||
state.activeIndex = undefined; |
||||
} |
||||
} |
||||
let val; |
||||
if (el.dataset["tabindex"]) { |
||||
val = el.dataset["tabindex"]; |
||||
} else { |
||||
val = Object.keys(state.elements).length+1; |
||||
} |
||||
el.tabIndex = val; |
||||
state.elements[val] = el; |
||||
el.addEventListener("focus", focus); |
||||
el.addEventListener("blur", blur); |
||||
el.blurcb = blur; |
||||
el.focuscb = focus; |
||||
}; |
||||
|
||||
const remove = (el) => { |
||||
delete el.removeAttribute("tabindex"); |
||||
delete state.elements[getId(el)] |
||||
// el.removeEventListener("blur", el.blurfn);
|
||||
// el.removeEventListener("focus", el.focusfn);
|
||||
} |
||||
|
||||
export const vIndex = { |
||||
updated(el, binding, vnode, prevVnode) { |
||||
if (binding.oldValue != binding.value){ |
||||
if (binding.oldValue) { |
||||
remove(el); |
||||
} else { |
||||
add(el); |
||||
} |
||||
} |
||||
}, |
||||
mounted(el, binding, vnode, prevVnode) { |
||||
add(el); |
||||
}, |
||||
beforeUnmount(el, binding, vnode, prevVnode) { |
||||
remove(el); |
||||
} |
||||
} |
@ -0,0 +1,152 @@ |
||||
import { ref, reactive, watch, computed } from "vue"; |
||||
|
||||
|
||||
function isIterable(obj) { |
||||
// checks for null and undefined
|
||||
if (obj == null) { |
||||
return false; |
||||
} |
||||
return typeof obj[Symbol.iterator] === 'function'; |
||||
} |
||||
|
||||
export async function homeAction(options = {}) { |
||||
return await ( await fetch("/", { |
||||
method: "POST", |
||||
headers: { |
||||
Accept: "application/json", |
||||
}, |
||||
body: JSON.stringify(options), |
||||
})).json(); |
||||
} |
||||
|
||||
export async function getQuery(query, options = {}) { |
||||
return (await ( await fetch("/api/query", { |
||||
method: "POST", |
||||
headers: { |
||||
Accept: "application/json", |
||||
// completely unsafe
|
||||
// Authorization: "Basic "+btoa("api@api.de:H]RcScp];76!-PB")
|
||||
}, |
||||
body: JSON.stringify({ |
||||
query: query, |
||||
...options |
||||
}), |
||||
})).json()).result; |
||||
} |
||||
|
||||
|
||||
async function getKirby(endpoint) { |
||||
return (await (await fetch("/api"+endpoint, { |
||||
method: "GET", |
||||
headers: { |
||||
// completely unsafe
|
||||
Authorization: "Basic "+btoa("api@api.de:H]RcScp];76!-PB") |
||||
} |
||||
})).json()).data; |
||||
// .catch(error => {
|
||||
// console.log("Error:", error);
|
||||
// });
|
||||
} |
||||
|
||||
export async function setKirby(endpoint, data) { |
||||
const response = await fetch(`/${endpoint}`, { |
||||
method: "POST", |
||||
cache: "no-cache", |
||||
headers: { |
||||
"Content-Type": "application/json", |
||||
}, |
||||
body: JSON.stringify({ |
||||
action: "update", |
||||
data: data |
||||
}), |
||||
}); |
||||
const ret = await response.json(); |
||||
return ret; |
||||
} |
||||
|
||||
function updateContent(endpoint, data) { |
||||
return fetch("/api"+endpoint, { |
||||
method: "PATCH", |
||||
headers: { |
||||
// "X-CSRF" : g_csrf,
|
||||
// completely unsafe
|
||||
"Authorization": "Basic "+btoa("api@api.de:H]RcScp];76!-PB") |
||||
}, |
||||
body: JSON.stringify(data) |
||||
}) |
||||
.then(response => response.json()) |
||||
.then(response => { |
||||
// console.log(response.data);
|
||||
}) |
||||
.catch(error => { |
||||
console.log("Error:", error); |
||||
}); |
||||
} |
||||
|
||||
|
||||
export const convertToPages = async (obj) => { |
||||
for(var key in obj){ |
||||
if (obj[key].hasOwnProperty("link")) { |
||||
obj[key] = await kirbyPage(obj[key].link); |
||||
} else if (Array.isArray(obj[key]) || typeof obj[key] === 'object' ) { |
||||
obj[key] = convertToPages(obj[key]); |
||||
} |
||||
} |
||||
return obj; |
||||
} |
||||
|
||||
|
||||
export const kirbyPage = async (endpoint) => { |
||||
if (!endpoint.startsWith("/pages/")){ |
||||
endpoint = "/pages/"+endpoint.replaceAll("/","+"); |
||||
} |
||||
|
||||
let patch = false; |
||||
const page = new Proxy(reactive({}), { |
||||
async set(target, key, value) { |
||||
if(value.hasOwnProperty("uuid") && value.uuid.startsWith("page://")){ |
||||
value = await kirbyPage(value.link); |
||||
} |
||||
if (Array.isArray(value) || typeof value === 'object' ) { |
||||
for(var i in value){ |
||||
if (value[i].hasOwnProperty("uuid") && value[i].uuid.startsWith("page://")) { |
||||
value[i] = await kirbyPage(value[i].link); |
||||
} |
||||
} |
||||
} |
||||
target[key] = value; |
||||
if (patch) { |
||||
const data = {}; |
||||
if (value.isPage) { |
||||
data[key] = "page://"+value.uuid; |
||||
} else if (Array.isArray(value) || typeof value === 'object' ) { |
||||
data[key] = []; |
||||
for (var i in value) { |
||||
if (value[i].isPage) { |
||||
data[key].push("page://"+value[i].uuid); |
||||
} else { |
||||
data[key].push(value[i]); |
||||
} |
||||
} |
||||
} else { |
||||
data[key] = value; |
||||
} |
||||
updateContent(endpoint, data); |
||||
} |
||||
return true; |
||||
}, |
||||
get(target, prop, receiver) { |
||||
if (prop == "isPage") { |
||||
return true; |
||||
} |
||||
return target[prop]; |
||||
} |
||||
} |
||||
); |
||||
const lastData = ref({}); |
||||
const kData = await getKirby(endpoint); |
||||
Object.assign(page, kData.content); |
||||
|
||||
patch = true; |
||||
return page; |
||||
} |
@ -0,0 +1,46 @@ |
||||
import { reactive, nextTick, watch, computed } from "vue"; |
||||
|
||||
export const state = reactive({ |
||||
_activeIndex: undefined, |
||||
get activeIndex(){ |
||||
return state._activeIndex; |
||||
}, |
||||
set activeIndex(val){ |
||||
// if (val === state._activeIndex) return;
|
||||
state._activeIndex = val; |
||||
if (val === undefined) return; |
||||
state._activeElement = document.querySelector(`[tabindex="${val}"]`); |
||||
nextTick(() => { |
||||
nextTick(() => { |
||||
state._activeElement?.focus(); |
||||
}) |
||||
}) |
||||
}, |
||||
_activeElement: undefined, |
||||
get activeElement(){ |
||||
return state._activeElement; |
||||
}, |
||||
set activeElement(val){ |
||||
// if (val === state._activeElement) return;
|
||||
state._activeElement = val; |
||||
if (val === undefined) return; |
||||
state.activeIndex = val.tabIndex; |
||||
}, |
||||
elements: {}, |
||||
sortedElements: computed(() => Object.keys(state.elements)), |
||||
init: true, |
||||
back: [], |
||||
backActive: [] |
||||
}); |
||||
|
||||
watch(() => state.sortedElements, () => { |
||||
if (state._activeIndex !== undefined && state.sortedElements.length > 0){ |
||||
state.activeIndex = state._activeIndex; |
||||
} |
||||
}) |
||||
|
||||
watch(() => state.activeIndex, () => { |
||||
if (state.activeIndex === undefined && state.sortedElements.length > 0){ |
||||
state.activeIndex = state.sortedElements[0]; |
||||
} |
||||
}) |
@ -0,0 +1,229 @@ |
||||
import { nextTick, watch, reactive, ref, computed, onMounted, onUnmounted } from "vue"; |
||||
import { getQuery, homeAction } from "../kirby.js"; |
||||
import { handleActive, ArrowVerticalKeyHandler, ArrowHorizontalKeyHandler, NumberKeyHandler } from "../handlers.js"; |
||||
import { overlayAndGet, powerStateMachine, popLastElem } from "../componentPromise.js"; |
||||
|
||||
const html = (v) => { return v[0] }; |
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Components
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export const selectMode = { |
||||
props: ['active', 'stack'], |
||||
setup(props, { emit }) { |
||||
handleActive(props, [NumberKeyHandler, ArrowHorizontalKeyHandler]); |
||||
|
||||
const children = reactive([ |
||||
{ |
||||
// component: "d-squareElem",
|
||||
// props:{
|
||||
// text: "Create Game",
|
||||
// icon: "/assets/img/dart-board.svg",
|
||||
// },
|
||||
// onClick: () => {
|
||||
// emit("resolve", "create");
|
||||
// }
|
||||
// }, {
|
||||
component: "d-squareElem", |
||||
props:{ |
||||
text: "Play Game", |
||||
icon: "/assets/img/dart.svg", |
||||
}, |
||||
onClick: () => { |
||||
emit("resolve", "play"); |
||||
} |
||||
} |
||||
]); |
||||
const canvas = ref([]); |
||||
return { children } |
||||
}, |
||||
template: html` |
||||
<d-list v-if="active" type="horizontal" :withshortkey="true" :elements="children" /> |
||||
` |
||||
} |
||||
|
||||
|
||||
export const selectTournament = { |
||||
props: ['tournaments', 'active', 'stack'], |
||||
setup(props, { emit }) { |
||||
handleActive(props, [NumberKeyHandler, ArrowVerticalKeyHandler]); |
||||
|
||||
const children = computed(() => { |
||||
const items = []; |
||||
for (let i in props.tournaments){ |
||||
const tournament = props.tournaments[i]; |
||||
items.push({ |
||||
component: "d-plainElem", |
||||
props: { |
||||
text: tournament.title, |
||||
}, |
||||
onClick: () => { |
||||
emit("resolve", tournament.id); |
||||
} |
||||
}) |
||||
} |
||||
items.push({ |
||||
component: "d-plainElem", |
||||
props: { |
||||
text: "Back", |
||||
class: "back" |
||||
}, |
||||
onClick: () => { |
||||
emit("reject"); |
||||
} |
||||
}); |
||||
return items; |
||||
}); |
||||
return { children } |
||||
}, |
||||
template: html` |
||||
<div v-if="active" class="mainmenu"> |
||||
<h1>Select Tournament</h1> |
||||
<d-list style="width: 65%" :withshortkey="true" :elements="children"/> |
||||
</div> |
||||
` |
||||
} |
||||
|
||||
|
||||
export const selectGame = { |
||||
props: ['games', 'active', 'stack'], |
||||
setup(props, { emit }) { |
||||
handleActive(props, [NumberKeyHandler, ArrowVerticalKeyHandler]); |
||||
|
||||
const children = computed(() => { |
||||
const items = []; |
||||
items.push({ |
||||
component: "d-plainElem", |
||||
props: { |
||||
text: "Create Game", |
||||
class: "new" |
||||
}, |
||||
onClick: () => { |
||||
emit("resolve", "create"); |
||||
} |
||||
}); |
||||
for (let i in props.games){ |
||||
const game = props.games[i]; |
||||
items.push({ |
||||
component: "d-plainElem", |
||||
props: { |
||||
text: game.player[0]?.forename + " vs. " + game.player[1]?.forename, |
||||
}, |
||||
onClick: () => { |
||||
emit("resolve", game.id); |
||||
} |
||||
}) |
||||
} |
||||
items.push({ |
||||
component: "d-plainElem", |
||||
props: { |
||||
text: "Back", |
||||
class: "back" |
||||
}, |
||||
onClick: () => { |
||||
emit("reject"); |
||||
} |
||||
}); |
||||
return items; |
||||
}); |
||||
return { children } |
||||
}, |
||||
template: html` |
||||
<div v-if="active" class="mainmenu"> |
||||
<h1>Select Game</h1> |
||||
<d-list style="width: 65%" :withshortkey="true" :elements="children"/> |
||||
</div> |
||||
` |
||||
} |
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// State Machine
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const stateMachine = (stack) => { |
||||
return { |
||||
0: async (input, reGet) => { |
||||
const [result, error] = await overlayAndGet("d-selectMode", { }, stack, reGet); |
||||
if (error != undefined) { |
||||
return [0, error]; |
||||
} |
||||
if (result == "play") { |
||||
return [1, result]; |
||||
} |
||||
if (result == "create") { |
||||
// TODO:
|
||||
return [1, result]; |
||||
} |
||||
}, |
||||
1: async (input, reGet) => { |
||||
const tournaments = await getQuery("site.find('seasons').getRunningTournaments", { |
||||
select: { |
||||
title: "page.title", |
||||
id: "page.id", |
||||
} |
||||
}); |
||||
const [result, error] = await overlayAndGet("d-selectTournament", { "tournaments": tournaments}, stack, reGet); |
||||
if (error != undefined) { |
||||
popLastElem(stack); |
||||
return [-1, error]; |
||||
} |
||||
return [2, result]; |
||||
}, |
||||
2: async (input, reGet) => { |
||||
const games = await getQuery(`site.find('${input}').getRunningGames`, { |
||||
select: { |
||||
title: "page.title", |
||||
id: "page.id", |
||||
uuid: "page.uuid", |
||||
player: { |
||||
query: "page.players.toPages", |
||||
select:{ |
||||
forename: "page.forename" |
||||
} |
||||
} |
||||
} |
||||
}); |
||||
const [result, error] = await overlayAndGet("d-selectGame", {"games": games}, stack, reGet); |
||||
if (error != undefined) { |
||||
popLastElem(stack); |
||||
return [-1, error]; |
||||
} |
||||
if (result == "create") { |
||||
// Create Game and redirect
|
||||
const ret = await homeAction({ |
||||
"action": "createGame", |
||||
"tournament": input |
||||
}) |
||||
if (ret.status == "ok") { |
||||
window.setTimeout(() => { |
||||
window.location.href = ret.url; |
||||
return false; |
||||
},1); |
||||
} |
||||
return [undefined, undefined]; |
||||
} |
||||
window.setTimeout(() => { |
||||
window.location.href = result; |
||||
return false; |
||||
},1); |
||||
return [undefined, undefined]; |
||||
} |
||||
}; |
||||
} |
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Exports
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export const homeHandler = async (stack) => { |
||||
const sm = stateMachine(stack); |
||||
powerStateMachine(sm, stack); |
||||
} |
||||
|
||||
export const initHomeView = (app) => { |
||||
app.component("d-selectMode", selectMode).component("d-selectTournament", selectTournament).component('d-selectGame', selectGame) |
||||
} |
@ -0,0 +1,499 @@ |
||||
import { nextTick, reactive, ref, computed, onMounted, onUnmounted } from "vue"; |
||||
import { state } from "../stateMgr.js"; |
||||
import { getQuery } from "../kirby.js"; |
||||
import { initSubState, ArrowVerticalKeyHandler, ArrowHorizontalKeyHandler, NumberKeyHandler } from "../handlers.js"; |
||||
|
||||
|
||||
const html = (v) => { return v[0] }; |
||||
|
||||
function initView(view){ |
||||
const back = state.view; |
||||
state.back.push(() => { state.view = back }); |
||||
state.backActive.push(state.activeIndex); |
||||
state.view = view; |
||||
} |
||||
|
||||
const togo = { |
||||
props: ['togo'], |
||||
setup(props) { |
||||
|
||||
return { } |
||||
}, |
||||
template: html` |
||||
<div class="bigToGo">{{ togo }}</div> |
||||
` |
||||
} |
||||
|
||||
const game = { |
||||
props: ['players', 'currentleg', 'max', 'sendvisit', 'modelValue', 'overlay'], |
||||
setup(props, context) { |
||||
const length = computed(() => props.currentleg?.visits.length ); |
||||
const visits = computed(() => props.currentleg? props.currentleg.visits:undefined ); |
||||
const loop = computed(() => Math.max(length.value?length.value+(length.value?length.value%2:0):0,18) ); |
||||
const getPlayerVisits = (uuid) => { |
||||
const vs = visits.value.filter((v) => v.player == uuid); |
||||
if (vs.length < 9) { |
||||
for (var i = vs.length; i < 9; i++) { |
||||
vs.push({ "sum": "", "toGo":["",""]}) |
||||
} |
||||
} else if (vs.length*2 < visits.value.length) { |
||||
|
||||
vs.push({ "sum": "", "toGo":["",""]}); |
||||
} |
||||
return vs; |
||||
} |
||||
const check_remove = (event) => { |
||||
if (!event.repeat && event.target.value.length < 1) { |
||||
context.emit('removeLastVisit', event.target); |
||||
} |
||||
} |
||||
const setup = ref(false); |
||||
const setupEnter = () => { |
||||
setup.value = true; |
||||
} |
||||
const confirmEnter = (evt) => { |
||||
if (setup.value) { |
||||
context.emit('sendvisit', evt.target) |
||||
} |
||||
setup.value = false; |
||||
} |
||||
return { length, visits, loop, getPlayerVisits, check_remove, confirmEnter, setupEnter } |
||||
}, |
||||
template: html` |
||||
<div class="game"> |
||||
<div class="headding"> |
||||
<div class="headding points player1">Points</div> |
||||
<div class="headding toGo player1">ToGo</div> |
||||
<div class="headding rounds">Round</div> |
||||
<div class="headding points player2">Points</div> |
||||
<div class="headding toGo player2">ToGo</div> |
||||
</div> |
||||
<div class="body"> |
||||
<div class="points player1"></div> |
||||
<div class="toGo player1">{{ max }}</div> |
||||
<div class="rounds">0</div> |
||||
<div class="points player2"></div> |
||||
<div class="toGo player2">{{ max }}</div> |
||||
<template v-for="j in [1,2]" v-if="players"> |
||||
<template v-for="visit, i in getPlayerVisits(players[j-1].uuid)"> |
||||
<template v-if="visit.toGo === undefined"> |
||||
<div :class="'points player'+j+' input'"><input v-autofocus :value="modelValue" |
||||
@input="$emit('update:modelValue', $event.target.value)" @keyup.enter="confirmEnter($event)" @keydown.enter="setupEnter($event)" @keydown.backspace="check_remove($event)" v-if="!overlay"></div> |
||||
<div :class="'toGo player'+j"></div> |
||||
</template> |
||||
<template v-if="visit.toGo !== undefined"> |
||||
<div :class="'points player'+j">{{ visit.sum }}</div> |
||||
<div :class="'toGo player'+j">{{ visit.toGo[j-1] }}</div> |
||||
</template> |
||||
<div class="rounds" v-if="j == 1">{{ (i+1)*3 }}</div> |
||||
</template> |
||||
</template> |
||||
</div> |
||||
</div> |
||||
` |
||||
} |
||||
|
||||
const overlay = { |
||||
props: ['data'], |
||||
setup(props, context) { |
||||
const keyhandler = (event) => { |
||||
ArrowHorizontalKeyHandler(event); |
||||
NumberKeyHandler(event); |
||||
if (event.key == "Escape") { |
||||
context.emit("closeOverlay") |
||||
} |
||||
} |
||||
return { keyhandler } |
||||
}, |
||||
template: html` |
||||
<div @keyup="keyhandler" class="overlay" v-if="data !== undefined" :class="{active: data.title != '' && data.text != ''}"> |
||||
<div class="box"> |
||||
<h1>{{ data.title }}</h1> |
||||
<p>{{ data.text }}</p> |
||||
<div class="buttons"> |
||||
<component v-for="(item, index) in data.buttons" :is="item.type" v-index="index+1" @click="item.onClick" :ref="(el) => { item.ref = el }" v-bind="item.props" v-autofocus="item.autofocus"></component> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
` |
||||
} |
||||
|
||||
const score = { |
||||
props: ['page', 'justlegs', 'currentset', 'currentleg'], |
||||
setup(props) { |
||||
return { } |
||||
}, |
||||
template: html` |
||||
<div class="score"> |
||||
<div v-if="!justlegs" class="sets"> |
||||
<h2 v-for="(idx) in [0,1]">{{ currentset?.points[idx] }}</h2> |
||||
<div class="info"> |
||||
<h3>Best of {{ page?.sets }}</h3> |
||||
<h2>Sets</h2> |
||||
</div> |
||||
</div> |
||||
<div class="legs"> |
||||
<h2 v-for="(idx) in [0,1]">{{ currentleg?.points[idx] }}</h2> |
||||
<div class="info"> |
||||
<h3>Best of {{ page?.legs }}</h3> |
||||
<h2>Legs</h2> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
` |
||||
} |
||||
|
||||
const player = { |
||||
props: ['player', 'stats', "id"], |
||||
setup(props) { |
||||
const current_set = computed( () => props.stats?.sets[props.stats.sets.length-1]); |
||||
const current_leg = computed( () => current_set.value?.legs[current_set.value.legs.length-1]); |
||||
const getAverage = (avg) => { |
||||
return avg && avg[1] != 0 ? ((3*avg[0])/avg[1]).toFixed(1) : "-"; |
||||
} |
||||
const getCheckout = (checkout) => { |
||||
return checkout && checkout[1] != 0 ? Math.round(1000*checkout[0]/checkout[1])/10 : "- " |
||||
} |
||||
const getMax = (checkouts) => { |
||||
if (checkouts && checkouts.length != 0) { |
||||
return Math.max(...checkouts); |
||||
} |
||||
return "-" |
||||
} |
||||
return { current_set, current_leg, getAverage, getCheckout, getMax } |
||||
}, |
||||
template: html` |
||||
<div class="player"> |
||||
<img style="width: 100%" :src="player?.img? player.img : '/assets/img/placeholder_person.png'"> |
||||
<h2 class="name">{{ player?.forename }} {{ player?.surname }}</h2> |
||||
<h3 class="nickname">{{ player?.nickname }}</h3> |
||||
<div class="stats"> |
||||
<div class="row header"> |
||||
<div>Stat</div><div>Match</div><div>Leg</div> |
||||
</div> |
||||
<div class="row"> |
||||
<div>Average:</div><div>{{ getAverage(stats?.stats[id].average) }}</div><div>{{ getAverage(current_leg?.stats[id].average) }}</div> |
||||
</div> |
||||
<div class="row"> |
||||
<div>First 9:</div><div>{{ getAverage(stats?.stats[id].first9) }}</div><div>{{ getAverage(current_leg?.stats[id].first9) }}</div> |
||||
</div> |
||||
<div class="row"> |
||||
<div>60+:</div><div>{{ stats?.stats[id]["60+"] }}</div><div>{{ current_leg?.stats[id]["60+"] }}</div> |
||||
</div> |
||||
<div class="row"> |
||||
<div>100+:</div><div>{{ stats?.stats[id]["100+"] }}</div><div>{{ current_leg?.stats[id]["100+"] }}</div> |
||||
</div> |
||||
<div class="row"> |
||||
<div>140+:</div><div>{{ stats?.stats[id]["140+"] }}</div><div>{{ current_leg?.stats[id]["140+"] }}</div> |
||||
</div> |
||||
<div class="row"> |
||||
<div>180:</div><div>{{ stats?.stats[id]["180"] }}</div><div>{{ current_leg?.stats[id]["180"] }}</div> |
||||
</div> |
||||
<div class="row"> |
||||
<div>Checkouts:</div><div>{{ getCheckout(stats?.stats[id].checkouts) }}%</div><div></div> |
||||
</div> |
||||
<div class="row"> |
||||
<div>Best Checkout:</div><div>{{ getMax(stats?.stats[id].checkoutPoints) }}</div><div></div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
` |
||||
} |
||||
|
||||
|
||||
export const xoi = { |
||||
components: { |
||||
"d-togo": togo, |
||||
"d-score": score, |
||||
"d-game": game, |
||||
"d-player": player, |
||||
"d-overlay": overlay |
||||
}, |
||||
setup(props, context) { |
||||
let page = ref(); |
||||
const updateGame = (reset) => { |
||||
getQuery(`site.find('${state.id}')`, { |
||||
select: { |
||||
title: "page.title", |
||||
id: "page.id", |
||||
modus: "page.max", |
||||
game: "page.rounds.parseJSON", |
||||
stats: "page.stats.parseJSON", |
||||
sets: "page.sets", |
||||
legs: "page.legs", |
||||
players: { |
||||
query: "page.players.toPages", |
||||
select:{ |
||||
forename: "page.forename", |
||||
surname: "page.surname", |
||||
nickname: "page.nickname", |
||||
uuid: "page.uuid", |
||||
img: "page.pic.toFile?.url" |
||||
} |
||||
} |
||||
} |
||||
}).then((res) => { |
||||
page.value = res; |
||||
if (reset != undefined) { |
||||
current_input.value = reset; |
||||
} |
||||
if (res.stats.winner){ |
||||
console.log(res.stats); |
||||
let winner = "Draw"; |
||||
if (res.players[0].uuid == res.stats.winner){ |
||||
winner = res.players[0].forename; |
||||
} else if (res.players[1].uuid == res.stats.winner) { |
||||
winner = res.players[1].forename; |
||||
} |
||||
overlay.value = { |
||||
"title": "Game Ended", |
||||
"text": `The winner is ${winner}`, |
||||
"buttons": [] |
||||
}; |
||||
} |
||||
}); |
||||
}; |
||||
updateGame(); |
||||
|
||||
let game = computed(() => page.value ? page.value.game: undefined) |
||||
let current_set = computed(() => { |
||||
if (game.value != undefined) { |
||||
return game.value.sets[game.value.sets.length-1] |
||||
} |
||||
return undefined; |
||||
}) |
||||
let current_leg = computed(() => { |
||||
if (current_set.value != undefined) { |
||||
return current_set.value.legs[current_set.value.legs.length-1] |
||||
} |
||||
return undefined; |
||||
}) |
||||
let current_toGo = computed(() => { |
||||
if (current_leg.value != undefined) { |
||||
if (current_leg.value.visits.length < 2) { |
||||
return [page.value.modus, page.value.modus] |
||||
} else { |
||||
|
||||
return current_leg.value.visits[current_leg.value.visits.length-2].toGo; |
||||
} |
||||
} |
||||
return [0,0]; |
||||
}) |
||||
let current_set_points = computed(() => { |
||||
if (current_set.value != undefined) { |
||||
return current_set.value.points; |
||||
} |
||||
return undefined; |
||||
}); |
||||
let current_leg_points = computed(() => { |
||||
if (current_leg.value != undefined) { |
||||
return current_leg.value.points; |
||||
} |
||||
return undefined; |
||||
}); |
||||
let current_player = computed(() => { |
||||
if (current_leg.value != undefined) { |
||||
const len = current_leg.value.visits.length; |
||||
return current_leg.value.visits[len-1].player == page.value.players[1].uuid; |
||||
} |
||||
return undefined; |
||||
}); |
||||
const current_input = ref(""); |
||||
const overlay = ref(); |
||||
return { page, state, game, current_set, current_leg, current_toGo, current_set_points, current_leg_points, current_player, updateGame, current_input, overlay } |
||||
}, |
||||
methods: { |
||||
async removeLastVisit(){ |
||||
let last = this.current_leg.visits.length-2 >= 0 ? this.current_leg.visits[this.current_leg.visits.length-2].throws : [""]; |
||||
last = last.join(","); |
||||
const response = await fetch(`/${state.id}`, { |
||||
method: "POST", |
||||
cache: "no-cache", |
||||
headers: { |
||||
"Content-Type": "application/json", |
||||
}, |
||||
body: JSON.stringify({ |
||||
"action": "deleteLastThrow", |
||||
"visit": true, |
||||
}), |
||||
}); |
||||
const ret = await response.json(); |
||||
if (ret.status == "ok"){ |
||||
this.updateGame(last); |
||||
} else { |
||||
console.log(ret); |
||||
} |
||||
}, |
||||
sum(tr){ |
||||
const val = tr.trim();; |
||||
if (val == "") { |
||||
return 0; |
||||
} |
||||
if (val == "SB"){ |
||||
return 25; |
||||
} |
||||
if (val == "DB"){ |
||||
return 50; |
||||
} |
||||
if (val[0] == "S" || val[0] == "O" || val[0] == "I"){ |
||||
return parseFloat(val.substring(1)); |
||||
} |
||||
if (val[0] == "D"){ |
||||
return 2*parseFloat(val.substring(1)); |
||||
} |
||||
if (val[0] == "T"){ |
||||
return 3*parseFloat(val.substring(1)); |
||||
} |
||||
if (val[0] == "M"){ |
||||
return 0; |
||||
} else { |
||||
// TODO: Check for Na
|
||||
return parseFloat(val); |
||||
} |
||||
}, |
||||
verify(sum){ |
||||
if (this.current_toGo[this.current_player*1]-sum == 0){ |
||||
return 0; |
||||
} else if (sum > 180 || [179, 178, 176, 175, 173, 172, 169, 166, 163].indexOf(sum) > -1) { |
||||
return -1 |
||||
} else if (this.current_toGo[this.current_player*1]-sum <= 50) { |
||||
return 1 |
||||
} |
||||
}, |
||||
openOverlay(overlay){ |
||||
this.overlay = overlay; |
||||
}, |
||||
closeOverlay(){ |
||||
this.overlay = undefined; |
||||
}, |
||||
checkout_question(throws){ |
||||
const buttons = []; |
||||
for (var i = 1; i <= 3; i++) { |
||||
const x = i; |
||||
buttons.push({ |
||||
"type": "input", |
||||
"props" : { |
||||
"type": "button", |
||||
"value": `${x} (${x})` |
||||
}, |
||||
"onClick": () => { |
||||
this.closeOverlay(); |
||||
this.checkouttries_question(throws, x, false); |
||||
} |
||||
}) |
||||
} |
||||
this.openOverlay({ |
||||
"title": "Congratulations!", |
||||
"text": `How many darts did you need?` , |
||||
"buttons": buttons |
||||
}); |
||||
}, |
||||
checkouttries_question(throws, numDarts, zero=true){ |
||||
const buttons = []; |
||||
for (var i = 1; i <= numDarts; i++) { |
||||
const x = i; |
||||
buttons.push({ |
||||
"type": "input", |
||||
"props" : { |
||||
"type": "button", |
||||
"value": `${x} (${x})` |
||||
}, |
||||
"autofocus": (i==1 && !zero), |
||||
"onClick": () => { |
||||
this.closeOverlay(); |
||||
this.send_visit(throws, numDarts, x) |
||||
} |
||||
}) |
||||
} |
||||
if (zero) { |
||||
buttons.push({ |
||||
"type": "input", |
||||
"props" : { |
||||
"type": "button", |
||||
"value": `0 (${numDarts+1})` |
||||
}, |
||||
"autofocus": true, |
||||
"onClick": () => { |
||||
this.closeOverlay(); |
||||
this.send_visit(throws, numDarts, 0) |
||||
} |
||||
}) |
||||
} |
||||
this.openOverlay({ |
||||
"title": "Checkout Tries", |
||||
"text": `How many tries on a Checkout?` , |
||||
"buttons": buttons |
||||
}); |
||||
}, |
||||
async send_visit(throws, numDarts=3, checkoutTries=0){ |
||||
const response = await fetch(`/${state.id}`, { |
||||
method: "POST", |
||||
cache: "no-cache", |
||||
headers: { |
||||
"Content-Type": "application/json", |
||||
}, |
||||
body: JSON.stringify({ |
||||
"action": "addThrows", |
||||
"throws": throws.value.split(","), |
||||
"checkoutTries": checkoutTries, |
||||
"numDarts": numDarts, |
||||
"done": true |
||||
}), |
||||
}); |
||||
const ret = await response.json(); |
||||
if (ret.status == "ok"){ |
||||
this.updateGame(""); |
||||
} else { |
||||
console.log(ret); |
||||
} |
||||
}, |
||||
async preprocess_visit(throws){ |
||||
const tr = throws.value.split(","); |
||||
let sum = 0; |
||||
tr.forEach((t, i) => { |
||||
sum += this.sum(t); |
||||
}); |
||||
const res = this.verify(sum); |
||||
if (res == 0){ |
||||
// Ask for num Darts
|
||||
this.checkout_question(throws); |
||||
return; |
||||
} else if (res == 1){ |
||||
// Ask for checkoutTries
|
||||
this.checkouttries_question(throws, 3); |
||||
return; |
||||
} else if (res == -1){ |
||||
this.openOverlay({ |
||||
"title": "Impossible", |
||||
"text": `A score of ${sum} is not possible`, |
||||
"buttons": [{ |
||||
"type": "input", |
||||
"props" : { |
||||
"type": "button", |
||||
"value": "ok" |
||||
}, |
||||
"onClick" : this.closeOverlay |
||||
}] |
||||
}); |
||||
return; |
||||
} |
||||
this.send_visit(throws); |
||||
} |
||||
}, |
||||
template: html` |
||||
<div class="xoi"> |
||||
<d-overlay @closeOverlay="closeOverlay" :data="overlay"></d-overlay> |
||||
<d-togo :togo="current_toGo[0]" class="one" :class="{'active' : current_player==0 }"></d-togo> |
||||
<d-togo :togo="current_toGo[1]" class="two" :class="{'active' : current_player==1 }"></d-togo> |
||||
<d-score :page="page" :justlegs="page && page.sets == 1" :currentset="current_set" :currentleg="current_leg"></d-score> |
||||
<d-game :overlay="overlay!=undefined" :players="page?.players" :max="page?.modus" @sendvisit="preprocess_visit" @removeLastVisit="removeLastVisit" :currentleg="current_leg" v-model="current_input"></d-game> |
||||
<d-player class="player1" :player="page?.players[0]" :stats="page?.stats" :id="0"></d-player> |
||||
<d-player class="player2" :player="page?.players[1]" :stats="page?.stats" :id="1"></d-player> |
||||
<div class="navi"></div> |
||||
</div> |
||||
` |
||||
} |
||||
|
||||
export const initXoiView = (app) => { |
||||
app.component('d-xoi', xoi) |
||||
} |
@ -0,0 +1,463 @@ |
||||
import { computed } from "vue"; |
||||
|
||||
function setPoints(page, set){ |
||||
let points = [0, 0]; // need to get updated for more player
|
||||
let ret; |
||||
const m = page.game.sets.length-1; |
||||
for (const i in page.game.sets) { |
||||
const set = page.game.sets[i]; |
||||
const points = legPoints(page, set, set.legs[set.legs.length-1]); |
||||
const winner = getWinner(points, page.legs); |
||||
if (winner > -1) |
||||
points[winner] += 1; |
||||
else |
||||
return points; |
||||
|
||||
if (set == set){ |
||||
return points; |
||||
} |
||||
} |
||||
return points; |
||||
} |
||||
|
||||
function legPoints(page, set, leg){ |
||||
let points = [0, 0]; // need to get updated for more player
|
||||
let ret; |
||||
const m = set.legs.length-1; |
||||
for (const i in set.legs) { |
||||
const l = set.legs[i]; |
||||
if (l.visits[l.visits.length-1].toGo === undefined) { |
||||
return points; |
||||
} |
||||
if (l.visits[l.visits.length-1].toGo[0] == 0) |
||||
points[0] += 1; |
||||
else if (l.visits[l.visits.length-1].toGo[1] == 0) |
||||
points[1] += 1; |
||||
if (l == leg){ |
||||
return points; |
||||
} |
||||
} |
||||
return points; |
||||
} |
||||
|
||||
export const getFinalWinner = (page) => { |
||||
const points = setPoints(page, page.sets[page.sets.length-1]); |
||||
return getWinner(points, page.sets); |
||||
}; |
||||
|
||||
export function getGameProps(page, current_set, current_leg) { |
||||
const ret = {}; |
||||
|
||||
if (current_set === undefined) { |
||||
current_set = computed(() => { |
||||
if (page.game != undefined) { |
||||
return page.game.sets[page.game.sets.length - 1] |
||||
} |
||||
return undefined; |
||||
}); |
||||
} |
||||
ret.current_set = current_set; |
||||
|
||||
|
||||
if (current_leg === undefined) { |
||||
current_leg = computed(() => { |
||||
|
||||
if (current_set.value != undefined) { |
||||
return current_set.value.legs[current_set.value.legs.length - 1] |
||||
} |
||||
return undefined; |
||||
}); |
||||
} |
||||
ret.current_leg = current_leg; |
||||
|
||||
const current_toGo = computed(() => { |
||||
if (current_leg.value != undefined) { |
||||
if (current_leg.value.visits.length < 2) { |
||||
return [page.modus, page.modus] |
||||
} else { |
||||
const end = current_leg.value.visits[current_leg.value.visits.length - 1].toGo; |
||||
if (end) return end; |
||||
return current_leg.value.visits[current_leg.value.visits.length - 2].toGo; |
||||
} |
||||
} |
||||
return [0, 0]; // need to get updated for more player
|
||||
}); |
||||
ret.current_toGo = current_toGo; |
||||
|
||||
|
||||
|
||||
const current_set_points = computed(() => { |
||||
if (current_set.value != undefined) { |
||||
return setPoints(page, current_set.value); |
||||
} |
||||
return undefined; |
||||
}); |
||||
ret.current_set_points = current_set_points; |
||||
|
||||
|
||||
const current_leg_points = computed(() => { |
||||
if (current_set.value != undefined || current_leg.value != undefined) { |
||||
return legPoints(page, current_set.value, current_leg.value); |
||||
} |
||||
return undefined; |
||||
}); |
||||
ret.current_leg_points = current_leg_points; |
||||
|
||||
|
||||
const current_player = computed(() => { |
||||
if (current_leg.value != undefined) { |
||||
const len = current_leg.value.visits.length; |
||||
return current_leg.value.visits[len - 1].player == page.players[1].uuid; |
||||
} |
||||
return undefined; |
||||
}); |
||||
ret.current_player = current_player; |
||||
|
||||
const getVal = (tr) => { |
||||
const val = tr.trim(); |
||||
if (val == "") { |
||||
return 0; |
||||
} |
||||
if (val == "SB") { |
||||
return 25; |
||||
} |
||||
if (val == "DB") { |
||||
return 50; |
||||
} |
||||
if (val[0] == "S" || val[0] == "O" || val[0] == "I") { |
||||
return parseFloat(val.substring(1)); |
||||
} |
||||
if (val[0] == "D") { |
||||
return 2 * parseFloat(val.substring(1)); |
||||
} |
||||
if (val[0] == "T") { |
||||
return 3 * parseFloat(val.substring(1)); |
||||
} |
||||
if (val[0] == "M") { |
||||
return 0; |
||||
} else { |
||||
// TODO: Check for Na
|
||||
return parseFloat(val); |
||||
} |
||||
} |
||||
const verifySum = (sum) => { |
||||
if (sum > 180 || [179, 178, 176, 175, 173, 172, 169, 166, 163].indexOf(sum) > -1) { |
||||
return -1; |
||||
} else if (current_toGo.value[current_player.value * 1] - sum < 0) { |
||||
return -2; |
||||
} if (current_toGo.value[current_player.value * 1] - sum == 0) { |
||||
// bogey numbers
|
||||
if (page.out == "Double" && (sum > 170 || [169, 168, 166, 165, 163, 162, 159].indexOf(sum) > -1)) { |
||||
return -1; |
||||
} |
||||
return 0; |
||||
} else if (current_toGo.value[current_player.value * 1] - sum <= 50) { |
||||
return 1; |
||||
} |
||||
return 2; |
||||
} |
||||
const checkVisit = (throws) => { |
||||
const tr = throws.split(","); |
||||
let sum = 0; |
||||
tr.forEach((t, i) => { |
||||
sum += getVal(t); |
||||
}); |
||||
const res = verifySum(sum); |
||||
return [res, sum]; |
||||
} |
||||
ret.checkVisit = checkVisit; |
||||
|
||||
return ret; |
||||
} |
||||
|
||||
function newPlayerStats(player) { |
||||
return { |
||||
player: player.uuid, |
||||
average: [0, 0], |
||||
first9: [0, 0], |
||||
checkouts: [0, 0], |
||||
checkoutPoints: [], |
||||
"60+": 0, |
||||
"100+": 0, |
||||
"140+": 0, |
||||
"180": 0 |
||||
}; |
||||
} |
||||
|
||||
function addNewSetStats(stats, players) { |
||||
stats["sets"].push(newStats(players)); |
||||
stats["sets"][0]["legs"] = []; |
||||
return stats; |
||||
} |
||||
|
||||
function addNewLegStats(stats, players) { |
||||
stats["sets"].at(-1)["legs"].push(newStats(players)); |
||||
return stats; |
||||
} |
||||
|
||||
function newStats(players) { |
||||
const stats = { |
||||
stats: [] |
||||
} |
||||
players.forEach((player, i) => { |
||||
const n = newPlayerStats(player); |
||||
stats.stats.push(n); |
||||
}); |
||||
return stats; |
||||
} |
||||
|
||||
export function initStats(page) { |
||||
const stats = newStats(page.players); |
||||
stats.sets = []; |
||||
addNewSetStats(stats, page.players); |
||||
addNewLegStats(stats, page.players); |
||||
page.stats = stats; |
||||
} |
||||
|
||||
function newVisit(playerUUID, round) { |
||||
return { |
||||
player: playerUUID, |
||||
throws: [], |
||||
visit: round, |
||||
checkoutTries: 0, |
||||
numDarts: 0, |
||||
}; |
||||
} |
||||
|
||||
export function initGame(page) { |
||||
page.game = { |
||||
sets: [{ |
||||
points: new Array(page.players.length).fill(0), |
||||
legs: [{ |
||||
points: new Array(page.players.length).fill(0), |
||||
visits: [newVisit(page.players[0].uuid, 1)] |
||||
}] |
||||
}] |
||||
} |
||||
} |
||||
|
||||
function updateStats(page, visit){ |
||||
const playerUUIDs = page.players.map((p) => p.uuid); |
||||
const k = playerUUIDs.indexOf(visit.player); |
||||
|
||||
const todos = [page.stats["stats"][k]]; |
||||
todos.push(page.stats["sets"].at(-1)["stats"][k]); |
||||
todos.push(page.stats["sets"].at(-1)["legs"].at(-1)["stats"][k]); |
||||
|
||||
todos.forEach((value, i) => { |
||||
todos[i]["average"][0] += visit["sum"]; |
||||
todos[i]["average"][1] += visit["numDarts"]; |
||||
if (visit["visit"] < 4) { |
||||
todos[i]["first9"][0] += visit["sum"]; |
||||
todos[i]["first9"][1] += visit["numDarts"]; |
||||
} |
||||
if (visit["toGo"][k] == 0) { |
||||
todos[i]["checkouts"][0] += 1; |
||||
todos[i]["checkoutPoints"].push(visit["sum"]); |
||||
} |
||||
todos[i]["checkouts"][1] += visit["checkoutTries"]; |
||||
if (visit["sum"] == 180) { |
||||
todos[i]["180"] += 1; |
||||
} else if (visit["sum"] >= 140) { |
||||
todos[i]["140+"] += 1; |
||||
} else if (visit["sum"] >= 100) { |
||||
todos[i]["100+"] += 1; |
||||
} else if (visit["sum"] >= 60) { |
||||
todos[i]["60+"] += 1; |
||||
} |
||||
}); |
||||
} |
||||
|
||||
function getWinner(points, mode) { |
||||
const sorted = [...points].map((e,i) => [e,i]).sort((a, b) => b[0] - a[0]); |
||||
const k = points.length; |
||||
const sum = points.reduce((a,c) => a+c, 0); |
||||
if (sum < mode && sorted[0][0] <= mode/k){ |
||||
// not over yet
|
||||
return -1; |
||||
} |
||||
if (sorted[0][0] == sorted[1][0]){ |
||||
// Draw
|
||||
return -2; |
||||
} |
||||
// winner id
|
||||
return sorted[0][1]; |
||||
} |
||||
|
||||
export const formatDate = (d) => { |
||||
return `${d.getFullYear()}-${(d.getMonth()+1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')} ${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`; |
||||
}; |
||||
|
||||
function addNewSet(page, points){ |
||||
const playerUUIDs = page.players.map((p) => p.uuid); |
||||
const set = page.game.sets[page.game.sets.length-1]; |
||||
const leg = set.legs[set.legs.length-1]; |
||||
const k = playerUUIDs.indexOf(set.legs[0].visits[0].player); |
||||
const p = playerUUIDs[(k+1)%playerUUIDs.length]; |
||||
page.game.sets.push({ |
||||
// points: [...points],
|
||||
legs: [{ |
||||
points: Array(playerUUIDs.length).fill(0), |
||||
visits: [newVisit(p, 1)] |
||||
}] |
||||
}); |
||||
} |
||||
|
||||
function addNewLeg(page){ |
||||
const playerUUIDs = page.players.map((p) => p.uuid); |
||||
const set = page.game.sets[page.game.sets.length-1]; |
||||
const leg = set.legs[set.legs.length-1]; |
||||
const k = playerUUIDs.indexOf(leg.visits[0].player); |
||||
const p = playerUUIDs[(k+1)%playerUUIDs.length]; |
||||
page.game.sets[page.game.sets.length-1].legs.push({ |
||||
// points: [...points],
|
||||
visits: [newVisit(p, 1)] |
||||
}); |
||||
} |
||||
|
||||
function clearLastVisit(page){ |
||||
const set = page.game.sets[page.game.sets.length-1]; |
||||
const leg = set.legs[set.legs.length-1]; |
||||
const visit = leg.visits[leg.visits.length-1]; |
||||
const ret = visit["throws"]; |
||||
visit["throws"] = []; |
||||
visit["checkoutTries"] = 0; |
||||
visit["numDarts"] = 0; |
||||
delete visit["sum"]; |
||||
delete visit["toGo"]; |
||||
return ret; |
||||
} |
||||
|
||||
export function recalcStats(page){ |
||||
const set = page.game.sets[page.game.sets.length-1]; |
||||
// const leg = set.legs[set.legs.length-1];
|
||||
// const visit = leg.visits[leg.visits.length-1];
|
||||
initStats(page); |
||||
const lastset = page.game["sets"].length-1; |
||||
page.game["sets"].forEach((set, i) => { |
||||
const lastleg = set.legs.length-1; |
||||
set.legs.forEach((leg, j) => { |
||||
const lastvisit = leg["visits"].length-1; |
||||
leg.visits.forEach((visit, k) => { |
||||
if (!(k == lastvisit && j == lastleg && i == lastset)) { |
||||
updateStats(page, visit); |
||||
} |
||||
if (k == lastvisit && j != lastleg) { |
||||
addNewLegStats(page.stats, page.players); |
||||
} |
||||
}); |
||||
if (j == lastleg && i != lastset) { |
||||
addNewSetStats(page.stats, page.players); |
||||
} |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
export function removeLastVisit(page){ |
||||
const set = page.game.sets[page.game.sets.length-1]; |
||||
const leg = set.legs[set.legs.length-1]; |
||||
const visit = leg.visits[leg.visits.length-1]; |
||||
|
||||
// delete last visit.
|
||||
leg["visits"].pop(); |
||||
if (leg["visits"].length == 0){ |
||||
set["legs"].pop(); |
||||
if (set["legs"].length == 0){ |
||||
page.game["sets"].pop(); |
||||
if (page.game["sets"].length == 0){ |
||||
initGame(page); |
||||
return; |
||||
} |
||||
} |
||||
} |
||||
// clearLastVisit
|
||||
const ret = clearLastVisit(page); |
||||
recalcStats(page); |
||||
return ret; |
||||
} |
||||
|
||||
export function storeVisit(page, throws, sum, numDarts, tries) { |
||||
const set = page.game.sets[page.game.sets.length-1]; |
||||
const leg = set.legs[set.legs.length-1]; |
||||
const visit = leg.visits[leg.visits.length-1]; |
||||
|
||||
visit["numDarts"] = numDarts; |
||||
visit["throws"] = throws; |
||||
visit["checkoutTries"] = tries; |
||||
visit["sum"] = sum |
||||
|
||||
const playerUUIDs = page.players.map((p) => p.uuid); |
||||
const k = playerUUIDs.indexOf(visit["player"]); |
||||
let toGo; |
||||
if (leg["visits"].length-2 < 0) { |
||||
toGo = Array(playerUUIDs.length).fill(page.modus); |
||||
} else { |
||||
toGo = [...leg["visits"][leg["visits"].length-2]["toGo"]]; |
||||
} |
||||
const rest = toGo[k] - visit["sum"]; |
||||
if (rest < 0 || (page.out == "Double" && rest == 1)) { |
||||
visit["sum"] = 0; |
||||
} else { |
||||
toGo[k] = rest; |
||||
} |
||||
visit["toGo"] = toGo; |
||||
// update stats
|
||||
updateStats(page, visit); |
||||
|
||||
let update = []; |
||||
if (rest != 0) { |
||||
// Normal case...next players turn
|
||||
const nextPlayer = playerUUIDs[(k+1)%playerUUIDs.length]; |
||||
const nVisit = newVisit(nextPlayer, visit["visit"]+1*(leg.visits[0]["player"] == nextPlayer)); |
||||
leg.visits.push(nVisit); |
||||
} |
||||
else { |
||||
// rest == 0 leg finished
|
||||
const newlegp = legPoints(page, set, leg); |
||||
let winner = getWinner(newlegp, page.legs); |
||||
if (winner == -1) { |
||||
// new Leg
|
||||
addNewLeg(page); |
||||
addNewLegStats(page.stats, page.players); |
||||
} else { |
||||
// new set?
|
||||
const newsetp = setPoints(page, set); |
||||
winner = getWinner(newsetp, page.sets); |
||||
if (winner == -1) { |
||||
// new Set
|
||||
addNewSet(page); |
||||
addNewSetStats(page.stats, page.players); |
||||
addNewLegStats(page.stats, page.players); |
||||
} else { |
||||
// ask for continue or game over
|
||||
if (winner == -2) { |
||||
page.stats["winner"] = "DRAW"; |
||||
} else { |
||||
page.stats["winner"] = visit["player"]; |
||||
} |
||||
|
||||
page.enddate = formatDate(new Date(Date.now())); |
||||
return [newsetp, newlegp]; |
||||
} |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
export function endGame(page){ |
||||
// const set = page.game.sets[page.game.sets.length-1];
|
||||
// const leg = set.legs[set.legs.length-1];
|
||||
// const visit = leg.visits[leg.visits.length-1];
|
||||
// const winner = getWinner(newsetp, page.sets);
|
||||
// set["points"] = newsetp;
|
||||
// leg["points"] = newlegp;
|
||||
} |
||||
|
||||
export function extension(page){ |
||||
const set = page.game.sets[page.game.sets.length-1]; |
||||
const leg = set.legs[set.legs.length-1]; |
||||
// leg["points"] = newlegp;
|
||||
// new Leg
|
||||
addNewLeg(page); |
||||
addNewLegStats(page.stats, page.players); |
||||
} |
@ -0,0 +1,698 @@ |
||||
import { nextTick, watch, reactive, ref, computed, onMounted, onUnmounted } from "vue"; |
||||
import { getQuery, setKirby } from "../../kirby.js"; |
||||
import { state } from "../../stateMgr.js"; |
||||
import { handleActive, ArrowVerticalKeyHandler, ArrowHorizontalKeyHandler, NumberKeyHandler } from "../../handlers.js"; |
||||
import { overlayAndGet, powerStateMachine, overlayAndPop, popLastElem } from "../../componentPromise.js"; |
||||
|
||||
import { getGameProps, initGame, initStats, storeVisit, formatDate, removeLastVisit, extension, getFinalWinner } from "./logic.js"; |
||||
|
||||
const html = (v) => { return v[0] }; |
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Components
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const pregame = { |
||||
props: ['page','active', 'stack'], |
||||
setup(props, context) { |
||||
handleActive(props, [ArrowVerticalKeyHandler]); |
||||
|
||||
const selectPlayer = async (i) => { |
||||
if (props.active) { |
||||
const [result, error] = await overlayAndPop("d-playerselect", { players: props.page.participants, class:"overlay"}, props.stack); |
||||
if (error === undefined) { |
||||
props.page.players[i] = result; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return { selectPlayer } |
||||
}, |
||||
template: html` |
||||
<nav class="gamesetup" :disabled="!active"> |
||||
<h1>Game Setup</h1> |
||||
<div class="menu"> |
||||
<!-- <div><span class="label">Name:</span><d-inputElem v-index="active" v-model="game.title"></d-inputElem></div> --> |
||||
<div><span class="label">Player 1:</span><d-playerElem v-index="active" @click="selectPlayer(0)" :player="page.players[0]"></d-playerElem></div> |
||||
<div><span class="label">Player 2:</span><d-playerElem v-index="active" @click="selectPlayer(1)" :player="page.players[1]"></d-playerElem></div> |
||||
<div><span class="label">Best of Sets:</span><d-inputElem v-index="active" v-model="page.sets"></d-inputElem></div> |
||||
<div><span class="label">Best of Legs (per Set):</span><d-inputElem v-index="active" v-model="page.legs"></d-inputElem></div> |
||||
<div><d-plainElem text="Start" v-index="active" @click="$emit('resolve', page)"></d-plainElem></div> |
||||
</div> |
||||
</nav> |
||||
` |
||||
} |
||||
|
||||
export const bullselect = { |
||||
props: ['players','active', 'stack'], |
||||
setup(props, { emit }) { |
||||
handleActive(props, [ArrowHorizontalKeyHandler, NumberKeyHandler]); |
||||
|
||||
const children = computed(() => { |
||||
const items = []; |
||||
for (let i in props.players){ |
||||
const player = props.players[i]; |
||||
items.push({ |
||||
component: "d-squareElem", |
||||
props: { |
||||
text: player.forename + " " + player.surname, |
||||
icon: player.img ? player.img : '/assets/img/placeholder_person.png' |
||||
}, |
||||
onClick: () => { |
||||
emit("resolve", player); |
||||
} |
||||
}) |
||||
} |
||||
return items; |
||||
}); |
||||
|
||||
return { children } |
||||
}, |
||||
template: html` |
||||
<div class="gamesetup"> |
||||
<h1 class="bull">Who won bull?</h1> |
||||
<d-list :withshortkey="true" :elements="children" type="horizontal" /> |
||||
</div> |
||||
` |
||||
} |
||||
|
||||
|
||||
|
||||
const score = { |
||||
props: ['page', 'justlegs', 'current_set_points', 'current_leg_points'], |
||||
setup(props) { |
||||
return { } |
||||
}, |
||||
template: html` |
||||
<div class="score"> |
||||
<div v-if="!justlegs" class="sets"> |
||||
<h2 v-for="(idx) in [0,1]">{{ current_set_points[idx] }}</h2> |
||||
<div class="info"> |
||||
<h3>Best of {{ page.sets }}</h3> |
||||
<h2>Sets</h2> |
||||
</div> |
||||
</div> |
||||
<div class="legs"> |
||||
<h2 v-for="(idx) in [0,1]">{{ current_leg_points[idx] }}</h2> |
||||
<div class="info"> |
||||
<h3>Best of {{ page.legs }}</h3> |
||||
<h2>Legs</h2> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
` |
||||
} |
||||
|
||||
const player = { |
||||
props: ['page', 'id', 'current_stat'], |
||||
setup(props) { |
||||
const addStats = (stat1, stat2) => { |
||||
return { |
||||
average: [stat1.average[0]+stat2.average[0], stat1.average[1]+stat2.average[1]], |
||||
first9: [stat1.first9[0]+stat2.first9[0], stat1.first9[1]+stat2.first9[1]], |
||||
"60+": stat1["60+"]+stat2["60+"], |
||||
"100+": stat1["100+"]+stat2["100+"], |
||||
"140+": stat1["140+"]+stat2["140+"], |
||||
"180": stat1["180"]+stat2["180"], |
||||
"checkouts": [stat1.checkouts[0]+stat2.checkouts[0], stat1.checkouts[1]+stat2.checkouts[1]], |
||||
"checkoutPoints": [...stat1.checkoutPoints, ...stat2.checkoutPoints] |
||||
} |
||||
} |
||||
const inspect = !!props.page.enddate; |
||||
const tourStats = computed(() => { |
||||
if (props.page.tournamentStats.length != 2){ |
||||
return [ |
||||
props.page.stats.stats[0], |
||||
props.page.stats.stats[1] |
||||
]; |
||||
} |
||||
if (inspect) { |
||||
return [ |
||||
props.page.tournamentStats[0], |
||||
props.page.tournamentStats[1] |
||||
]; |
||||
} |
||||
return [ |
||||
addStats(props.page.tournamentStats[0], props.page.stats.stats[0]), |
||||
addStats(props.page.tournamentStats[1], props.page.stats.stats[1]) |
||||
]; |
||||
}); |
||||
const getAverage = (avg) => { |
||||
return avg && avg[1] != 0 ? ((3*avg[0])/avg[1]).toFixed(1) : "-"; |
||||
} |
||||
const getCheckout = (checkout) => { |
||||
return checkout && checkout[1] != 0 ? Math.round(1000*checkout[0]/checkout[1])/10 : "- " |
||||
} |
||||
const getMax = (checkouts) => { |
||||
if (checkouts && checkouts.length != 0) { |
||||
return Math.max(...checkouts); |
||||
} |
||||
return "-" |
||||
} |
||||
const player = computed(() => props.page.players[props.id]) |
||||
return { tourStats, getAverage, getCheckout, getMax, player } |
||||
}, |
||||
template: html` |
||||
<div class="player"> |
||||
<img style="width: 100%" :src="player.img ? player.img : '/assets/img/placeholder_person.png'"> |
||||
<h2 class="name">{{ player.forename }} {{ player.surname }}</h2> |
||||
<h3 class="nickname">{{ player.nickname }}</h3> |
||||
<div class="stats"> |
||||
<div class="row header"> |
||||
<div>Stat</div><div>Tnm</div><div>Match</div><div>Leg</div> |
||||
</div> |
||||
<div class="row"> |
||||
<div>Avg:</div><div> |
||||
{{ getAverage(tourStats[id].average) }} |
||||
</div><div> |
||||
{{ getAverage(page.stats.stats[id].average) }} |
||||
</div><div> |
||||
{{ getAverage(current_stat.stats[id].average) }} |
||||
</div> |
||||
</div> |
||||
<div class="row"> |
||||
<div>First 9:</div><div> |
||||
{{ getAverage(tourStats[id].first9) }} |
||||
</div><div> |
||||
{{ getAverage(page.stats.stats[id].first9) }} |
||||
</div><div> |
||||
{{ getAverage(current_stat.stats[id].first9) }} |
||||
</div> |
||||
</div> |
||||
<div class="row"> |
||||
<div>60+:</div><div>{{ tourStats[id]["60+"] }}</div><div>{{ page.stats.stats[id]["60+"] }}</div><div>{{ current_stat.stats[id]["60+"] }}</div> |
||||
</div> |
||||
<div class="row"> |
||||
<div>100+:</div><div>{{ tourStats[id]["100+"] }}</div><div>{{ page.stats.stats[id]["100+"] }}</div><div>{{ current_stat.stats[id]["100+"] }}</div> |
||||
</div> |
||||
<div class="row"> |
||||
<div>140+:</div><div>{{ tourStats[id]["140+"] }}</div><div>{{ page.stats.stats[id]["140+"] }}</div><div>{{ current_stat.stats[id]["140+"] }}</div> |
||||
</div> |
||||
<div class="row"> |
||||
<div>180:</div><div>{{ tourStats[id]["180"] }}</div><div>{{ page.stats.stats[id]["180"] }}</div><div>{{ current_stat.stats[id]["180"] }}</div> |
||||
</div> |
||||
<div class="row"> |
||||
<div>Ch. %:</div><div>{{ getCheckout(tourStats[id].checkouts) }}%</div><div>{{ getCheckout(page.stats.stats[id].checkouts) }}%</div><div></div> |
||||
</div> |
||||
<div class="row"> |
||||
<div>Best Ch.:</div><div>{{ getMax(tourStats[id].checkoutPoints) }}</div><div>{{ getMax(page.stats.stats[id].checkoutPoints) }}</div><div></div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
` |
||||
} |
||||
|
||||
|
||||
const game = { |
||||
props: ['active', 'stack', 'players','current_leg', 'max'], |
||||
setup(props, context) { |
||||
const visits = computed(() => props.current_leg ? props.current_leg.visits:undefined ); |
||||
const getPlayerVisits = (uuid) => { |
||||
const vs = visits.value.filter((v) => v.player == uuid); |
||||
if (vs.length < 9) { |
||||
for (var i = vs.length; i < 9; i++) { |
||||
vs.push({ "sum": "", "toGo":["",""]}) |
||||
} |
||||
} else if (vs.length*2 < visits.value.length) { |
||||
|
||||
vs.push({ "sum": "", "toGo":["",""]}); |
||||
} |
||||
return vs; |
||||
} |
||||
return { getPlayerVisits } |
||||
}, |
||||
template: html` |
||||
<div class="game"> |
||||
<div class="headding"> |
||||
<div class="headding points player1">Points</div> |
||||
<div class="headding toGo player1">ToGo</div> |
||||
<div class="headding rounds">Round</div> |
||||
<div class="headding points player2">Points</div> |
||||
<div class="headding toGo player2">ToGo</div> |
||||
</div> |
||||
<div class="body"> |
||||
<div class="points player1"></div> |
||||
<div class="toGo player1">{{ max }}</div> |
||||
<div class="rounds">0</div> |
||||
<div class="points player2"></div> |
||||
<div class="toGo player2">{{ max }}</div> |
||||
<template v-for="j in [1,2]" v-if="players"> |
||||
<template v-for="visit, i in getPlayerVisits(players[j-1].uuid)"> |
||||
<template v-if="visit.toGo === undefined"> |
||||
<div :class="'points player'+j+' input'"> |
||||
<slot /> |
||||
</div> |
||||
<div :class="'toGo player'+j"></div> |
||||
</template> |
||||
<template v-if="visit.toGo !== undefined"> |
||||
<div :class="'points player'+j">{{ visit.sum }}</div> |
||||
<div :class="'toGo player'+j">{{ visit.toGo[j-1] }}</div> |
||||
</template> |
||||
<div class="rounds" v-if="j == 1">{{ (i+1)*3 }}</div> |
||||
</template> |
||||
</template> |
||||
</div> |
||||
</div> |
||||
` |
||||
} |
||||
|
||||
const gameinput = { |
||||
props: ['input','active', 'stack'], |
||||
setup(props, context) { |
||||
handleActive(props, []); |
||||
const check_remove = (event) => { |
||||
if (!event.repeat && event.target.value.length < 1) { |
||||
event.preventDefault(); |
||||
context.emit('reject', -2); |
||||
} |
||||
} |
||||
const keyhandler = (e) => { |
||||
if (e.key == "F1" || e.keyCode == 112) { |
||||
e.preventDefault(); |
||||
context.emit('resolve', "60"); |
||||
} else if (e.key == "F2" || e.keyCode == 113) { |
||||
e.preventDefault(); |
||||
context.emit('resolve', "45"); |
||||
} else if (e.key == "F3" || e.keyCode == 114) { |
||||
e.preventDefault(); |
||||
context.emit('resolve', "41"); |
||||
} else if (e.key == "F4" || e.keyCode == 115) { |
||||
e.preventDefault(); |
||||
context.emit('resolve', "26"); |
||||
} |
||||
|
||||
} |
||||
return { check_remove, keyhandler } |
||||
}, |
||||
template: html` |
||||
<input v-index="active" :value="input" @keydown="keyhandler" @keydown.enter.stop="$emit('resolve', $event.target.value)" @keydown.backspace="check_remove($event)"> |
||||
` |
||||
} |
||||
|
||||
|
||||
const xoi = { |
||||
props: ['page', 'active', 'stack', 'inspect'], |
||||
components: { |
||||
"d-player": player, |
||||
"d-score": score, |
||||
"d-game": game |
||||
}, |
||||
setup(props, context) { |
||||
const gamestack = reactive([]); |
||||
const inspectstack = reactive([]); |
||||
let current_set, current_leg; |
||||
const set_id = ref(props.page.game.sets.length - 1); |
||||
const leg_id = ref(props.page.game.sets[set_id.value].legs.length - 1); |
||||
let current_stat; |
||||
if (props.inspect) { |
||||
current_set = computed(() => props.page.game.sets[set_id.value]); |
||||
current_leg = computed(() => current_set.value.legs[leg_id.value]); |
||||
current_stat = computed( () => props.page.stats?.sets[set_id.value].legs[leg_id.value]); |
||||
} |
||||
const computedProps = getGameProps(props.page, current_set, current_leg); |
||||
if (!props.inspect) { |
||||
current_stat = computed( () => props.page.stats?.sets[props.page.stats.sets.length-1].legs[computedProps.current_set.value.legs.length-1]); |
||||
} |
||||
|
||||
const mounted = onMounted(async () => { |
||||
if (props.inspect) { |
||||
} else { |
||||
const winner = await gameHandler(gamestack, props.stack, props.page, computedProps) |
||||
context.emit('resolve', winner); |
||||
} |
||||
}) |
||||
return { ...computedProps, set_id, leg_id, gamestack, inspectstack, current_stat } |
||||
}, |
||||
template: html` |
||||
<div class="xoi"> |
||||
<div class="bigToGo one" :class="{'active' : current_player==0 }">{{ current_toGo[0] }}</div> |
||||
<d-score :page="page" :justlegs="page && page.sets == 1" :current_set_points="current_set_points" :current_leg_points="current_leg_points"></d-score> |
||||
<div class="bigToGo two" :class="{'active' : current_player==1 }">{{ current_toGo[1] }}</div> |
||||
<d-player class="player1" :page="page" :id="0" :current_stat="current_stat"></d-player> |
||||
<d-game :active="active" :stack="stack" :players="page.players" :max="page.modus" :current_leg="current_leg" > |
||||
<d-renderer :stack="gamestack"></d-renderer> |
||||
</d-game> |
||||
<d-player class="player2" :page="page" :id="1" :current_stat="current_stat"></d-player> |
||||
<div class="nav" v-if="inspect"> |
||||
<span class="label" v-if="page.sets != 1">Set:</span><d-plainElem :text="set_id+1" v-if="page.sets != 1" v-index="active" @click="set_id = (set_id+1)%page.game.sets.length; leg_id=0"></d-plainElem><span class="label">Leg:</span><d-plainElem style="padding:0.2em 0.5em" :text="leg_id+1" v-index="active" v-autofocus="true" @click="leg_id = (leg_id+1)%current_set.legs.length"></d-plainElem> |
||||
</div> |
||||
<div class="nav" v-if="!!!inspect"> |
||||
<span>Shortkeys:</span><span>60 (F1)</span><span>45 (F2)</span><span>41 (F3)</span><span>26 (F4)</span> |
||||
</div> |
||||
</div> |
||||
` |
||||
} |
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Dialogs
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const numDartsDialog = () => { |
||||
const buttons = []; |
||||
for (var i = 1; i <= 3; i++) { |
||||
buttons.push({ |
||||
"component": "d-plainElem", |
||||
"props" : { |
||||
"text": `${i}` |
||||
}, |
||||
"result" : i |
||||
}) |
||||
} |
||||
return { |
||||
"withshortkey": true, |
||||
"title": "Congratulations!", |
||||
"text": `How many darts did you need?`, |
||||
"buttons": buttons |
||||
}; |
||||
} |
||||
|
||||
const numCheckoutTriesDialog = (numDarts, start=1) => { |
||||
const buttons = []; |
||||
for (var i = start; i <= numDarts; i++) { |
||||
buttons.push({ |
||||
"component": "d-plainElem", |
||||
"props" : { |
||||
"text": `${i}`, |
||||
"autofocus": i == 0, |
||||
"data-tabindex": i |
||||
}, |
||||
"result" : i |
||||
}) |
||||
} |
||||
return { |
||||
"withshortkey": true, |
||||
"title": "Checkout Tries", |
||||
"text": `How many tries on a Checkout?` , |
||||
"buttons": buttons |
||||
}; |
||||
} |
||||
|
||||
const impossibleDialog = (sum) => { |
||||
return { |
||||
"withshortkey": true, |
||||
"title": "Impossible", |
||||
"text": `A score of ${sum} is not possible`, |
||||
"buttons": [{ |
||||
"component": "d-plainElem", |
||||
"props" : { |
||||
"text": "ok" |
||||
}, |
||||
"result" : "ok" |
||||
}]} |
||||
} |
||||
|
||||
const gameOverDialog = (winner, points) => { |
||||
return { |
||||
"withshortkey": true, |
||||
"title": `Game Over`, |
||||
"text": `${winner != "DRAW" ? "The winner is": ""} ${winner} with ${points[0]}-${points[1]}`, |
||||
"buttons": [ |
||||
{ |
||||
"component": "d-plainElem", |
||||
"props" : { |
||||
"text": "End Game" |
||||
}, |
||||
"result" : "end" |
||||
},{ |
||||
"component": "d-plainElem", |
||||
"props" : { |
||||
"text": "Continue" |
||||
}, |
||||
"result" : "continue" |
||||
}] |
||||
} |
||||
} |
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// API
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function getGame(id){ |
||||
return getQuery(`site.find('${id}')`, { |
||||
select: { |
||||
title: "page.title", |
||||
id: "page.id", |
||||
modus: "page.max.toInt", |
||||
out: "page.out", |
||||
game: "page.rounds.parseJSON", |
||||
stats: "page.stats.parseJSON", |
||||
sets: "page.sets.toInt", |
||||
legs: "page.legs.toInt", |
||||
startdate: "page.Startdate", |
||||
enddate: "page.Enddate", |
||||
tournamentStats: "page.tournamentStats", |
||||
participants: { |
||||
query: "page.parent.participants.toPages.sortBy('forename')", |
||||
select:{ |
||||
forename: "page.forename", |
||||
surname: "page.surname", |
||||
nickname: "page.nickname", |
||||
uuid: "page.uuid", |
||||
img: "page.pic.toFile?.url" |
||||
} |
||||
}, |
||||
players: { |
||||
query: "page.players.toPages", |
||||
select:{ |
||||
forename: "page.forename", |
||||
surname: "page.surname", |
||||
nickname: "page.nickname", |
||||
uuid: "page.uuid", |
||||
img: "page.pic.toFile?.thumbnail(350).url" |
||||
} |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
|
||||
function savePregame(page){ |
||||
return setKirby(page.id, { |
||||
sets: page.sets, |
||||
legs: page.legs, |
||||
players: page.players.map((p) => p.uuid), |
||||
startdate: page.startdate, |
||||
rounds: page.game ? JSON.stringify(page.game) : "", |
||||
stats: page.stats ? JSON.stringify(page.stats) : "" |
||||
}); |
||||
} |
||||
|
||||
function saveGame(page){ |
||||
return setKirby(page.id, { |
||||
rounds: page.game ? JSON.stringify(page.game) : "", |
||||
stats: page.stats ? JSON.stringify(page.stats) : "", |
||||
enddate: page.enddate, |
||||
}); |
||||
} |
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// State Machine
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Checkout Pipeline
|
||||
const checkoutPipeline = (stack) => { |
||||
return { |
||||
0: async (sum, reGet) => { |
||||
// Ask for num Darts
|
||||
const [numDarts, error] = await overlayAndGet("d-dialog", numDartsDialog(), stack, reGet); |
||||
if (error != undefined) { |
||||
popLastElem(stack); |
||||
return [undefined, [-1, -1]]; |
||||
} |
||||
return [1, [numDarts, 1]]; |
||||
}, |
||||
1: async ([numDarts, start], reGet) => { |
||||
// Ask for checkoutTries
|
||||
if (numDarts <= start) { |
||||
// Store with one checkout try
|
||||
popLastElem(stack); |
||||
return [undefined, [1, 1]]; |
||||
} |
||||
const [tries, error] = await overlayAndPop("d-dialog", numCheckoutTriesDialog(numDarts, start), stack); |
||||
if (error != undefined) { |
||||
if (start == 0) { |
||||
return [undefined, [-1, -1]]; |
||||
} |
||||
return [-1, undefined]; |
||||
} |
||||
if (start == 1) { |
||||
popLastElem(stack); |
||||
} |
||||
return [undefined, [numDarts, tries]]; |
||||
} |
||||
}; |
||||
}; |
||||
|
||||
|
||||
// Game State Machine
|
||||
const gameStateMachine = (gamestack, stack, page, computedProps) => { |
||||
return { |
||||
0: async (input, reGet) => { |
||||
// Dispatcher
|
||||
if (page.stats.winner){ |
||||
return [undefined, page.stats.winner]; |
||||
} |
||||
// Check for Game State
|
||||
return [1, undefined]; |
||||
}, |
||||
1: async (input, reGet) => { |
||||
// Normal Game Loop
|
||||
// Get Game Input
|
||||
const [visit, error] = await overlayAndPop("d-gameinput", { input: input }, gamestack); |
||||
// back/delete last throw
|
||||
if (error != undefined) { |
||||
const val = removeLastVisit(page); |
||||
saveGame(page); |
||||
if (val == undefined) return [1, val]; |
||||
return [1, val.join(",")]; |
||||
} |
||||
|
||||
// Validate throw
|
||||
let numDarts, tries; |
||||
const [ret, sum] = computedProps.checkVisit(visit); |
||||
if (ret == -1) { |
||||
// Impossible
|
||||
const [_, error] = await overlayAndPop("d-dialog", impossibleDialog(sum), stack); |
||||
return [1, visit]; |
||||
} else if (ret == -2) { |
||||
// Bust TODO
|
||||
const [_, error] = await overlayAndPop("d-dialog", impossibleDialog(sum), stack); |
||||
return [1, visit]; |
||||
} else if (ret == 2) { |
||||
// Normal
|
||||
storeVisit(page, visit.split(","), sum, 3, 0); |
||||
saveGame(page); |
||||
return [1, undefined] |
||||
} |
||||
// checkout:
|
||||
if (ret == 0) { |
||||
// Checkout: Ask for num Darts
|
||||
[numDarts, tries] = await powerStateMachine(checkoutPipeline(stack), stack, /*initState=*/0, /*initInput=*/sum); |
||||
} else if (ret == 1) { |
||||
// <=50: Ask for checkout tries
|
||||
[numDarts, tries] = await powerStateMachine(checkoutPipeline(stack), stack, /*initState=*/1, /*initInput=*/[3,0]); |
||||
} |
||||
|
||||
if (numDarts == -1) { |
||||
// Error/Back
|
||||
return [1, visit]; |
||||
} else { |
||||
const points = storeVisit(page, visit.split(","), sum, numDarts, tries); |
||||
if (points){ |
||||
// Game Over
|
||||
const winner = getFinalWinner(page); |
||||
let name = "DRAW" |
||||
if (winner != -2){ |
||||
name = page.players[winner].forename; |
||||
} |
||||
const [answer, error] = await overlayAndPop("d-dialog", gameOverDialog(name, points[1]), stack); |
||||
if (answer == "end") { |
||||
saveGame(page); |
||||
return [undefined, page.stats.winner]; |
||||
} else { |
||||
extension(page); |
||||
saveGame(page); |
||||
} |
||||
} else { |
||||
saveGame(page); |
||||
} |
||||
return [1, undefined]; |
||||
} |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
export const gameHandler = async (gamestack, stack, page, computedProps) => { |
||||
const sm = gameStateMachine(gamestack, stack, page, computedProps); |
||||
return powerStateMachine(sm, gamestack); |
||||
} |
||||
|
||||
|
||||
// General State Machine
|
||||
const stateMachine = (stack, page) => { |
||||
return { |
||||
0: async (input, reGet) => { |
||||
// Dispatcher
|
||||
// Check Game State:
|
||||
if (page.players.length != 2 || page.startdate === undefined || page.startdate === ""){ |
||||
// Pre Game
|
||||
return [1, page]; |
||||
} else if (page.enddate === undefined || page.enddate === ""){ |
||||
if (page.game === "" || page.game === undefined || page.game === null) { |
||||
// Who won Bull?
|
||||
return [2, page]; |
||||
} else { |
||||
// In Game
|
||||
return [3, page]; |
||||
} |
||||
} else { |
||||
// Post Game
|
||||
return [4, undefined]; |
||||
} |
||||
}, |
||||
1: async (page, reGet) => { |
||||
// Pre Game
|
||||
const [result, error] = await overlayAndPop("d-pregame", { page: page}, stack); |
||||
if (error != undefined) { |
||||
var re = /^https?:\/\/[^/]+/i; |
||||
window.setTimeout(() => { |
||||
window.location.href = re.exec(window.location.href)[0]; |
||||
return false; |
||||
},1); |
||||
return [undefined, undefined]; |
||||
} |
||||
page.startdate = formatDate(new Date(Date.now())); |
||||
// Update game in database
|
||||
const ret = await savePregame(page); |
||||
if (ret.status != "ok") { |
||||
console.error("Error save page:", ret.status, ret.error); |
||||
} |
||||
return [0, page]; |
||||
}, |
||||
2: async (page, reGet) => { |
||||
// Ask for Bull
|
||||
const [result, error] = await overlayAndPop("d-bullselect", { players: page.players}, stack); |
||||
if (error != undefined) { |
||||
return [1, page]; |
||||
} |
||||
// reorderPlayer
|
||||
if (result !== page.players[0]){ |
||||
page.players = [page.players[1], page.players[0]]; |
||||
} |
||||
// Setup Game
|
||||
initGame(page); |
||||
initStats(page); |
||||
const ret = await savePregame(page); |
||||
return [3, page]; |
||||
}, |
||||
3: async (page, reGet) => { |
||||
// In Game
|
||||
const [result, error] = await overlayAndPop("d-xoi", { page: page, inspect: false }, stack); |
||||
return [4, result]; |
||||
}, |
||||
4: async (winnerUUID, reGet) => { |
||||
const [res, e] = await overlayAndPop("d-xoi", { page: page, inspect: true }, stack); |
||||
var re = /^https?:\/\/[^/]+/i; |
||||
window.setTimeout(() => { |
||||
window.location.href = re.exec(window.location.href)[0]; |
||||
return false; |
||||
},1); |
||||
return [4, res]; |
||||
} |
||||
}; |
||||
} |
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Exports
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export const initXoiView = (app) => { |
||||
app.component('d-pregame', pregame).component('d-xoi', xoi).component('d-bullselect', bullselect).component('d-gameinput', gameinput) |
||||
} |
||||
|
||||
export const xoiHandler = async (stack, id) => { |
||||
const page = reactive(await getGame(id)); |
||||
const sm = stateMachine(stack, page); |
||||
await powerStateMachine(sm, stack); |
||||
} |
@ -0,0 +1,18 @@ |
||||
export function connect(endpoint, callback){ |
||||
const socket = new WebSocket("ws://localhost:4040/subscribe"); |
||||
|
||||
// Connection opened
|
||||
socket.addEventListener("open", (event) => { |
||||
socket.send(JSON.stringify({ |
||||
"action": "subscribe", |
||||
"value": endpoint |
||||
})); |
||||
}); |
||||
|
||||
// Listen for messages
|
||||
socket.addEventListener("message", callback); |
||||
|
||||
window.onbeforeunload = function() { |
||||
socket.close(); |
||||
}; |
||||
} |
@ -0,0 +1,40 @@ |
||||
{ |
||||
"name": "getkirby/plainkit", |
||||
"description": "Kirby Plainkit", |
||||
"type": "project", |
||||
"keywords": [ |
||||
"kirby", |
||||
"cms", |
||||
"starterkit" |
||||
], |
||||
"authors": [ |
||||
{ |
||||
"name": "Bastian Allgeier", |
||||
"email": "bastian@getkirby.com", |
||||
"homepage": "https://getkirby.com" |
||||
} |
||||
], |
||||
"homepage": "https://getkirby.com", |
||||
"support": { |
||||
"email": "support@getkirby.com", |
||||
"issues": "https://github.com/getkirby/starterkit/issues", |
||||
"forum": "https://forum.getkirby.com", |
||||
"source": "https://github.com/getkirby/starterkit" |
||||
}, |
||||
"require": { |
||||
"php": ">=8.0.0 <8.3.0", |
||||
"getkirby/cms": "^3.9" |
||||
}, |
||||
"config": { |
||||
"allow-plugins": { |
||||
"getkirby/composer-installer": true |
||||
}, |
||||
"optimize-autoloader": true |
||||
}, |
||||
"scripts": { |
||||
"start": [ |
||||
"Composer\\Config::disableProcessTimeout", |
||||
"@php -S localhost:8000 kirby/router.php" |
||||
] |
||||
} |
||||
} |
@ -0,0 +1,25 @@ |
||||
Title: 23 |
||||
|
||||
---- |
||||
|
||||
Forename: Tobias |
||||
|
||||
---- |
||||
|
||||
Surname: Grosser |
||||
|
||||
---- |
||||
|
||||
Nickname: The Galvanizer |
||||
|
||||
---- |
||||
|
||||
Number: 23 |
||||
|
||||
---- |
||||
|
||||
Pic: - file://PiQjrMnI09NJAqCt |
||||
|
||||
---- |
||||
|
||||
Uuid: x8tm8FzBBY95HqIi |
After Width: | Height: | Size: 290 KiB |
@ -0,0 +1 @@ |
||||
Uuid: PiQjrMnI09NJAqCt |
@ -0,0 +1,25 @@ |
||||
Title: 8 |
||||
|
||||
---- |
||||
|
||||
Forename: Tim |
||||
|
||||
---- |
||||
|
||||
Surname: Aguirre |
||||
|
||||
---- |
||||
|
||||
Nickname: The Chainmaker |
||||
|
||||
---- |
||||
|
||||
Number: 8 |
||||
|
||||
---- |
||||
|
||||
Pic: - file://doN0XBYRAfmbXAXD |
||||
|
||||
---- |
||||
|
||||
Uuid: gQUWNqwvWYkT41Ql |
After Width: | Height: | Size: 213 KiB |
@ -0,0 +1 @@ |
||||
Uuid: doN0XBYRAfmbXAXD |
@ -0,0 +1,25 @@ |
||||
Title: 99 |
||||
|
||||
---- |
||||
|
||||
Forename: Kerem |
||||
|
||||
---- |
||||
|
||||
Surname: Akkaya |
||||
|
||||
---- |
||||
|
||||
Nickname: The Turkster |
||||
|
||||
---- |
||||
|
||||
Number: 99 |
||||
|
||||
---- |
||||
|
||||
Pic: - file://6ifthAJzq86i5mNn |
||||
|
||||
---- |
||||
|
||||
Uuid: 2ymjLWSMV1xXil3P |
After Width: | Height: | Size: 298 KiB |
@ -0,0 +1 @@ |
||||
Uuid: 6ifthAJzq86i5mNn |
@ -0,0 +1,25 @@ |
||||
Title: 18 |
||||
|
||||
---- |
||||
|
||||
Forename: Jonas |
||||
|
||||
---- |
||||
|
||||
Surname: Milek |
||||
|
||||
---- |
||||
|
||||
Nickname: Jonny from the Block |
||||
|
||||
---- |
||||
|
||||
Number: 18 |
||||
|
||||
---- |
||||
|
||||
Pic: - file://EonmAdCPuztUnzjI |
||||
|
||||
---- |
||||
|
||||
Uuid: PGHC3ukSd89IJvZ0 |
After Width: | Height: | Size: 226 KiB |
@ -0,0 +1 @@ |
||||
Uuid: EonmAdCPuztUnzjI |
@ -0,0 +1,25 @@ |
||||
Title: 001 |
||||
|
||||
---- |
||||
|
||||
Forename: Marvin |
||||
|
||||
---- |
||||
|
||||
Surname: Zmiewski |
||||
|
||||
---- |
||||
|
||||
Nickname: Warranty |
||||
|
||||
---- |
||||
|
||||
Number: 1 |
||||
|
||||
---- |
||||
|
||||
Pic: |
||||
|
||||
---- |
||||
|
||||
Uuid: X9VLqkbzJPrGw0tW |
@ -0,0 +1,25 @@ |
||||
Title: 14 |
||||
|
||||
---- |
||||
|
||||
Forename: Dennis |
||||
|
||||
---- |
||||
|
||||
Surname: Eibach |
||||
|
||||
---- |
||||
|
||||
Nickname: Dart'agnan |
||||
|
||||
---- |
||||
|
||||
Number: 14 |
||||
|
||||
---- |
||||
|
||||
Pic: - file://ZF9zbwGwAx7v0sjE |
||||
|
||||
---- |
||||
|
||||
Uuid: 90CUYLfokdOm9Gw0 |
After Width: | Height: | Size: 277 KiB |
@ -0,0 +1 @@ |
||||
Uuid: ZF9zbwGwAx7v0sjE |
@ -0,0 +1,25 @@ |
||||
Title: 9 |
||||
|
||||
---- |
||||
|
||||
Forename: Julian |
||||
|
||||
---- |
||||
|
||||
Surname: Milek |
||||
|
||||
---- |
||||
|
||||
Nickname: The Beast from the East |
||||
|
||||
---- |
||||
|
||||
Number: 9 |
||||
|
||||
---- |
||||
|
||||
Pic: |
||||
|
||||
---- |
||||
|
||||
Uuid: CRQRBY9Ac3tXXQse |
@ -0,0 +1,25 @@ |
||||
Title: 51 |
||||
|
||||
---- |
||||
|
||||
Forename: Nils |
||||
|
||||
---- |
||||
|
||||
Surname: Knörnschild |
||||
|
||||
---- |
||||
|
||||
Nickname: The Psycho |
||||
|
||||
---- |
||||
|
||||
Number: 51 |
||||
|
||||
---- |
||||
|
||||
Pic: |
||||
|
||||
---- |
||||
|
||||
Uuid: j0DfiLQO2GgJ9RN1 |
@ -0,0 +1,25 @@ |
||||
Title: 116 |
||||
|
||||
---- |
||||
|
||||
Forename: Christian |
||||
|
||||
---- |
||||
|
||||
Surname: Motzek |
||||
|
||||
---- |
||||
|
||||
Nickname: ? |
||||
|
||||
---- |
||||
|
||||
Number: 116 |
||||
|
||||
---- |
||||
|
||||
Pic: |
||||
|
||||
---- |
||||
|
||||
Uuid: 9S2pH2C0vG8XUrye |
@ -0,0 +1,25 @@ |
||||
Title: 4 |
||||
|
||||
---- |
||||
|
||||
Forename: Finn |
||||
|
||||
---- |
||||
|
||||
Surname: Gonschior |
||||
|
||||
---- |
||||
|
||||
Nickname: Finguin |
||||
|
||||
---- |
||||
|
||||
Number: 4 |
||||
|
||||
---- |
||||
|
||||
Uuid: aHO5FUnIphBwYEjS |
||||
|
||||
---- |
||||
|
||||
Pic: |
@ -0,0 +1,25 @@ |
||||
Title: 32 |
||||
|
||||
---- |
||||
|
||||
Forename: Ugo |
||||
|
||||
---- |
||||
|
||||
Surname: Finnendahl |
||||
|
||||
---- |
||||
|
||||
Nickname: The Program |
||||
|
||||
---- |
||||
|
||||
Number: 32 |
||||
|
||||
---- |
||||
|
||||
Image: |
||||
|
||||
---- |
||||
|
||||
Uuid: TbZF36aHK27hZvnK |
@ -0,0 +1,25 @@ |
||||
Title: 7 |
||||
|
||||
---- |
||||
|
||||
Forename: Robert |
||||
|
||||
---- |
||||
|
||||
Surname: Hilprecht |
||||
|
||||
---- |
||||
|
||||
Nickname: The Pathfinder |
||||
|
||||
---- |
||||
|
||||
Number: 7 |
||||
|
||||
---- |
||||
|
||||
Pic: |
||||
|
||||
---- |
||||
|
||||
Uuid: XDZibCkgJ9repYEx |
@ -0,0 +1,25 @@ |
||||
Title: 77 |
||||
|
||||
---- |
||||
|
||||
Forename: Thorsten |
||||
|
||||
---- |
||||
|
||||
Surname: Wassermeyer |
||||
|
||||
---- |
||||
|
||||
Nickname: Sisy Phos |
||||
|
||||
---- |
||||
|
||||
Number: 77 |
||||
|
||||
---- |
||||
|
||||
Uuid: 5bw1wQY8DAfxUVAw |
||||
|
||||
---- |
||||
|
||||
Pic: |
@ -0,0 +1,25 @@ |
||||
Title: 285 |
||||
|
||||
---- |
||||
|
||||
Forename: Simon |
||||
|
||||
---- |
||||
|
||||
Surname: Zeiter |
||||
|
||||
---- |
||||
|
||||
Nickname: Der Zar |
||||
|
||||
---- |
||||
|
||||
Number: 285 |
||||
|
||||
---- |
||||
|
||||
Pic: |
||||
|
||||
---- |
||||
|
||||
Uuid: 8TlbKlF7fVBwS24c |
@ -0,0 +1,25 @@ |
||||
Title: TODO |
||||
|
||||
---- |
||||
|
||||
Forename: Thomas |
||||
|
||||
---- |
||||
|
||||
Surname: Engel |
||||
|
||||
---- |
||||
|
||||
Nickname: Only 19 |
||||
|
||||
---- |
||||
|
||||
Number: 19 |
||||
|
||||
---- |
||||
|
||||
Uuid: 5qeooGppOFqpprW5 |
||||
|
||||
---- |
||||
|
||||
Pic: |
@ -0,0 +1,29 @@ |
||||
Title: 92 |
||||
|
||||
---- |
||||
|
||||
Forename: Jacob |
||||
|
||||
---- |
||||
|
||||
Surname: Otto |
||||
|
||||
---- |
||||
|
||||
Nickname: Cool Crab |
||||
|
||||
---- |
||||
|
||||
Number: 92 |
||||
|
||||
---- |
||||
|
||||
Pic: - file://4cGjeP4QOkq6a46R |
||||
|
||||
---- |
||||
|
||||
Image: |
||||
|
||||
---- |
||||
|
||||
Uuid: 7GAUHPdaKdD2sHm9 |
After Width: | Height: | Size: 253 KiB |
@ -0,0 +1 @@ |
||||
Uuid: 4cGjeP4QOkq6a46R |
@ -0,0 +1,25 @@ |
||||
Title: 45 |
||||
|
||||
---- |
||||
|
||||
Forename: Vincent |
||||
|
||||
---- |
||||
|
||||
Surname: Weigelt |
||||
|
||||
---- |
||||
|
||||
Nickname: Unimpossible |
||||
|
||||
---- |
||||
|
||||
Number: 45 |
||||
|
||||
---- |
||||
|
||||
Pic: - file://ZLhSSU1MU9Hy0Ru3 |
||||
|
||||
---- |
||||
|
||||
Uuid: ZUPj0Nw2DcaIQ1H1 |
After Width: | Height: | Size: 214 KiB |
@ -0,0 +1 @@ |
||||
Uuid: ZLhSSU1MU9Hy0Ru3 |
@ -0,0 +1,29 @@ |
||||
Title: 180 |
||||
|
||||
---- |
||||
|
||||
Forename: Alexander |
||||
|
||||
---- |
||||
|
||||
Surname: Ecke |
||||
|
||||
---- |
||||
|
||||
Nickname: One Hundred and Ecki |
||||
|
||||
---- |
||||
|
||||
Number: 180 |
||||
|
||||
---- |
||||
|
||||
Pic: - file://YXZh1UDA03dJwFYT |
||||
|
||||
---- |
||||
|
||||
Uuid: fShTMgFob20fogyr |
||||
|
||||
---- |
||||
|
||||
Image: |
After Width: | Height: | Size: 222 KiB |
@ -0,0 +1 @@ |
||||
Uuid: YXZh1UDA03dJwFYT |
@ -0,0 +1,25 @@ |
||||
Title: 57 |
||||
|
||||
---- |
||||
|
||||
Forename: Patrick |
||||
|
||||
---- |
||||
|
||||
Surname: Wendhaus |
||||
|
||||
---- |
||||
|
||||
Nickname: The Roman Sage |
||||
|
||||
---- |
||||
|
||||
Number: 57 |
||||
|
||||
---- |
||||
|
||||
Uuid: 4CPpeIGeeK8UHTf8 |
||||
|
||||
---- |
||||
|
||||
Image: |
@ -0,0 +1,25 @@ |
||||
Title: 5 |
||||
|
||||
---- |
||||
|
||||
Forename: Melvin |
||||
|
||||
---- |
||||
|
||||
Surname: Malinowsky |
||||
|
||||
---- |
||||
|
||||
Nickname: Diver |
||||
|
||||
---- |
||||
|
||||
Number: 5 |
||||
|
||||
---- |
||||
|
||||
Pic: |
||||
|
||||
---- |
||||
|
||||
Uuid: Ue0F1SstBqE7xVKr |
@ -0,0 +1,25 @@ |
||||
Title: 26 |
||||
|
||||
---- |
||||
|
||||
Forename: Hannes |
||||
|
||||
---- |
||||
|
||||
Surname: Gonschior |
||||
|
||||
---- |
||||
|
||||
Nickname: The Answer |
||||
|
||||
---- |
||||
|
||||
Number: 26 |
||||
|
||||
---- |
||||
|
||||
Pic: - file://uQkuW135VcDc2lRV |
||||
|
||||
---- |
||||
|
||||
Uuid: UkQNJytohCGIXySl |
After Width: | Height: | Size: 960 KiB |
@ -0,0 +1 @@ |
||||
Uuid: uQkuW135VcDc2lRV |
@ -0,0 +1,25 @@ |
||||
Title: 11 |
||||
|
||||
---- |
||||
|
||||
Forename: Nicolai |
||||
|
||||
---- |
||||
|
||||
Surname: Jelitto |
||||
|
||||
---- |
||||
|
||||
Nickname: El Sid |
||||
|
||||
---- |
||||
|
||||
Number: 11 |
||||
|
||||
---- |
||||
|
||||
Uuid: xRomcRk5I5n4gkq5 |
||||
|
||||
---- |
||||
|
||||
Pic: |
@ -0,0 +1,25 @@ |
||||
Title: 13 |
||||
|
||||
---- |
||||
|
||||
Forename: Felix |
||||
|
||||
---- |
||||
|
||||
Surname: Meißner |
||||
|
||||
---- |
||||
|
||||
Nickname: The Droog |
||||
|
||||
---- |
||||
|
||||
Number: 13 |
||||
|
||||
---- |
||||
|
||||
Uuid: Jz127lCNtxHgUj2b |
||||
|
||||
---- |
||||
|
||||
Pic: |
@ -0,0 +1,5 @@ |
||||
Title: Members |
||||
|
||||
---- |
||||
|
||||
Uuid: dKjmIGtTxipFe6jf |
@ -0,0 +1,26 @@ |
||||
Title: Summer Slam |
||||
|
||||
---- |
||||
|
||||
Participants: |
||||
|
||||
- page://TbZF36aHK27hZvnK |
||||
- page://7GAUHPdaKdD2sHm9 |
||||
- page://XDZibCkgJ9repYEx |
||||
- page://4CPpeIGeeK8UHTf8 |
||||
- page://aHO5FUnIphBwYEjS |
||||
- page://90CUYLfokdOm9Gw0 |
||||
- page://5bw1wQY8DAfxUVAw |
||||
- page://fShTMgFob20fogyr |
||||
- page://UkQNJytohCGIXySl |
||||
- page://5qeooGppOFqpprW5 |
||||
- page://ZUPj0Nw2DcaIQ1H1 |
||||
- page://9S2pH2C0vG8XUrye |
||||
|
||||
---- |
||||
|
||||
Date: 2024-06-15 |
||||
|
||||
---- |
||||
|
||||
Uuid: w7RmvzZeMb8NzkeX |